@vizzly-testing/cli 0.29.6 → 0.31.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 (87) hide show
  1. package/README.md +34 -6
  2. package/dist/api/client.js +6 -3
  3. package/dist/api/endpoints.js +59 -0
  4. package/dist/api/index.js +1 -1
  5. package/dist/auth/core.js +1 -1
  6. package/dist/auth/index.js +2 -1
  7. package/dist/cli.js +206 -51
  8. package/dist/client/index.js +21 -29
  9. package/dist/commands/api.js +145 -90
  10. package/dist/commands/baselines.js +78 -47
  11. package/dist/commands/builds.js +9 -8
  12. package/dist/commands/comparisons.js +7 -8
  13. package/dist/commands/config-cmd.js +15 -7
  14. package/dist/commands/context.js +870 -0
  15. package/dist/commands/doctor.js +116 -81
  16. package/dist/commands/init.js +202 -201
  17. package/dist/commands/login.js +134 -65
  18. package/dist/commands/logout.js +18 -7
  19. package/dist/commands/preview.js +29 -57
  20. package/dist/commands/project.js +120 -0
  21. package/dist/commands/projects.js +9 -2
  22. package/dist/commands/review.js +236 -202
  23. package/dist/commands/run.js +75 -72
  24. package/dist/commands/status.js +228 -150
  25. package/dist/commands/tdd-daemon-process.js +98 -0
  26. package/dist/commands/tdd-daemon.js +318 -212
  27. package/dist/commands/tdd.js +23 -8
  28. package/dist/commands/upload.js +10 -9
  29. package/dist/commands/whoami.js +64 -26
  30. package/dist/config/core.js +37 -36
  31. package/dist/context/local-workspace-provider.js +532 -0
  32. package/dist/context/provider-resolver.js +36 -0
  33. package/dist/index.js +5 -4
  34. package/dist/plugin-loader.js +55 -49
  35. package/dist/project/core.js +2 -2
  36. package/dist/project/operations.js +6 -3
  37. package/dist/reporter/reporter-bundle.css +2 -1
  38. package/dist/reporter/reporter-bundle.iife.js +119 -119
  39. package/dist/reporter-ssr/ssr-entry.js +605 -509
  40. package/dist/sdk/index.js +162 -107
  41. package/dist/server/handlers/tdd-handler.js +30 -54
  42. package/dist/server/routers/assets.js +4 -0
  43. package/dist/server/routers/baseline.js +195 -178
  44. package/dist/server/routers/events.js +226 -148
  45. package/dist/server/routers/projects.js +14 -5
  46. package/dist/server/routers/screenshot.js +1 -31
  47. package/dist/services/build-manager.js +19 -97
  48. package/dist/services/config-service.js +26 -34
  49. package/dist/services/index.js +2 -4
  50. package/dist/services/static-report-generator.js +11 -12
  51. package/dist/services/test-runner.js +21 -27
  52. package/dist/tdd/core/hotspot-coverage.js +1 -1
  53. package/dist/tdd/index.js +1 -1
  54. package/dist/tdd/metadata/region-metadata.js +0 -37
  55. package/dist/tdd/runtime-context.js +78 -0
  56. package/dist/tdd/server-registry.js +38 -17
  57. package/dist/tdd/services/baseline-manager.js +23 -0
  58. package/dist/tdd/services/hotspot-service.js +1 -14
  59. package/dist/tdd/tdd-service.js +82 -113
  60. package/dist/types/client.d.ts +36 -1
  61. package/dist/types/index.d.ts +114 -22
  62. package/dist/types/sdk.d.ts +47 -7
  63. package/dist/uploader/core.js +5 -4
  64. package/dist/uploader/index.js +79 -2
  65. package/dist/uploader/operations.js +40 -5
  66. package/dist/utils/api-url.js +14 -0
  67. package/dist/utils/async-utils.js +26 -0
  68. package/dist/utils/ci-env.js +19 -12
  69. package/dist/utils/colors.js +46 -44
  70. package/dist/utils/config-loader.js +39 -65
  71. package/dist/utils/config-schema.js +19 -55
  72. package/dist/utils/fetch-utils.js +12 -5
  73. package/dist/utils/git.js +35 -72
  74. package/dist/utils/global-config.js +22 -3
  75. package/dist/utils/output.js +19 -7
  76. package/dist/utils/patterns.js +14 -0
  77. package/dist/utils/project-link-store.js +105 -0
  78. package/dist/utils/report-data.js +31 -0
  79. package/dist/utils/screenshot-options.js +28 -0
  80. package/dist/utils/secret-store.js +65 -0
  81. package/dist/utils/security.js +3 -2
  82. package/package.json +14 -11
  83. package/dist/services/screenshot-server.js +0 -65
  84. package/dist/services/server-manager.js +0 -81
  85. package/dist/services/uploader.js +0 -100
  86. package/dist/tdd/services/region-service.js +0 -75
  87. package/dist/utils/build-history.js +0 -103
package/dist/sdk/index.js CHANGED
@@ -11,13 +11,15 @@
11
11
  */
12
12
 
13
13
  import { EventEmitter } from 'node:events';
14
+ import { createServer } from 'node:http';
14
15
  import { VizzlyError } from '../errors/vizzly-error.js';
15
- import { ScreenshotServer } from '../services/screenshot-server.js';
16
- import { createUploader } from '../services/uploader.js';
16
+ import { handleRequest as handleScreenshotRequest, startServer as startScreenshotServer, stopServer as stopScreenshotServer } from '../screenshot-server/index.js';
17
17
  import { createTDDService } from '../tdd/tdd-service.js';
18
+ import { createUploader } from '../uploader/index.js';
18
19
  import { loadConfig } from '../utils/config-loader.js';
19
20
  import { resolveImageBuffer } from '../utils/file-helpers.js';
20
21
  import * as output from '../utils/output.js';
22
+ import { createScreenshotProperties } from '../utils/screenshot-options.js';
21
23
 
22
24
  /**
23
25
  * Create a new Vizzly instance with custom configuration
@@ -52,79 +54,94 @@ import * as output from '../utils/output.js';
52
54
  * // Cleanup
53
55
  * await vizzly.stop();
54
56
  */
55
- export function createVizzly(config = {}, options = {}) {
57
+ export async function createVizzly(config = {}, options = {}) {
56
58
  // Configure output based on options
57
59
  output.configure({
58
60
  verbose: options.verbose || false
59
61
  });
60
-
61
- // Merge with loaded config
62
- const resolvedConfig = {
62
+ let loadConfigFn = options.loadConfig || loadConfig;
63
+ let fileConfig = await loadConfigFn();
64
+ let resolvedConfig = {
65
+ ...fileConfig,
63
66
  ...config
64
67
  };
65
-
66
- /**
67
- * Initialize SDK with config loading
68
- */
69
- const init = async () => {
70
- const fileConfig = await loadConfig();
71
- Object.assign(resolvedConfig, fileConfig, config); // CLI config takes precedence
72
- return resolvedConfig;
73
- };
74
-
75
- /**
76
- * Create uploader service
77
- */
78
- const createUploaderService = (uploaderOptions = {}) => {
79
- return createUploader({
80
- apiKey: resolvedConfig.apiKey,
81
- apiUrl: resolvedConfig.apiUrl
82
- }, {
83
- ...options,
84
- ...uploaderOptions
85
- });
86
- };
87
-
88
- /**
89
- * Create TDD service
90
- */
91
- const createTDDServiceInstance = (tddOptions = {}) => {
92
- return createTDDService(resolvedConfig, {
93
- ...options,
94
- ...tddOptions
95
- });
96
- };
97
-
98
- /**
99
- * Upload screenshots (convenience method)
100
- */
101
- const upload = async uploadOptions => {
102
- const uploader = createUploaderService();
103
- return uploader.upload(uploadOptions);
68
+ return new VizzlySDK(resolvedConfig, {
69
+ createUploader: (uploaderOptions = {}, activeConfig = resolvedConfig) => {
70
+ let {
71
+ upload: uploadConfig = activeConfig.upload,
72
+ ...serviceOptions
73
+ } = uploaderOptions;
74
+ let createUploaderService = options.createUploader || createUploader;
75
+ return createUploaderService({
76
+ apiKey: activeConfig.apiKey,
77
+ apiUrl: activeConfig.apiUrl,
78
+ upload: uploadConfig
79
+ }, {
80
+ ...options,
81
+ ...serviceOptions
82
+ });
83
+ },
84
+ createTDDService: (tddOptions = {}, activeConfig = resolvedConfig) => {
85
+ let createTDDServiceInstance = options.createTDDService || createTDDService;
86
+ return createTDDServiceInstance(activeConfig, {
87
+ ...options,
88
+ ...tddOptions
89
+ });
90
+ },
91
+ createScreenshotServer: createLocalScreenshotServer,
92
+ fetch: options.fetch,
93
+ loadConfig: loadConfigFn
94
+ });
95
+ }
96
+ function createLocalBuildManager() {
97
+ let screenshots = new Map();
98
+ return {
99
+ async addScreenshot(buildId, screenshot) {
100
+ if (!screenshots.has(buildId)) {
101
+ screenshots.set(buildId, []);
102
+ }
103
+ screenshots.get(buildId).push(screenshot);
104
+ },
105
+ getScreenshots(buildId) {
106
+ return screenshots.get(buildId) || [];
107
+ }
104
108
  };
105
-
106
- /**
107
- * Start TDD mode (convenience method)
108
- */
109
- const startTDD = async (tddOptions = {}) => {
110
- const tddService = createTDDServiceInstance();
111
- return tddService.start(tddOptions);
109
+ }
110
+ function createLocalScreenshotServer(config, buildManager, options = {}) {
111
+ let server = null;
112
+ let deps = {
113
+ createHttpServer: options.createHttpServer || createServer,
114
+ output: options.output || output,
115
+ createError: options.createError || ((message, code) => new VizzlyError(message, code))
112
116
  };
113
117
  return {
114
- // Core methods
115
- init,
116
- upload,
117
- startTDD,
118
- // Service factories
119
- createUploader: createUploaderService,
120
- createTDDService: createTDDServiceInstance,
121
- // Utilities
122
- loadConfig: () => loadConfig(),
123
- // Config access
124
- getConfig: () => ({
125
- ...resolvedConfig
126
- }),
127
- updateConfig: newConfig => Object.assign(resolvedConfig, newConfig)
118
+ async start() {
119
+ server = await startScreenshotServer({
120
+ config,
121
+ requestHandler: (req, res) => handleScreenshotRequest({
122
+ req,
123
+ res,
124
+ deps: {
125
+ buildManager,
126
+ createError: deps.createError,
127
+ output: deps.output
128
+ }
129
+ }),
130
+ deps
131
+ });
132
+ },
133
+ async stop() {
134
+ await stopScreenshotServer({
135
+ server,
136
+ deps: {
137
+ output: deps.output
138
+ }
139
+ });
140
+ server = null;
141
+ },
142
+ isRunning() {
143
+ return Boolean(server?.listening);
144
+ }
128
145
  };
129
146
  }
130
147
 
@@ -153,7 +170,7 @@ export class VizzlySDK extends EventEmitter {
153
170
  constructor(config, services) {
154
171
  super();
155
172
  this.config = config;
156
- this.services = services;
173
+ this.services = services || {};
157
174
  this.server = null;
158
175
  this.currentBuildId = null;
159
176
  }
@@ -181,6 +198,59 @@ export class VizzlySDK extends EventEmitter {
181
198
  };
182
199
  }
183
200
 
201
+ /**
202
+ * Merge new config values into the active SDK config.
203
+ * @param {Object} newConfig - Config updates
204
+ * @returns {Object} Updated config
205
+ */
206
+ updateConfig(newConfig) {
207
+ Object.assign(this.config, newConfig);
208
+ return this.getConfig();
209
+ }
210
+
211
+ /**
212
+ * Reload file config and re-apply current in-memory overrides.
213
+ * @returns {Promise<Object>} Updated config
214
+ */
215
+ async init() {
216
+ let loadConfigFn = this.services.loadConfig || loadConfig;
217
+ let fileConfig = await loadConfigFn();
218
+ let currentConfig = {
219
+ ...this.config
220
+ };
221
+ Object.assign(this.config, fileConfig, currentConfig);
222
+ return this.getConfig();
223
+ }
224
+ createUploader(options = {}) {
225
+ let {
226
+ upload: uploadConfig = this.config.upload,
227
+ ...serviceOptions
228
+ } = options;
229
+ let createUploaderService = this.services.createUploader || ((uploaderOptions, activeConfig = this.config) => {
230
+ let {
231
+ upload: resolvedUploadConfig = activeConfig.upload,
232
+ ...resolvedServiceOptions
233
+ } = uploaderOptions;
234
+ return createUploader({
235
+ apiKey: activeConfig.apiKey,
236
+ apiUrl: activeConfig.apiUrl,
237
+ upload: resolvedUploadConfig
238
+ }, resolvedServiceOptions);
239
+ });
240
+ return createUploaderService({
241
+ upload: uploadConfig,
242
+ ...serviceOptions
243
+ }, this.config);
244
+ }
245
+ createTDDService(options = {}) {
246
+ let createTDDServiceInstance = this.services.createTDDService || ((tddOptions, activeConfig = this.config) => createTDDService(activeConfig, tddOptions));
247
+ return createTDDServiceInstance(options, this.config);
248
+ }
249
+ async startTDD(options = {}) {
250
+ let tddService = this.createTDDService(options);
251
+ return tddService.start(options);
252
+ }
253
+
184
254
  /**
185
255
  * Start the Vizzly server
186
256
  * @returns {Promise<{port: number, url: string}>} Server information
@@ -193,25 +263,12 @@ export class VizzlySDK extends EventEmitter {
193
263
  url: `http://localhost:${this.config.server?.port || 3000}`
194
264
  };
195
265
  }
196
-
197
- // Create a simple build manager for screenshot collection
198
- const buildManager = {
199
- screenshots: new Map(),
200
- currentBuildId: null,
201
- async addScreenshot(buildId, screenshot) {
202
- if (!this.screenshots.has(buildId)) {
203
- this.screenshots.set(buildId, []);
204
- }
205
- this.screenshots.get(buildId).push(screenshot);
206
- },
207
- getScreenshots(buildId) {
208
- return this.screenshots.get(buildId) || [];
209
- }
210
- };
211
- this.server = new ScreenshotServer(this.config, buildManager);
266
+ let createScreenshotServer = this.services.createScreenshotServer || createLocalScreenshotServer;
267
+ let buildManager = createLocalBuildManager();
268
+ this.server = createScreenshotServer(this.config, buildManager);
212
269
  await this.server.start();
213
- const port = this.config.server?.port || 3000;
214
- const serverInfo = {
270
+ let port = this.config.server?.port || 3000;
271
+ let serverInfo = {
215
272
  port,
216
273
  url: `http://localhost:${port}`
217
274
  };
@@ -230,31 +287,32 @@ export class VizzlySDK extends EventEmitter {
230
287
  * @throws {VizzlyError} When file cannot be read due to permissions or I/O errors
231
288
  */
232
289
  async screenshot(name, imageBuffer, options = {}) {
233
- if (!this.server || !this.server.isRunning()) {
290
+ if (this.server?.isRunning?.() !== true) {
234
291
  throw new VizzlyError('Server not running. Call start() first.', 'SERVER_NOT_RUNNING');
235
292
  }
236
293
 
237
294
  // Resolve Buffer or file path using shared utility
238
- const buffer = resolveImageBuffer(imageBuffer, 'screenshot');
295
+ let buffer = resolveImageBuffer(imageBuffer, 'screenshot');
239
296
 
240
297
  // Generate or use provided build ID
241
- const buildId = options.buildId || this.currentBuildId || 'default';
298
+ let buildId = options.buildId || this.currentBuildId || 'default';
242
299
  this.currentBuildId = buildId;
243
300
 
244
301
  // Convert Buffer to base64 for JSON transport
245
- const imageBase64 = buffer.toString('base64');
246
- const screenshotData = {
302
+ let imageBase64 = buffer.toString('base64');
303
+ let screenshotData = {
247
304
  buildId,
248
305
  name,
249
306
  image: imageBase64,
250
307
  type: 'base64',
251
- properties: options.properties || {}
308
+ properties: createScreenshotProperties(options)
252
309
  };
253
310
 
254
311
  // POST to the local screenshot server
255
- const serverUrl = `http://localhost:${this.config.server?.port || 3000}`;
312
+ let serverUrl = `http://localhost:${this.config.server?.port || 3000}`;
313
+ let fetchFn = this.services.fetch || fetch;
256
314
  try {
257
- const response = await fetch(`${serverUrl}/screenshot`, {
315
+ let response = await fetchFn(`${serverUrl}/screenshot`, {
258
316
  method: 'POST',
259
317
  headers: {
260
318
  'Content-Type': 'application/json'
@@ -262,7 +320,7 @@ export class VizzlySDK extends EventEmitter {
262
320
  body: JSON.stringify(screenshotData)
263
321
  });
264
322
  if (!response.ok) {
265
- const errorData = await response.json().catch(() => ({
323
+ let errorData = await response.json().catch(() => ({
266
324
  error: 'Unknown error'
267
325
  }));
268
326
  throw new VizzlyError(`Screenshot capture failed: ${errorData.error}`, 'SCREENSHOT_FAILED', {
@@ -295,23 +353,21 @@ export class VizzlySDK extends EventEmitter {
295
353
  async upload(options = {}) {
296
354
  if (!this.services?.uploader) {
297
355
  this.services = this.services || {};
298
- this.services.uploader = createUploader({
299
- apiKey: this.config.apiKey,
300
- apiUrl: this.config.apiUrl,
356
+ this.services.uploader = this.createUploader({
301
357
  upload: this.config.upload
302
358
  });
303
359
  }
304
360
 
305
361
  // Get the screenshots directory from config or default
306
- const screenshotsDir = options.screenshotsDir || this.config?.upload?.screenshotsDir || './screenshots';
307
- const uploadOptions = {
362
+ let screenshotsDir = options.screenshotsDir || this.config?.upload?.screenshotsDir || './screenshots';
363
+ let uploadOptions = {
308
364
  screenshotsDir,
309
365
  buildName: options.buildName || this.config.buildName,
310
366
  branch: options.branch || this.config.branch,
311
367
  commit: options.commit || this.config.commit,
312
368
  message: options.message || this.config.message,
313
369
  environment: options.environment || this.config.environment || 'production',
314
- threshold: options.threshold || this.config.threshold,
370
+ threshold: options.threshold ?? this.config.threshold,
315
371
  onProgress: progress => {
316
372
  this.emit('upload:progress', progress);
317
373
  if (options.onProgress) {
@@ -320,7 +376,7 @@ export class VizzlySDK extends EventEmitter {
320
376
  }
321
377
  };
322
378
  try {
323
- const result = await this.services.uploader.upload(uploadOptions);
379
+ let result = await this.services.uploader.upload(uploadOptions);
324
380
  this.emit('upload:completed', result);
325
381
  return result;
326
382
  } catch (error) {
@@ -340,13 +396,13 @@ export class VizzlySDK extends EventEmitter {
340
396
  async compare(name, imageBuffer) {
341
397
  if (!this.services?.tddService) {
342
398
  this.services = this.services || {};
343
- this.services.tddService = createTDDService(this.config);
399
+ this.services.tddService = this.createTDDService();
344
400
  }
345
401
 
346
402
  // Resolve Buffer or file path using shared utility
347
- const buffer = resolveImageBuffer(imageBuffer, 'compare');
403
+ let buffer = resolveImageBuffer(imageBuffer, 'compare');
348
404
  try {
349
- const result = await this.services.tddService.compareScreenshot(name, buffer);
405
+ let result = await this.services.tddService.compareScreenshot(name, buffer);
350
406
  this.emit('comparison:completed', result);
351
407
  return result;
352
408
  } catch (error) {
@@ -358,10 +414,9 @@ export class VizzlySDK extends EventEmitter {
358
414
  }
359
415
  }
360
416
  }
361
-
362
- // Export service creators for advanced usage
363
- export { createUploader } from '../services/uploader.js';
364
417
  export { createTDDService } from '../tdd/tdd-service.js';
418
+ // Export service creators for advanced usage
419
+ export { createUploader } from '../uploader/index.js';
365
420
  // Re-export key utilities and errors
366
421
  export { loadConfig } from '../utils/config-loader.js';
367
422
  export * as output from '../utils/output.js';
@@ -157,6 +157,16 @@ export const createTddHandler = (config, workingDir, baselineBuild, baselineComp
157
157
  const tddService = new TddService(config, workingDir, setBaseline);
158
158
  const reportPath = join(workingDir, '.vizzly', 'report-data.json');
159
159
  const detailsPath = join(workingDir, '.vizzly', 'comparison-details.json');
160
+ let clearJsonFile = path => {
161
+ if (existsSync(path)) {
162
+ unlinkSync(path);
163
+ }
164
+ };
165
+ let prepareRunArtifacts = () => {
166
+ clearJsonFile(reportPath);
167
+ clearJsonFile(detailsPath);
168
+ tddService.clearRunData?.();
169
+ };
160
170
 
161
171
  /**
162
172
  * Read heavy comparison details from comparison-details.json
@@ -267,6 +277,7 @@ export const createTddHandler = (config, workingDir, baselineBuild, baselineComp
267
277
  };
268
278
  const initialize = async () => {
269
279
  output.debug('tdd', 'initializing local mode');
280
+ prepareRunArtifacts();
270
281
 
271
282
  // In baseline update mode, skip all baseline loading/downloading
272
283
  if (setBaseline) {
@@ -631,68 +642,33 @@ export const createTddHandler = (config, workingDir, baselineBuild, baselineComp
631
642
  let deletedBaselines = 0;
632
643
  let deletedCurrents = 0;
633
644
  let deletedDiffs = 0;
634
-
635
- // Delete all baseline, current, and diff images
636
- for (const comparison of reportData.comparisons) {
637
- // Delete baseline image if it exists
638
- if (comparison.baseline) {
639
- const baselinePath = join(workingDir, '.vizzly', comparison.baseline.replace('/images/', ''));
640
- if (existsSync(baselinePath)) {
641
- try {
642
- const {
643
- unlinkSync
644
- } = await import('node:fs');
645
- unlinkSync(baselinePath);
646
- deletedBaselines++;
647
- // Silent deletion
648
- } catch (error) {
649
- output.warn(`Failed to delete baseline for ${comparison.name}: ${error.message}`);
650
- }
651
- }
652
- }
653
-
654
- // Delete current screenshot if it exists
655
- if (comparison.current) {
656
- const currentPath = join(workingDir, '.vizzly', comparison.current.replace('/images/', ''));
657
- if (existsSync(currentPath)) {
658
- try {
659
- const {
660
- unlinkSync
661
- } = await import('node:fs');
662
- unlinkSync(currentPath);
663
- deletedCurrents++;
664
- // Silent deletion
665
- } catch (error) {
666
- output.warn(`Failed to delete current screenshot for ${comparison.name}: ${error.message}`);
667
- }
645
+ let deleteResetFile = (imagePath, label, name) => {
646
+ if (!imagePath) return 0;
647
+ try {
648
+ let filePath = safePath(workingDir, '.vizzly', imagePath.replace('/images/', ''));
649
+ if (!existsSync(filePath)) return 0;
650
+ unlinkSync(filePath);
651
+ if (label === 'diff') {
652
+ output.debug(`Deleted diff for ${name}`);
668
653
  }
654
+ return 1;
655
+ } catch (error) {
656
+ output.warn(`Failed to delete ${label} for ${name}: ${error.message}`);
657
+ return 0;
669
658
  }
659
+ };
670
660
 
671
- // Delete diff image if it exists
672
- if (comparison.diff) {
673
- const diffPath = join(workingDir, '.vizzly', comparison.diff.replace('/images/', ''));
674
- if (existsSync(diffPath)) {
675
- try {
676
- const {
677
- unlinkSync
678
- } = await import('node:fs');
679
- unlinkSync(diffPath);
680
- deletedDiffs++;
681
- output.debug(`Deleted diff for ${comparison.name}`);
682
- } catch (error) {
683
- output.warn(`Failed to delete diff for ${comparison.name}: ${error.message}`);
684
- }
685
- }
686
- }
661
+ // Delete all baseline, current, and diff images
662
+ for (let comparison of reportData.comparisons) {
663
+ deletedBaselines += deleteResetFile(comparison.baseline, 'baseline', comparison.name);
664
+ deletedCurrents += deleteResetFile(comparison.current, 'current screenshot', comparison.name);
665
+ deletedDiffs += deleteResetFile(comparison.diff, 'diff', comparison.name);
687
666
  }
688
667
 
689
668
  // Delete baseline metadata
690
- const metadataPath = join(workingDir, '.vizzly', 'baselines', 'metadata.json');
669
+ let metadataPath = join(workingDir, '.vizzly', 'baselines', 'metadata.json');
691
670
  if (existsSync(metadataPath)) {
692
671
  try {
693
- const {
694
- unlinkSync
695
- } = await import('node:fs');
696
672
  unlinkSync(metadataPath);
697
673
  output.debug('Deleted baseline metadata');
698
674
  } catch (error) {
@@ -72,6 +72,10 @@ export function createAssetsRouter() {
72
72
  if (existsSync(fullImagePath)) {
73
73
  try {
74
74
  const imageData = readFileSync(fullImagePath);
75
+ // Images are rewritten in place between TDD runs, so disable browser caching.
76
+ res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate');
77
+ res.setHeader('Pragma', 'no-cache');
78
+ res.setHeader('Expires', '0');
75
79
  sendFile(res, imageData, 'image/png');
76
80
  return true;
77
81
  } catch (error) {