@vizzly-testing/cli 0.29.0 → 0.29.2
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/endpoints.js +3 -1
- package/dist/cli.js +5 -3
- package/dist/commands/builds.js +47 -4
- package/dist/commands/comparisons.js +1 -0
- package/dist/reporter/reporter-bundle.iife.js +46 -46
- package/dist/server/handlers/tdd-handler.js +81 -26
- package/dist/server/routers/dashboard.js +50 -1
- package/package.json +1 -1
|
@@ -156,18 +156,51 @@ export const createTddHandler = (config, workingDir, baselineBuild, baselineComp
|
|
|
156
156
|
} = deps;
|
|
157
157
|
const tddService = new TddService(config, workingDir, setBaseline);
|
|
158
158
|
const reportPath = join(workingDir, '.vizzly', 'report-data.json');
|
|
159
|
+
const detailsPath = join(workingDir, '.vizzly', 'comparison-details.json');
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Read heavy comparison details from comparison-details.json
|
|
163
|
+
* Returns a map of comparison ID -> heavy fields
|
|
164
|
+
*/
|
|
165
|
+
const readComparisonDetails = () => {
|
|
166
|
+
try {
|
|
167
|
+
if (!existsSync(detailsPath)) return {};
|
|
168
|
+
return JSON.parse(readFileSync(detailsPath, 'utf8'));
|
|
169
|
+
} catch (error) {
|
|
170
|
+
output.debug('Failed to read comparison details:', error);
|
|
171
|
+
return {};
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Persist heavy fields for a comparison to comparison-details.json
|
|
177
|
+
* This file is NOT watched by SSE, so writes here don't trigger broadcasts
|
|
178
|
+
* Skips writing if all heavy fields are empty (passed comparisons)
|
|
179
|
+
*/
|
|
180
|
+
const updateComparisonDetails = (id, heavyFields) => {
|
|
181
|
+
let hasData = Object.values(heavyFields).some(v => v != null && (!Array.isArray(v) || v.length > 0));
|
|
182
|
+
if (!hasData) return;
|
|
183
|
+
let details = readComparisonDetails();
|
|
184
|
+
details[id] = heavyFields;
|
|
185
|
+
writeFileSync(detailsPath, JSON.stringify(details));
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Remove a comparison's heavy fields from comparison-details.json
|
|
190
|
+
*/
|
|
191
|
+
const removeComparisonDetails = id => {
|
|
192
|
+
let details = readComparisonDetails();
|
|
193
|
+
delete details[id];
|
|
194
|
+
writeFileSync(detailsPath, JSON.stringify(details));
|
|
195
|
+
};
|
|
159
196
|
const readReportData = () => {
|
|
160
197
|
try {
|
|
161
198
|
if (!existsSync(reportPath)) {
|
|
162
199
|
return {
|
|
163
200
|
timestamp: Date.now(),
|
|
164
201
|
comparisons: [],
|
|
165
|
-
// Internal flat list for easy updates
|
|
166
|
-
groups: [],
|
|
167
|
-
// Grouped structure for UI
|
|
168
202
|
summary: {
|
|
169
203
|
total: 0,
|
|
170
|
-
groups: 0,
|
|
171
204
|
passed: 0,
|
|
172
205
|
failed: 0,
|
|
173
206
|
errors: 0
|
|
@@ -181,10 +214,8 @@ export const createTddHandler = (config, workingDir, baselineBuild, baselineComp
|
|
|
181
214
|
return {
|
|
182
215
|
timestamp: Date.now(),
|
|
183
216
|
comparisons: [],
|
|
184
|
-
groups: [],
|
|
185
217
|
summary: {
|
|
186
218
|
total: 0,
|
|
187
|
-
groups: 0,
|
|
188
219
|
passed: 0,
|
|
189
220
|
failed: 0,
|
|
190
221
|
errors: 0
|
|
@@ -220,20 +251,16 @@ export const createTddHandler = (config, workingDir, baselineBuild, baselineComp
|
|
|
220
251
|
});
|
|
221
252
|
}
|
|
222
253
|
|
|
223
|
-
//
|
|
224
|
-
reportData.groups = groupComparisons(reportData.comparisons);
|
|
225
|
-
|
|
226
|
-
// Update summary
|
|
254
|
+
// Update summary (groups computed client-side from comparisons)
|
|
227
255
|
reportData.timestamp = Date.now();
|
|
228
256
|
reportData.summary = {
|
|
229
257
|
total: reportData.comparisons.length,
|
|
230
|
-
groups: reportData.groups.length,
|
|
231
258
|
passed: reportData.comparisons.filter(c => c.status === 'passed' || c.status === 'baseline-created' || c.status === 'new').length,
|
|
232
259
|
failed: reportData.comparisons.filter(c => c.status === 'failed').length,
|
|
233
260
|
rejected: reportData.comparisons.filter(c => c.status === 'rejected').length,
|
|
234
261
|
errors: reportData.comparisons.filter(c => c.status === 'error').length
|
|
235
262
|
};
|
|
236
|
-
writeFileSync(reportPath, JSON.stringify(reportData
|
|
263
|
+
writeFileSync(reportPath, JSON.stringify(reportData));
|
|
237
264
|
} catch (error) {
|
|
238
265
|
output.error('Failed to update comparison:', error);
|
|
239
266
|
}
|
|
@@ -389,22 +416,46 @@ export const createTddHandler = (config, workingDir, baselineBuild, baselineComp
|
|
|
389
416
|
const vizzlyDir = join(workingDir, '.vizzly');
|
|
390
417
|
|
|
391
418
|
// Record the comparison for the dashboard
|
|
392
|
-
//
|
|
419
|
+
// Only include lightweight fields in report-data.json (broadcast via SSE)
|
|
393
420
|
const newComparison = {
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
421
|
+
id: comparison.id,
|
|
422
|
+
name: comparison.name,
|
|
423
|
+
status: comparison.status,
|
|
424
|
+
signature: comparison.signature,
|
|
397
425
|
baseline: convertPathToUrl(comparison.baseline, vizzlyDir),
|
|
398
426
|
current: convertPathToUrl(comparison.current, vizzlyDir),
|
|
399
427
|
diff: convertPathToUrl(comparison.diff, vizzlyDir),
|
|
400
|
-
// Use extracted properties with top-level viewport_width/browser
|
|
401
428
|
properties: extractedProperties,
|
|
402
|
-
|
|
429
|
+
threshold: comparison.threshold,
|
|
430
|
+
minClusterSize: comparison.minClusterSize,
|
|
431
|
+
diffPercentage: comparison.diffPercentage,
|
|
432
|
+
diffCount: comparison.diffCount,
|
|
433
|
+
reason: comparison.reason,
|
|
434
|
+
totalPixels: comparison.totalPixels,
|
|
435
|
+
aaPixelsIgnored: comparison.aaPixelsIgnored,
|
|
436
|
+
aaPercentage: comparison.aaPercentage,
|
|
437
|
+
heightDiff: comparison.heightDiff,
|
|
438
|
+
error: comparison.error,
|
|
439
|
+
originalName: name,
|
|
440
|
+
timestamp: Date.now(),
|
|
441
|
+
// Boolean hints so UI can show toggle buttons without fetching heavy data
|
|
442
|
+
hasDiffClusters: comparison.diffClusters?.length > 0,
|
|
443
|
+
hasConfirmedRegions: comparison.confirmedRegions?.length > 0
|
|
403
444
|
};
|
|
404
445
|
|
|
405
|
-
// Update comparison in report
|
|
446
|
+
// Update lightweight comparison in report-data.json (triggers SSE broadcast)
|
|
406
447
|
updateComparison(newComparison);
|
|
407
448
|
|
|
449
|
+
// Persist heavy fields separately (NOT broadcast via SSE)
|
|
450
|
+
updateComparisonDetails(comparison.id, {
|
|
451
|
+
diffClusters: comparison.diffClusters,
|
|
452
|
+
intensityStats: comparison.intensityStats,
|
|
453
|
+
boundingBox: comparison.boundingBox,
|
|
454
|
+
regionAnalysis: comparison.regionAnalysis,
|
|
455
|
+
hotspotAnalysis: comparison.hotspotAnalysis,
|
|
456
|
+
confirmedRegions: comparison.confirmedRegions
|
|
457
|
+
});
|
|
458
|
+
|
|
408
459
|
// Log screenshot event for menubar
|
|
409
460
|
// Normalize status to match HTTP response ('failed' -> 'diff')
|
|
410
461
|
let logStatus = comparison.status === 'failed' ? 'diff' : comparison.status;
|
|
@@ -653,16 +704,19 @@ export const createTddHandler = (config, workingDir, baselineBuild, baselineComp
|
|
|
653
704
|
const freshReportData = {
|
|
654
705
|
timestamp: Date.now(),
|
|
655
706
|
comparisons: [],
|
|
656
|
-
groups: [],
|
|
657
707
|
summary: {
|
|
658
708
|
total: 0,
|
|
659
|
-
groups: 0,
|
|
660
709
|
passed: 0,
|
|
661
710
|
failed: 0,
|
|
662
711
|
errors: 0
|
|
663
712
|
}
|
|
664
713
|
};
|
|
665
|
-
writeFileSync(reportPath, JSON.stringify(freshReportData
|
|
714
|
+
writeFileSync(reportPath, JSON.stringify(freshReportData));
|
|
715
|
+
|
|
716
|
+
// Clear comparison details
|
|
717
|
+
if (existsSync(detailsPath)) {
|
|
718
|
+
writeFileSync(detailsPath, JSON.stringify({}));
|
|
719
|
+
}
|
|
666
720
|
output.info(`Baselines reset - ${deletedBaselines} baselines deleted, ${deletedCurrents} current screenshots deleted, ${deletedDiffs} diffs deleted`);
|
|
667
721
|
return {
|
|
668
722
|
success: true,
|
|
@@ -728,21 +782,22 @@ export const createTddHandler = (config, workingDir, baselineBuild, baselineComp
|
|
|
728
782
|
output.warn(`Failed to update baseline metadata: ${error.message}`);
|
|
729
783
|
}
|
|
730
784
|
|
|
785
|
+
// Remove heavy fields from comparison-details.json
|
|
786
|
+
removeComparisonDetails(comparisonId);
|
|
787
|
+
|
|
731
788
|
// Remove comparison from report data
|
|
732
789
|
reportData.comparisons = reportData.comparisons.filter(c => c.id !== comparisonId);
|
|
733
790
|
|
|
734
|
-
// Regenerate groups
|
|
735
|
-
reportData.groups = groupComparisons(reportData.comparisons);
|
|
791
|
+
// Regenerate summary (groups computed client-side)
|
|
736
792
|
reportData.timestamp = Date.now();
|
|
737
793
|
reportData.summary = {
|
|
738
794
|
total: reportData.comparisons.length,
|
|
739
|
-
groups: reportData.groups.length,
|
|
740
795
|
passed: reportData.comparisons.filter(c => c.status === 'passed' || c.status === 'baseline-created' || c.status === 'new').length,
|
|
741
796
|
failed: reportData.comparisons.filter(c => c.status === 'failed').length,
|
|
742
797
|
rejected: reportData.comparisons.filter(c => c.status === 'rejected').length,
|
|
743
798
|
errors: reportData.comparisons.filter(c => c.status === 'error').length
|
|
744
799
|
};
|
|
745
|
-
writeFileSync(reportPath, JSON.stringify(reportData
|
|
800
|
+
writeFileSync(reportPath, JSON.stringify(reportData));
|
|
746
801
|
output.info(`Deleted comparison ${comparisonId} (${comparison.name})`);
|
|
747
802
|
return {
|
|
748
803
|
success: true,
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
import { existsSync, readFileSync } from 'node:fs';
|
|
7
7
|
import { join } from 'node:path';
|
|
8
8
|
import * as output from '../../utils/output.js';
|
|
9
|
-
import { sendHtml, sendSuccess } from '../middleware/response.js';
|
|
9
|
+
import { sendError, sendHtml, sendSuccess } from '../middleware/response.js';
|
|
10
10
|
|
|
11
11
|
// SPA routes that should serve the dashboard HTML
|
|
12
12
|
const SPA_ROUTES = ['/', '/stats', '/settings', '/projects', '/builds'];
|
|
@@ -69,6 +69,55 @@ export function createDashboardRouter(context) {
|
|
|
69
69
|
}
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
+
// API endpoint for fetching full comparison details (lightweight + heavy fields)
|
|
73
|
+
let comparisonMatch = pathname.match(/^\/api\/comparison\/(.+)$/);
|
|
74
|
+
if (comparisonMatch) {
|
|
75
|
+
let comparisonId = decodeURIComponent(comparisonMatch[1]);
|
|
76
|
+
if (!comparisonId) {
|
|
77
|
+
sendError(res, 400, 'Comparison ID is required');
|
|
78
|
+
return true;
|
|
79
|
+
}
|
|
80
|
+
let reportDataPath = join(workingDir, '.vizzly', 'report-data.json');
|
|
81
|
+
if (!existsSync(reportDataPath)) {
|
|
82
|
+
sendError(res, 404, 'No report data found');
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
try {
|
|
86
|
+
let reportData = JSON.parse(readFileSync(reportDataPath, 'utf8'));
|
|
87
|
+
let comparison = (reportData.comparisons || []).find(c => c.id === comparisonId || c.signature === comparisonId || c.name === comparisonId);
|
|
88
|
+
if (!comparison) {
|
|
89
|
+
sendError(res, 404, 'Comparison not found');
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Merge with heavy fields from comparison-details.json
|
|
94
|
+
let detailsPath = join(workingDir, '.vizzly', 'comparison-details.json');
|
|
95
|
+
if (existsSync(detailsPath)) {
|
|
96
|
+
try {
|
|
97
|
+
let details = JSON.parse(readFileSync(detailsPath, 'utf8'));
|
|
98
|
+
let heavy = details[comparison.id];
|
|
99
|
+
if (heavy) {
|
|
100
|
+
comparison = {
|
|
101
|
+
...comparison,
|
|
102
|
+
...heavy
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
} catch (error) {
|
|
106
|
+
output.debug('Failed to read comparison details:', {
|
|
107
|
+
error: error.message
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
sendSuccess(res, comparison);
|
|
112
|
+
} catch (error) {
|
|
113
|
+
output.debug('Error reading comparison data:', {
|
|
114
|
+
error: error.message
|
|
115
|
+
});
|
|
116
|
+
sendError(res, 500, 'Failed to read comparison data');
|
|
117
|
+
}
|
|
118
|
+
return true;
|
|
119
|
+
}
|
|
120
|
+
|
|
72
121
|
// Serve React SPA for dashboard routes
|
|
73
122
|
if (SPA_ROUTES.includes(pathname) || pathname.startsWith('/comparison/')) {
|
|
74
123
|
const reportDataPath = join(workingDir, '.vizzly', 'report-data.json');
|