happo 2.4.0 → 2.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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