@vizzly-testing/cli 0.19.2 → 0.20.1-beta.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/dist/api/client.js +134 -0
- package/dist/api/core.js +341 -0
- package/dist/api/endpoints.js +314 -0
- package/dist/api/index.js +19 -0
- package/dist/auth/client.js +91 -0
- package/dist/auth/core.js +176 -0
- package/dist/auth/index.js +30 -0
- package/dist/auth/operations.js +148 -0
- package/dist/cli.js +1 -1
- package/dist/client/index.js +0 -1
- package/dist/commands/doctor.js +3 -3
- package/dist/commands/finalize.js +41 -15
- package/dist/commands/login.js +7 -6
- package/dist/commands/logout.js +4 -4
- package/dist/commands/project.js +5 -4
- package/dist/commands/run.js +158 -90
- package/dist/commands/status.js +22 -18
- package/dist/commands/tdd.js +105 -78
- package/dist/commands/upload.js +61 -26
- package/dist/commands/whoami.js +4 -4
- package/dist/config/core.js +438 -0
- package/dist/config/index.js +13 -0
- package/dist/config/operations.js +327 -0
- package/dist/index.js +1 -1
- package/dist/project/core.js +295 -0
- package/dist/project/index.js +13 -0
- package/dist/project/operations.js +393 -0
- package/dist/report-generator/core.js +315 -0
- package/dist/report-generator/index.js +8 -0
- package/dist/report-generator/operations.js +196 -0
- package/dist/reporter/reporter-bundle.iife.js +16 -16
- package/dist/screenshot-server/core.js +157 -0
- package/dist/screenshot-server/index.js +11 -0
- package/dist/screenshot-server/operations.js +183 -0
- package/dist/sdk/index.js +3 -2
- package/dist/server/handlers/api-handler.js +14 -5
- package/dist/server/handlers/tdd-handler.js +80 -48
- package/dist/server-manager/core.js +183 -0
- package/dist/server-manager/index.js +81 -0
- package/dist/server-manager/operations.js +208 -0
- package/dist/services/build-manager.js +2 -69
- package/dist/services/index.js +21 -48
- package/dist/services/screenshot-server.js +40 -74
- package/dist/services/server-manager.js +45 -80
- package/dist/services/static-report-generator.js +21 -163
- package/dist/services/test-runner.js +90 -249
- package/dist/services/uploader.js +56 -358
- package/dist/tdd/core/hotspot-coverage.js +112 -0
- package/dist/tdd/core/signature.js +101 -0
- package/dist/tdd/index.js +19 -0
- package/dist/tdd/metadata/baseline-metadata.js +103 -0
- package/dist/tdd/metadata/hotspot-metadata.js +93 -0
- package/dist/tdd/services/baseline-downloader.js +151 -0
- package/dist/tdd/services/baseline-manager.js +166 -0
- package/dist/tdd/services/comparison-service.js +230 -0
- package/dist/tdd/services/hotspot-service.js +71 -0
- package/dist/tdd/services/result-service.js +123 -0
- package/dist/tdd/tdd-service.js +1081 -0
- package/dist/test-runner/core.js +255 -0
- package/dist/test-runner/index.js +13 -0
- package/dist/test-runner/operations.js +483 -0
- package/dist/types/client.d.ts +4 -2
- package/dist/types/index.d.ts +5 -0
- package/dist/uploader/core.js +396 -0
- package/dist/uploader/index.js +11 -0
- package/dist/uploader/operations.js +412 -0
- package/dist/utils/config-schema.js +8 -3
- package/package.json +7 -12
- package/dist/services/api-service.js +0 -412
- package/dist/services/auth-service.js +0 -226
- package/dist/services/config-service.js +0 -369
- package/dist/services/html-report-generator.js +0 -455
- package/dist/services/project-service.js +0 -326
- package/dist/services/report-generator/report.css +0 -411
- package/dist/services/report-generator/viewer.js +0 -102
- package/dist/services/tdd-service.js +0 -1429
|
@@ -1,455 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* HTML Report Generator for TDD visual comparison results
|
|
3
|
-
* Creates an interactive report with overlay, toggle, and onion skin modes
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { existsSync } from 'node:fs';
|
|
7
|
-
import { mkdir, writeFile } from 'node:fs/promises';
|
|
8
|
-
import { dirname, join, relative } from 'node:path';
|
|
9
|
-
import { fileURLToPath } from 'node:url';
|
|
10
|
-
import * as output from '../utils/output.js';
|
|
11
|
-
export class HtmlReportGenerator {
|
|
12
|
-
constructor(workingDir, config) {
|
|
13
|
-
this.workingDir = workingDir;
|
|
14
|
-
this.config = config;
|
|
15
|
-
this.reportDir = join(workingDir, '.vizzly', 'report');
|
|
16
|
-
this.reportPath = join(this.reportDir, 'index.html');
|
|
17
|
-
|
|
18
|
-
// Get path to the CSS file that ships with the package
|
|
19
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
20
|
-
const __dirname = dirname(__filename);
|
|
21
|
-
this.cssPath = join(__dirname, 'report-generator', 'report.css');
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Sanitize HTML content to prevent XSS attacks
|
|
26
|
-
* @param {string} text - Text to sanitize
|
|
27
|
-
* @returns {string} Sanitized text
|
|
28
|
-
*/
|
|
29
|
-
sanitizeHtml(text) {
|
|
30
|
-
if (typeof text !== 'string') return '';
|
|
31
|
-
return text.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, ''');
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Sanitize build info object
|
|
36
|
-
* @param {Object} buildInfo - Build information to sanitize
|
|
37
|
-
* @returns {Object} Sanitized build info
|
|
38
|
-
*/
|
|
39
|
-
sanitizeBuildInfo(buildInfo = {}) {
|
|
40
|
-
const sanitized = {};
|
|
41
|
-
if (buildInfo.baseline && typeof buildInfo.baseline === 'object') {
|
|
42
|
-
sanitized.baseline = {
|
|
43
|
-
buildId: this.sanitizeHtml(buildInfo.baseline.buildId || ''),
|
|
44
|
-
buildName: this.sanitizeHtml(buildInfo.baseline.buildName || ''),
|
|
45
|
-
environment: this.sanitizeHtml(buildInfo.baseline.environment || ''),
|
|
46
|
-
branch: this.sanitizeHtml(buildInfo.baseline.branch || '')
|
|
47
|
-
};
|
|
48
|
-
}
|
|
49
|
-
if (typeof buildInfo.threshold === 'number') {
|
|
50
|
-
sanitized.threshold = Math.max(0, Math.min(1, buildInfo.threshold));
|
|
51
|
-
}
|
|
52
|
-
return sanitized;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* Generate HTML report from TDD results
|
|
57
|
-
* @param {Object} results - TDD comparison results
|
|
58
|
-
* @param {Object} buildInfo - Build information
|
|
59
|
-
* @returns {string} Path to generated report
|
|
60
|
-
*/
|
|
61
|
-
async generateReport(results, buildInfo = {}) {
|
|
62
|
-
// Validate inputs
|
|
63
|
-
if (!results || typeof results !== 'object') {
|
|
64
|
-
throw new Error('Invalid results object provided');
|
|
65
|
-
}
|
|
66
|
-
const {
|
|
67
|
-
comparisons = [],
|
|
68
|
-
passed = 0,
|
|
69
|
-
failed = 0,
|
|
70
|
-
total = 0
|
|
71
|
-
} = results;
|
|
72
|
-
|
|
73
|
-
// Filter only failed comparisons for the report
|
|
74
|
-
const failedComparisons = comparisons.filter(comp => comp && comp.status === 'failed');
|
|
75
|
-
const reportData = {
|
|
76
|
-
buildInfo: {
|
|
77
|
-
timestamp: new Date().toISOString(),
|
|
78
|
-
...this.sanitizeBuildInfo(buildInfo)
|
|
79
|
-
},
|
|
80
|
-
summary: {
|
|
81
|
-
total,
|
|
82
|
-
passed,
|
|
83
|
-
failed,
|
|
84
|
-
passRate: total > 0 ? (passed / total * 100).toFixed(1) : '0.0'
|
|
85
|
-
},
|
|
86
|
-
comparisons: failedComparisons.map(comp => this.processComparison(comp)).filter(Boolean)
|
|
87
|
-
};
|
|
88
|
-
const htmlContent = this.generateHtmlTemplate(reportData);
|
|
89
|
-
try {
|
|
90
|
-
// Ensure report directory exists
|
|
91
|
-
await mkdir(this.reportDir, {
|
|
92
|
-
recursive: true
|
|
93
|
-
});
|
|
94
|
-
await writeFile(this.reportPath, htmlContent, 'utf8');
|
|
95
|
-
output.debug('report', 'generated html report');
|
|
96
|
-
return this.reportPath;
|
|
97
|
-
} catch (error) {
|
|
98
|
-
output.debug('report', 'html generation failed', {
|
|
99
|
-
error: error.message
|
|
100
|
-
});
|
|
101
|
-
throw new Error(`Report generation failed: ${error.message}`);
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
/**
|
|
106
|
-
* Process comparison data for HTML report
|
|
107
|
-
* @param {Object} comparison - Comparison object
|
|
108
|
-
* @returns {Object} Processed comparison data
|
|
109
|
-
*/
|
|
110
|
-
processComparison(comparison) {
|
|
111
|
-
if (!comparison || typeof comparison !== 'object') {
|
|
112
|
-
output.warn('Invalid comparison object provided');
|
|
113
|
-
return null;
|
|
114
|
-
}
|
|
115
|
-
return {
|
|
116
|
-
name: comparison.name || 'unnamed',
|
|
117
|
-
status: comparison.status,
|
|
118
|
-
baseline: this.getRelativePath(comparison.baseline, this.reportDir),
|
|
119
|
-
current: this.getRelativePath(comparison.current, this.reportDir),
|
|
120
|
-
diff: this.getRelativePath(comparison.diff, this.reportDir),
|
|
121
|
-
threshold: comparison.threshold || 0,
|
|
122
|
-
diffPercentage: comparison.diffPercentage || 0
|
|
123
|
-
};
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
/**
|
|
127
|
-
* Get relative path from report directory to image file
|
|
128
|
-
* @param {string} imagePath - Absolute path to image
|
|
129
|
-
* @param {string} reportDir - Report directory path
|
|
130
|
-
* @returns {string|null} Relative path or null if invalid
|
|
131
|
-
*/
|
|
132
|
-
getRelativePath(imagePath, reportDir) {
|
|
133
|
-
if (!imagePath || !existsSync(imagePath)) {
|
|
134
|
-
return null;
|
|
135
|
-
}
|
|
136
|
-
return relative(reportDir, imagePath);
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
/**
|
|
140
|
-
* Generate the complete HTML template
|
|
141
|
-
* @param {Object} data - Report data
|
|
142
|
-
* @returns {string} HTML content
|
|
143
|
-
*/
|
|
144
|
-
generateHtmlTemplate(data) {
|
|
145
|
-
return `<!DOCTYPE html>
|
|
146
|
-
<html lang="en">
|
|
147
|
-
<head>
|
|
148
|
-
<meta charset="UTF-8">
|
|
149
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
150
|
-
<title>Vizzly TDD Report</title>
|
|
151
|
-
<link rel="stylesheet" href="file://${this.cssPath}">
|
|
152
|
-
</head>
|
|
153
|
-
<body>
|
|
154
|
-
<div class="container">
|
|
155
|
-
<header class="header">
|
|
156
|
-
<h1>🐻 Vizzly Visual Testing Report</h1>
|
|
157
|
-
<div class="summary">
|
|
158
|
-
<div class="stat">
|
|
159
|
-
<span class="stat-number">${data.summary.total}</span>
|
|
160
|
-
<span class="stat-label">Total</span>
|
|
161
|
-
</div>
|
|
162
|
-
<div class="stat passed">
|
|
163
|
-
<span class="stat-number">${data.summary.passed}</span>
|
|
164
|
-
<span class="stat-label">Passed</span>
|
|
165
|
-
</div>
|
|
166
|
-
<div class="stat failed">
|
|
167
|
-
<span class="stat-number">${data.summary.failed}</span>
|
|
168
|
-
<span class="stat-label">Failed</span>
|
|
169
|
-
</div>
|
|
170
|
-
<div class="stat">
|
|
171
|
-
<span class="stat-number">${data.summary.passRate}%</span>
|
|
172
|
-
<span class="stat-label">Pass Rate</span>
|
|
173
|
-
</div>
|
|
174
|
-
</div>
|
|
175
|
-
<div class="build-info">
|
|
176
|
-
<span>Generated: ${new Date(data.buildInfo.timestamp).toLocaleString()}</span>
|
|
177
|
-
</div>
|
|
178
|
-
</header>
|
|
179
|
-
|
|
180
|
-
${data.comparisons.length === 0 ? '<div class="no-failures">🎉 All tests passed! No visual differences detected.</div>' : `<main class="comparisons">
|
|
181
|
-
${data.comparisons.map(comp => this.generateComparisonHtml(comp)).join('')}
|
|
182
|
-
</main>`}
|
|
183
|
-
</div>
|
|
184
|
-
|
|
185
|
-
<script>
|
|
186
|
-
document.addEventListener('DOMContentLoaded', function () {
|
|
187
|
-
// Handle view mode switching
|
|
188
|
-
document.querySelectorAll('.view-mode-btn').forEach(btn => {
|
|
189
|
-
btn.addEventListener('click', function () {
|
|
190
|
-
let comparison = this.closest('.comparison');
|
|
191
|
-
let mode = this.dataset.mode;
|
|
192
|
-
|
|
193
|
-
// Update active button
|
|
194
|
-
comparison
|
|
195
|
-
.querySelectorAll('.view-mode-btn')
|
|
196
|
-
.forEach(b => b.classList.remove('active'));
|
|
197
|
-
this.classList.add('active');
|
|
198
|
-
|
|
199
|
-
// Update viewer mode
|
|
200
|
-
let viewer = comparison.querySelector('.comparison-viewer');
|
|
201
|
-
viewer.dataset.mode = mode;
|
|
202
|
-
|
|
203
|
-
// Hide all mode containers
|
|
204
|
-
viewer.querySelectorAll('.mode-container').forEach(container => {
|
|
205
|
-
container.style.display = 'none';
|
|
206
|
-
});
|
|
207
|
-
|
|
208
|
-
// Show appropriate mode container
|
|
209
|
-
let activeContainer = viewer.querySelector('.' + mode + '-mode');
|
|
210
|
-
if (activeContainer) {
|
|
211
|
-
activeContainer.style.display = 'block';
|
|
212
|
-
}
|
|
213
|
-
});
|
|
214
|
-
});
|
|
215
|
-
|
|
216
|
-
// Handle onion skin drag-to-reveal
|
|
217
|
-
document.querySelectorAll('.onion-container').forEach(container => {
|
|
218
|
-
let isDragging = false;
|
|
219
|
-
|
|
220
|
-
function updateOnionSkin(x) {
|
|
221
|
-
let rect = container.getBoundingClientRect();
|
|
222
|
-
let percentage = Math.max(
|
|
223
|
-
0,
|
|
224
|
-
Math.min(100, ((x - rect.left) / rect.width) * 100)
|
|
225
|
-
);
|
|
226
|
-
|
|
227
|
-
let currentImg = container.querySelector('.onion-current');
|
|
228
|
-
let divider = container.querySelector('.onion-divider');
|
|
229
|
-
|
|
230
|
-
if (currentImg && divider) {
|
|
231
|
-
currentImg.style.clipPath = 'inset(0 ' + (100 - percentage) + '% 0 0)';
|
|
232
|
-
divider.style.left = percentage + '%';
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
container.addEventListener('mousedown', function (e) {
|
|
237
|
-
isDragging = true;
|
|
238
|
-
updateOnionSkin(e.clientX);
|
|
239
|
-
e.preventDefault();
|
|
240
|
-
});
|
|
241
|
-
|
|
242
|
-
container.addEventListener('mousemove', function (e) {
|
|
243
|
-
if (isDragging) {
|
|
244
|
-
updateOnionSkin(e.clientX);
|
|
245
|
-
}
|
|
246
|
-
});
|
|
247
|
-
|
|
248
|
-
document.addEventListener('mouseup', function () {
|
|
249
|
-
isDragging = false;
|
|
250
|
-
});
|
|
251
|
-
|
|
252
|
-
// Touch events for mobile
|
|
253
|
-
container.addEventListener('touchstart', function (e) {
|
|
254
|
-
isDragging = true;
|
|
255
|
-
updateOnionSkin(e.touches[0].clientX);
|
|
256
|
-
e.preventDefault();
|
|
257
|
-
});
|
|
258
|
-
|
|
259
|
-
container.addEventListener('touchmove', function (e) {
|
|
260
|
-
if (isDragging) {
|
|
261
|
-
updateOnionSkin(e.touches[0].clientX);
|
|
262
|
-
e.preventDefault();
|
|
263
|
-
}
|
|
264
|
-
});
|
|
265
|
-
|
|
266
|
-
document.addEventListener('touchend', function () {
|
|
267
|
-
isDragging = false;
|
|
268
|
-
});
|
|
269
|
-
});
|
|
270
|
-
|
|
271
|
-
// Handle overlay mode clicking
|
|
272
|
-
document.querySelectorAll('.overlay-container').forEach(container => {
|
|
273
|
-
container.addEventListener('click', function () {
|
|
274
|
-
let diffImage = this.querySelector('.diff-image');
|
|
275
|
-
if (diffImage) {
|
|
276
|
-
// Toggle diff visibility
|
|
277
|
-
let isVisible = diffImage.style.opacity === '1';
|
|
278
|
-
diffImage.style.opacity = isVisible ? '0' : '1';
|
|
279
|
-
}
|
|
280
|
-
});
|
|
281
|
-
});
|
|
282
|
-
|
|
283
|
-
// Handle toggle mode clicking
|
|
284
|
-
document.querySelectorAll('.toggle-container img').forEach(img => {
|
|
285
|
-
let isBaseline = true;
|
|
286
|
-
let comparison = img.closest('.comparison');
|
|
287
|
-
let baselineSrc = comparison.querySelector('.baseline-image').src;
|
|
288
|
-
let currentSrc = comparison.querySelector('.current-image').src;
|
|
289
|
-
|
|
290
|
-
img.addEventListener('click', function () {
|
|
291
|
-
isBaseline = !isBaseline;
|
|
292
|
-
this.src = isBaseline ? baselineSrc : currentSrc;
|
|
293
|
-
|
|
294
|
-
// Update cursor style to indicate interactivity
|
|
295
|
-
this.style.cursor = 'pointer';
|
|
296
|
-
});
|
|
297
|
-
});
|
|
298
|
-
|
|
299
|
-
console.log('Vizzly TDD Report loaded successfully');
|
|
300
|
-
});
|
|
301
|
-
|
|
302
|
-
// Accept/Reject baseline functions
|
|
303
|
-
async function acceptBaseline(screenshotName) {
|
|
304
|
-
const button = document.querySelector(\`button[onclick*="\${screenshotName}"]\`);
|
|
305
|
-
if (button) {
|
|
306
|
-
button.disabled = true;
|
|
307
|
-
button.innerHTML = '⏳ Accepting...';
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
try {
|
|
311
|
-
const response = await fetch('/accept-baseline', {
|
|
312
|
-
method: 'POST',
|
|
313
|
-
headers: { 'Content-Type': 'application/json' },
|
|
314
|
-
body: JSON.stringify({ name: screenshotName })
|
|
315
|
-
});
|
|
316
|
-
|
|
317
|
-
if (response.ok) {
|
|
318
|
-
// Mark as accepted and hide the comparison
|
|
319
|
-
const comparison = document.querySelector(\`[data-comparison="\${screenshotName}"]\`);
|
|
320
|
-
if (comparison) {
|
|
321
|
-
comparison.style.background = '#e8f5e8';
|
|
322
|
-
comparison.style.border = '2px solid #4caf50';
|
|
323
|
-
|
|
324
|
-
const status = comparison.querySelector('.diff-status');
|
|
325
|
-
if (status) {
|
|
326
|
-
status.innerHTML = '✅ Accepted as new baseline';
|
|
327
|
-
status.style.color = '#4caf50';
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
const actions = comparison.querySelector('.comparison-actions');
|
|
331
|
-
if (actions) {
|
|
332
|
-
actions.innerHTML = '<div style="color: #4caf50; padding: 0.5rem;">✅ Screenshot accepted as new baseline</div>';
|
|
333
|
-
}
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
// Auto-refresh after short delay to show updated report
|
|
337
|
-
setTimeout(() => window.location.reload(), 2000);
|
|
338
|
-
} else {
|
|
339
|
-
throw new Error('Failed to accept baseline');
|
|
340
|
-
}
|
|
341
|
-
} catch (error) {
|
|
342
|
-
console.error('Error accepting baseline:', error);
|
|
343
|
-
if (button) {
|
|
344
|
-
button.disabled = false;
|
|
345
|
-
button.innerHTML = '✅ Accept as Baseline';
|
|
346
|
-
}
|
|
347
|
-
alert('Failed to accept baseline. Please try again.');
|
|
348
|
-
}
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
function rejectChanges(screenshotName) {
|
|
352
|
-
const comparison = document.querySelector(\`[data-comparison="\${screenshotName}"]\`);
|
|
353
|
-
if (comparison) {
|
|
354
|
-
comparison.style.background = '#fff3cd';
|
|
355
|
-
comparison.style.border = '2px solid #ffc107';
|
|
356
|
-
|
|
357
|
-
const status = comparison.querySelector('.diff-status');
|
|
358
|
-
if (status) {
|
|
359
|
-
status.innerHTML = '⚠️ Changes rejected - baseline unchanged';
|
|
360
|
-
status.style.color = '#856404';
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
const actions = comparison.querySelector('.comparison-actions');
|
|
364
|
-
if (actions) {
|
|
365
|
-
actions.innerHTML = '<div style="color: #856404; padding: 0.5rem;">⚠️ Changes rejected - baseline kept as-is</div>';
|
|
366
|
-
}
|
|
367
|
-
}
|
|
368
|
-
}
|
|
369
|
-
</script>
|
|
370
|
-
</body>
|
|
371
|
-
</html>`;
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
/**
|
|
375
|
-
* Generate HTML for a single comparison
|
|
376
|
-
* @param {Object} comparison - Comparison data
|
|
377
|
-
* @returns {string} HTML content
|
|
378
|
-
*/
|
|
379
|
-
generateComparisonHtml(comparison) {
|
|
380
|
-
if (!comparison || !comparison.baseline || !comparison.current || !comparison.diff) {
|
|
381
|
-
return `<div class="comparison error">
|
|
382
|
-
<h3>${this.sanitizeHtml(comparison?.name || 'Unknown')}</h3>
|
|
383
|
-
<p>Missing comparison images</p>
|
|
384
|
-
</div>`;
|
|
385
|
-
}
|
|
386
|
-
const safeName = this.sanitizeHtml(comparison.name);
|
|
387
|
-
return `
|
|
388
|
-
<div class="comparison" data-comparison="${safeName}">
|
|
389
|
-
<div class="comparison-header">
|
|
390
|
-
<h3>${safeName}</h3>
|
|
391
|
-
<div class="comparison-meta">
|
|
392
|
-
<span class="diff-status">Visual differences detected</span>
|
|
393
|
-
</div>
|
|
394
|
-
</div>
|
|
395
|
-
|
|
396
|
-
<div class="comparison-controls">
|
|
397
|
-
<button class="view-mode-btn active" data-mode="overlay">Overlay</button>
|
|
398
|
-
<button class="view-mode-btn" data-mode="toggle">Toggle</button>
|
|
399
|
-
<button class="view-mode-btn" data-mode="onion">Onion Skin</button>
|
|
400
|
-
<button class="view-mode-btn" data-mode="side-by-side">Side by Side</button>
|
|
401
|
-
</div>
|
|
402
|
-
|
|
403
|
-
<div class="comparison-actions">
|
|
404
|
-
<button class="accept-btn" onclick="acceptBaseline('${safeName}')">
|
|
405
|
-
✅ Accept as Baseline
|
|
406
|
-
</button>
|
|
407
|
-
<button class="reject-btn" onclick="rejectChanges('${safeName}')">
|
|
408
|
-
❌ Keep Current Baseline
|
|
409
|
-
</button>
|
|
410
|
-
</div>
|
|
411
|
-
|
|
412
|
-
<div class="comparison-viewer">
|
|
413
|
-
<!-- Overlay Mode -->
|
|
414
|
-
<div class="mode-container overlay-mode" data-mode="overlay">
|
|
415
|
-
<div class="overlay-container">
|
|
416
|
-
<img class="current-image" src="${comparison.current}" alt="Current" />
|
|
417
|
-
<img class="baseline-image" src="${comparison.baseline}" alt="Baseline" />
|
|
418
|
-
<img class="diff-image" src="${comparison.diff}" alt="Diff" />
|
|
419
|
-
</div>
|
|
420
|
-
</div>
|
|
421
|
-
|
|
422
|
-
<!-- Toggle Mode -->
|
|
423
|
-
<div class="mode-container toggle-mode" data-mode="toggle" style="display: none;">
|
|
424
|
-
<div class="toggle-container">
|
|
425
|
-
<img class="toggle-image" src="${comparison.baseline}" alt="Baseline" />
|
|
426
|
-
</div>
|
|
427
|
-
</div>
|
|
428
|
-
|
|
429
|
-
<!-- Onion Skin Mode -->
|
|
430
|
-
<div class="mode-container onion-mode" data-mode="onion" style="display: none;">
|
|
431
|
-
<div class="onion-container">
|
|
432
|
-
<img class="onion-baseline" src="${comparison.baseline}" alt="Baseline" />
|
|
433
|
-
<img class="onion-current" src="${comparison.current}" alt="Current" />
|
|
434
|
-
<div class="onion-divider"></div>
|
|
435
|
-
</div>
|
|
436
|
-
</div>
|
|
437
|
-
|
|
438
|
-
<!-- Side by Side Mode -->
|
|
439
|
-
<div class="mode-container side-by-side-mode" data-mode="side-by-side" style="display: none;">
|
|
440
|
-
<div class="side-by-side-container">
|
|
441
|
-
<div class="side-by-side-image">
|
|
442
|
-
<img src="${comparison.baseline}" alt="Baseline" />
|
|
443
|
-
<label>Baseline</label>
|
|
444
|
-
</div>
|
|
445
|
-
<div class="side-by-side-image">
|
|
446
|
-
<img src="${comparison.current}" alt="Current" />
|
|
447
|
-
<label>Current</label>
|
|
448
|
-
</div>
|
|
449
|
-
</div>
|
|
450
|
-
</div>
|
|
451
|
-
</div>
|
|
452
|
-
|
|
453
|
-
</div>`;
|
|
454
|
-
}
|
|
455
|
-
}
|