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