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 +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
|