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 +4 -4
- data/lib/happo/public/HappoDiffs.jsx +352 -46
- data/lib/happo/public/happo-runner.js +42 -31
- data/lib/happo/public/happo-styles.css +87 -8
- data/lib/happo/server.rb +29 -20
- data/lib/happo/snapshot_comparer.rb +4 -14
- data/lib/happo/uploader.rb +40 -34
- data/lib/happo/version.rb +1 -1
- data/lib/happo/views/diffs.erb +3 -3
- metadata +6 -8
- data/lib/happo/snapshot_comparison_image/after.rb +0 -21
- data/lib/happo/snapshot_comparison_image/before.rb +0 -21
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: dbd179b01168cbba70f43a4bcaf1ebf1d94490f3
|
4
|
+
data.tar.gz: 6ea1662f9bdfad75f768809d208e81afcd760efb
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
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
|
-
|
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 +
|
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
|
50
|
+
function NewImage({ image }) {
|
14
51
|
return (
|
15
52
|
<div>
|
16
|
-
<
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
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
|
29
|
-
if (!
|
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
|
-
<
|
37
|
-
Diffs ({
|
38
|
-
</
|
75
|
+
<InlineLink to='diffs'>
|
76
|
+
Diffs ({ images.length })
|
77
|
+
</InlineLink>
|
39
78
|
</h2>
|
40
79
|
|
41
|
-
{
|
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
|
47
|
-
if (!
|
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
|
-
<
|
55
|
-
New examples ({
|
56
|
-
</
|
101
|
+
<InlineLink to='new'>
|
102
|
+
New examples ({ images.length })
|
103
|
+
</InlineLink>
|
57
104
|
</h2>
|
58
105
|
|
59
|
-
{
|
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
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
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
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
</
|
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
|
-
|
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 =
|
213
|
-
box.height =
|
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
|
-
.
|
24
|
+
.InlineLink {
|
25
25
|
color: #000000;
|
26
26
|
text-decoration: none;
|
27
27
|
}
|
28
28
|
|
29
|
-
.
|
29
|
+
.InlineLink:hover {
|
30
30
|
text-decoration: underline;
|
31
31
|
}
|
32
32
|
|
33
|
-
.
|
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
|
-
.
|
44
|
+
.InlineLink:hover::after {
|
45
45
|
opacity: 1;
|
46
46
|
}
|
47
47
|
|
48
|
-
.
|
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
|
-
.
|
58
|
+
.HappoDiffs__header > :first-child {
|
59
59
|
margin-right: 10px;
|
60
60
|
}
|
61
61
|
|
62
|
-
.
|
62
|
+
.HappoDiffs__headerTitle {
|
63
63
|
margin: 0;
|
64
64
|
}
|
65
65
|
|
66
|
-
.
|
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 |
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
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 |
|
42
|
+
new_images = result_summary[:new_examples].map do |image|
|
41
43
|
file_path = Happo::Utils.path_to(
|
42
|
-
|
43
|
-
|
44
|
+
image[:description],
|
45
|
+
image[:viewport],
|
44
46
|
'current.png'
|
45
47
|
)
|
46
|
-
|
47
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
87
|
+
current: 'http://placehold.it/350x150',
|
79
88
|
},
|
80
89
|
{
|
81
90
|
description: '<New>',
|
82
91
|
viewport: 'medium',
|
83
|
-
|
92
|
+
current: 'http://placehold.it/550x150',
|
84
93
|
},
|
85
94
|
{
|
86
95
|
description: '<New>',
|
87
96
|
viewport: 'large',
|
88
|
-
|
97
|
+
current: 'http://placehold.it/850x150',
|
89
98
|
},
|
90
99
|
{
|
91
100
|
description: '<SomethingElseNew>',
|
92
101
|
viewport: 'small',
|
93
|
-
|
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 =
|
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
|
data/lib/happo/uploader.rb
CHANGED
@@ -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
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
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 |
|
40
|
-
|
41
|
-
image
|
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 =
|
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
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
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
data/lib/happo/views/diffs.erb
CHANGED
@@ -12,9 +12,9 @@
|
|
12
12
|
<%= Happo::Utils.css_styles %>
|
13
13
|
</style>
|
14
14
|
|
15
|
-
<script src="https://
|
16
|
-
<script src="https://
|
17
|
-
<script src="https://
|
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
|
+
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-
|
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.
|
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.
|
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
|