happo 2.7.7 → 2.8.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/bin/happo +1 -1
- data/lib/happo/public/0144a92667ed2e8a8b13.worker.js +56 -0
- data/lib/happo/public/0622a1eb2a5b4dab0d7d.worker.js +62 -0
- data/lib/happo/public/088cc9d4615d51d37661.worker.js +68 -0
- data/lib/happo/public/11b439cedd6aa6a01e71.worker.js +68 -0
- data/lib/happo/public/15b1495833b5b13f5f51.worker.js +62 -0
- data/lib/happo/public/167f0a5b59a8c27815fe.worker.js +62 -0
- data/lib/happo/public/19f5f7403fb4a2ba42e2.worker.js +62 -0
- data/lib/happo/public/1a86029ce53b1aa81f68.worker.js +62 -0
- data/lib/happo/public/1b6803fbc7b07f61cba2.worker.js +74 -0
- data/lib/happo/public/1d3dc560f1f39393dafe.worker.js +62 -0
- data/lib/happo/public/203272bf00c349325c60.worker.js +62 -0
- data/lib/happo/public/220e711d4abdbc4bd32f.worker.js +62 -0
- data/lib/happo/public/25ad7a1e52bd424ca81d.worker.js +62 -0
- data/lib/happo/public/26d95a76207ba0fe6c25.worker.js +74 -0
- data/lib/happo/public/28237f0668f12dfb456c.worker.js +62 -0
- data/lib/happo/public/2a7a935f3b9526c6f7ab.worker.js +74 -0
- data/lib/happo/public/2c00cb8d0a2eb5dd4078.worker.js +68 -0
- data/lib/happo/public/2c71ddb5b15a755d4644.worker.js +68 -0
- data/lib/happo/public/32648d3dfb22995f4066.worker.js +68 -0
- data/lib/happo/public/3452650686a8096b2bb7.worker.js +68 -0
- data/lib/happo/public/346cefcc1572890455b8.worker.js +68 -0
- data/lib/happo/public/35bd63f58bb69ac267ea.worker.js +68 -0
- data/lib/happo/public/360e0acb872760f43b07.worker.js +68 -0
- data/lib/happo/public/3818f1c91ce4d4d8b724.worker.js +62 -0
- data/lib/happo/public/3cc2e649d9b6ff62f4ab.worker.js +62 -0
- data/lib/happo/public/3deccedc8fbb96bb1aa7.worker.js +68 -0
- data/lib/happo/public/3e672be26a7a8864a342.worker.js +68 -0
- data/lib/happo/public/41aa7471344a79ff98f2.worker.js +68 -0
- data/lib/happo/public/423ba9f549f3829873e6.worker.js +62 -0
- data/lib/happo/public/463eb0ecadbb11365ca4.worker.js +62 -0
- data/lib/happo/public/47c52ff0e060fedb841f.worker.js +62 -0
- data/lib/happo/public/4e685162de1d8a9b102d.worker.js +56 -0
- data/lib/happo/public/4eea1b4008ed68e41f82.worker.js +62 -0
- data/lib/happo/public/4f6b0faf1b43e93a1404.worker.js +62 -0
- data/lib/happo/public/54ba8bc7a595358209d2.worker.js +68 -0
- data/lib/happo/public/589c2f9547d240ac8a57.worker.js +68 -0
- data/lib/happo/public/5a8c0588ca745d7904f7.worker.js +68 -0
- data/lib/happo/public/5b3cabd81f7d8688f7a5.worker.js +62 -0
- data/lib/happo/public/5f5c9f07cb5117c523a2.worker.js +68 -0
- data/lib/happo/public/5fb8c7066659ea1b57e2.worker.js +62 -0
- data/lib/happo/public/5fb962cc191a60b42af0.worker.js +68 -0
- data/lib/happo/public/62c7585d1d23297b316f.worker.js +62 -0
- data/lib/happo/public/62e676d31cbf8aaa9359.worker.js +68 -0
- data/lib/happo/public/65efcf6aee5e3ef33539.worker.js +68 -0
- data/lib/happo/public/693d4918a5dae465c134.worker.js +68 -0
- data/lib/happo/public/69d106b071dd31ad86de.worker.js +68 -0
- data/lib/happo/public/730c035f5c2b404ff225.worker.js +62 -0
- data/lib/happo/public/7609088c49b73ee2e3e1.worker.js +68 -0
- data/lib/happo/public/76161d401db5bb36980a.worker.js +62 -0
- data/lib/happo/public/7d9febb46b37ddffa2d5.worker.js +62 -0
- data/lib/happo/public/7f29af56bd0ea82d77d9.worker.js +68 -0
- data/lib/happo/public/80009e8bc0ffd29d3390.worker.js +62 -0
- data/lib/happo/public/830db6cc9c99c2e8e9c1.worker.js +62 -0
- data/lib/happo/public/85ad69599657de02d20d.worker.js +68 -0
- data/lib/happo/public/875e790f3e64476b3aa0.worker.js +68 -0
- data/lib/happo/public/8fa9a7b2a5f19075eedc.worker.js +74 -0
- data/lib/happo/public/906ce908877052838b8e.worker.js +62 -0
- data/lib/happo/public/9472102d9a6d2577f988.worker.js +62 -0
- data/lib/happo/public/9ac309ab34f48e7af07c.worker.js +68 -0
- data/lib/happo/public/9cdd6f0763fd866a5118.worker.js +62 -0
- data/lib/happo/public/HappoApp.bundle.js +2 -0
- data/lib/happo/public/a8bfebcafc4935d15b00.worker.js +1 -0
- data/lib/happo/public/aa7ae8013f9780eb8acb.worker.js +68 -0
- data/lib/happo/public/b20a32590dfe259f4fe3.worker.js +62 -0
- data/lib/happo/public/b25ebdd4e2d2dee8c7b1.worker.js +68 -0
- data/lib/happo/public/b7a49d603b1d50f535b2.worker.js +68 -0
- data/lib/happo/public/b8e667564ad96f5c77f4.worker.js +68 -0
- data/lib/happo/public/b913c562805ec9a01635.worker.js +68 -0
- data/lib/happo/public/bab832c7110d15a5f43b.worker.js +62 -0
- data/lib/happo/public/bfcf99c9b2bf3d58ec7f.worker.js +62 -0
- data/lib/happo/public/c0aeff8b32c9911c8547.worker.js +68 -0
- data/lib/happo/public/c2297ce40f0d071ec173.worker.js +1 -0
- data/lib/happo/public/c3d7b17359c21ff82281.worker.js +68 -0
- data/lib/happo/public/c9f7adcce3de80eb759a.worker.js +68 -0
- data/lib/happo/public/card-current.png +0 -0
- data/lib/happo/public/card-previous.png +0 -0
- data/lib/happo/public/d7b87aec5f1e4f2a7a5d.worker.js +68 -0
- data/lib/happo/public/d8fefa1bd98baa4e38da.worker.js +62 -0
- data/lib/happo/public/db617ae6458966f4acae.worker.js +68 -0
- data/lib/happo/public/dialog-current.png +0 -0
- data/lib/happo/public/dialog-previous.png +0 -0
- data/lib/happo/public/e247eaa7506f44b5b847.worker.js +62 -0
- data/lib/happo/public/e92bb4e1e06843fa771e.worker.js +62 -0
- data/lib/happo/public/e9e2d97848e0059e15c5.worker.js +62 -0
- data/lib/happo/public/ea87c48552c960ed7a1c.worker.js +74 -0
- data/lib/happo/public/eb5aafba8d16a397517a.worker.js +68 -0
- data/lib/happo/public/edc218b655b9ef694b3e.worker.js +74 -0
- data/lib/happo/public/eea64a6efa0620a576d7.worker.js +68 -0
- data/lib/happo/public/f269a8e7ffca13e9366a.worker.js +68 -0
- data/lib/happo/public/f81ed0eec7edfa2dfadb.worker.js +62 -0
- data/lib/happo/public/fcd20ee3e5a10e8667bb.worker.js +68 -0
- data/lib/happo/public/full-page-large-current.png +0 -0
- data/lib/happo/public/full-page-large-previous.png +0 -0
- data/lib/happo/public/full-page-small-current.png +0 -0
- data/lib/happo/public/full-page-small-previous.png +0 -0
- data/lib/happo/public/happo-styles.css +28 -3
- data/lib/happo/public/major-diff-large-current.png +0 -0
- data/lib/happo/public/major-diff-large-previous.png +0 -0
- data/lib/happo/public/major-diff-small-current.png +0 -0
- data/lib/happo/public/major-diff-small-previous.png +0 -0
- data/lib/happo/public/modal-current.png +0 -0
- data/lib/happo/public/modal-previous.png +0 -0
- data/lib/happo/public/small-current.png +0 -0
- data/lib/happo/public/small-previous.png +0 -0
- data/lib/happo/public/wide-current.png +0 -0
- data/lib/happo/public/wide-previous.png +0 -0
- data/lib/happo/runner.rb +25 -31
- data/lib/happo/server.rb +81 -12
- data/lib/happo/uploader.rb +1 -2
- data/lib/happo/utils.rb +1 -1
- data/lib/happo/version.rb +1 -1
- data/lib/happo/views/diffs.erb +9 -14
- data/lib/happo.rb +0 -1
- metadata +107 -21
- data/lib/happo/public/HappoDiffs.jsx +0 -391
- data/lib/happo/snapshot_comparer.rb +0 -81
- data/lib/happo/snapshot_comparison_image/base.rb +0 -108
- data/lib/happo/snapshot_comparison_image/gutter.rb +0 -34
- data/lib/happo/snapshot_comparison_image/overlayed.rb +0 -88
|
@@ -1,391 +0,0 @@
|
|
|
1
|
-
/* global React */
|
|
2
|
-
/* eslint-disable react/no-multi-comp */
|
|
3
|
-
const PropTypes = React.PropTypes;
|
|
4
|
-
|
|
5
|
-
const VIEWS = {
|
|
6
|
-
SIDE_BY_SIDE: 'Side-by-side',
|
|
7
|
-
SWIPE: 'Swipe',
|
|
8
|
-
DIFF: 'Diff',
|
|
9
|
-
};
|
|
10
|
-
|
|
11
|
-
const imageShape = {
|
|
12
|
-
description: PropTypes.string.isRequired,
|
|
13
|
-
viewport: PropTypes.string.isRequired,
|
|
14
|
-
diff: PropTypes.string,
|
|
15
|
-
previous: PropTypes.string,
|
|
16
|
-
current: PropTypes.string.isRequired,
|
|
17
|
-
};
|
|
18
|
-
|
|
19
|
-
function imageSlug(image) {
|
|
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
|
-
);
|
|
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
|
-
};
|
|
49
|
-
|
|
50
|
-
function NewImage({ image }) {
|
|
51
|
-
return (
|
|
52
|
-
<div>
|
|
53
|
-
<ImageHeading
|
|
54
|
-
image={image}
|
|
55
|
-
/>
|
|
56
|
-
<img
|
|
57
|
-
role='presentation'
|
|
58
|
-
src={image.current}
|
|
59
|
-
/>
|
|
60
|
-
</div>
|
|
61
|
-
);
|
|
62
|
-
}
|
|
63
|
-
NewImage.propTypes = {
|
|
64
|
-
image: PropTypes.shape(imageShape).isRequired,
|
|
65
|
-
};
|
|
66
|
-
|
|
67
|
-
function DiffImages({ images }) {
|
|
68
|
-
if (!images.length) {
|
|
69
|
-
return null;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
return (
|
|
73
|
-
<div>
|
|
74
|
-
<h2 id='diffs'>
|
|
75
|
-
<InlineLink to='diffs'>
|
|
76
|
-
Diffs ({ images.length })
|
|
77
|
-
</InlineLink>
|
|
78
|
-
</h2>
|
|
79
|
-
|
|
80
|
-
{images.map((image) => (
|
|
81
|
-
<DiffController
|
|
82
|
-
key={image.current}
|
|
83
|
-
image={image}
|
|
84
|
-
/>
|
|
85
|
-
))}
|
|
86
|
-
</div>
|
|
87
|
-
);
|
|
88
|
-
}
|
|
89
|
-
DiffImages.propTypes = {
|
|
90
|
-
images: PropTypes.arrayOf(PropTypes.shape(imageShape)).isRequired,
|
|
91
|
-
};
|
|
92
|
-
|
|
93
|
-
function NewImages({ images }) {
|
|
94
|
-
if (!images.length) {
|
|
95
|
-
return null;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
return (
|
|
99
|
-
<div>
|
|
100
|
-
<h2 id='new'>
|
|
101
|
-
<InlineLink to='new'>
|
|
102
|
-
New examples ({ images.length })
|
|
103
|
-
</InlineLink>
|
|
104
|
-
</h2>
|
|
105
|
-
|
|
106
|
-
{images.map((image) => (
|
|
107
|
-
<NewImage
|
|
108
|
-
key={image.current}
|
|
109
|
-
image={image}
|
|
110
|
-
/>
|
|
111
|
-
))}
|
|
112
|
-
</div>
|
|
113
|
-
);
|
|
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
|
-
}
|
|
149
|
-
|
|
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
|
-
}
|
|
188
|
-
|
|
189
|
-
render() {
|
|
190
|
-
const { previous, current } = this.props;
|
|
191
|
-
const { cursorLeft, height, width } = this.state;
|
|
192
|
-
|
|
193
|
-
return (
|
|
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
|
-
/>
|
|
231
|
-
</div>
|
|
232
|
-
);
|
|
233
|
-
}
|
|
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
|
-
<img
|
|
244
|
-
className='SideBySide__image'
|
|
245
|
-
role='presentation'
|
|
246
|
-
src={previous}
|
|
247
|
-
title='Before'
|
|
248
|
-
/>
|
|
249
|
-
{' '}
|
|
250
|
-
<img
|
|
251
|
-
className='SideBySide__image'
|
|
252
|
-
role='presentation'
|
|
253
|
-
src={current}
|
|
254
|
-
title='After'
|
|
255
|
-
/>
|
|
256
|
-
</div>
|
|
257
|
-
);
|
|
258
|
-
}
|
|
259
|
-
SideBySide.propTypes = {
|
|
260
|
-
previous: PropTypes.string.isRequired,
|
|
261
|
-
current: PropTypes.string.isRequired,
|
|
262
|
-
};
|
|
263
|
-
|
|
264
|
-
function SelectedView({ image, selectedView }) {
|
|
265
|
-
if (selectedView === VIEWS.SIDE_BY_SIDE) {
|
|
266
|
-
return (
|
|
267
|
-
<SideBySide
|
|
268
|
-
previous={image.previous}
|
|
269
|
-
current={image.current}
|
|
270
|
-
/>
|
|
271
|
-
);
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
if (selectedView === VIEWS.DIFF) {
|
|
275
|
-
return (
|
|
276
|
-
<img role='presentation' src={image.diff} />
|
|
277
|
-
);
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
if (selectedView === VIEWS.SWIPE) {
|
|
281
|
-
return (
|
|
282
|
-
<Swiper
|
|
283
|
-
previous={image.previous}
|
|
284
|
-
current={image.current}
|
|
285
|
-
/>
|
|
286
|
-
);
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
SelectedView.propTypes = {
|
|
290
|
-
image: PropTypes.shape(imageShape).isRequired,
|
|
291
|
-
selectedView: PropTypes.oneOf(Object.keys(VIEWS).map(key => VIEWS[key])).isRequired,
|
|
292
|
-
};
|
|
293
|
-
|
|
294
|
-
class DiffController extends React.Component {
|
|
295
|
-
constructor(props) {
|
|
296
|
-
super(props);
|
|
297
|
-
this.state = {
|
|
298
|
-
selectedView: VIEWS.SIDE_BY_SIDE,
|
|
299
|
-
};
|
|
300
|
-
|
|
301
|
-
this.handleClick = this.handleClick.bind(this);
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
handleClick(view) {
|
|
305
|
-
this.setState({ selectedView: view });
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
render() {
|
|
309
|
-
return (
|
|
310
|
-
<Diff
|
|
311
|
-
image={this.props.image}
|
|
312
|
-
selectedView={this.state.selectedView}
|
|
313
|
-
onClick={this.handleClick}
|
|
314
|
-
/>
|
|
315
|
-
);
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
DiffController.propTypes = {
|
|
319
|
-
image: PropTypes.shape(imageShape).isRequired,
|
|
320
|
-
};
|
|
321
|
-
|
|
322
|
-
function Diff({ image, selectedView, onClick }) {
|
|
323
|
-
return (
|
|
324
|
-
<div>
|
|
325
|
-
<ImageHeading
|
|
326
|
-
image={image}
|
|
327
|
-
/>
|
|
328
|
-
<div className='Diff__buttons'>
|
|
329
|
-
{Object.keys(VIEWS).map(key => VIEWS[key]).map((view, i) => {
|
|
330
|
-
const classes = ['Diff__button'];
|
|
331
|
-
if (i === 0) {
|
|
332
|
-
classes.push('Diff__button--first');
|
|
333
|
-
} else if (i === Object.keys(VIEWS).length - 1) {
|
|
334
|
-
classes.push('Diff__button--last');
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
return (
|
|
338
|
-
<button
|
|
339
|
-
key={view}
|
|
340
|
-
className={classes.join(' ')}
|
|
341
|
-
aria-pressed={view === selectedView}
|
|
342
|
-
onClick={() => { onClick(view); }}
|
|
343
|
-
>
|
|
344
|
-
{view}
|
|
345
|
-
</button>
|
|
346
|
-
);
|
|
347
|
-
})}
|
|
348
|
-
</div>
|
|
349
|
-
<div className='Diff__images'>
|
|
350
|
-
<SelectedView image={image} selectedView={selectedView} />
|
|
351
|
-
</div>
|
|
352
|
-
</div>
|
|
353
|
-
);
|
|
354
|
-
}
|
|
355
|
-
Diff.propTypes = {
|
|
356
|
-
image: PropTypes.shape(imageShape).isRequired,
|
|
357
|
-
onClick: PropTypes.func.isRequired,
|
|
358
|
-
selectedView: PropTypes.oneOf(Object.keys(VIEWS).map(key => VIEWS[key])).isRequired,
|
|
359
|
-
};
|
|
360
|
-
|
|
361
|
-
function HappoDiffs({ pageTitle, generatedAt, diffImages, newImages }) {
|
|
362
|
-
return (
|
|
363
|
-
<div>
|
|
364
|
-
<header className='HappoDiffs__header'>
|
|
365
|
-
<h1 className='HappoDiffs__headerTitle'>
|
|
366
|
-
{pageTitle}
|
|
367
|
-
</h1>
|
|
368
|
-
<div className='HappoDiffs__headerTime'>
|
|
369
|
-
Generated: {generatedAt}
|
|
370
|
-
</div>
|
|
371
|
-
</header>
|
|
372
|
-
|
|
373
|
-
<main className='HappoDiffs__main'>
|
|
374
|
-
<DiffImages
|
|
375
|
-
images={diffImages}
|
|
376
|
-
/>
|
|
377
|
-
<NewImages
|
|
378
|
-
images={newImages}
|
|
379
|
-
/>
|
|
380
|
-
</main>
|
|
381
|
-
</div>
|
|
382
|
-
);
|
|
383
|
-
}
|
|
384
|
-
HappoDiffs.propTypes = {
|
|
385
|
-
pageTitle: PropTypes.string.isRequired,
|
|
386
|
-
diffImages: PropTypes.arrayOf(imageShape).isRequired,
|
|
387
|
-
newImages: PropTypes.arrayOf(imageShape).isRequired,
|
|
388
|
-
generatedAt: PropTypes.string.isRequired,
|
|
389
|
-
};
|
|
390
|
-
|
|
391
|
-
window.HappoDiffs = HappoDiffs;
|
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
require 'oily_png'
|
|
2
|
-
require 'diff-lcs'
|
|
3
|
-
require_relative 'snapshot_comparison_image/base'
|
|
4
|
-
require_relative 'snapshot_comparison_image/gutter'
|
|
5
|
-
require_relative 'snapshot_comparison_image/overlayed'
|
|
6
|
-
|
|
7
|
-
module Happo
|
|
8
|
-
# This class is responsible for comparing two Snapshots and generating a diff.
|
|
9
|
-
class SnapshotComparer
|
|
10
|
-
# @param png_before [ChunkyPNG::Image]
|
|
11
|
-
# @param png_after [ChunkyPNG::Image]
|
|
12
|
-
def initialize(png_before, png_after)
|
|
13
|
-
@png_after = png_after
|
|
14
|
-
@png_before = png_before
|
|
15
|
-
end
|
|
16
|
-
|
|
17
|
-
# @return [Hash]
|
|
18
|
-
def compare!
|
|
19
|
-
no_diff = {
|
|
20
|
-
diff_in_percent: 0,
|
|
21
|
-
diff_image: nil,
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
# If these images are totally identical, we don't need to do any more
|
|
25
|
-
# work.
|
|
26
|
-
return no_diff if @png_before == @png_after
|
|
27
|
-
|
|
28
|
-
array_before = to_array_of_arrays(@png_before)
|
|
29
|
-
array_after = to_array_of_arrays(@png_after)
|
|
30
|
-
|
|
31
|
-
# If the arrays of arrays of colors are identical, we don't need to do any
|
|
32
|
-
# more work. This might happen if some of the headers are different.
|
|
33
|
-
return no_diff if array_before == array_after
|
|
34
|
-
|
|
35
|
-
sdiff = Diff::LCS.sdiff(array_before, array_after)
|
|
36
|
-
number_of_different_rows = 0
|
|
37
|
-
|
|
38
|
-
sprite, all_comparisons = initialize_comparison_images(
|
|
39
|
-
[@png_after.width, @png_before.width].max, sdiff.size
|
|
40
|
-
)
|
|
41
|
-
|
|
42
|
-
sdiff.each_with_index do |row, y|
|
|
43
|
-
# each row is a Diff::LCS::ContextChange instance
|
|
44
|
-
all_comparisons.each { |image| image.render_row(y, row) }
|
|
45
|
-
number_of_different_rows += 1 unless row.unchanged?
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
percent_changed = number_of_different_rows.to_f / sdiff.size * 100
|
|
49
|
-
{
|
|
50
|
-
diff_in_percent: percent_changed,
|
|
51
|
-
diff_image: (sprite if percent_changed > 0.0),
|
|
52
|
-
}
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
private
|
|
56
|
-
|
|
57
|
-
# @param [ChunkyPNG::Image]
|
|
58
|
-
# @return [Array<Array<Integer>>]
|
|
59
|
-
def to_array_of_arrays(chunky_png)
|
|
60
|
-
array_of_arrays = []
|
|
61
|
-
chunky_png.height.times do |y|
|
|
62
|
-
array_of_arrays << chunky_png.row(y)
|
|
63
|
-
end
|
|
64
|
-
array_of_arrays
|
|
65
|
-
end
|
|
66
|
-
|
|
67
|
-
# @param canvas [ChunkyPNG::Image] The output image to draw pixels on
|
|
68
|
-
# @return [Array<SnapshotComparisonImage>]
|
|
69
|
-
def initialize_comparison_images(width, height)
|
|
70
|
-
gutter_width = Happo::SnapshotComparisonImage::Gutter::WIDTH
|
|
71
|
-
total_width = gutter_width + width
|
|
72
|
-
|
|
73
|
-
sprite = ChunkyPNG::Image.new(total_width, height)
|
|
74
|
-
offset, comparison_images = 0, []
|
|
75
|
-
comparison_images << Happo::SnapshotComparisonImage::Gutter.new(offset, sprite)
|
|
76
|
-
offset += gutter_width
|
|
77
|
-
comparison_images << Happo::SnapshotComparisonImage::Overlayed.new(offset, sprite)
|
|
78
|
-
[sprite, comparison_images]
|
|
79
|
-
end
|
|
80
|
-
end
|
|
81
|
-
end
|
|
@@ -1,108 +0,0 @@
|
|
|
1
|
-
require 'chunky_png'
|
|
2
|
-
|
|
3
|
-
module Happo
|
|
4
|
-
module SnapshotComparisonImage
|
|
5
|
-
# This model represents a "comparison image". Basically it's just a wrapper
|
|
6
|
-
# around a ChunkyPNG image with some nice methods to make life easier in the
|
|
7
|
-
# world of diffs.
|
|
8
|
-
#
|
|
9
|
-
# This model is never persisted.
|
|
10
|
-
class Base
|
|
11
|
-
include ChunkyPNG::Color
|
|
12
|
-
|
|
13
|
-
BASE_OPACITY = 0.1
|
|
14
|
-
BASE_ALPHA = (255 * BASE_OPACITY).round
|
|
15
|
-
BASE_DIFF_ALPHA = BASE_ALPHA * 2
|
|
16
|
-
|
|
17
|
-
MAGENTA = ChunkyPNG::Color.from_hex '#b33682'
|
|
18
|
-
RED = ChunkyPNG::Color.from_hex '#dc322f'
|
|
19
|
-
GREEN = ChunkyPNG::Color.from_hex '#859900'
|
|
20
|
-
|
|
21
|
-
# @param offset [Integer] the x-offset that this comparison image should
|
|
22
|
-
# use when rendering on the canvas image.
|
|
23
|
-
# @param canvas [ChunkyPNG::Image] The canvas image to render pixels on.
|
|
24
|
-
def initialize(offset, canvas)
|
|
25
|
-
@offset = offset
|
|
26
|
-
@canvas = canvas
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
# @param y [Integer]
|
|
30
|
-
# @param row [Diff::LCS:ContextChange]
|
|
31
|
-
def render_row(y, row)
|
|
32
|
-
if row.unchanged?
|
|
33
|
-
render_unchanged_row(y, row)
|
|
34
|
-
elsif row.deleting?
|
|
35
|
-
render_deleted_row(y, row)
|
|
36
|
-
elsif row.adding?
|
|
37
|
-
render_added_row(y, row)
|
|
38
|
-
else # changing?
|
|
39
|
-
render_changed_row(y, row)
|
|
40
|
-
end
|
|
41
|
-
end
|
|
42
|
-
|
|
43
|
-
# @param y [Integer]
|
|
44
|
-
# @param row [Diff::LCS:ContextChange]
|
|
45
|
-
def render_unchanged_row(y, row)
|
|
46
|
-
row.new_element.each_with_index do |pixel, x|
|
|
47
|
-
# Render the unchanged pixel as-is
|
|
48
|
-
render_pixel(x, y, pixel)
|
|
49
|
-
end
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
# @param y [Integer]
|
|
53
|
-
# @param row [Diff::LCS:ContextChange]
|
|
54
|
-
def render_changed_row(y, row)
|
|
55
|
-
# no default implementation
|
|
56
|
-
end
|
|
57
|
-
|
|
58
|
-
# @param y [Integer]
|
|
59
|
-
# @param row [Diff::LCS:ContextChange]
|
|
60
|
-
def render_added_row(y, row)
|
|
61
|
-
# no default implementation
|
|
62
|
-
end
|
|
63
|
-
|
|
64
|
-
# @param y [Integer]
|
|
65
|
-
# @param row [Diff::LCS:ContextChange]
|
|
66
|
-
def render_deleted_row(y, row)
|
|
67
|
-
# no default implementation
|
|
68
|
-
end
|
|
69
|
-
|
|
70
|
-
# Compute a score that represents the difference between 2 pixels
|
|
71
|
-
#
|
|
72
|
-
# This method simply takes the Euclidean distance between the RGBA channels
|
|
73
|
-
# of 2 colors over the maximum possible Euclidean distance. This gives us a
|
|
74
|
-
# percentage of how different the two colors are.
|
|
75
|
-
#
|
|
76
|
-
# Although it would be more perceptually accurate to calculate a proper
|
|
77
|
-
# Delta E in Lab colorspace, we probably don't need perceptual accuracy for
|
|
78
|
-
# this application, and it is nice to avoid the overhead of converting RGBA
|
|
79
|
-
# to Lab.
|
|
80
|
-
#
|
|
81
|
-
# @param pixel_after [Integer]
|
|
82
|
-
# @param pixel_before [Integer]
|
|
83
|
-
# @return [Float] number between 0 and 1 where 1 is completely different
|
|
84
|
-
# and 0 is no difference
|
|
85
|
-
def pixel_diff_score(pixel_after, pixel_before)
|
|
86
|
-
ChunkyPNG::Color::euclidean_distance_rgba(pixel_after, pixel_before) /
|
|
87
|
-
ChunkyPNG::Color::MAX_EUCLIDEAN_DISTANCE_RGBA
|
|
88
|
-
end
|
|
89
|
-
|
|
90
|
-
# @param diff_score [Float]
|
|
91
|
-
# @return [Integer] a number between 0 and 255 that represents the alpha
|
|
92
|
-
# channel of of the difference
|
|
93
|
-
def diff_alpha(diff_score)
|
|
94
|
-
(BASE_DIFF_ALPHA + ((255 - BASE_DIFF_ALPHA) * diff_score)).round
|
|
95
|
-
end
|
|
96
|
-
|
|
97
|
-
# Renders a pixel on the specified x and y position. Uses the offset that
|
|
98
|
-
# the comparison image has been configured with.
|
|
99
|
-
#
|
|
100
|
-
# @param x [Integer]
|
|
101
|
-
# @param y [Integer]
|
|
102
|
-
# @param pixel [Integer]
|
|
103
|
-
def render_pixel(x, y, pixel)
|
|
104
|
-
@canvas.set_pixel(x + @offset, y, pixel)
|
|
105
|
-
end
|
|
106
|
-
end
|
|
107
|
-
end
|
|
108
|
-
end
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
module Happo
|
|
2
|
-
module SnapshotComparisonImage
|
|
3
|
-
# This class renders a gutter-column with a color representing the type of
|
|
4
|
-
# change that has happened.
|
|
5
|
-
class Gutter < SnapshotComparisonImage::Base
|
|
6
|
-
WIDTH = 10
|
|
7
|
-
GRAY = ChunkyPNG::Color.from_hex '#cccccc'
|
|
8
|
-
|
|
9
|
-
def render_row(y, row)
|
|
10
|
-
WIDTH.times do |x|
|
|
11
|
-
render_pixel(x, y, gutter_color(row))
|
|
12
|
-
end
|
|
13
|
-
# render a two-pixel empty column
|
|
14
|
-
2.times do |x|
|
|
15
|
-
render_pixel(WIDTH - 1 - x, y, WHITE)
|
|
16
|
-
end
|
|
17
|
-
end
|
|
18
|
-
|
|
19
|
-
private
|
|
20
|
-
|
|
21
|
-
def gutter_color(row)
|
|
22
|
-
if row.unchanged?
|
|
23
|
-
WHITE
|
|
24
|
-
elsif row.deleting?
|
|
25
|
-
RED
|
|
26
|
-
elsif row.adding?
|
|
27
|
-
GREEN
|
|
28
|
-
else # changed?
|
|
29
|
-
GRAY
|
|
30
|
-
end
|
|
31
|
-
end
|
|
32
|
-
end
|
|
33
|
-
end
|
|
34
|
-
end
|
|
@@ -1,88 +0,0 @@
|
|
|
1
|
-
module Happo
|
|
2
|
-
module SnapshotComparisonImage
|
|
3
|
-
# This subclass of `SnapshotComparisonImage` knows how to overlay the
|
|
4
|
-
# after-image on top of the before-image, and render the difference in a
|
|
5
|
-
# scaled magenta color.
|
|
6
|
-
class Overlayed < SnapshotComparisonImage::Base
|
|
7
|
-
WHITE_OVERLAY = ChunkyPNG::Color.fade(WHITE, 1 - BASE_ALPHA)
|
|
8
|
-
|
|
9
|
-
# @param offset [Integer]
|
|
10
|
-
# @param canvas [ChunkyPNG::Image]
|
|
11
|
-
# @see SnapshotComparisonImage::Base
|
|
12
|
-
def initialize(offset, canvas)
|
|
13
|
-
@diff_pixels = {}
|
|
14
|
-
@faded_pixels = {}
|
|
15
|
-
super
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
# @param y [Integer]
|
|
19
|
-
# @param row [Diff::LCS:ContextChange]
|
|
20
|
-
def render_unchanged_row(y, row)
|
|
21
|
-
# Render translucent original pixels
|
|
22
|
-
row.new_element.each_with_index do |pixel, x|
|
|
23
|
-
render_faded_pixel(x, y, pixel)
|
|
24
|
-
end
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
# @param y [Integer]
|
|
28
|
-
# @param row [Diff::LCS:ContextChange]
|
|
29
|
-
def render_deleted_row(y, row)
|
|
30
|
-
row.old_element.each_with_index do |pixel_before, x|
|
|
31
|
-
render_faded_magenta_pixel(TRANSPARENT, pixel_before, x, y)
|
|
32
|
-
end
|
|
33
|
-
end
|
|
34
|
-
|
|
35
|
-
# @param y [Integer]
|
|
36
|
-
# @param row [Diff::LCS:ContextChange]
|
|
37
|
-
def render_added_row(y, row)
|
|
38
|
-
row.new_element.each_with_index do |pixel_after, x|
|
|
39
|
-
render_faded_magenta_pixel(pixel_after, TRANSPARENT, x, y)
|
|
40
|
-
end
|
|
41
|
-
end
|
|
42
|
-
|
|
43
|
-
# @param y [Integer]
|
|
44
|
-
# @param row [Diff::LCS:ContextChange]
|
|
45
|
-
def render_changed_row(y, row)
|
|
46
|
-
row.old_element.zip(row.new_element).each_with_index do |pixels, x|
|
|
47
|
-
pixel_before, pixel_after = pixels
|
|
48
|
-
render_faded_magenta_pixel(
|
|
49
|
-
pixel_after || TRANSPARENT,
|
|
50
|
-
pixel_before || TRANSPARENT,
|
|
51
|
-
x, y)
|
|
52
|
-
end
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
private
|
|
56
|
-
|
|
57
|
-
# @param pixel_after [Integer]
|
|
58
|
-
# @param pixel_before [Integer]
|
|
59
|
-
# @param x [Integer]
|
|
60
|
-
# @param y [Integer]
|
|
61
|
-
def render_faded_magenta_pixel(pixel_after, pixel_before, x, y)
|
|
62
|
-
score = pixel_diff_score(pixel_after, pixel_before)
|
|
63
|
-
if score > 0
|
|
64
|
-
render_diff_pixel(x, y, score)
|
|
65
|
-
else
|
|
66
|
-
render_faded_pixel(x, y, pixel_after)
|
|
67
|
-
end
|
|
68
|
-
end
|
|
69
|
-
|
|
70
|
-
# @param x [Integer]
|
|
71
|
-
# @param y [Integer]
|
|
72
|
-
# @param score [Float]
|
|
73
|
-
def render_diff_pixel(x, y, score)
|
|
74
|
-
@diff_pixels[score] ||= compose_quick(fade(MAGENTA, diff_alpha(score)),
|
|
75
|
-
WHITE)
|
|
76
|
-
render_pixel(x, y, @diff_pixels[score])
|
|
77
|
-
end
|
|
78
|
-
|
|
79
|
-
# @param x [Integer]
|
|
80
|
-
# @param y [Integer]
|
|
81
|
-
# @param pixel [Integer]
|
|
82
|
-
def render_faded_pixel(x, y, pixel)
|
|
83
|
-
@faded_pixels[pixel] ||= compose_quick(WHITE_OVERLAY, pixel)
|
|
84
|
-
render_pixel(x, y, @faded_pixels[pixel])
|
|
85
|
-
end
|
|
86
|
-
end
|
|
87
|
-
end
|
|
88
|
-
end
|