@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.
@@ -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
- // Generate grouped structure from flat comparisons
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, null, 2));
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
- // Spread the full comparison to include regionAnalysis, confirmedRegions, hotspotAnalysis, etc.
419
+ // Only include lightweight fields in report-data.json (broadcast via SSE)
393
420
  const newComparison = {
394
- ...comparison,
395
- originalName: name,
396
- // Convert absolute file paths to web-accessible URLs
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
- timestamp: Date.now()
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 data file
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, null, 2));
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 and summary
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, null, 2));
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');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vizzly-testing/cli",
3
- "version": "0.29.0",
3
+ "version": "0.29.2",
4
4
  "description": "Visual review platform for UI developers and designers",
5
5
  "keywords": [
6
6
  "visual-testing",