@vizzly-testing/cli 0.7.2 → 0.9.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.
Files changed (77) hide show
  1. package/README.md +27 -14
  2. package/dist/cli.js +25 -1
  3. package/dist/client/index.js +77 -11
  4. package/dist/commands/init.js +23 -17
  5. package/dist/commands/tdd-daemon.js +312 -0
  6. package/dist/commands/tdd.js +45 -14
  7. package/dist/commands/upload.js +3 -1
  8. package/dist/reporter/reporter-bundle.css +1 -0
  9. package/dist/reporter/reporter-bundle.iife.js +57 -0
  10. package/dist/sdk/index.js +1 -1
  11. package/dist/server/handlers/api-handler.js +98 -30
  12. package/dist/server/handlers/tdd-handler.js +264 -77
  13. package/dist/server/http-server.js +358 -15
  14. package/dist/services/api-service.js +6 -1
  15. package/dist/services/html-report-generator.js +77 -0
  16. package/dist/services/report-generator/report.css +56 -0
  17. package/dist/services/screenshot-server.js +6 -3
  18. package/dist/services/server-manager.js +2 -9
  19. package/dist/services/tdd-service.js +188 -25
  20. package/dist/services/test-runner.js +43 -1
  21. package/dist/types/commands/tdd-daemon.d.ts +18 -0
  22. package/dist/types/container/index.d.ts +1 -3
  23. package/dist/types/reporter/src/components/app-router.d.ts +3 -0
  24. package/dist/types/reporter/src/components/comparison/comparison-actions.d.ts +5 -0
  25. package/dist/types/reporter/src/components/comparison/comparison-card.d.ts +6 -0
  26. package/dist/types/reporter/src/components/comparison/comparison-list.d.ts +6 -0
  27. package/dist/types/reporter/src/components/comparison/comparison-viewer.d.ts +4 -0
  28. package/dist/types/reporter/src/components/comparison/view-mode-selector.d.ts +4 -0
  29. package/dist/types/reporter/src/components/comparison/viewer-modes/onion-viewer.d.ts +3 -0
  30. package/dist/types/reporter/src/components/comparison/viewer-modes/overlay-viewer.d.ts +3 -0
  31. package/dist/types/reporter/src/components/comparison/viewer-modes/side-by-side-viewer.d.ts +3 -0
  32. package/dist/types/reporter/src/components/comparison/viewer-modes/toggle-viewer.d.ts +3 -0
  33. package/dist/types/reporter/src/components/dashboard/dashboard-filters.d.ts +16 -0
  34. package/dist/types/reporter/src/components/dashboard/dashboard-header.d.ts +5 -0
  35. package/dist/types/reporter/src/components/dashboard/dashboard-stats.d.ts +4 -0
  36. package/dist/types/reporter/src/components/dashboard/empty-state.d.ts +8 -0
  37. package/dist/types/reporter/src/components/ui/smart-image.d.ts +7 -0
  38. package/dist/types/reporter/src/components/ui/status-badge.d.ts +5 -0
  39. package/dist/types/reporter/src/components/ui/toast.d.ts +4 -0
  40. package/dist/types/reporter/src/components/views/comparisons-view.d.ts +6 -0
  41. package/dist/types/reporter/src/components/views/stats-view.d.ts +6 -0
  42. package/dist/types/reporter/src/hooks/use-baseline-actions.d.ts +5 -0
  43. package/dist/types/reporter/src/hooks/use-comparison-filters.d.ts +20 -0
  44. package/dist/types/reporter/src/hooks/use-image-loader.d.ts +1 -0
  45. package/dist/types/reporter/src/hooks/use-report-data.d.ts +7 -0
  46. package/dist/types/reporter/src/hooks/use-vizzly-api.d.ts +9 -0
  47. package/dist/types/reporter/src/main.d.ts +1 -0
  48. package/dist/types/reporter/src/services/api-client.d.ts +4 -0
  49. package/dist/types/reporter/src/utils/comparison-helpers.d.ts +16 -0
  50. package/dist/types/reporter/src/utils/constants.d.ts +37 -0
  51. package/dist/types/reporter/vite.config.d.ts +2 -0
  52. package/dist/types/reporter/vite.dev.config.d.ts +2 -0
  53. package/dist/types/sdk/index.d.ts +2 -3
  54. package/dist/types/server/handlers/api-handler.d.ts +5 -14
  55. package/dist/types/server/handlers/tdd-handler.d.ts +18 -17
  56. package/dist/types/server/http-server.d.ts +2 -1
  57. package/dist/types/services/base-service.d.ts +1 -2
  58. package/dist/types/services/html-report-generator.d.ts +3 -3
  59. package/dist/types/services/screenshot-server.d.ts +1 -1
  60. package/dist/types/services/server-manager.d.ts +25 -35
  61. package/dist/types/services/tdd-service.d.ts +7 -1
  62. package/dist/types/services/test-runner.d.ts +6 -1
  63. package/dist/types/utils/build-history.d.ts +16 -0
  64. package/dist/types/utils/config-loader.d.ts +1 -1
  65. package/dist/types/utils/console-ui.d.ts +1 -1
  66. package/dist/types/utils/git.d.ts +4 -4
  67. package/dist/types/utils/security.d.ts +2 -1
  68. package/dist/utils/build-history.js +103 -0
  69. package/dist/utils/config-loader.js +1 -1
  70. package/dist/utils/console-ui.js +2 -1
  71. package/dist/utils/environment-config.js +1 -1
  72. package/dist/utils/security.js +14 -5
  73. package/docs/api-reference.md +2 -4
  74. package/docs/doctor-command.md +1 -1
  75. package/docs/getting-started.md +1 -1
  76. package/docs/tdd-mode.md +176 -112
  77. package/package.json +17 -4
package/dist/sdk/index.js CHANGED
@@ -30,7 +30,7 @@ import { VizzlyError } from '../errors/vizzly-error.js';
30
30
  *
31
31
  * const vizzly = await createVizzly({
32
32
  * apiKey: process.env.VIZZLY_TOKEN,
33
- * apiUrl: 'https://vizzly.dev',
33
+ * apiUrl: 'https://app.vizzly.dev',
34
34
  * server: {
35
35
  * port: 3003,
36
36
  * enabled: true
@@ -1,9 +1,41 @@
1
1
  import { Buffer } from 'buffer';
2
2
  import { createServiceLogger } from '../../utils/logger-factory.js';
3
3
  const logger = createServiceLogger('API-HANDLER');
4
+
5
+ /**
6
+ * API Handler - Non-blocking screenshot upload
7
+ *
8
+ * Flow:
9
+ * ┌─────────────────────────────────────────────────────────────┐
10
+ * │ Test Suite │
11
+ * │ ↓ vizzlyScreenshot() │
12
+ * │ ↓ HTTP POST to localhost │
13
+ * │ ↓ │
14
+ * │ Screenshot Server │
15
+ * │ ↓ handleScreenshot() │
16
+ * │ ├─→ Convert base64 to Buffer │
17
+ * │ ├─→ Fire upload promise (NO AWAIT) ─────┐ │
18
+ * │ └─→ Return 200 immediately │ │
19
+ * │ │ │
20
+ * │ Test continues (NO BLOCKING) ✓ │ │
21
+ * │ ↓ │
22
+ * │ Background Upload │
23
+ * │ (to Vizzly API) │
24
+ * │ ↓ │
25
+ * │ Promise resolves/rejects │
26
+ * │ │
27
+ * │ Build Finalization │
28
+ * │ ↓ flush() │
29
+ * │ └─→ await Promise.allSettled(uploadPromises) │
30
+ * │ ↓ │
31
+ * │ All uploads complete ✓ │
32
+ * └─────────────────────────────────────────────────────────────┘
33
+ */
34
+
4
35
  export const createApiHandler = apiService => {
5
36
  let vizzlyDisabled = false;
6
37
  let screenshotCount = 0;
38
+ let uploadPromises = [];
7
39
  const handleScreenshot = async (buildId, name, image, properties = {}) => {
8
40
  if (vizzlyDisabled) {
9
41
  logger.debug(`Screenshot captured (Vizzly disabled): ${name}`);
@@ -17,14 +49,9 @@ export const createApiHandler = apiService => {
17
49
  }
18
50
  };
19
51
  }
20
- if (!buildId) {
21
- return {
22
- statusCode: 400,
23
- body: {
24
- error: 'Build ID is required for screenshot upload'
25
- }
26
- };
27
- }
52
+
53
+ // buildId is optional - API service will handle it appropriately
54
+
28
55
  if (!apiService) {
29
56
  return {
30
57
  statusCode: 500,
@@ -33,51 +60,92 @@ export const createApiHandler = apiService => {
33
60
  }
34
61
  };
35
62
  }
36
- try {
37
- const imageBuffer = Buffer.from(image, 'base64');
38
- const result = await apiService.uploadScreenshot(buildId, name, imageBuffer, properties ?? {});
63
+ const imageBuffer = Buffer.from(image, 'base64');
64
+ screenshotCount++;
65
+
66
+ // Fire upload in background - DON'T AWAIT!
67
+ const uploadPromise = apiService.uploadScreenshot(buildId, name, imageBuffer, properties ?? {}).then(result => {
39
68
  if (result.skipped) {
40
69
  logger.debug(`Screenshot already exists, skipped: ${name}`);
41
70
  } else {
42
71
  logger.debug(`Screenshot uploaded: ${name}`);
43
72
  }
44
- if (!result.skipped) {
45
- screenshotCount++;
46
- }
47
73
  return {
48
- statusCode: 200,
49
- body: {
50
- success: true,
51
- name,
52
- skipped: result.skipped,
53
- count: screenshotCount
54
- }
74
+ success: true,
75
+ name,
76
+ result
55
77
  };
56
- } catch (uploadError) {
78
+ }).catch(uploadError => {
57
79
  logger.error(`❌ Failed to upload screenshot ${name}:`, uploadError.message);
58
80
  vizzlyDisabled = true;
59
81
  const disabledMessage = '⚠️ Vizzly disabled due to upload error - continuing tests without visual testing';
60
82
  logger.warn(disabledMessage);
61
83
  return {
62
- statusCode: 200,
63
- body: {
64
- success: true,
65
- name,
66
- disabled: true,
67
- message: disabledMessage
68
- }
84
+ success: false,
85
+ name,
86
+ error: uploadError
69
87
  };
70
- }
88
+ });
89
+
90
+ // Collect promise for later flushing
91
+ uploadPromises.push(uploadPromise);
92
+
93
+ // Return immediately - test continues without waiting!
94
+ return {
95
+ statusCode: 200,
96
+ body: {
97
+ success: true,
98
+ name,
99
+ count: screenshotCount
100
+ }
101
+ };
71
102
  };
72
103
  const getScreenshotCount = () => screenshotCount;
104
+
105
+ /**
106
+ * Wait for all background uploads to complete
107
+ * Call this before build finalization to ensure all uploads finish
108
+ */
109
+ const flush = async () => {
110
+ if (uploadPromises.length === 0) {
111
+ logger.debug('No uploads to flush');
112
+ return {
113
+ uploaded: 0,
114
+ failed: 0,
115
+ total: 0
116
+ };
117
+ }
118
+ logger.debug(`Flushing ${uploadPromises.length} background uploads...`);
119
+ const results = await Promise.allSettled(uploadPromises);
120
+ let uploaded = 0;
121
+ let failed = 0;
122
+ results.forEach(result => {
123
+ if (result.status === 'fulfilled' && result.value.success) {
124
+ uploaded++;
125
+ } else {
126
+ failed++;
127
+ }
128
+ });
129
+ logger.debug(`Upload flush complete: ${uploaded} uploaded, ${failed} failed`);
130
+
131
+ // Clear promises array
132
+ uploadPromises = [];
133
+ return {
134
+ uploaded,
135
+ failed,
136
+ total: results.length
137
+ };
138
+ };
73
139
  const cleanup = () => {
74
140
  vizzlyDisabled = false;
75
141
  screenshotCount = 0;
142
+ uploadPromises = [];
76
143
  logger.debug('API handler cleanup completed');
77
144
  };
78
145
  return {
79
146
  handleScreenshot,
80
147
  getScreenshotCount,
148
+ flush,
81
149
  cleanup
82
150
  };
83
151
  };
@@ -1,57 +1,101 @@
1
1
  import { Buffer } from 'buffer';
2
+ import { writeFileSync, readFileSync, existsSync } from 'fs';
3
+ import { join } from 'path';
2
4
  import { createServiceLogger } from '../../utils/logger-factory.js';
3
5
  import { TddService } from '../../services/tdd-service.js';
4
- import { colors } from '../../utils/colors.js';
5
6
  import { sanitizeScreenshotName, validateScreenshotProperties } from '../../utils/security.js';
6
7
  const logger = createServiceLogger('TDD-HANDLER');
7
8
  export const createTddHandler = (config, workingDir, baselineBuild, baselineComparison, setBaseline = false) => {
8
9
  const tddService = new TddService(config, workingDir, setBaseline);
9
- const builds = new Map();
10
+ const reportPath = join(workingDir, '.vizzly', 'report-data.json');
11
+ const readReportData = () => {
12
+ try {
13
+ if (!existsSync(reportPath)) {
14
+ return {
15
+ timestamp: Date.now(),
16
+ comparisons: [],
17
+ summary: {
18
+ total: 0,
19
+ passed: 0,
20
+ failed: 0,
21
+ errors: 0
22
+ }
23
+ };
24
+ }
25
+ const data = readFileSync(reportPath, 'utf8');
26
+ return JSON.parse(data);
27
+ } catch (error) {
28
+ logger.error('Failed to read report data:', error);
29
+ return {
30
+ timestamp: Date.now(),
31
+ comparisons: [],
32
+ summary: {
33
+ total: 0,
34
+ passed: 0,
35
+ failed: 0,
36
+ errors: 0
37
+ }
38
+ };
39
+ }
40
+ };
41
+ const updateComparison = newComparison => {
42
+ try {
43
+ const reportData = readReportData();
44
+
45
+ // Find existing comparison with same name and replace it, or add new one
46
+ const existingIndex = reportData.comparisons.findIndex(c => c.name === newComparison.name);
47
+ if (existingIndex >= 0) {
48
+ reportData.comparisons[existingIndex] = newComparison;
49
+ logger.debug(`Updated comparison for ${newComparison.name}`);
50
+ } else {
51
+ reportData.comparisons.push(newComparison);
52
+ logger.debug(`Added new comparison for ${newComparison.name}`);
53
+ }
54
+
55
+ // Update summary
56
+ reportData.timestamp = Date.now();
57
+ reportData.summary = {
58
+ total: reportData.comparisons.length,
59
+ passed: reportData.comparisons.filter(c => c.status === 'passed' || c.status === 'baseline-created').length,
60
+ failed: reportData.comparisons.filter(c => c.status === 'failed').length,
61
+ errors: reportData.comparisons.filter(c => c.status === 'error').length
62
+ };
63
+ writeFileSync(reportPath, JSON.stringify(reportData, null, 2));
64
+ logger.debug('Report data saved to report-data.json');
65
+ } catch (error) {
66
+ logger.error('Failed to update comparison:', error);
67
+ }
68
+ };
10
69
  const initialize = async () => {
11
- logger.info('🔄 TDD mode enabled - setting up local comparison...');
70
+ logger.debug('TDD mode enabled - setting up local comparison');
12
71
 
13
72
  // In baseline update mode, skip all baseline loading/downloading
14
73
  if (setBaseline) {
15
- logger.info('📁 Ready for new baseline creation - all screenshots will be treated as new baselines');
74
+ logger.debug('Ready for new baseline creation - all screenshots will be treated as new baselines');
16
75
  return;
17
76
  }
18
77
 
19
78
  // Check if we have baseline override flags that should force a fresh download
20
79
  const shouldForceDownload = (baselineBuild || baselineComparison) && config.apiKey;
21
80
  if (shouldForceDownload) {
22
- logger.info('📥 Baseline override specified, downloading fresh baselines from Vizzly...');
81
+ logger.debug('Baseline override specified, downloading fresh baselines from Vizzly');
23
82
  await tddService.downloadBaselines(config.build?.environment || 'test', config.build?.branch || null, baselineBuild, baselineComparison);
24
83
  return;
25
84
  }
26
85
  const baseline = await tddService.loadBaseline();
27
86
  if (!baseline) {
28
- if (config.apiKey) {
29
- logger.info('📥 No local baseline found, downloading from Vizzly...');
87
+ // Only download baselines if explicitly requested via baseline flags
88
+ if ((baselineBuild || baselineComparison) && config.apiKey) {
89
+ logger.debug('No local baseline found, downloading from Vizzly');
30
90
  await tddService.downloadBaselines(config.build?.environment || 'test', config.build?.branch || null, baselineBuild, baselineComparison);
31
91
  } else {
32
- logger.info('📝 No local baseline found and no API token - all screenshots will be marked as new');
92
+ logger.debug('No local baseline found - will create new baselines from first screenshots');
33
93
  }
34
94
  } else {
35
- logger.info(`✅ Using existing baseline: ${colors.cyan(baseline.buildName)}`);
95
+ logger.debug(`Using existing baseline: ${baseline.buildName}`);
36
96
  }
37
97
  };
38
- const registerBuild = buildId => {
39
- builds.set(buildId, {
40
- id: buildId,
41
- name: `TDD Build ${buildId}`,
42
- branch: 'current',
43
- environment: 'test',
44
- screenshots: [],
45
- createdAt: Date.now()
46
- });
47
- logger.debug(`Registered TDD build: ${buildId}`);
48
- };
49
98
  const handleScreenshot = async (buildId, name, image, properties = {}) => {
50
- const build = builds.get(buildId);
51
- if (!build) {
52
- throw new Error(`Build ${buildId} not found`);
53
- }
54
-
55
99
  // Validate and sanitize screenshot name
56
100
  let sanitizedName;
57
101
  try {
@@ -81,42 +125,44 @@ export const createTddHandler = (config, workingDir, baselineBuild, baselineComp
81
125
  }
82
126
  };
83
127
  }
128
+ const imageBuffer = Buffer.from(image, 'base64');
129
+ logger.debug(`Received screenshot: ${name}`);
130
+ logger.debug(`Image size: ${imageBuffer.length} bytes`);
131
+ logger.debug(`Properties: ${JSON.stringify(validatedProperties)}`);
84
132
 
85
- // Create unique screenshot name based on properties
86
- let uniqueName = sanitizedName;
87
- const relevantProps = [];
88
-
89
- // Add browser to name if provided (already validated)
90
- if (validatedProperties.browser) {
91
- relevantProps.push(validatedProperties.browser);
92
- }
93
-
94
- // Add viewport info if provided (already validated)
95
- if (validatedProperties.viewport && validatedProperties.viewport.width && validatedProperties.viewport.height) {
96
- relevantProps.push(`${validatedProperties.viewport.width}x${validatedProperties.viewport.height}`);
97
- }
133
+ // Use the sanitized name as-is (no modification with browser/viewport)
134
+ // Baseline matching uses signature logic (name + viewport_width + browser)
135
+ const comparison = await tddService.compareScreenshot(sanitizedName, imageBuffer, validatedProperties);
136
+ logger.debug(`Comparison result: ${comparison.status}`);
98
137
 
99
- // Combine base name with relevant properties and sanitize the result
100
- if (relevantProps.length > 0) {
101
- let proposedUniqueName = `${sanitizedName}-${relevantProps.join('-')}`;
102
- try {
103
- uniqueName = sanitizeScreenshotName(proposedUniqueName);
104
- } catch (error) {
105
- // If the combined name is invalid, fall back to the base sanitized name
106
- uniqueName = sanitizedName;
107
- logger.warn(`Combined screenshot name invalid (${error.message}), using base name: ${uniqueName}`);
138
+ // Convert absolute file paths to web-accessible URLs
139
+ const convertPathToUrl = filePath => {
140
+ if (!filePath) return null;
141
+ // Convert absolute path to relative path from .vizzly directory
142
+ const vizzlyDir = join(workingDir, '.vizzly');
143
+ if (filePath.startsWith(vizzlyDir)) {
144
+ const relativePath = filePath.substring(vizzlyDir.length + 1);
145
+ return `/images/${relativePath}`;
108
146
  }
109
- }
110
- const screenshot = {
111
- name: uniqueName,
147
+ return filePath;
148
+ };
149
+
150
+ // Record the comparison for the dashboard
151
+ const newComparison = {
152
+ name: comparison.name,
112
153
  originalName: name,
113
- imageData: image,
154
+ status: comparison.status,
155
+ baseline: convertPathToUrl(comparison.baseline),
156
+ current: convertPathToUrl(comparison.current),
157
+ diff: convertPathToUrl(comparison.diff),
158
+ diffPercentage: comparison.diffPercentage,
159
+ threshold: comparison.threshold,
114
160
  properties: validatedProperties,
115
161
  timestamp: Date.now()
116
162
  };
117
- build.screenshots.push(screenshot);
118
- const imageBuffer = Buffer.from(image, 'base64');
119
- const comparison = await tddService.compareScreenshot(uniqueName, imageBuffer, validatedProperties);
163
+
164
+ // Update comparison in report data file
165
+ updateComparison(newComparison);
120
166
  if (comparison.status === 'failed') {
121
167
  return {
122
168
  statusCode: 422,
@@ -174,39 +220,180 @@ export const createTddHandler = (config, workingDir, baselineBuild, baselineComp
174
220
  }
175
221
  };
176
222
  };
177
- const getScreenshotCount = buildId => {
178
- const build = builds.get(buildId);
179
- return build ? build.screenshots.length : 0;
223
+ const getResults = async () => {
224
+ return await tddService.printResults();
180
225
  };
181
- const finishBuild = async buildId => {
182
- const build = builds.get(buildId);
183
- if (!build) {
184
- throw new Error(`Build ${buildId} not found`);
226
+ const acceptBaseline = async screenshotName => {
227
+ try {
228
+ logger.debug(`Accepting baseline for screenshot: ${screenshotName}`);
229
+
230
+ // Use TDD service to accept the baseline
231
+ const result = await tddService.acceptBaseline(screenshotName);
232
+
233
+ // Read current report data and update the comparison status
234
+ const reportData = readReportData();
235
+ const comparison = reportData.comparisons.find(c => c.name === screenshotName);
236
+ if (comparison) {
237
+ // Update the comparison to passed status
238
+ const updatedComparison = {
239
+ ...comparison,
240
+ status: 'passed',
241
+ diffPercentage: 0,
242
+ diff: null
243
+ };
244
+ updateComparison(updatedComparison);
245
+ logger.debug('Comparison updated in report-data.json');
246
+ } else {
247
+ logger.error(`Comparison not found in report data for: ${screenshotName}`);
248
+ }
249
+ logger.info(`Baseline accepted for ${screenshotName}`);
250
+ return result;
251
+ } catch (error) {
252
+ logger.error(`Failed to accept baseline for ${screenshotName}:`, error);
253
+ throw error;
185
254
  }
186
- if (build.screenshots.length === 0) {
187
- throw new Error('No screenshots to process. Make sure your tests are calling the Vizzly screenshot function.');
255
+ };
256
+ const acceptAllBaselines = async () => {
257
+ try {
258
+ logger.debug('Accepting all baselines');
259
+ const reportData = readReportData();
260
+ let acceptedCount = 0;
261
+
262
+ // Accept all failed or new comparisons
263
+ for (const comparison of reportData.comparisons) {
264
+ if (comparison.status === 'failed' || comparison.status === 'new') {
265
+ await tddService.acceptBaseline(comparison.name);
266
+
267
+ // Update the comparison to passed status
268
+ updateComparison({
269
+ ...comparison,
270
+ status: 'passed',
271
+ diffPercentage: 0,
272
+ diff: null
273
+ });
274
+ acceptedCount++;
275
+ }
276
+ }
277
+ logger.info(`Accepted ${acceptedCount} baselines`);
278
+ return {
279
+ count: acceptedCount
280
+ };
281
+ } catch (error) {
282
+ logger.error('Failed to accept all baselines:', error);
283
+ throw error;
284
+ }
285
+ };
286
+ const resetBaselines = async () => {
287
+ try {
288
+ logger.debug('Resetting baselines');
289
+ const reportData = readReportData();
290
+ let deletedBaselines = 0;
291
+ let deletedCurrents = 0;
292
+ let deletedDiffs = 0;
293
+
294
+ // Delete all baseline, current, and diff images
295
+ for (const comparison of reportData.comparisons) {
296
+ // Delete baseline image if it exists
297
+ if (comparison.baseline) {
298
+ const baselinePath = join(workingDir, '.vizzly', comparison.baseline.replace('/images/', ''));
299
+ if (existsSync(baselinePath)) {
300
+ try {
301
+ const {
302
+ unlinkSync
303
+ } = await import('fs');
304
+ unlinkSync(baselinePath);
305
+ deletedBaselines++;
306
+ logger.debug(`Deleted baseline for ${comparison.name}`);
307
+ } catch (error) {
308
+ logger.warn(`Failed to delete baseline for ${comparison.name}: ${error.message}`);
309
+ }
310
+ }
311
+ }
312
+
313
+ // Delete current screenshot if it exists
314
+ if (comparison.current) {
315
+ const currentPath = join(workingDir, '.vizzly', comparison.current.replace('/images/', ''));
316
+ if (existsSync(currentPath)) {
317
+ try {
318
+ const {
319
+ unlinkSync
320
+ } = await import('fs');
321
+ unlinkSync(currentPath);
322
+ deletedCurrents++;
323
+ logger.debug(`Deleted current screenshot for ${comparison.name}`);
324
+ } catch (error) {
325
+ logger.warn(`Failed to delete current screenshot for ${comparison.name}: ${error.message}`);
326
+ }
327
+ }
328
+ }
329
+
330
+ // Delete diff image if it exists
331
+ if (comparison.diff) {
332
+ const diffPath = join(workingDir, '.vizzly', comparison.diff.replace('/images/', ''));
333
+ if (existsSync(diffPath)) {
334
+ try {
335
+ const {
336
+ unlinkSync
337
+ } = await import('fs');
338
+ unlinkSync(diffPath);
339
+ deletedDiffs++;
340
+ logger.debug(`Deleted diff for ${comparison.name}`);
341
+ } catch (error) {
342
+ logger.warn(`Failed to delete diff for ${comparison.name}: ${error.message}`);
343
+ }
344
+ }
345
+ }
346
+ }
347
+
348
+ // Delete baseline metadata
349
+ const metadataPath = join(workingDir, '.vizzly', 'baselines', 'metadata.json');
350
+ if (existsSync(metadataPath)) {
351
+ try {
352
+ const {
353
+ unlinkSync
354
+ } = await import('fs');
355
+ unlinkSync(metadataPath);
356
+ logger.debug('Deleted baseline metadata');
357
+ } catch (error) {
358
+ logger.warn(`Failed to delete baseline metadata: ${error.message}`);
359
+ }
360
+ }
361
+
362
+ // Clear the report data entirely - fresh start
363
+ const freshReportData = {
364
+ timestamp: Date.now(),
365
+ comparisons: [],
366
+ summary: {
367
+ total: 0,
368
+ passed: 0,
369
+ failed: 0,
370
+ errors: 0
371
+ }
372
+ };
373
+ writeFileSync(reportPath, JSON.stringify(freshReportData, null, 2));
374
+ logger.info(`Baselines reset - ${deletedBaselines} baselines deleted, ${deletedCurrents} current screenshots deleted, ${deletedDiffs} diffs deleted`);
375
+ return {
376
+ success: true,
377
+ deletedBaselines,
378
+ deletedCurrents,
379
+ deletedDiffs
380
+ };
381
+ } catch (error) {
382
+ logger.error('Failed to reset baselines:', error);
383
+ throw error;
188
384
  }
189
- const results = await tddService.printResults();
190
- builds.delete(buildId);
191
- return {
192
- id: buildId,
193
- name: build.name,
194
- tddMode: true,
195
- results,
196
- url: null,
197
- passed: results.failed === 0 && results.errors === 0
198
- };
199
385
  };
200
386
  const cleanup = () => {
201
- builds.clear();
387
+ // Report data is persisted to file, no in-memory cleanup needed
202
388
  logger.debug('TDD handler cleanup completed');
203
389
  };
204
390
  return {
205
391
  initialize,
206
- registerBuild,
207
392
  handleScreenshot,
208
- getScreenshotCount,
209
- finishBuild,
393
+ getResults,
394
+ acceptBaseline,
395
+ acceptAllBaselines,
396
+ resetBaselines,
210
397
  cleanup
211
398
  };
212
399
  };