@pixsame/cypress-plugin-visual-regression-diff 0.0.0-stage → 1.0.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.
- package/LICENSE +21 -0
- package/README.md +562 -2
- package/bin/cli.js +4 -0
- package/dist/commands.d.ts +60 -0
- package/dist/constants.d.ts +28 -0
- package/dist/constants.js +43 -0
- package/dist/constants.js.map +1 -0
- package/dist/constants.mjs +34 -0
- package/dist/constants.mjs.map +1 -0
- package/dist/migrate.d.ts +22 -0
- package/dist/migrate.js +554 -0
- package/dist/migrate.js.map +1 -0
- package/dist/plugins.d.ts +1 -0
- package/dist/plugins.js +575 -0
- package/dist/plugins.js.map +1 -0
- package/dist/plugins.mjs +567 -0
- package/dist/plugins.mjs.map +1 -0
- package/dist/support.d.ts +12 -0
- package/dist/support.js +647 -0
- package/dist/support.js.map +1 -0
- package/dist/support.mjs +633 -0
- package/dist/support.mjs.map +1 -0
- package/dist/types.d.ts +20 -0
- package/package.json +123 -3
package/dist/support.mjs
ADDED
|
@@ -0,0 +1,633 @@
|
|
|
1
|
+
import * as Base64 from '@frsource/base64';
|
|
2
|
+
|
|
3
|
+
function _extends() {
|
|
4
|
+
return _extends = Object.assign ? Object.assign.bind() : function (n) {
|
|
5
|
+
for (var e = 1; e < arguments.length; e++) {
|
|
6
|
+
var t = arguments[e];
|
|
7
|
+
for (var r in t) ({}).hasOwnProperty.call(t, r) && (n[r] = t[r]);
|
|
8
|
+
}
|
|
9
|
+
return n;
|
|
10
|
+
}, _extends.apply(null, arguments);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const PLUGIN_NAME = 'cp-visual-regression-diff';
|
|
14
|
+
const LINK_PREFIX = `#${PLUGIN_NAME}-`;
|
|
15
|
+
const OVERLAY_CLASS = `${PLUGIN_NAME}-overlay`;
|
|
16
|
+
const FAB_CLASS = `${PLUGIN_NAME}-fab`;
|
|
17
|
+
const FAB_BADGE_CLASS = `${PLUGIN_NAME}-fab-badge`;
|
|
18
|
+
var FILE_SUFFIX;
|
|
19
|
+
(function (FILE_SUFFIX) {
|
|
20
|
+
FILE_SUFFIX["diff"] = ".diff";
|
|
21
|
+
FILE_SUFFIX["actual"] = ".actual";
|
|
22
|
+
})(FILE_SUFFIX || (FILE_SUFFIX = {}));
|
|
23
|
+
const TASK = {
|
|
24
|
+
getScreenshotPathInfo: `${PLUGIN_NAME}-getScreenshotPathInfo`,
|
|
25
|
+
compareImages: `${PLUGIN_NAME}-compareImages`,
|
|
26
|
+
approveImage: `${PLUGIN_NAME}-approveImage`,
|
|
27
|
+
cleanupImages: `${PLUGIN_NAME}-cleanupImages`,
|
|
28
|
+
doesFileExist: `${PLUGIN_NAME}-doesFileExist`,
|
|
29
|
+
processImgPath: `${PLUGIN_NAME}-processImgPath`,
|
|
30
|
+
recordPendingDiff: `${PLUGIN_NAME}-recordPendingDiff`,
|
|
31
|
+
getPendingDiffs: `${PLUGIN_NAME}-getPendingDiffs`,
|
|
32
|
+
clearPendingDiffs: `${PLUGIN_NAME}-clearPendingDiffs`
|
|
33
|
+
/* c8 ignore next */
|
|
34
|
+
};
|
|
35
|
+
const LS_SHOW_NON_FAILING_DIFFS = 'cp-vrd-show-non-failing-diffs';
|
|
36
|
+
|
|
37
|
+
const supportsExpose = version => {
|
|
38
|
+
const [major, minor] = version.split('.').map(Number);
|
|
39
|
+
return major > 15 || major === 15 && minor >= 10;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const constructCypressError = (log, err) => {
|
|
43
|
+
// only way to throw & log the message properly in Cypress
|
|
44
|
+
// https://github.com/cypress-io/cypress/blob/5f94cad3cb4126e0567290b957050c33e3a78e3c/packages/driver/src/cypress/error_utils.ts#L214-L216
|
|
45
|
+
err.onFail = err => log.error(err);
|
|
46
|
+
return err;
|
|
47
|
+
};
|
|
48
|
+
const capitalize = text => text.charAt(0).toUpperCase() + text.slice(1);
|
|
49
|
+
const getPluginEnv = key => {
|
|
50
|
+
const envKey = `pluginVisualRegression${capitalize(key)}`;
|
|
51
|
+
if (supportsExpose(Cypress.version)) {
|
|
52
|
+
return Cypress.expose(envKey);
|
|
53
|
+
}
|
|
54
|
+
return Cypress.env(envKey);
|
|
55
|
+
};
|
|
56
|
+
const booleanOption = (options, key, truthyValue, falsyValue) => options[key] === false || getPluginEnv(key) === false ? truthyValue : falsyValue;
|
|
57
|
+
const optionWithDefaults = (options, key, defaultValue) => {
|
|
58
|
+
var _ref, _options$key;
|
|
59
|
+
return (_ref = (_options$key = options[key]) != null ? _options$key : getPluginEnv(key)) != null ? _ref : defaultValue;
|
|
60
|
+
};
|
|
61
|
+
const getImagesPath = options => optionWithDefaults(options, 'imagesPath', '{spec_path}/__image_snapshots__');
|
|
62
|
+
const getConfig = options => ({
|
|
63
|
+
scaleFactor: booleanOption(options, 'forceDeviceScaleFactor', 1, 1 / window.devicePixelRatio),
|
|
64
|
+
createMissingImages: optionWithDefaults(options, 'createMissingImages', true),
|
|
65
|
+
updateImages: optionWithDefaults(options, 'updateImages', false),
|
|
66
|
+
imagesPath: getImagesPath(options),
|
|
67
|
+
maxDiffThreshold: optionWithDefaults(options, 'maxDiffThreshold', 0.01),
|
|
68
|
+
diffConfig: optionWithDefaults(options, 'diffConfig', {}),
|
|
69
|
+
screenshotConfig: optionWithDefaults(options, 'screenshotConfig', {}),
|
|
70
|
+
matchAgainstPath: options.matchAgainstPath || undefined
|
|
71
|
+
});
|
|
72
|
+
Cypress.Commands.add('matchImage', {
|
|
73
|
+
prevSubject: 'optional'
|
|
74
|
+
}, (subject, options = {}) => {
|
|
75
|
+
var _test$id;
|
|
76
|
+
const $el = subject;
|
|
77
|
+
let title;
|
|
78
|
+
/* c8 ignore next */
|
|
79
|
+
let pendingPassingRecord = null;
|
|
80
|
+
const {
|
|
81
|
+
scaleFactor,
|
|
82
|
+
createMissingImages,
|
|
83
|
+
updateImages,
|
|
84
|
+
imagesPath,
|
|
85
|
+
maxDiffThreshold,
|
|
86
|
+
diffConfig,
|
|
87
|
+
screenshotConfig,
|
|
88
|
+
matchAgainstPath
|
|
89
|
+
} = getConfig(options);
|
|
90
|
+
const test = cy.state('test');
|
|
91
|
+
const currentRetryNumber = test.currentRetry();
|
|
92
|
+
// lets the plugin tell a retry of this test apart from the next test
|
|
93
|
+
const testId = (_test$id = test.id) != null ? _test$id : [Cypress.spec.relative, ...Cypress.currentTest.titlePath].join(' ');
|
|
94
|
+
return cy.then(() => cy.task(TASK.getScreenshotPathInfo, {
|
|
95
|
+
titleFromOptions: options.title || Cypress.currentTest.titlePath.join(' '),
|
|
96
|
+
imagesPath,
|
|
97
|
+
specPath: Cypress.spec.relative,
|
|
98
|
+
currentRetryNumber,
|
|
99
|
+
testId
|
|
100
|
+
}, {
|
|
101
|
+
log: false
|
|
102
|
+
})).then(({
|
|
103
|
+
screenshotPath,
|
|
104
|
+
title: titleFromTask
|
|
105
|
+
}) => {
|
|
106
|
+
title = titleFromTask;
|
|
107
|
+
let imgPath;
|
|
108
|
+
return ($el ? cy.wrap($el) : cy).screenshot(screenshotPath, _extends({}, screenshotConfig, {
|
|
109
|
+
onAfterScreenshot(el, props) {
|
|
110
|
+
imgPath = props.path;
|
|
111
|
+
screenshotConfig.onAfterScreenshot == null || screenshotConfig.onAfterScreenshot(el, props);
|
|
112
|
+
},
|
|
113
|
+
log: false
|
|
114
|
+
})).then(() => cy.task(TASK.processImgPath, {
|
|
115
|
+
path: imgPath
|
|
116
|
+
}, {
|
|
117
|
+
log: false
|
|
118
|
+
}).then(newImgPath => {
|
|
119
|
+
imgPath = newImgPath;
|
|
120
|
+
return imgPath;
|
|
121
|
+
}));
|
|
122
|
+
}).then(imgPath => cy.task(TASK.compareImages, {
|
|
123
|
+
scaleFactor,
|
|
124
|
+
imgNew: imgPath,
|
|
125
|
+
imgOld: matchAgainstPath || imgPath.replace(FILE_SUFFIX.actual, ''),
|
|
126
|
+
createMissingImages,
|
|
127
|
+
updateImages,
|
|
128
|
+
maxDiffThreshold,
|
|
129
|
+
diffConfig
|
|
130
|
+
}, {
|
|
131
|
+
log: false
|
|
132
|
+
}).then(res => ({
|
|
133
|
+
res,
|
|
134
|
+
imgPath
|
|
135
|
+
}))).then(({
|
|
136
|
+
res,
|
|
137
|
+
imgPath
|
|
138
|
+
}) => {
|
|
139
|
+
const log = Cypress.log({
|
|
140
|
+
name: 'log',
|
|
141
|
+
displayName: 'Match image',
|
|
142
|
+
$el
|
|
143
|
+
});
|
|
144
|
+
if (!res) {
|
|
145
|
+
log.set('message', 'Unexpected error!');
|
|
146
|
+
throw constructCypressError(log, new Error('Unexpected error!'));
|
|
147
|
+
}
|
|
148
|
+
const imgOldPath = matchAgainstPath || imgPath.replace(FILE_SUFFIX.actual, '');
|
|
149
|
+
log.set('message', `[${title}] ${res.message}${res.imgDiffBase64 && res.imgNewBase64 && res.imgOldBase64 ? `\n[See comparison](${LINK_PREFIX}${Base64.encode(encodeURIComponent(JSON.stringify({
|
|
150
|
+
title,
|
|
151
|
+
imgPath,
|
|
152
|
+
imgOldPath,
|
|
153
|
+
imgDiffBase64: res.imgDiffBase64,
|
|
154
|
+
imgNewBase64: res.imgNewBase64,
|
|
155
|
+
imgOldBase64: res.imgOldBase64,
|
|
156
|
+
error: res.error
|
|
157
|
+
})))})` : ''}`);
|
|
158
|
+
const matchImageReturn = {
|
|
159
|
+
diffValue: res.imgDiff,
|
|
160
|
+
imgNewPath: imgPath,
|
|
161
|
+
imgPath: imgPath.replace(FILE_SUFFIX.actual, ''),
|
|
162
|
+
imgDiffPath: imgPath.replace(FILE_SUFFIX.actual, FILE_SUFFIX.diff),
|
|
163
|
+
imgNewBase64: res.imgNewBase64,
|
|
164
|
+
imgBase64: res.imgOldBase64,
|
|
165
|
+
imgDiffBase64: res.imgDiffBase64,
|
|
166
|
+
imgNew: typeof res.imgNewBase64 === 'string' ? Cypress.Buffer.from(res.imgNewBase64, 'base64') : undefined,
|
|
167
|
+
img: typeof res.imgOldBase64 === 'string' ? Cypress.Buffer.from(res.imgOldBase64, 'base64') : undefined,
|
|
168
|
+
imgDiff: typeof res.imgDiffBase64 === 'string' ? Cypress.Buffer.from(res.imgDiffBase64, 'base64') : undefined
|
|
169
|
+
};
|
|
170
|
+
if (res.error) {
|
|
171
|
+
var _res$imgNewBase, _res$imgOldBase, _res$imgDiffBase, _res$message;
|
|
172
|
+
log.set('consoleProps', () => res);
|
|
173
|
+
const deferred = supportsExpose(Cypress.version) ? !!Cypress.expose('pluginVisualRegressionBatchReviewMode') : !!Cypress.env('pluginVisualRegressionBatchReviewMode');
|
|
174
|
+
const record = {
|
|
175
|
+
title,
|
|
176
|
+
imgPath,
|
|
177
|
+
imgOldPath,
|
|
178
|
+
imgNewBase64: (_res$imgNewBase = res.imgNewBase64) != null ? _res$imgNewBase : '',
|
|
179
|
+
imgOldBase64: (_res$imgOldBase = res.imgOldBase64) != null ? _res$imgOldBase : '',
|
|
180
|
+
imgDiffBase64: (_res$imgDiffBase = res.imgDiffBase64) != null ? _res$imgDiffBase : '',
|
|
181
|
+
message: (_res$message = res.message) != null ? _res$message : '',
|
|
182
|
+
deferred
|
|
183
|
+
};
|
|
184
|
+
return cy.task(TASK.recordPendingDiff, record, {
|
|
185
|
+
log: false
|
|
186
|
+
}).then(count => {
|
|
187
|
+
/* c8 ignore start */
|
|
188
|
+
if (top) {
|
|
189
|
+
const badge = top.document.querySelector(`.${FAB_BADGE_CLASS}`);
|
|
190
|
+
if (badge) {
|
|
191
|
+
badge.textContent = String(count);
|
|
192
|
+
badge.style.display = 'flex';
|
|
193
|
+
badge.classList.add(`${FAB_BADGE_CLASS}--pulse`);
|
|
194
|
+
setTimeout(() => badge.classList.remove(`${FAB_BADGE_CLASS}--pulse`), 700);
|
|
195
|
+
}
|
|
196
|
+
if (deferred) {
|
|
197
|
+
top.__cpvrdDeferredCount = (top.__cpvrdDeferredCount || 0) + 1;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
/* c8 ignore stop */
|
|
201
|
+
if (!deferred) {
|
|
202
|
+
throw constructCypressError(log, new Error(res.message));
|
|
203
|
+
}
|
|
204
|
+
return matchImageReturn;
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
/* c8 ignore start */
|
|
208
|
+
if (res.imgDiffBase64 && res.imgNewBase64 && res.imgOldBase64) {
|
|
209
|
+
var _res$message2;
|
|
210
|
+
pendingPassingRecord = {
|
|
211
|
+
title,
|
|
212
|
+
imgPath,
|
|
213
|
+
imgOldPath,
|
|
214
|
+
imgNewBase64: res.imgNewBase64,
|
|
215
|
+
imgOldBase64: res.imgOldBase64,
|
|
216
|
+
imgDiffBase64: res.imgDiffBase64,
|
|
217
|
+
message: (_res$message2 = res.message) != null ? _res$message2 : '',
|
|
218
|
+
passed: true
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
/* c8 ignore stop */
|
|
222
|
+
return matchImageReturn;
|
|
223
|
+
}).then(result => {
|
|
224
|
+
/* c8 ignore start */
|
|
225
|
+
if (!pendingPassingRecord) return cy.wrap(result, {
|
|
226
|
+
log: false
|
|
227
|
+
});
|
|
228
|
+
const record = pendingPassingRecord;
|
|
229
|
+
pendingPassingRecord = null;
|
|
230
|
+
return cy.task(TASK.recordPendingDiff, record, {
|
|
231
|
+
log: false
|
|
232
|
+
}).then(() => result);
|
|
233
|
+
/* c8 ignore stop */
|
|
234
|
+
});
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
const CAROUSEL_CLASS = 'cp-visual-regression-diff-carousel';
|
|
238
|
+
const LOGO_SVG = `<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" viewBox="0 0 72 72"><circle cx="35.5" cy="36.5" r="27.5" fill="#4A4A4D" stroke="#FFF"/><g fill="#FFF" transform="matrix(.075 0 0 .075 16 17)"><circle cx="259.8" cy="259.9" r="80"/><path d="M511.7 237.7C450.2 163.4 357 92 259.8 92 162.5 91.9 69.4 163.4 8 237.7A34.8 34.8 0 0 0 8 282a520.8 520.8 0 0 0 91 86.2c109 79.3 212.4 79.5 321.6 0a520.8 520.8 0 0 0 91-86.2 34.8 34.8 0 0 0 0-44.3zM259.8 148a112.1 112.1 0 0 1 0 224 112.1 112.1 0 0 1 0-224z"/></g></svg>`;
|
|
239
|
+
const CHECK_SVG = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>`;
|
|
240
|
+
/* c8 ignore start */
|
|
241
|
+
function queueClear() {
|
|
242
|
+
cy.queue.reset == null || cy.queue.reset();
|
|
243
|
+
cy.queue.clear();
|
|
244
|
+
cy.state('index', 0);
|
|
245
|
+
}
|
|
246
|
+
function queueRun() {
|
|
247
|
+
// needed to run a task outside of the test processing flow
|
|
248
|
+
cy.queue.run();
|
|
249
|
+
}
|
|
250
|
+
/* c8 ignore stop */
|
|
251
|
+
const getShowPassingImagesConfig = () => {
|
|
252
|
+
const key = 'pluginVisualRegressionBatchReviewModeShowPassingImages';
|
|
253
|
+
if (supportsExpose(Cypress.version)) {
|
|
254
|
+
return !!Cypress.expose(key);
|
|
255
|
+
}
|
|
256
|
+
return !!Cypress.env(key);
|
|
257
|
+
};
|
|
258
|
+
const getBatchReviewMode = () => {
|
|
259
|
+
const key = 'pluginVisualRegressionBatchReviewMode';
|
|
260
|
+
if (supportsExpose(Cypress.version)) {
|
|
261
|
+
return !!Cypress.expose(key);
|
|
262
|
+
}
|
|
263
|
+
return !!Cypress.env(key);
|
|
264
|
+
};
|
|
265
|
+
const getEffectiveShowPassingImages = () => {
|
|
266
|
+
if (!top) return getShowPassingImagesConfig();
|
|
267
|
+
const stored = top.localStorage.getItem(LS_SHOW_NON_FAILING_DIFFS);
|
|
268
|
+
if (stored !== null) return stored === 'true';
|
|
269
|
+
return getShowPassingImagesConfig();
|
|
270
|
+
};
|
|
271
|
+
const generateOverlayTemplate = ({
|
|
272
|
+
title,
|
|
273
|
+
imgNewBase64,
|
|
274
|
+
imgOldBase64,
|
|
275
|
+
imgDiffBase64,
|
|
276
|
+
wasImageNotUpdatedYet,
|
|
277
|
+
error
|
|
278
|
+
}) => `<div class="${OVERLAY_CLASS}" style="position:fixed;z-index:100000;top:0;bottom:0;left:0;right:0;display:flex;flex-flow:column;background:#1a202c;font-family:sans-serif">
|
|
279
|
+
<header style="background:#2d3748;padding:12px 16px;display:flex;align-items:center;justify-content:space-between;gap:12px;flex-shrink:0">
|
|
280
|
+
<div style="display:flex;align-items:center;gap:12px">
|
|
281
|
+
<div style="width:28px;height:28px;flex-shrink:0">${LOGO_SVG}</div>
|
|
282
|
+
<span style="font-weight:600;color:#fff;font-size:.95em">${title} – screenshot diff</span>
|
|
283
|
+
</div>
|
|
284
|
+
<button type="button" data-type="close" style="background:transparent;border:1px solid #4a5568;color:#a0aec0;padding:6px 12px;border-radius:4px;cursor:pointer;font-size:.85em">✕ Close</button>
|
|
285
|
+
</header>
|
|
286
|
+
<div data-diff-base64="${imgDiffBase64}" style="flex:1;overflow:auto;padding:20px;color:#fff">
|
|
287
|
+
${generateDiffPanelHTML({
|
|
288
|
+
imgNewBase64,
|
|
289
|
+
imgOldBase64
|
|
290
|
+
})}
|
|
291
|
+
</div>
|
|
292
|
+
<div style="background:#1a202c;padding:0 16px 10px;display:flex;justify-content:flex-end;flex-shrink:0">
|
|
293
|
+
<button data-type="see-diff" style="background:#4a5568;border:none;color:#fff;padding:6px 14px;border-radius:4px;cursor:pointer;font-size:.85em">See diff image →</button>
|
|
294
|
+
</div>
|
|
295
|
+
<footer style="background:#2d3748;padding:12px 16px;display:flex;align-items:center;justify-content:space-between;gap:8px;flex-shrink:0">
|
|
296
|
+
<div></div>
|
|
297
|
+
<div style="display:flex;gap:8px">
|
|
298
|
+
${wasImageNotUpdatedYet ? `<button type="button" data-type="replace" style="background:#38a169;border:none;color:#fff;padding:8px 16px;border-radius:4px;cursor:pointer;font-weight:600">✓ Replace image</button>` : error ? `<span style="color:#fc8181;font-size:.85em">Image was already updated, rerun test to see new comparison</span>` : ''}
|
|
299
|
+
</div>
|
|
300
|
+
<div></div>
|
|
301
|
+
</footer>
|
|
302
|
+
</div>`;
|
|
303
|
+
/* c8 ignore start */
|
|
304
|
+
const generateDiffPanelHTML = ({
|
|
305
|
+
imgNewBase64,
|
|
306
|
+
imgOldBase64
|
|
307
|
+
}) => `<div style="display:flex;justify-content:center;align-items:flex-start;gap:15px;flex-wrap:wrap">
|
|
308
|
+
<div
|
|
309
|
+
style="position:relative;background:#fff;border:solid 15px #fff;overflow:hidden;color:#1a202c"
|
|
310
|
+
onmouseover="this.querySelector('div').style.opacity=0,this.querySelector('img').style.opacity=1"
|
|
311
|
+
onmouseleave="this.querySelector('div').style.opacity=1,this.querySelector('img').style.opacity=0"
|
|
312
|
+
>
|
|
313
|
+
<h3 style="margin:0 0 8px">New screenshot (hover off to see old one):</h3>
|
|
314
|
+
<img style="min-width:200px;max-width:100%;opacity:0" src="data:image/png;base64,${imgNewBase64}" />
|
|
315
|
+
<div style="position:absolute;top:0;left:0;width:100%;background:#fff">
|
|
316
|
+
<h3 style="margin:0 0 8px">Old screenshot (hover to see new one):</h3>
|
|
317
|
+
<img style="min-width:200px;max-width:100%" src="data:image/png;base64,${imgOldBase64}" />
|
|
318
|
+
</div>
|
|
319
|
+
</div>
|
|
320
|
+
</div>`;
|
|
321
|
+
const generateFABTemplate = () => `<button class="${FAB_CLASS}" title="Cypress Plugin Visual Regression Diff" style="position:fixed;bottom:20px;right:20px;z-index:99999;width:52px;height:52px;border-radius:50%;border:2px solid rgba(255,255,255,.2);background:#4A4A4D;cursor:pointer;padding:0;box-shadow:0 2px 10px rgba(0,0,0,.5);display:flex;align-items:center;justify-content:center;overflow:visible;transition:transform .15s">
|
|
322
|
+
<span class="${FAB_BADGE_CLASS}" style="position:absolute;top:-6px;right:-6px;background:#e53e3e;color:#fff;font-size:11px;font-weight:700;min-width:18px;height:18px;border-radius:9px;display:none;align-items:center;justify-content:center;padding:0 4px;box-shadow:0 1px 3px rgba(0,0,0,.4);pointer-events:none"></span>
|
|
323
|
+
<span class="${FAB_CLASS}-logo" style="display:flex;align-items:center;justify-content:center;width:36px;height:36px">${LOGO_SVG}</span>
|
|
324
|
+
<span class="${FAB_CLASS}-check" style="display:none;align-items:center;justify-content:center;width:36px;height:36px">${CHECK_SVG}</span>
|
|
325
|
+
</button>
|
|
326
|
+
<style>
|
|
327
|
+
@keyframes ${FAB_BADGE_CLASS}-pulse {
|
|
328
|
+
0%,100%{transform:scale(1)}
|
|
329
|
+
50%{transform:scale(1.35)}
|
|
330
|
+
}
|
|
331
|
+
.${FAB_BADGE_CLASS}--pulse { animation: ${FAB_BADGE_CLASS}-pulse .5s ease; }
|
|
332
|
+
@keyframes ${FAB_CLASS}-success {
|
|
333
|
+
0%{opacity:0;transform:scale(.5)}
|
|
334
|
+
40%{opacity:1;transform:scale(1.1)}
|
|
335
|
+
100%{opacity:1;transform:scale(1)}
|
|
336
|
+
}
|
|
337
|
+
.${FAB_CLASS}[data-success] .${FAB_CLASS}-logo { display:none!important; }
|
|
338
|
+
.${FAB_CLASS}[data-success] .${FAB_CLASS}-check { display:flex!important; animation:${FAB_CLASS}-success .3s ease forwards; }
|
|
339
|
+
.${FAB_CLASS}:hover { transform:scale(1.08); }
|
|
340
|
+
.${FAB_CLASS}[data-running] { opacity:.45;pointer-events:none; }
|
|
341
|
+
@keyframes ${FAB_CLASS}-attention {
|
|
342
|
+
0%,100%{transform:scale(1)}
|
|
343
|
+
20%{transform:scale(1.18)}
|
|
344
|
+
40%{transform:scale(1)}
|
|
345
|
+
60%{transform:scale(1.12)}
|
|
346
|
+
80%{transform:scale(1)}
|
|
347
|
+
}
|
|
348
|
+
.${FAB_CLASS}--attention { animation:${FAB_CLASS}-attention 1s ease 3; }
|
|
349
|
+
</style>`;
|
|
350
|
+
const generateCarouselTemplate = () => `<div class="${CAROUSEL_CLASS}" style="position:fixed;z-index:100000;top:0;bottom:0;left:0;right:0;display:flex;flex-flow:column;background:#1a202c;font-family:sans-serif">
|
|
351
|
+
<header style="background:#2d3748;padding:12px 16px;display:flex;align-items:center;justify-content:space-between;gap:12px;flex-shrink:0">
|
|
352
|
+
<div style="display:flex;align-items:center;gap:12px">
|
|
353
|
+
<div style="width:28px;height:28px;flex-shrink:0">${LOGO_SVG}</div>
|
|
354
|
+
<div>
|
|
355
|
+
<span data-carousel-counter style="font-size:.8em;color:#a0aec0;display:block;visibility:hidden">Image</span>
|
|
356
|
+
<span data-carousel-title style="font-weight:600;color:#fff;font-size:.95em;visibility:hidden">placeholder</span>
|
|
357
|
+
</div>
|
|
358
|
+
</div>
|
|
359
|
+
<div style="display:flex;align-items:center;gap:12px">
|
|
360
|
+
<label style="display:flex;align-items:center;gap:6px;color:#a0aec0;font-size:.8em;cursor:pointer;user-select:none;white-space:nowrap">
|
|
361
|
+
<input type="checkbox" data-type="show-non-failing" style="cursor:pointer;width:13px;height:13px;flex-shrink:0" />
|
|
362
|
+
Show passing
|
|
363
|
+
</label>
|
|
364
|
+
<button data-type="close" style="background:transparent;border:1px solid #4a5568;color:#a0aec0;padding:6px 12px;border-radius:4px;cursor:pointer;font-size:.85em">✕ Close</button>
|
|
365
|
+
</div>
|
|
366
|
+
</header>
|
|
367
|
+
<div data-carousel-content style="flex:1;overflow:auto;padding:20px;color:#fff"></div>
|
|
368
|
+
<div data-see-diff-row style="background:#1a202c;padding:0 16px 10px;display:flex;justify-content:flex-end;flex-shrink:0">
|
|
369
|
+
<button data-type="see-diff" style="background:#4a5568;border:none;color:#fff;padding:6px 14px;border-radius:4px;cursor:pointer;font-size:.85em">See diff image →</button>
|
|
370
|
+
</div>
|
|
371
|
+
<footer style="background:#2d3748;padding:12px 16px;display:flex;align-items:center;justify-content:space-between;gap:8px;flex-shrink:0;flex-wrap:wrap">
|
|
372
|
+
<button data-type="prev" style="background:transparent;border:1px solid #4a5568;color:#a0aec0;padding:8px 14px;border-radius:4px;cursor:pointer">← Prev</button>
|
|
373
|
+
<div style="display:flex;gap:8px">
|
|
374
|
+
<button data-type="skip" style="background:#4a5568;border:none;color:#fff;padding:8px 16px;border-radius:4px;cursor:pointer">Skip this change →</button>
|
|
375
|
+
<span data-replaced-info style="display:none;align-items:center;color:#68d391;padding:8px 16px;font-size:.9em;font-weight:600">✓ Image was already replaced</span>
|
|
376
|
+
<button data-type="replace" style="background:#38a169;border:none;color:#fff;padding:8px 16px;border-radius:4px;cursor:pointer;font-weight:600">✓ Replace image</button>
|
|
377
|
+
</div>
|
|
378
|
+
<button data-type="next" style="background:transparent;border:1px solid #4a5568;color:#a0aec0;padding:8px 14px;border-radius:4px;cursor:pointer">Next →</button>
|
|
379
|
+
</footer>
|
|
380
|
+
</div>
|
|
381
|
+
<style>
|
|
382
|
+
.${CAROUSEL_CLASS} button[data-type="prev"]:disabled,
|
|
383
|
+
.${CAROUSEL_CLASS} button[data-type="next"]:disabled {
|
|
384
|
+
opacity:.3;cursor:not-allowed;pointer-events:none;
|
|
385
|
+
}
|
|
386
|
+
</style>`;
|
|
387
|
+
/* c8 ignore stop */
|
|
388
|
+
/* c8 ignore start */
|
|
389
|
+
function showFABSuccess() {
|
|
390
|
+
if (!top) return;
|
|
391
|
+
const fab = top.document.querySelector(`.${FAB_CLASS}`);
|
|
392
|
+
if (!fab) return;
|
|
393
|
+
const badge = fab.querySelector(`.${FAB_BADGE_CLASS}`);
|
|
394
|
+
if (badge) badge.style.display = 'none';
|
|
395
|
+
fab.setAttribute('data-success', 'true');
|
|
396
|
+
// Use the top window's timer: the spec iframe (and its timers) is torn down
|
|
397
|
+
// when the spec is rerun, which would leave the FAB stuck in success state.
|
|
398
|
+
top.setTimeout(() => fab.removeAttribute('data-success'), 1800);
|
|
399
|
+
}
|
|
400
|
+
function pulseFAB() {
|
|
401
|
+
if (!top) return;
|
|
402
|
+
const fab = top.document.querySelector(`.${FAB_CLASS}`);
|
|
403
|
+
if (!fab) return;
|
|
404
|
+
fab.classList.remove(`${FAB_CLASS}--attention`);
|
|
405
|
+
fab.classList.add(`${FAB_CLASS}--attention`);
|
|
406
|
+
}
|
|
407
|
+
function openDiffLightbox(imgBase64) {
|
|
408
|
+
if (!top) return;
|
|
409
|
+
Cypress.$(`<div style="position:fixed;z-index:200000;top:0;bottom:0;left:0;right:0;background:rgba(0,0,0,.92);display:flex;align-items:center;justify-content:center;cursor:zoom-out">
|
|
410
|
+
<img src="data:image/png;base64,${imgBase64}" style="max-width:95vw;max-height:95vh;object-fit:contain" />
|
|
411
|
+
</div>`).appendTo(top.document.body).on('click', function () {
|
|
412
|
+
Cypress.$(this).remove();
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
function openCarousel(initialDiffs, allDiffs = initialDiffs) {
|
|
416
|
+
if (!top) return;
|
|
417
|
+
let diffs = initialDiffs;
|
|
418
|
+
let currentIndex = 0;
|
|
419
|
+
let replacedCount = 0;
|
|
420
|
+
const replacedDiffs = new Set();
|
|
421
|
+
const carouselEl = Cypress.$(generateCarouselTemplate()).appendTo(top.document.body);
|
|
422
|
+
function renderDiff(index) {
|
|
423
|
+
currentIndex = index;
|
|
424
|
+
const diff = diffs[index];
|
|
425
|
+
carouselEl.find('[data-carousel-content]').html(generateDiffPanelHTML(diff));
|
|
426
|
+
carouselEl.find('[data-carousel-counter]').css('visibility', '').text(`Image ${index + 1} of ${diffs.length}`);
|
|
427
|
+
const passedBadge = diff.passed ? ` <span style="background:#38a169;color:#fff;font-size:.7em;padding:2px 7px;border-radius:10px;vertical-align:middle;font-weight:600">passed</span>` : '';
|
|
428
|
+
carouselEl.find('[data-carousel-title]').css('visibility', '').html(`${diff.title} – screenshot diff${passedBadge}`);
|
|
429
|
+
carouselEl.find('[data-type="prev"]')[0].disabled = index === 0;
|
|
430
|
+
carouselEl.find('[data-type="next"]')[0].disabled = index === diffs.length - 1;
|
|
431
|
+
const isReplaced = replacedDiffs.has(diff);
|
|
432
|
+
carouselEl.find('[data-type="replace"]').toggle(!isReplaced);
|
|
433
|
+
carouselEl.find('[data-replaced-info]').css('display', isReplaced ? 'flex' : 'none');
|
|
434
|
+
}
|
|
435
|
+
function closeCarousel() {
|
|
436
|
+
if (!top) return;
|
|
437
|
+
top.document.removeEventListener('keydown', handleKey);
|
|
438
|
+
carouselEl.remove();
|
|
439
|
+
if (replacedCount > 0) showFABSuccess();
|
|
440
|
+
}
|
|
441
|
+
function advance() {
|
|
442
|
+
if (currentIndex < diffs.length - 1) {
|
|
443
|
+
renderDiff(currentIndex + 1);
|
|
444
|
+
} else {
|
|
445
|
+
closeCarousel();
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
function handleKey(e) {
|
|
449
|
+
if (inPlaceholderMode) {
|
|
450
|
+
if (e.key === 'Escape') closeCarousel();
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
if (e.key === 'ArrowLeft' && currentIndex > 0) renderDiff(currentIndex - 1);else if (e.key === 'ArrowRight' && currentIndex < diffs.length - 1) renderDiff(currentIndex + 1);else if (e.key === 'Escape') closeCarousel();
|
|
454
|
+
}
|
|
455
|
+
top.document.addEventListener('keydown', handleKey);
|
|
456
|
+
const showNonFailingCheckbox = carouselEl.find('[data-type="show-non-failing"]')[0];
|
|
457
|
+
if (showNonFailingCheckbox) {
|
|
458
|
+
showNonFailingCheckbox.checked = getEffectiveShowPassingImages();
|
|
459
|
+
}
|
|
460
|
+
let inPlaceholderMode = false;
|
|
461
|
+
function showPlaceholder() {
|
|
462
|
+
inPlaceholderMode = true;
|
|
463
|
+
carouselEl.find('[data-carousel-content]').html(`<div style="display:flex;align-items:center;justify-content:center;height:100%;color:#718096;text-align:center;padding:20px">
|
|
464
|
+
<div>
|
|
465
|
+
<p style="margin:0 0 6px;color:#a0aec0;font-size:.95em">No failing images to review.</p>
|
|
466
|
+
<p style="margin:0;font-size:.85em">Enable <strong style="color:#a0aec0">Show passing</strong> to view passing captures.</p>
|
|
467
|
+
</div>
|
|
468
|
+
</div>`);
|
|
469
|
+
carouselEl.find('[data-carousel-counter]').css('visibility', 'hidden');
|
|
470
|
+
carouselEl.find('[data-carousel-title]').css('visibility', 'hidden');
|
|
471
|
+
carouselEl.find('[data-see-diff-row],[data-type="skip"],[data-type="replace"],[data-replaced-info],[data-type="prev"],[data-type="next"]').hide();
|
|
472
|
+
}
|
|
473
|
+
function restoreFromPlaceholder() {
|
|
474
|
+
inPlaceholderMode = false;
|
|
475
|
+
carouselEl.find('[data-see-diff-row],[data-type="skip"],[data-type="replace"],[data-type="prev"],[data-type="next"]').show();
|
|
476
|
+
renderDiff(currentIndex);
|
|
477
|
+
}
|
|
478
|
+
if (diffs.length === 0) showPlaceholder();else renderDiff(0);
|
|
479
|
+
carouselEl.on('change', '[data-type="show-non-failing"]', function (e) {
|
|
480
|
+
if (!top) return;
|
|
481
|
+
const checked = e.target.checked;
|
|
482
|
+
top.localStorage.setItem(LS_SHOW_NON_FAILING_DIFFS, String(checked));
|
|
483
|
+
if (!checked) {
|
|
484
|
+
const failingDiffs = allDiffs.filter(d => !d.passed);
|
|
485
|
+
diffs = failingDiffs;
|
|
486
|
+
currentIndex = 0;
|
|
487
|
+
if (failingDiffs.length === 0) showPlaceholder();else if (inPlaceholderMode) restoreFromPlaceholder();else renderDiff(0);
|
|
488
|
+
} else {
|
|
489
|
+
const passingDiffs = allDiffs.filter(d => d.passed);
|
|
490
|
+
if (passingDiffs.length > 0) {
|
|
491
|
+
diffs = passingDiffs;
|
|
492
|
+
currentIndex = 0;
|
|
493
|
+
if (inPlaceholderMode) restoreFromPlaceholder();else renderDiff(0);
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
});
|
|
497
|
+
carouselEl.on('click', '[data-type="close"]', closeCarousel);
|
|
498
|
+
carouselEl.on('click', '[data-type="prev"]', () => renderDiff(currentIndex - 1));
|
|
499
|
+
carouselEl.on('click', '[data-type="next"]', () => renderDiff(currentIndex + 1));
|
|
500
|
+
carouselEl.on('click', '[data-type="skip"]', advance);
|
|
501
|
+
carouselEl.on('click', '[data-type="see-diff"]', () => {
|
|
502
|
+
openDiffLightbox(diffs[currentIndex].imgDiffBase64);
|
|
503
|
+
});
|
|
504
|
+
carouselEl.on('click', '[data-type="replace"]', () => {
|
|
505
|
+
queueClear();
|
|
506
|
+
const diff = diffs[currentIndex];
|
|
507
|
+
cy.task(TASK.approveImage, {
|
|
508
|
+
img: diff.imgPath,
|
|
509
|
+
imgOld: diff.imgOldPath
|
|
510
|
+
}).then(() => {
|
|
511
|
+
replacedCount++;
|
|
512
|
+
replacedDiffs.add(diff);
|
|
513
|
+
advance();
|
|
514
|
+
});
|
|
515
|
+
queueRun();
|
|
516
|
+
});
|
|
517
|
+
}
|
|
518
|
+
/* c8 ignore stop */
|
|
519
|
+
/* c8 ignore start */
|
|
520
|
+
before(() => {
|
|
521
|
+
if (!top) return null;
|
|
522
|
+
Cypress.$(`.${OVERLAY_CLASS}`, top.document.body).remove();
|
|
523
|
+
if (getBatchReviewMode()) {
|
|
524
|
+
var _top$document$querySe;
|
|
525
|
+
// Inject the persistent FAB if not already present
|
|
526
|
+
if (!top.document.querySelector(`.${FAB_CLASS}`)) {
|
|
527
|
+
Cypress.$(generateFABTemplate()).appendTo(top.document.body);
|
|
528
|
+
}
|
|
529
|
+
// Reset badge and success state at the start of each spec run
|
|
530
|
+
(_top$document$querySe = top.document.querySelector(`.${FAB_CLASS}`)) == null || _top$document$querySe.removeAttribute('data-success');
|
|
531
|
+
const badge = top.document.querySelector(`.${FAB_BADGE_CLASS}`);
|
|
532
|
+
if (badge) {
|
|
533
|
+
badge.style.display = 'none';
|
|
534
|
+
badge.textContent = '';
|
|
535
|
+
}
|
|
536
|
+
Cypress.$(top.document.body).off('click', `.${FAB_CLASS}`).on('click', `.${FAB_CLASS}`, () => {
|
|
537
|
+
queueClear();
|
|
538
|
+
cy.task(TASK.getPendingDiffs, null, {
|
|
539
|
+
log: false
|
|
540
|
+
}).then(diffs => {
|
|
541
|
+
const failingDiffs = diffs.filter(d => !d.passed);
|
|
542
|
+
if (failingDiffs.length > 0) {
|
|
543
|
+
openCarousel(failingDiffs, diffs);
|
|
544
|
+
} else if (getEffectiveShowPassingImages()) {
|
|
545
|
+
const passingDiffs = diffs.filter(d => d.passed);
|
|
546
|
+
openCarousel(passingDiffs, diffs);
|
|
547
|
+
} else {
|
|
548
|
+
openCarousel([], diffs);
|
|
549
|
+
}
|
|
550
|
+
});
|
|
551
|
+
queueRun();
|
|
552
|
+
});
|
|
553
|
+
}
|
|
554
|
+
// Reset client-side deferred count and clean up artifacts from the previous spec
|
|
555
|
+
if (top) top.__cpvrdDeferredCount = 0;
|
|
556
|
+
cy.task(TASK.cleanupImages, {
|
|
557
|
+
log: false
|
|
558
|
+
});
|
|
559
|
+
cy.task(TASK.clearPendingDiffs, null, {
|
|
560
|
+
log: false
|
|
561
|
+
});
|
|
562
|
+
});
|
|
563
|
+
beforeEach(() => {
|
|
564
|
+
var _top$document$querySe2;
|
|
565
|
+
if (!top || !getBatchReviewMode()) return null;
|
|
566
|
+
(_top$document$querySe2 = top.document.querySelector(`.${FAB_CLASS}`)) == null || _top$document$querySe2.setAttribute('data-running', '');
|
|
567
|
+
});
|
|
568
|
+
afterEach(() => {
|
|
569
|
+
var _top$document$querySe3;
|
|
570
|
+
if (!top || !getBatchReviewMode()) return null;
|
|
571
|
+
(_top$document$querySe3 = top.document.querySelector(`.${FAB_CLASS}`)) == null || _top$document$querySe3.removeAttribute('data-running');
|
|
572
|
+
});
|
|
573
|
+
after(() => {
|
|
574
|
+
var _top;
|
|
575
|
+
if (!top) return null;
|
|
576
|
+
Cypress.$(top.document.body).off('click', `a[href^="${LINK_PREFIX}"]`).on('click', `a[href^="${LINK_PREFIX}"]`, function (e) {
|
|
577
|
+
e.preventDefault();
|
|
578
|
+
if (!top) return false;
|
|
579
|
+
const {
|
|
580
|
+
title,
|
|
581
|
+
imgPath,
|
|
582
|
+
imgOldPath,
|
|
583
|
+
imgDiffBase64,
|
|
584
|
+
imgNewBase64,
|
|
585
|
+
imgOldBase64,
|
|
586
|
+
error
|
|
587
|
+
} = JSON.parse(decodeURIComponent(Base64.decode(e.currentTarget.getAttribute('href').substring(LINK_PREFIX.length))));
|
|
588
|
+
queueClear();
|
|
589
|
+
cy.task(TASK.doesFileExist, {
|
|
590
|
+
path: imgPath
|
|
591
|
+
}, {
|
|
592
|
+
log: false
|
|
593
|
+
}).then(wasImageNotUpdatedYet => {
|
|
594
|
+
if (!top) return false;
|
|
595
|
+
Cypress.$(generateOverlayTemplate({
|
|
596
|
+
title,
|
|
597
|
+
imgNewBase64,
|
|
598
|
+
imgOldBase64,
|
|
599
|
+
imgDiffBase64,
|
|
600
|
+
error,
|
|
601
|
+
wasImageNotUpdatedYet
|
|
602
|
+
})).appendTo(top.document.body);
|
|
603
|
+
const wrapper = Cypress.$(`.${OVERLAY_CLASS}`, top.document.body);
|
|
604
|
+
wrapper.on('click', 'button[data-type="close"]', function () {
|
|
605
|
+
wrapper.remove();
|
|
606
|
+
});
|
|
607
|
+
wrapper.on('click', '[data-type="see-diff"]', function () {
|
|
608
|
+
var _wrapper$find$attr;
|
|
609
|
+
const base64 = (_wrapper$find$attr = wrapper.find('[data-diff-base64]').attr('data-diff-base64')) != null ? _wrapper$find$attr : '';
|
|
610
|
+
openDiffLightbox(base64);
|
|
611
|
+
});
|
|
612
|
+
wrapper.on('click', 'button[data-type="replace"]', function () {
|
|
613
|
+
queueClear();
|
|
614
|
+
cy.task(TASK.approveImage, {
|
|
615
|
+
img: imgPath,
|
|
616
|
+
imgOld: imgOldPath
|
|
617
|
+
}).then(() => wrapper.remove());
|
|
618
|
+
queueRun();
|
|
619
|
+
});
|
|
620
|
+
});
|
|
621
|
+
queueRun();
|
|
622
|
+
return false;
|
|
623
|
+
});
|
|
624
|
+
const deferredCount = ((_top = top) == null ? void 0 : _top.__cpvrdDeferredCount) || 0;
|
|
625
|
+
if (deferredCount > 0) {
|
|
626
|
+
pulseFAB();
|
|
627
|
+
throw new Error(`[cypress-plugin-visual-regression-diff] ${deferredCount} image snapshot(s) exceeded the diff threshold. Review the changes using the plugin UI (bottom-right button).`);
|
|
628
|
+
}
|
|
629
|
+
});
|
|
630
|
+
/* c8 ignore stop */
|
|
631
|
+
|
|
632
|
+
export { generateOverlayTemplate, getBatchReviewMode, getEffectiveShowPassingImages, getShowPassingImagesConfig };
|
|
633
|
+
//# sourceMappingURL=support.mjs.map
|