happo 2.4.0 → 2.5.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 8151e36e534eae38fab096a18a81ae2cb6e61cf0
4
- data.tar.gz: f28280aea360ccf7e2a413bb9e83889e404928df
3
+ metadata.gz: dbd179b01168cbba70f43a4bcaf1ebf1d94490f3
4
+ data.tar.gz: 6ea1662f9bdfad75f768809d208e81afcd760efb
5
5
  SHA512:
6
- metadata.gz: 5583155f820c9c67ca92c94f2f88a659f00f7c54c53873c9b370fd85351bacaea54aa729f318ad6794e25e7f2387e39212eaa196611e3ffaee260169ec09ef78
7
- data.tar.gz: 8d39502a92d286e08cd85f3013f8419e7601f73c662ba06b99e57dbd57c5c197ded39edfbe40c255ae46f985341e45c4874536ff5bcda35b3188614708bfd909
6
+ metadata.gz: 7956f0d125a8bb1f28e95cdc59188addf921d6890e499589a78693c1b04b16874233458108b5943f2a5c856d3a1316feabefda99e78b059ee3c227ab13da5880
7
+ data.tar.gz: af8f7aae63bd847275ef37d494c07b722036f9b2e9ebc47ed50e3c81c9c33017e30a02afa3ac2733e1761a8991da2608c59997edeeef4c112834804d3abdfe48
@@ -1,91 +1,397 @@
1
+ /* global React */
2
+ /* eslint-disable react/no-multi-comp */
1
3
  const PropTypes = React.PropTypes;
2
4
 
3
- const imageObjectStructure = {
5
+ const VIEWS = {
6
+ SIDE_BY_SIDE: 'Side-by-side',
7
+ SWIPE: 'Swipe',
8
+ DIFF: 'Diff',
9
+ };
10
+
11
+ const imageShape = {
4
12
  description: PropTypes.string.isRequired,
5
13
  viewport: PropTypes.string.isRequired,
6
- url: PropTypes.string.isRequired,
14
+ diff: PropTypes.string,
15
+ previous: PropTypes.string,
16
+ current: PropTypes.string.isRequired,
7
17
  };
8
18
 
9
19
  function imageSlug(image) {
10
- return btoa(image.description + image.viewport);
20
+ return btoa(image.description + image.viewport);
21
+ }
22
+
23
+ function InlineLink({ children, to }) {
24
+ return (
25
+ <a className='InlineLink' href={`#${to}`}>
26
+ {children}
27
+ </a>
28
+ );
11
29
  }
30
+ InlineLink.propTypes = {
31
+ children: PropTypes.node.isRequired,
32
+ to: PropTypes.string.isRequired,
33
+ };
34
+
35
+ function ImageHeading({ image }) {
36
+ return (
37
+ <h3 id={imageSlug(image)}>
38
+ <InlineLink to={imageSlug(image)}>
39
+ {image.description}
40
+ {' @ '}
41
+ {image.viewport}
42
+ </InlineLink>
43
+ </h3>
44
+ );
45
+ }
46
+ ImageHeading.propTypes = {
47
+ image: PropTypes.shape(imageShape).isRequired,
48
+ };
12
49
 
13
- function renderImage(image) {
50
+ function NewImage({ image }) {
14
51
  return (
15
52
  <div>
16
- <h3 id={imageSlug(image)}>
17
- <a className='anchored' href={`#${imageSlug(image)}`}>
18
- {image.description}
19
- {' @ '}
20
- {image.viewport}
21
- </a>
22
- </h3>
23
- <img src={image.url} />
53
+ <ImageHeading
54
+ image={image}
55
+ />
56
+ <img
57
+ role='presentation'
58
+ src={image.current}
59
+ />
24
60
  </div>
25
61
  );
26
62
  }
63
+ NewImage.propTypes = {
64
+ image: PropTypes.shape(imageShape).isRequired,
65
+ };
27
66
 
28
- function renderDiffImages(diffImages) {
29
- if (!diffImages.length) {
67
+ function DiffImages({ images }) {
68
+ if (!images.length) {
30
69
  return null;
31
70
  }
32
71
 
33
72
  return (
34
73
  <div>
35
74
  <h2 id='diffs'>
36
- <a className='anchored' href='#diffs'>
37
- Diffs ({ diffImages.length })
38
- </a>
75
+ <InlineLink to='diffs'>
76
+ Diffs ({ images.length })
77
+ </InlineLink>
39
78
  </h2>
40
79
 
41
- {diffImages.map(renderImage)}
80
+ {images.map((image) => (
81
+ <DiffController
82
+ key={image.current}
83
+ image={image}
84
+ />
85
+ ))}
42
86
  </div>
43
87
  );
44
88
  }
89
+ DiffImages.propTypes = {
90
+ images: PropTypes.arrayOf(PropTypes.shape(imageShape)).isRequired,
91
+ };
45
92
 
46
- function renderNewImages(newImages) {
47
- if (!newImages.length) {
93
+ function NewImages({ images }) {
94
+ if (!images.length) {
48
95
  return null;
49
96
  }
50
97
 
51
98
  return (
52
99
  <div>
53
100
  <h2 id='new'>
54
- <a className='anchored' href='#new'>
55
- New examples ({ newImages.length })
56
- </a>
101
+ <InlineLink to='new'>
102
+ New examples ({ images.length })
103
+ </InlineLink>
57
104
  </h2>
58
105
 
59
- {newImages.map(renderImage)}
106
+ {images.map((image) => (
107
+ <NewImage
108
+ key={image.current}
109
+ image={image}
110
+ />
111
+ ))}
60
112
  </div>
61
113
  );
62
114
  }
115
+ NewImages.propTypes = {
116
+ images: PropTypes.arrayOf(PropTypes.shape(imageShape)).isRequired,
117
+ };
118
+
119
+ function maxImageSize(...imageUrls) {
120
+ const dimensions = {};
121
+
122
+ return new Promise((resolve, reject) => {
123
+ imageUrls.forEach((url, i) => {
124
+ const image = new Image();
125
+
126
+ image.onerrer = function handleImageError(e) {
127
+ reject(e);
128
+ };
129
+
130
+ image.onload = function handleImageLoad() {
131
+ const { width, height } = this;
132
+
133
+ // Use the index in case the URL is somehow duplicated.
134
+ dimensions[i] = { width, height };
135
+
136
+ if (Object.keys(dimensions).length >= imageUrls.length) {
137
+ // We are done, so compute the max width and height and resolve.
138
+ const values = Object.keys(dimensions).map(key => dimensions[key]);
139
+ const maxWidth = Math.max(...values.map(value => value.width));
140
+ const maxHeight = Math.max(...values.map(value => value.height));
141
+ resolve({ width: maxWidth, height: maxHeight });
142
+ }
143
+ };
144
+
145
+ image.src = url;
146
+ });
147
+ });
148
+ }
63
149
 
64
- window.HappoDiffs = React.createClass({
65
- propTypes: {
66
- pageTitle: PropTypes.string.isRequired,
67
- diffImages: PropTypes.arrayOf(imageObjectStructure).isRequired,
68
- newImages: PropTypes.arrayOf(imageObjectStructure).isRequired,
69
- generatedAt: PropTypes.string.isRequired,
70
- },
150
+ class Swiper extends React.Component {
151
+ constructor(props) {
152
+ super(props);
153
+ this.state = {
154
+ cursorLeft: 0,
155
+ height: 'auto',
156
+ width: 'auto',
157
+ };
158
+ this.handleMouseMove = this.handleMouseMove.bind(this);
159
+ }
160
+
161
+ componentWillMount() {
162
+ this.updateSize(this.props)
163
+ .then(({ width }) => {
164
+ // Start in the center
165
+ this.setState({ cursorLeft: width / 2 });
166
+ });
167
+ }
168
+
169
+ componentWillReceiveProps(nextProps) {
170
+ this.updateSize(nextProps);
171
+ }
172
+
173
+ updateSize({ current, previous }) {
174
+ const sizes = maxImageSize(current, previous)
175
+ .then(({ width, height }) => {
176
+ this.setState({ width, height });
177
+ return { width, height };
178
+ });
179
+
180
+ return Promise.resolve(sizes);
181
+ }
182
+
183
+ handleMouseMove(event) {
184
+ this.setState({
185
+ cursorLeft: event.pageX - event.target.offsetLeft,
186
+ });
187
+ }
71
188
 
72
189
  render() {
190
+ const { previous, current } = this.props;
191
+ const { cursorLeft, height, width } = this.state;
192
+
73
193
  return (
74
- <div>
75
- <header className='header'>
76
- <h1 className='header_title'>
77
- {this.props.pageTitle}
78
- </h1>
79
- <div className='header__time'>
80
- Generated: {this.props.generatedAt}
81
- </div>
82
- </header>
83
-
84
- <main className='main'>
85
- {renderDiffImages(this.props.diffImages)}
86
- {renderNewImages(this.props.newImages)}
87
- </main>
194
+ <div
195
+ className='Swiper'
196
+ style={{ height, width }}
197
+ onMouseMove={this.handleMouseMove}
198
+ >
199
+ <div
200
+ className='Swiper__image'
201
+ style={{ width: cursorLeft }}
202
+ >
203
+ <img
204
+ src={previous}
205
+ role='presentation'
206
+ />
207
+ </div>
208
+
209
+ <div
210
+ className='Swiper__image'
211
+ style={{
212
+ transform: `translateX(${cursorLeft}px)`,
213
+ width: width - cursorLeft,
214
+ }}
215
+ >
216
+ <img
217
+ src={current}
218
+ style={{
219
+ transform: `translateX(-${cursorLeft}px)`,
220
+ }}
221
+ role='presentation'
222
+ />
223
+ </div>
224
+
225
+ <div
226
+ className='Swiper__cursor'
227
+ style={{
228
+ transform: `translateX(${cursorLeft}px)`,
229
+ }}
230
+ />
88
231
  </div>
89
232
  );
90
233
  }
91
- });
234
+ }
235
+ Swiper.propTypes = {
236
+ previous: PropTypes.string.isRequired,
237
+ current: PropTypes.string.isRequired,
238
+ };
239
+
240
+ function SideBySide({ previous, current }) {
241
+ return (
242
+ <div className='SideBySide'>
243
+ <div className='SideBySide__image'>
244
+ <img
245
+ role='presentation'
246
+ src={previous}
247
+ />
248
+ <div className='SideBySide__caption'>
249
+ Before
250
+ </div>
251
+ </div>
252
+ {' '}
253
+ <div className='SideBySide__image'>
254
+ <img
255
+ role='presentation'
256
+ src={current}
257
+ />
258
+ <div className='SideBySide__caption'>
259
+ After
260
+ </div>
261
+ </div>
262
+ </div>
263
+ );
264
+ }
265
+ SideBySide.propTypes = {
266
+ previous: PropTypes.string.isRequired,
267
+ current: PropTypes.string.isRequired,
268
+ };
269
+
270
+ function SelectedView({ image, selectedView }) {
271
+ if (selectedView === VIEWS.SIDE_BY_SIDE) {
272
+ return (
273
+ <SideBySide
274
+ previous={image.previous}
275
+ current={image.current}
276
+ />
277
+ );
278
+ }
279
+
280
+ if (selectedView === VIEWS.DIFF) {
281
+ return (
282
+ <img role='presentation' src={image.diff} />
283
+ );
284
+ }
285
+
286
+ if (selectedView === VIEWS.SWIPE) {
287
+ return (
288
+ <Swiper
289
+ previous={image.previous}
290
+ current={image.current}
291
+ />
292
+ );
293
+ }
294
+ }
295
+ SelectedView.propTypes = {
296
+ image: PropTypes.shape(imageShape).isRequired,
297
+ selectedView: PropTypes.oneOf(Object.keys(VIEWS).map(key => VIEWS[key])).isRequired,
298
+ };
299
+
300
+ class DiffController extends React.Component {
301
+ constructor(props) {
302
+ super(props);
303
+ this.state = {
304
+ selectedView: VIEWS.SIDE_BY_SIDE,
305
+ };
306
+
307
+ this.handleClick = this.handleClick.bind(this);
308
+ }
309
+
310
+ handleClick(view) {
311
+ this.setState({ selectedView: view });
312
+ }
313
+
314
+ render() {
315
+ return (
316
+ <Diff
317
+ image={this.props.image}
318
+ selectedView={this.state.selectedView}
319
+ onClick={this.handleClick}
320
+ />
321
+ );
322
+ }
323
+ }
324
+ DiffController.propTypes = {
325
+ image: PropTypes.shape(imageShape).isRequired,
326
+ };
327
+
328
+ function Diff({ image, selectedView, onClick }) {
329
+ return (
330
+ <div>
331
+ <ImageHeading
332
+ image={image}
333
+ />
334
+ <div className='Diff__buttons'>
335
+ {Object.keys(VIEWS).map(key => VIEWS[key]).map((view, i) => {
336
+ const classes = ['Diff__button'];
337
+ if (i === 0) {
338
+ classes.push('Diff__button--first');
339
+ } else if (i === Object.keys(VIEWS).length - 1) {
340
+ classes.push('Diff__button--last');
341
+ }
342
+
343
+ return (
344
+ <button
345
+ key={view}
346
+ className={classes.join(' ')}
347
+ aria-pressed={view === selectedView}
348
+ onClick={() => { onClick(view); }}
349
+ >
350
+ {view}
351
+ </button>
352
+ );
353
+ })}
354
+ </div>
355
+ <div className='Diff__images'>
356
+ <SelectedView image={image} selectedView={selectedView} />
357
+ </div>
358
+ </div>
359
+ );
360
+ }
361
+ Diff.propTypes = {
362
+ image: PropTypes.shape(imageShape).isRequired,
363
+ onClick: PropTypes.func.isRequired,
364
+ selectedView: PropTypes.oneOf(Object.keys(VIEWS).map(key => VIEWS[key])).isRequired,
365
+ };
366
+
367
+ function HappoDiffs({ pageTitle, generatedAt, diffImages, newImages }) {
368
+ return (
369
+ <div>
370
+ <header className='HappoDiffs__header'>
371
+ <h1 className='HappoDiffs__headerTitle'>
372
+ {pageTitle}
373
+ </h1>
374
+ <div className='HappoDiffs__headerTime'>
375
+ Generated: {generatedAt}
376
+ </div>
377
+ </header>
378
+
379
+ <main className='HappoDiffs__main'>
380
+ <DiffImages
381
+ images={diffImages}
382
+ />
383
+ <NewImages
384
+ images={newImages}
385
+ />
386
+ </main>
387
+ </div>
388
+ );
389
+ }
390
+ HappoDiffs.propTypes = {
391
+ pageTitle: PropTypes.string.isRequired,
392
+ diffImages: PropTypes.arrayOf(imageShape).isRequired,
393
+ newImages: PropTypes.arrayOf(imageShape).isRequired,
394
+ generatedAt: PropTypes.string.isRequired,
395
+ };
396
+
397
+ window.HappoDiffs = HappoDiffs;
@@ -1,3 +1,10 @@
1
+ /* eslint strict: 0 */
2
+ /* eslint object-shorthand: 0 */
3
+ /* eslint prefer-template: 0 */
4
+ /* eslint comma-dangle: 0 */
5
+ /* eslint no-var: 0 */
6
+ /* eslint prefer-arrow-callback: 0 */
7
+ /* eslint vars-on-top: 1 */
1
8
  'use strict';
2
9
 
3
10
  window.happo = {
@@ -6,11 +13,11 @@ window.happo = {
6
13
  currentRenderedElement: undefined,
7
14
  errors: [],
8
15
 
9
- define: function(description, func, options) {
16
+ define: function define(description, func, options) {
10
17
  // Make sure we don't have a duplicate description
11
18
  if (this.defined[description]) {
12
- throw 'Error while defining "' + description +
13
- '": Duplicate description detected'
19
+ throw new Error('Error while defining "' + description +
20
+ '": Duplicate description detected');
14
21
  }
15
22
  this.defined[description] = {
16
23
  description: description,
@@ -19,7 +26,7 @@ window.happo = {
19
26
  };
20
27
  },
21
28
 
22
- fdefine: function(description, func, options) {
29
+ fdefine: function fdefine(description, func, options) {
23
30
  this.define(description, func, options); // add the example
24
31
  this.fdefined.push(description);
25
32
  },
@@ -27,12 +34,12 @@ window.happo = {
27
34
  /**
28
35
  * @return {Array.<Object>}
29
36
  */
30
- getAllExamples: function() {
37
+ getAllExamples: function getAllExamples() {
31
38
  var descriptions = this.fdefined.length ?
32
39
  this.fdefined :
33
40
  Object.keys(this.defined);
34
41
 
35
- return descriptions.map(function(description) {
42
+ return descriptions.map(function processDescription(description) {
36
43
  var example = this.defined[description];
37
44
  // We return a subset of the properties of an example (only those relevant
38
45
  // for happo_runner.rb).
@@ -43,7 +50,7 @@ window.happo = {
43
50
  }.bind(this));
44
51
  },
45
52
 
46
- handleError: function(currentExample, error) {
53
+ handleError: function handleError(currentExample, error) {
47
54
  console.error(error.stack);
48
55
  return {
49
56
  description: currentExample.description,
@@ -57,23 +64,24 @@ window.happo = {
57
64
  * that is called when it is done.
58
65
  * @return {Promise}
59
66
  */
60
- tryAsync: function(func) {
61
- return new Promise(function(resolve, reject) {
67
+ tryAsync: function tryAsync(func) {
68
+ return new Promise(function tryAsyncPromise(resolve, reject) {
62
69
  // Safety valve: if the function does not finish after 3s, then something
63
70
  // went haywire and we need to move on.
64
- var timeout = setTimeout(function() {
71
+ var timeout = setTimeout(function tryAsyncTimeout() {
65
72
  reject(new Error('Async callback was not invoked within timeout.'));
66
73
  }, 3000);
67
74
 
68
75
  // This function is called by the example when it is done executing.
69
- var doneCallback = function(elem) {
76
+ var doneCallback = function doneCallback(elem) {
70
77
  clearTimeout(timeout);
71
78
 
72
79
  if (!arguments.length) {
73
- return reject(new Error(
80
+ reject(new Error(
74
81
  'The async done callback expects the rendered element as an ' +
75
82
  'argument, but there were no arguments.'
76
83
  ));
84
+ return;
77
85
  }
78
86
 
79
87
  resolve(elem);
@@ -91,7 +99,7 @@ window.happo = {
91
99
  *
92
100
  * @param {Object} renderedElement
93
101
  */
94
- cleanOutElement: function(renderedElement) {
102
+ cleanOutElement: function cleanOutElement(renderedElement) {
95
103
  renderedElement.parentNode.removeChild(renderedElement);
96
104
  },
97
105
 
@@ -103,9 +111,10 @@ window.happo = {
103
111
  * @param {Function} doneFunc injected by driver.execute_async_script in
104
112
  * happo/runner.rb
105
113
  */
106
- renderExample: function(exampleDescription, doneFunc) {
114
+ renderExample: function renderExample(exampleDescription, doneFunc) {
115
+ var currentExample = this.defined[exampleDescription];
116
+
107
117
  try {
108
- var currentExample = this.defined[exampleDescription];
109
118
  if (!currentExample) {
110
119
  throw new Error(
111
120
  'No example found with description "' + exampleDescription + '"');
@@ -124,9 +133,9 @@ window.happo = {
124
133
  // The function takes an argument, which is a callback that is called
125
134
  // once it is done executing. This can be used to write functions that
126
135
  // have asynchronous code in them.
127
- this.tryAsync(func).then(function(elem) {
136
+ this.tryAsync(func).then(function (elem) {
128
137
  doneFunc(this.processElem(currentExample, elem));
129
- }.bind(this)).catch(function(error) {
138
+ }.bind(this)).catch(function (error) {
130
139
  doneFunc(this.handleError(currentExample, error));
131
140
  }.bind(this));
132
141
  } else {
@@ -137,9 +146,9 @@ window.happo = {
137
146
  if (result instanceof Promise) {
138
147
  // The function returned a promise, so we need to wait for it to
139
148
  // resolve before proceeding.
140
- result.then(function(elem) {
149
+ result.then(function (elem) {
141
150
  doneFunc(this.processElem(currentExample, elem));
142
- }.bind(this)).catch(function(error) {
151
+ }.bind(this)).catch(function (error) {
143
152
  doneFunc(this.handleError(currentExample, error));
144
153
  }.bind(this));
145
154
  } else {
@@ -154,13 +163,15 @@ window.happo = {
154
163
  },
155
164
 
156
165
  // This function takes a node and a box object that we will mutate.
157
- getFullRectRecursive: function(node, box) {
166
+ getFullRectRecursive: function getFullRectRecursive(node, box) {
158
167
  var rect = node.getBoundingClientRect();
159
168
 
169
+ /* eslint-disable no-param-reassign */
160
170
  box.bottom = Math.max(box.bottom, rect.bottom);
161
171
  box.left = Math.min(box.left, rect.left);
162
172
  box.right = Math.max(box.right, rect.right);
163
173
  box.top = Math.min(box.top, rect.top);
174
+ /* eslint-enable no-param-reassign */
164
175
 
165
176
  for (var i = 0; i < node.children.length; i++) {
166
177
  this.getFullRectRecursive(node.children[i], box);
@@ -171,7 +182,7 @@ window.happo = {
171
182
  // descendent nodes. This allows us to ensure that the screenshot includes
172
183
  // absolutely positioned elements. It is important that this is fast, since we
173
184
  // may be iterating over a high number of nodes.
174
- getFullRect: function(node) {
185
+ getFullRect: function getFullRect(node) {
175
186
  var rect = node.getBoundingClientRect();
176
187
 
177
188
  // Set up the initial object that we will mutate in our recursive function.
@@ -209,13 +220,13 @@ window.happo = {
209
220
  box.left = Math.max(box.left, 0);
210
221
  box.top = Math.max(box.top, 0);
211
222
 
212
- box.width = box.right - box.left;
213
- box.height = box.bottom - box.top;
223
+ box.width = box.right - box.left;
224
+ box.height = box.bottom - box.top;
214
225
 
215
226
  return box;
216
227
  },
217
228
 
218
- processElem: function(currentExample, elem) {
229
+ processElem: function processElem(currentExample, elem) {
219
230
  try {
220
231
  this.currentRenderedElement = elem;
221
232
 
@@ -246,35 +257,35 @@ window.happo = {
246
257
  }
247
258
  };
248
259
 
249
- window.addEventListener('load', function() {
260
+ window.addEventListener('load', function handleWindowLoad() {
250
261
  var matches = window.location.search.match(/description=([^&]*)/);
251
262
  if (!matches) {
252
263
  return;
253
264
  }
254
265
  var example = decodeURIComponent(matches[1]);
255
- window.happo.renderExample(example, function() {});
266
+ window.happo.renderExample(example, function () {});
256
267
  });
257
268
 
258
269
  // We need to redefine a few global functions that halt execution. Without this,
259
270
  // there's a chance that the Ruby code can't communicate with the browser.
260
- window.alert = function(message) {
271
+ window.alert = function alert(message) {
261
272
  console.log('`window.alert` called', message);
262
273
  };
263
274
 
264
- window.confirm = function(message) {
275
+ window.confirm = function confirm(message) {
265
276
  console.log('`window.confirm` called', message);
266
277
  return true;
267
278
  };
268
279
 
269
- window.prompt = function(message, value) {
280
+ window.prompt = function prompt(message, value) {
270
281
  console.log('`window.prompt` called', message, value);
271
282
  return null;
272
283
  };
273
284
 
274
- window.onerror = function(message, url, lineNumber) {
285
+ window.onerror = function onerror(message, url, lineNumber) {
275
286
  window.happo.errors.push({
276
287
  message: message,
277
288
  url: url,
278
289
  lineNumber: lineNumber
279
290
  });
280
- }
291
+ };
@@ -21,16 +21,16 @@ body {
21
21
  margin: 0;
22
22
  }
23
23
 
24
- .anchored {
24
+ .InlineLink {
25
25
  color: #000000;
26
26
  text-decoration: none;
27
27
  }
28
28
 
29
- .anchored:hover {
29
+ .InlineLink:hover {
30
30
  text-decoration: underline;
31
31
  }
32
32
 
33
- .anchored::after {
33
+ .InlineLink::after {
34
34
  background-image: url(data:image/svg+xml;base64,PHN2ZyBmaWxsPSIjMDAwMDAwIiBoZWlnaHQ9IjI0IiB2aWV3Qm94PSIwIDAgMjQgMjQiIHdpZHRoPSIyNCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4gICAgPHBhdGggZD0iTTAgMGgyNHYyNEgweiIgZmlsbD0ibm9uZSIvPiAgICA8cGF0aCBkPSJNMy45IDEyYzAtMS43MSAxLjM5LTMuMSAzLjEtMy4xaDRWN0g3Yy0yLjc2IDAtNSAyLjI0LTUgNXMyLjI0IDUgNSA1aDR2LTEuOUg3Yy0xLjcxIDAtMy4xLTEuMzktMy4xLTMuMXpNOCAxM2g4di0ySDh2MnptOS02aC00djEuOWg0YzEuNzEgMCAzLjEgMS4zOSAzLjEgMy4xcy0xLjM5IDMuMS0zLjEgMy4xaC00VjE3aDRjMi43NiAwIDUtMi4yNCA1LTVzLTIuMjQtNS01LTV6Ii8+PC9zdmc+);
35
35
  content: '';
36
36
  display: inline-block;
@@ -41,11 +41,11 @@ body {
41
41
  margin-left: 5px;
42
42
  }
43
43
 
44
- .anchored:hover::after {
44
+ .InlineLink:hover::after {
45
45
  opacity: 1;
46
46
  }
47
47
 
48
- .header {
48
+ .HappoDiffs__header {
49
49
  align-items: baseline;
50
50
  background-color: #ffffff;
51
51
  box-shadow: 0 0 4px rgba(0, 0, 0, .3);
@@ -55,14 +55,93 @@ body {
55
55
  padding: 20px 10px 15px;
56
56
  }
57
57
 
58
- .header > :first-child {
58
+ .HappoDiffs__header > :first-child {
59
59
  margin-right: 10px;
60
60
  }
61
61
 
62
- .header__title {
62
+ .HappoDiffs__headerTitle {
63
63
  margin: 0;
64
64
  }
65
65
 
66
- .main {
66
+ .HappoDiffs__main {
67
67
  padding: 10px;
68
68
  }
69
+
70
+ .Diff__images {
71
+ white-space: nowrap;
72
+ }
73
+
74
+ .Diff__buttons {
75
+ margin-bottom: 5px;
76
+ }
77
+
78
+ .Diff__button {
79
+ border: 1px solid #ccc;
80
+ border-left-width: 0;
81
+ background-color: #f0f0f0;
82
+ padding: 5px 15px;
83
+ }
84
+
85
+ .Diff__button[aria-pressed="true"] {
86
+ background-color: #666;
87
+ border-color: #444;
88
+ color: white;
89
+ }
90
+
91
+ .Diff__button--first {
92
+ border-left-width: 1px;
93
+ border-radius: 3px 0 0 3px;
94
+ }
95
+
96
+ .Diff__button--last {
97
+ border-radius: 0 3px 3px 0;
98
+ }
99
+
100
+ .Swiper {
101
+ display: inline-block;
102
+ line-height: 0;
103
+ position: relative;
104
+ }
105
+
106
+ .Swiper__cursor {
107
+ box-shadow: 0px 0px 2px 0px #ffffff;
108
+ position: absolute;
109
+ left: 0;
110
+ top: 0;
111
+ width: 1px;
112
+ height: 100%;
113
+ background-color: #666666;
114
+ pointer-events: none;
115
+ }
116
+
117
+ .Swiper__image {
118
+ left: 0;
119
+ overflow: hidden;
120
+ pointer-events: none;
121
+ position: absolute;
122
+ top: 0;
123
+ }
124
+
125
+ .SideBySide__image {
126
+ display: inline-block;
127
+ line-height: 0;
128
+ position: relative;
129
+ vertical-align: top;
130
+ }
131
+
132
+ .SideBySide__caption {
133
+ background-color: #666666;
134
+ bottom: 0;
135
+ color: #ffffff;
136
+ font-size: 10px;
137
+ line-height: 1;
138
+ opacity: 0;
139
+ padding: 3px 6px;
140
+ position: absolute;
141
+ right: 0;
142
+ transition: opacity .2s;
143
+ }
144
+
145
+ .SideBySide__image:hover .SideBySide__caption {
146
+ opacity: 1;
147
+ }
data/lib/happo/server.rb CHANGED
@@ -27,25 +27,28 @@ module Happo
27
27
  get '/review' do
28
28
  result_summary = Happo::Utils.last_result_summary
29
29
 
30
- diff_images = result_summary[:diff_examples].map do |example|
31
- file_path = Happo::Utils.path_to(
32
- example[:description],
33
- example[:viewport],
34
- 'diff.png'
35
- )
36
- example[:url] = "/resource?file=#{ERB::Util.url_encode(file_path)}"
37
- example
30
+ diff_images = result_summary[:diff_examples].map do |image|
31
+ [:previous, :diff, :current].each do |variant|
32
+ file_path = Happo::Utils.path_to(
33
+ image[:description],
34
+ image[:viewport],
35
+ "#{variant}.png"
36
+ )
37
+ image[variant] = "/resource?file=#{ERB::Util.url_encode(file_path)}"
38
+ end
39
+ image
38
40
  end
39
41
 
40
- new_images = result_summary[:new_examples].map do |example|
42
+ new_images = result_summary[:new_examples].map do |image|
41
43
  file_path = Happo::Utils.path_to(
42
- example[:description],
43
- example[:viewport],
44
+ image[:description],
45
+ image[:viewport],
44
46
  'current.png'
45
47
  )
46
- example[:url] = "/resource?file=#{ERB::Util.url_encode(file_path)}"
47
- example
48
+ image[:current] = "/resource?file=#{ERB::Util.url_encode(file_path)}"
49
+ image
48
50
  end
51
+
49
52
  erb :diffs, locals: {
50
53
  diff_images: diff_images,
51
54
  new_images: new_images
@@ -57,17 +60,23 @@ module Happo
57
60
  {
58
61
  description: '<First> with "test"',
59
62
  viewport: 'small',
60
- url: 'http://placehold.it/350x150',
63
+ previous: 'http://placehold.it/350x150',
64
+ diff: 'http://placehold.it/350x150',
65
+ current: 'http://placehold.it/450x190',
61
66
  },
62
67
  {
63
68
  description: '<First> some other \'test\'',
64
69
  viewport: 'medium',
65
- url: 'http://placehold.it/550x150',
70
+ previous: 'http://placehold.it/550x150',
71
+ diff: 'http://placehold.it/550x150',
72
+ current: 'http://placehold.it/450x110',
66
73
  },
67
74
  {
68
75
  description: '<First>',
69
76
  viewport: 'large',
70
- url: 'http://placehold.it/850x150',
77
+ previous: 'http://placehold.it/850x150',
78
+ diff: 'http://placehold.it/850x150',
79
+ current: 'http://placehold.it/850x150',
71
80
  },
72
81
  ]
73
82
 
@@ -75,22 +84,22 @@ module Happo
75
84
  {
76
85
  description: '<New>',
77
86
  viewport: 'small',
78
- url: 'http://placehold.it/350x150',
87
+ current: 'http://placehold.it/350x150',
79
88
  },
80
89
  {
81
90
  description: '<New>',
82
91
  viewport: 'medium',
83
- url: 'http://placehold.it/550x150',
92
+ current: 'http://placehold.it/550x150',
84
93
  },
85
94
  {
86
95
  description: '<New>',
87
96
  viewport: 'large',
88
- url: 'http://placehold.it/850x150',
97
+ current: 'http://placehold.it/850x150',
89
98
  },
90
99
  {
91
100
  description: '<SomethingElseNew>',
92
101
  viewport: 'small',
93
- url: 'http://placehold.it/350x150',
102
+ current: 'http://placehold.it/350x150',
94
103
  },
95
104
  ]
96
105
 
@@ -2,9 +2,7 @@ require 'oily_png'
2
2
  require 'diff-lcs'
3
3
  require_relative 'snapshot_comparison_image/base'
4
4
  require_relative 'snapshot_comparison_image/gutter'
5
- require_relative 'snapshot_comparison_image/before'
6
5
  require_relative 'snapshot_comparison_image/overlayed'
7
- require_relative 'snapshot_comparison_image/after'
8
6
 
9
7
  module Happo
10
8
  # This class is responsible for comparing two Snapshots and generating a diff.
@@ -38,7 +36,8 @@ module Happo
38
36
  number_of_different_rows = 0
39
37
 
40
38
  sprite, all_comparisons = initialize_comparison_images(
41
- [@png_after.width, @png_before.width].max, sdiff.size)
39
+ [@png_after.width, @png_before.width].max, sdiff.size
40
+ )
42
41
 
43
42
  sdiff.each_with_index do |row, y|
44
43
  # each row is a Diff::LCS::ContextChange instance
@@ -49,7 +48,7 @@ module Happo
49
48
  percent_changed = number_of_different_rows.to_f / sdiff.size * 100
50
49
  {
51
50
  diff_in_percent: percent_changed,
52
- diff_image: (sprite if percent_changed > 0),
51
+ diff_image: (sprite if percent_changed > 0.0),
53
52
  }
54
53
  end
55
54
 
@@ -69,22 +68,13 @@ module Happo
69
68
  # @return [Array<SnapshotComparisonImage>]
70
69
  def initialize_comparison_images(width, height)
71
70
  gutter_width = Happo::SnapshotComparisonImage::Gutter::WIDTH
72
- total_width = (width * 3) + (gutter_width * 3)
71
+ total_width = gutter_width + width
73
72
 
74
73
  sprite = ChunkyPNG::Image.new(total_width, height)
75
74
  offset, comparison_images = 0, []
76
75
  comparison_images << Happo::SnapshotComparisonImage::Gutter.new(offset, sprite)
77
76
  offset += gutter_width
78
- comparison_images << Happo::SnapshotComparisonImage::Before.new(offset, sprite)
79
- offset += width
80
- comparison_images << Happo::SnapshotComparisonImage::Gutter.new(offset, sprite)
81
- offset += gutter_width
82
77
  comparison_images << Happo::SnapshotComparisonImage::Overlayed.new(offset, sprite)
83
- offset += width
84
- comparison_images << Happo::SnapshotComparisonImage::Gutter.new(offset, sprite)
85
- offset += gutter_width
86
- comparison_images << Happo::SnapshotComparisonImage::After.new(offset, sprite)
87
-
88
78
  [sprite, comparison_images]
89
79
  end
90
80
  end
@@ -3,6 +3,8 @@ require 'securerandom'
3
3
  require 'uri'
4
4
 
5
5
  module Happo
6
+ # Handles uploading diffs (serialized html doc + images) to an Amazon S3
7
+ # account.
6
8
  class Uploader
7
9
  def initialize
8
10
  @s3_access_key_id = ENV['S3_ACCESS_KEY_ID']
@@ -17,40 +19,22 @@ module Happo
17
19
  return [] if result_summary[:diff_examples].empty? &&
18
20
  result_summary[:new_examples].empty?
19
21
 
20
- bucket = find_or_build_bucket
21
- dir = if @s3_bucket_path.nil? || @s3_bucket_path.empty?
22
- SecureRandom.uuid
23
- else
24
- File.join(@s3_bucket_path, SecureRandom.uuid)
25
- end
26
-
27
- diff_images = result_summary[:diff_examples].map do |diff|
28
- img_name = "#{diff[:description]}_#{diff[:viewport]}.png"
29
- image = bucket.objects.build("#{dir}/#{img_name}")
30
- image.content = open(Happo::Utils.path_to(diff[:description],
31
- diff[:viewport],
32
- 'diff.png'))
33
- image.content_type = 'image/png'
34
- image.save
35
- diff[:url] = URI.escape(img_name)
36
- diff
22
+ diff_images = result_summary[:diff_examples].map do |image|
23
+ image[:previous] = upload_image(image, 'previous')
24
+ image[:diff] = upload_image(image, 'diff')
25
+ image[:current] = upload_image(image, 'current')
26
+ image
37
27
  end
38
28
 
39
- new_images = result_summary[:new_examples].map do |example|
40
- img_name = "#{example[:description]}_#{example[:viewport]}.png"
41
- image = bucket.objects.build("#{dir}/#{img_name}")
42
- image.content = open(Happo::Utils.path_to(example[:description],
43
- example[:viewport],
44
- 'current.png'))
45
- image.content_type = 'image/png'
46
- image.save
47
- example[:url] = URI.escape(img_name)
48
- example
29
+ new_images = result_summary[:new_examples].map do |image|
30
+ image[:current] = upload_image(image, 'current')
31
+ image
49
32
  end
50
33
 
51
- html = bucket.objects.build("#{dir}/index.html")
34
+ html = find_or_build_bucket.objects.build("#{directory}/index.html")
52
35
  path = File.expand_path(
53
- File.join(File.dirname(__FILE__), 'views', 'diffs.erb'))
36
+ File.join(File.dirname(__FILE__), 'views', 'diffs.erb')
37
+ )
54
38
  html.content = ERB.new(File.read(path)).result(binding)
55
39
  html.content_encoding = 'utf-8'
56
40
  html.content_type = 'text/html'
@@ -61,11 +45,33 @@ module Happo
61
45
  private
62
46
 
63
47
  def find_or_build_bucket
64
- service = S3::Service.new(access_key_id: @s3_access_key_id,
65
- secret_access_key: @s3_secret_access_key)
66
- bucket = service.bucket(@s3_bucket_name)
67
- bucket.save(location: :us) unless bucket.exists?
68
- bucket
48
+ @bucket ||= begin
49
+ service = S3::Service.new(access_key_id: @s3_access_key_id,
50
+ secret_access_key: @s3_secret_access_key)
51
+ bucket = service.bucket(@s3_bucket_name)
52
+ bucket.save(location: :us) unless bucket.exists?
53
+ bucket
54
+ end
55
+ end
56
+
57
+ def directory
58
+ if @s3_bucket_path.nil? || @s3_bucket_path.empty?
59
+ SecureRandom.uuid
60
+ else
61
+ File.join(@s3_bucket_path, SecureRandom.uuid)
62
+ end
63
+ end
64
+
65
+ def upload_image(image, variant)
66
+ bucket = find_or_build_bucket
67
+ img_name = "#{image[:description]}_#{image[:viewport]}_#{variant}.png"
68
+ s3_image = bucket.objects.build("#{directory}/#{img_name}")
69
+ s3_image.content = open(Happo::Utils.path_to(image[:description],
70
+ image[:viewport],
71
+ "#{variant}.png"))
72
+ s3_image.content_type = 'image/png'
73
+ s3_image.save
74
+ URI.escape(img_name)
69
75
  end
70
76
  end
71
77
  end
data/lib/happo/version.rb CHANGED
@@ -1,4 +1,4 @@
1
1
  # Defines the gem version.
2
2
  module Happo
3
- VERSION = '2.4.0'
3
+ VERSION = '2.5.0'
4
4
  end
@@ -12,9 +12,9 @@
12
12
  <%= Happo::Utils.css_styles %>
13
13
  </style>
14
14
 
15
- <script src="https://npmcdn.com/react@15.3.1/dist/react.min.js"></script>
16
- <script src="https://npmcdn.com/react-dom@15.3.1/dist/react-dom.min.js"></script>
17
- <script src="https://npmcdn.com/babel-core@5.8.38/browser.min.js"></script>
15
+ <script src="https://unpkg.com/react@15.3.1/dist/react.min.js"></script>
16
+ <script src="https://unpkg.com/react-dom@15.3.1/dist/react-dom.min.js"></script>
17
+ <script src="https://unpkg.com/babel-core@5.8.38/browser.min.js"></script>
18
18
 
19
19
  <script type="text/babel">
20
20
  <%= Happo::Utils.jsx_code %>
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: happo
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.4.0
4
+ version: 2.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Henric Trotzig
@@ -9,22 +9,22 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2016-08-29 00:00:00.000000000 Z
12
+ date: 2016-09-01 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: chunky_png
16
16
  requirement: !ruby/object:Gem::Requirement
17
17
  requirements:
18
- - - "~>"
18
+ - - '='
19
19
  - !ruby/object:Gem::Version
20
- version: 1.3.1
20
+ version: 1.3.6
21
21
  type: :runtime
22
22
  prerelease: false
23
23
  version_requirements: !ruby/object:Gem::Requirement
24
24
  requirements:
25
- - - "~>"
25
+ - - '='
26
26
  - !ruby/object:Gem::Version
27
- version: 1.3.1
27
+ version: 1.3.6
28
28
  - !ruby/object:Gem::Dependency
29
29
  name: diff-lcs
30
30
  requirement: !ruby/object:Gem::Requirement
@@ -152,9 +152,7 @@ files:
152
152
  - lib/happo/runner.rb
153
153
  - lib/happo/server.rb
154
154
  - lib/happo/snapshot_comparer.rb
155
- - lib/happo/snapshot_comparison_image/after.rb
156
155
  - lib/happo/snapshot_comparison_image/base.rb
157
- - lib/happo/snapshot_comparison_image/before.rb
158
156
  - lib/happo/snapshot_comparison_image/gutter.rb
159
157
  - lib/happo/snapshot_comparison_image/overlayed.rb
160
158
  - lib/happo/uploader.rb
@@ -1,21 +0,0 @@
1
- module Happo
2
- module SnapshotComparisonImage
3
- # This subclass of `SnapshotComparisonImage` knows how to draw the
4
- # representation of the "after" image.
5
- class After < SnapshotComparisonImage::Base
6
- # @param y [Integer]
7
- # @param row [Diff::LCS:ContextChange]
8
- def render_changed_row(y, row)
9
- render_added_row(y, row)
10
- end
11
-
12
- # @param y [Integer]
13
- # @param row [Diff::LCS:ContextChange]
14
- def render_added_row(y, row)
15
- row.new_element.each_with_index do |pixel_after, x|
16
- render_pixel(x, y, pixel_after)
17
- end
18
- end
19
- end
20
- end
21
- end
@@ -1,21 +0,0 @@
1
- module Happo
2
- module SnapshotComparisonImage
3
- # This subclass of `SnapshotComparisonImage` knows how to draw the
4
- # representation of the "before" image.
5
- class Before < SnapshotComparisonImage::Base
6
- # @param y [Integer]
7
- # @param row [Diff::LCS:ContextChange]
8
- def render_changed_row(y, row)
9
- render_deleted_row(y, row)
10
- end
11
-
12
- # @param y [Integer]
13
- # @param row [Diff::LCS:ContextChange]
14
- def render_deleted_row(y, row)
15
- row.old_element.each_with_index do |pixel_before, x|
16
- render_pixel(x, y, pixel_before)
17
- end
18
- end
19
- end
20
- end
21
- end