@vizzly-testing/cli 0.34.0 → 0.35.1

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 (37) hide show
  1. package/README.md +67 -7
  2. package/dist/api/core.js +18 -4
  3. package/dist/api/endpoints.js +38 -15
  4. package/dist/api/index.js +1 -1
  5. package/dist/cli.js +270 -194
  6. package/dist/client/index.js +62 -21
  7. package/dist/commands/api.js +9 -3
  8. package/dist/commands/preview.js +30 -26
  9. package/dist/commands/project.js +1 -1
  10. package/dist/commands/run.js +25 -10
  11. package/dist/commands/status.js +14 -1
  12. package/dist/commands/tdd-daemon-process.js +5 -1
  13. package/dist/commands/tdd-daemon.js +217 -139
  14. package/dist/commands/tdd.js +26 -5
  15. package/dist/commands/upload.js +31 -13
  16. package/dist/reporter/reporter-bundle.iife.js +6 -6
  17. package/dist/sdk/index.js +23 -9
  18. package/dist/server/handlers/api-handler.js +3 -2
  19. package/dist/server/handlers/tdd-handler.js +5 -1
  20. package/dist/server/routers/screenshot.js +11 -4
  21. package/dist/services/build-manager.js +6 -1
  22. package/dist/tdd/tdd-service.js +6 -2
  23. package/dist/test-runner/core.js +8 -3
  24. package/dist/test-runner/operations.js +19 -3
  25. package/dist/types/client.d.ts +32 -5
  26. package/dist/types/index.d.ts +33 -5
  27. package/dist/uploader/core.js +41 -9
  28. package/dist/uploader/operations.js +19 -17
  29. package/dist/utils/config-loader.js +13 -6
  30. package/dist/utils/context.js +46 -76
  31. package/dist/utils/environment-config.js +20 -0
  32. package/dist/utils/output.js +8 -5
  33. package/dist/utils/screenshot-options.js +60 -10
  34. package/package.json +1 -1
  35. package/skills/vizzly/SKILL.md +12 -4
  36. package/skills/vizzly/references/cli-context.md +8 -2
  37. package/skills/vizzly/references/setup-ci.md +3 -1
package/dist/sdk/index.js CHANGED
@@ -19,7 +19,7 @@ import { createUploader } from '../uploader/index.js';
19
19
  import { loadConfig } from '../utils/config-loader.js';
20
20
  import { resolveImageBuffer } from '../utils/file-helpers.js';
21
21
  import * as output from '../utils/output.js';
22
- import { createScreenshotProperties } from '../utils/screenshot-options.js';
22
+ import { normalizeScreenshotOptions } from '../utils/screenshot-options.js';
23
23
 
24
24
  /**
25
25
  * Create a new Vizzly instance with custom configuration
@@ -293,9 +293,16 @@ export class VizzlySDK extends EventEmitter {
293
293
 
294
294
  // Resolve Buffer or file path using shared utility
295
295
  let buffer = resolveImageBuffer(imageBuffer, 'screenshot');
296
+ let normalizedOptions = normalizeScreenshotOptions(options);
297
+ for (let warning of normalizedOptions.warnings) {
298
+ output.warn(warning.message, {
299
+ code: warning.code,
300
+ option: warning.option
301
+ });
302
+ }
296
303
 
297
304
  // Generate or use provided build ID
298
- let buildId = options.buildId || this.currentBuildId || 'default';
305
+ let buildId = normalizedOptions.buildId || this.currentBuildId || 'default';
299
306
  this.currentBuildId = buildId;
300
307
 
301
308
  // Convert Buffer to base64 for JSON transport
@@ -305,8 +312,11 @@ export class VizzlySDK extends EventEmitter {
305
312
  name,
306
313
  image: imageBase64,
307
314
  type: 'base64',
308
- properties: createScreenshotProperties(options)
315
+ properties: normalizedOptions.properties
309
316
  };
317
+ if (normalizedOptions.warnings.length > 0) {
318
+ screenshotData.warnings = normalizedOptions.warnings;
319
+ }
310
320
 
311
321
  // POST to the local screenshot server
312
322
  let serverUrl = `http://localhost:${this.config.server?.port || 3000}`;
@@ -362,12 +372,16 @@ export class VizzlySDK extends EventEmitter {
362
372
  let screenshotsDir = options.screenshotsDir || this.config?.upload?.screenshotsDir || './screenshots';
363
373
  let uploadOptions = {
364
374
  screenshotsDir,
365
- buildName: options.buildName || this.config.buildName,
366
- branch: options.branch || this.config.branch,
367
- commit: options.commit || this.config.commit,
368
- message: options.message || this.config.message,
369
- environment: options.environment || this.config.environment || 'production',
370
- threshold: options.threshold ?? this.config.threshold,
375
+ buildName: options.buildName || this.config.build?.name,
376
+ branch: options.branch || this.config.build?.branch,
377
+ commit: options.commit || this.config.build?.commit,
378
+ message: options.message || this.config.build?.message,
379
+ environment: options.environment || this.config.build?.environment || 'production',
380
+ threshold: options.threshold ?? this.config.comparison?.threshold,
381
+ minClusterSize: options.minClusterSize ?? this.config.comparison?.minClusterSize,
382
+ metadata: options.metadata || {},
383
+ pullRequestNumber: options.pullRequestNumber,
384
+ parallelId: options.parallelId || this.config.parallelId,
371
385
  onProgress: progress => {
372
386
  this.emit('upload:progress', progress);
373
387
  if (options.onProgress) {
@@ -47,7 +47,7 @@ export const createApiHandler = (client, {
47
47
  let vizzlyDisabled = false;
48
48
  let screenshotCount = 0;
49
49
  let uploadPromises = [];
50
- const handleScreenshot = async (buildId, name, image, properties = {}, type) => {
50
+ const handleScreenshot = async (buildId, name, image, properties = {}, type, warnings = []) => {
51
51
  let handlerStart = Date.now();
52
52
  output.debug('upload', `${name} received`, {
53
53
  buildId: buildId?.slice(0, 8)
@@ -177,7 +177,8 @@ export const createApiHandler = (client, {
177
177
  body: {
178
178
  success: true,
179
179
  name,
180
- count: screenshotCount
180
+ count: screenshotCount,
181
+ warnings
181
182
  }
182
183
  };
183
184
  };
@@ -305,7 +305,7 @@ export const createTddHandler = (config, workingDir, baselineBuild, baselineComp
305
305
  output.debug('tdd', `baseline: ${baseline.buildName}`);
306
306
  }
307
307
  };
308
- const handleScreenshot = async (_buildId, name, image, properties = {}, type) => {
308
+ const handleScreenshot = async (_buildId, name, image, properties = {}, type, warnings = []) => {
309
309
  let handlerStart = Date.now();
310
310
  output.debug('tdd', `${name} received`);
311
311
 
@@ -447,6 +447,7 @@ export const createTddHandler = (config, workingDir, baselineBuild, baselineComp
447
447
  aaPercentage: comparison.aaPercentage,
448
448
  heightDiff: comparison.heightDiff,
449
449
  error: comparison.error,
450
+ warnings,
450
451
  originalName: name,
451
452
  timestamp: Date.now(),
452
453
  // Boolean hints so UI can show toggle buttons without fetching heavy data
@@ -490,6 +491,7 @@ export const createTddHandler = (config, workingDir, baselineBuild, baselineComp
490
491
  diff: comparison.diff,
491
492
  diffPercentage: comparison.diffPercentage,
492
493
  threshold: comparison.threshold,
494
+ warnings,
493
495
  tddMode: true
494
496
  }
495
497
  };
@@ -503,6 +505,7 @@ export const createTddHandler = (config, workingDir, baselineBuild, baselineComp
503
505
  message: `Baseline updated for '${name}'`,
504
506
  baseline: comparison.baseline,
505
507
  current: comparison.current,
508
+ warnings,
506
509
  tddMode: true
507
510
  }
508
511
  };
@@ -530,6 +533,7 @@ export const createTddHandler = (config, workingDir, baselineBuild, baselineComp
530
533
  name: comparison.name,
531
534
  baseline: comparison.baseline,
532
535
  current: comparison.current,
536
+ warnings,
533
537
  tddMode: true
534
538
  }
535
539
  };
@@ -32,7 +32,8 @@ export function createScreenshotRouter({
32
32
  name,
33
33
  properties,
34
34
  image,
35
- type
35
+ type,
36
+ warnings
36
37
  } = body;
37
38
  if (!name || !image) {
38
39
  sendError(res, 400, 'name and image are required');
@@ -41,7 +42,7 @@ export function createScreenshotRouter({
41
42
 
42
43
  // Use buildId from request body, or fall back to server's buildId
43
44
  const effectiveBuildId = buildId || defaultBuildId;
44
- const result = await screenshotHandler.handleScreenshot(effectiveBuildId, name, image, properties, type);
45
+ const result = await screenshotHandler.handleScreenshot(effectiveBuildId, name, image, properties, type, warnings);
45
46
  sendJson(res, result.statusCode, result.body);
46
47
  return true;
47
48
  } catch (error) {
@@ -57,9 +58,15 @@ export function createScreenshotRouter({
57
58
  // Flush endpoint - signals test completion and prints summary
58
59
  if (pathname === '/flush') {
59
60
  try {
60
- if (screenshotHandler.getResults) {
61
+ if (screenshotHandler.flush) {
62
+ let stats = await screenshotHandler.flush();
63
+ sendJson(res, 200, {
64
+ success: true,
65
+ ...stats
66
+ });
67
+ } else if (screenshotHandler.getResults) {
61
68
  // This triggers printResults() which outputs the summary
62
- const results = await screenshotHandler.getResults();
69
+ let results = await screenshotHandler.getResults();
63
70
  sendJson(res, 200, {
64
71
  success: true,
65
72
  summary: {
@@ -20,9 +20,12 @@ import { randomUUID } from 'node:crypto';
20
20
  export function createBuildObject(buildOptions, deps = {}) {
21
21
  let {
22
22
  name,
23
+ buildName,
23
24
  branch,
24
25
  commit,
26
+ message,
25
27
  environment = 'test',
28
+ parallelId,
26
29
  metadata = {}
27
30
  } = buildOptions;
28
31
  let randomId = deps.randomId || randomUUID;
@@ -30,10 +33,12 @@ export function createBuildObject(buildOptions, deps = {}) {
30
33
  let timestamp = deps.timestamp || Date.now;
31
34
  return {
32
35
  id: `build-${randomId()}`,
33
- name: name || `build-${timestamp()}`,
36
+ name: name || buildName || `build-${timestamp()}`,
34
37
  branch,
35
38
  commit,
39
+ message,
36
40
  environment,
41
+ parallelId,
37
42
  metadata,
38
43
  status: 'pending',
39
44
  createdAt: now(),
@@ -1163,6 +1163,9 @@ export class TddService {
1163
1163
  let newComparisons = getNewComparisons(this.comparisons);
1164
1164
  let passedComparisons = this.comparisons.filter(c => c.status === 'passed' || c.status === 'baseline-created' || c.status === 'baseline-updated');
1165
1165
  let hasChanges = failedComparisons.length > 0 || newComparisons.length > 0;
1166
+ if (output.isJson?.()) {
1167
+ return results;
1168
+ }
1166
1169
 
1167
1170
  // Header with summary - use bear emoji as Vizzly mascot
1168
1171
  output.blank();
@@ -1239,9 +1242,10 @@ export class TddService {
1239
1242
  let serverFile = `${this.workingDir}/.vizzly/server.json`;
1240
1243
  let serverRunning = false;
1241
1244
  let serverPort = 47392;
1245
+ let serverInfo = {};
1242
1246
  try {
1243
1247
  if (existsSync(serverFile)) {
1244
- let serverInfo = JSON.parse(readFileSync(serverFile, 'utf8'));
1248
+ serverInfo = JSON.parse(readFileSync(serverFile, 'utf8'));
1245
1249
  if (serverInfo.port) {
1246
1250
  serverPort = serverInfo.port;
1247
1251
  serverRunning = true;
@@ -1250,7 +1254,7 @@ export class TddService {
1250
1254
  } catch {
1251
1255
  // Ignore errors reading server file
1252
1256
  }
1253
- if (serverRunning) {
1257
+ if (serverRunning && !serverInfo.buildId) {
1254
1258
  // Server is running - show the dashboard URL
1255
1259
  output.print(` ${textTertiary('→')} Review changes: ${infoColor(colors.underline(`http://localhost:${serverPort}`))}`);
1256
1260
  } else {
@@ -14,6 +14,7 @@
14
14
  * @param {number} options.port - Server port
15
15
  * @param {string} options.buildId - Build ID
16
16
  * @param {boolean} [options.setBaseline] - Whether to set baseline
17
+ * @param {boolean} [options.failOnDiff] - Whether visual diffs should fail tests
17
18
  * @param {Object} [options.baseEnv] - Base environment (defaults to process.env)
18
19
  * @returns {Object} Environment variables object
19
20
  */
@@ -21,6 +22,7 @@ export function buildTestEnv({
21
22
  port,
22
23
  buildId,
23
24
  setBaseline = false,
25
+ failOnDiff = false,
24
26
  baseEnv = process.env
25
27
  }) {
26
28
  return {
@@ -28,7 +30,8 @@ export function buildTestEnv({
28
30
  VIZZLY_SERVER_URL: `http://localhost:${port}`,
29
31
  VIZZLY_BUILD_ID: buildId,
30
32
  VIZZLY_ENABLED: 'true',
31
- VIZZLY_SET_BASELINE: setBaseline ? 'true' : 'false'
33
+ VIZZLY_SET_BASELINE: setBaseline ? 'true' : 'false',
34
+ VIZZLY_FAIL_ON_DIFF: failOnDiff ? 'true' : 'false'
32
35
  };
33
36
  }
34
37
 
@@ -231,12 +234,14 @@ export function hasApiKey(config) {
231
234
  /**
232
235
  * Build spawn options for test command execution
233
236
  * @param {Object} env - Environment variables
237
+ * @param {Object} [options] - Spawn display options
238
+ * @param {boolean} [options.json] - Keep stdout reserved for CLI JSON output
234
239
  * @returns {Object} Spawn options
235
240
  */
236
- export function buildSpawnOptions(env) {
241
+ export function buildSpawnOptions(env, options = {}) {
237
242
  return {
238
243
  env,
239
- stdio: 'inherit',
244
+ stdio: options.json ? ['inherit', 'pipe', 'pipe'] : 'inherit',
240
245
  shell: true
241
246
  };
242
247
  }
@@ -188,6 +188,7 @@ export async function finalizeBuild({
188
188
  export function executeTestCommand({
189
189
  command,
190
190
  env,
191
+ json = false,
191
192
  deps
192
193
  }) {
193
194
  let {
@@ -195,8 +196,18 @@ export function executeTestCommand({
195
196
  createError
196
197
  } = deps;
197
198
  return new Promise((resolve, reject) => {
198
- let spawnOptions = buildSpawnOptions(env);
199
+ let spawnOptions = buildSpawnOptions(env, {
200
+ json
201
+ });
199
202
  let testProcess = spawn(command, spawnOptions);
203
+ if (json) {
204
+ testProcess.stdout?.on('data', data => {
205
+ process.stderr.write(data);
206
+ });
207
+ testProcess.stderr?.on('data', data => {
208
+ process.stderr.write(data);
209
+ });
210
+ }
200
211
  testProcess.on('error', error => {
201
212
  reject(createError(`Failed to run test command: ${error.message}`, 'TEST_COMMAND_FAILED'));
202
213
  });
@@ -271,6 +282,7 @@ export async function runTests({
271
282
  await executeTestCommand({
272
283
  command: testCommand,
273
284
  env,
285
+ json: runOptions.json,
274
286
  deps: {
275
287
  spawn,
276
288
  createError
@@ -334,12 +346,14 @@ export async function runTests({
334
346
  let env = buildTestEnv({
335
347
  port: config.server?.port,
336
348
  buildId,
337
- setBaseline
349
+ setBaseline,
350
+ failOnDiff: runOptions.failOnDiff || false
338
351
  });
339
352
  try {
340
353
  await executeTestCommand({
341
354
  command: testCommand,
342
355
  env,
356
+ json: runOptions.json,
343
357
  deps: {
344
358
  spawn,
345
359
  createError
@@ -408,7 +422,9 @@ export async function runTests({
408
422
 
409
423
  // Throw test error after cleanup
410
424
  if (testError) {
411
- output.error('Test run failed:', testError);
425
+ if (!runOptions.json) {
426
+ output.error('Test run failed:', testError);
427
+ }
412
428
  throw testError;
413
429
  }
414
430
  return buildRunResult({
@@ -24,6 +24,7 @@ export function autoDiscoverTddServer(
24
24
  deps?: {
25
25
  exists?: (path: string) => boolean;
26
26
  readFile?: (path: string, encoding: BufferEncoding) => string | Buffer;
27
+ env?: Record<string, string | undefined>;
27
28
  }
28
29
  ): string | null;
29
30
 
@@ -32,7 +33,14 @@ export function autoDiscoverTddServer(
32
33
  */
33
34
  export interface ScreenshotResult {
34
35
  success: boolean;
35
- status?: 'passed' | 'failed' | 'new';
36
+ status?:
37
+ | 'passed'
38
+ | 'failed'
39
+ | 'new'
40
+ | 'match'
41
+ | 'diff'
42
+ | 'baseline-updated'
43
+ | 'error';
36
44
  name?: string;
37
45
  diffPercentage?: number;
38
46
  [key: string]: unknown;
@@ -59,10 +67,16 @@ export interface ScreenshotResult {
59
67
  * @example
60
68
  * // With properties and comparison settings
61
69
  * await vizzlyScreenshot('checkout-form', screenshot, {
62
- * properties: { browser: 'chrome', viewport: '1920x1080' },
70
+ * properties: { browser: 'chrome', viewport: { width: 1920, height: 1080 } },
63
71
  * threshold: 5,
64
- * minClusterSize: 10
72
+ * minClusterSize: 10,
73
+ * fullPage: true,
74
+ * requestTimeout: 5000
65
75
  * });
76
+ *
77
+ * `properties` is the user metadata bag. Comparison options are normalized
78
+ * into the server metadata payload, while `requestTimeout` stays on the
79
+ * client request and `buildId` only routes the screenshot to a build.
66
80
  */
67
81
  export function vizzlyScreenshot(
68
82
  name: string,
@@ -72,7 +86,10 @@ export function vizzlyScreenshot(
72
86
  threshold?: number;
73
87
  minClusterSize?: number;
74
88
  fullPage?: boolean;
75
- [key: string]: unknown;
89
+ /** Transport-only build ID used to route the request. */
90
+ buildId?: string;
91
+ /** Client-side HTTP timeout in milliseconds; not stored as metadata. */
92
+ requestTimeout?: number;
76
93
  }
77
94
  ): Promise<ScreenshotResult | null>;
78
95
 
@@ -81,7 +98,15 @@ export function vizzlyScreenshot(
81
98
  */
82
99
  export interface FlushResult {
83
100
  success: boolean;
84
- summary: {
101
+ uploaded?: number;
102
+ flushed?: boolean;
103
+ total?: number;
104
+ passed?: number;
105
+ failed?: number;
106
+ new?: number;
107
+ errors?: number;
108
+ message?: string;
109
+ summary?: {
85
110
  total: number;
86
111
  passed: number;
87
112
  failed: number;
@@ -124,6 +149,7 @@ export function isVizzlyReady(): boolean;
124
149
  export function configure(config?: {
125
150
  serverUrl?: string;
126
151
  enabled?: boolean;
152
+ failOnDiff?: boolean;
127
153
  }): void;
128
154
 
129
155
  /**
@@ -145,4 +171,5 @@ export function getVizzlyInfo(): {
145
171
  buildId: string | null;
146
172
  tddMode: boolean;
147
173
  disabled: boolean;
174
+ failOnDiff: boolean;
148
175
  };
@@ -24,7 +24,9 @@ export interface BuildConfig {
24
24
 
25
25
  export interface UploadConfig {
26
26
  screenshotsDir?: string | string[];
27
+ /** Batch size used when uploading screenshots. */
27
28
  batchSize?: number;
29
+ /** Timeout in milliseconds for waiting on build processing after upload. */
28
30
  timeout?: number;
29
31
  }
30
32
 
@@ -76,13 +78,22 @@ export interface ScreenshotOptions {
76
78
  threshold?: number;
77
79
  minClusterSize?: number;
78
80
  fullPage?: boolean;
81
+ /** Transport-only build ID used to route the request. */
79
82
  buildId?: string;
80
- [key: string]: unknown;
83
+ /** Client-side HTTP timeout in milliseconds; not stored as metadata. */
84
+ requestTimeout?: number;
81
85
  }
82
86
 
83
87
  export interface ScreenshotResult {
84
88
  success: boolean;
85
- status?: 'passed' | 'failed' | 'new';
89
+ status?:
90
+ | 'passed'
91
+ | 'failed'
92
+ | 'new'
93
+ | 'match'
94
+ | 'diff'
95
+ | 'baseline-updated'
96
+ | 'error';
86
97
  name?: string;
87
98
  diffPercentage?: number;
88
99
  [key: string]: unknown;
@@ -95,7 +106,13 @@ export interface ScreenshotResult {
95
106
  export interface ComparisonResult {
96
107
  id: string;
97
108
  name: string;
98
- status: 'passed' | 'failed' | 'new' | 'error' | 'baseline-updated';
109
+ status:
110
+ | 'passed'
111
+ | 'failed'
112
+ | 'new'
113
+ | 'error'
114
+ | 'baseline-created'
115
+ | 'baseline-updated';
99
116
  baseline: string;
100
117
  current: string;
101
118
  diff: string | null;
@@ -150,7 +167,9 @@ export interface UploadOptions {
150
167
  message?: string;
151
168
  environment?: string;
152
169
  threshold?: number;
153
- pullRequestNumber?: string;
170
+ minClusterSize?: number;
171
+ metadata?: Record<string, unknown>;
172
+ pullRequestNumber?: string | number;
154
173
  parallelId?: string;
155
174
  onProgress?: (progress: UploadProgress) => void;
156
175
  }
@@ -186,8 +205,10 @@ export interface BuildResult {
186
205
  status: 'completed' | 'failed' | 'pending';
187
206
  build: unknown;
188
207
  comparisons?: number;
208
+ totalComparisons: number;
189
209
  passedComparisons?: number;
190
210
  failedComparisons?: number;
211
+ newComparisons: number;
191
212
  url?: string;
192
213
  }
193
214
 
@@ -487,15 +508,19 @@ export interface BuildOptions {
487
508
  buildName?: string;
488
509
  branch?: string;
489
510
  commit?: string;
511
+ commit_sha?: string;
490
512
  message?: string;
513
+ commit_message?: string;
491
514
  environment?: string;
492
515
  threshold?: number;
493
516
  eager?: boolean;
494
517
  allowNoToken?: boolean;
495
518
  wait?: boolean;
496
519
  uploadAll?: boolean;
497
- pullRequestNumber?: string;
520
+ pullRequestNumber?: string | number;
521
+ github_pull_request_number?: string | number;
498
522
  parallelId?: string;
523
+ parallel_id?: string;
499
524
  }
500
525
 
501
526
  /**
@@ -565,6 +590,8 @@ export function vizzlyScreenshot(
565
590
  threshold?: number;
566
591
  minClusterSize?: number;
567
592
  fullPage?: boolean;
593
+ requestTimeout?: number;
594
+ buildId?: string;
568
595
  [key: string]: unknown;
569
596
  }
570
597
  ): Promise<ScreenshotResult | null>;
@@ -573,6 +600,7 @@ export function vizzlyScreenshot(
573
600
  export function configure(config?: {
574
601
  serverUrl?: string;
575
602
  enabled?: boolean;
603
+ failOnDiff?: boolean;
576
604
  }): void;
577
605
 
578
606
  /** Enable or disable screenshot capture */
@@ -123,6 +123,25 @@ export function extractBrowserFromFilename(filename) {
123
123
  // Build Info Construction
124
124
  // ============================================================================
125
125
 
126
+ function buildUploadMetadata(options) {
127
+ let metadata = {
128
+ ...(options.metadata || {})
129
+ };
130
+ let comparison = {
131
+ ...(metadata.comparison || {})
132
+ };
133
+ if (options.threshold !== undefined) {
134
+ comparison.threshold = options.threshold;
135
+ }
136
+ if (options.minClusterSize !== undefined) {
137
+ comparison.minClusterSize = options.minClusterSize;
138
+ }
139
+ if (Object.keys(comparison).length > 0) {
140
+ metadata.comparison = comparison;
141
+ }
142
+ return Object.keys(metadata).length > 0 ? metadata : undefined;
143
+ }
144
+
126
145
  /**
127
146
  * Build API build info payload
128
147
  * @param {Object} options - Upload options
@@ -130,7 +149,7 @@ export function extractBrowserFromFilename(filename) {
130
149
  * @returns {Object} Build info payload
131
150
  */
132
151
  export function buildBuildInfo(options, defaultBranch = 'main') {
133
- return {
152
+ let buildInfo = {
134
153
  name: options.buildName || `Upload ${new Date().toISOString()}`,
135
154
  branch: options.branch || defaultBranch || 'main',
136
155
  commit_sha: options.commit,
@@ -140,6 +159,11 @@ export function buildBuildInfo(options, defaultBranch = 'main') {
140
159
  github_pull_request_number: options.pullRequestNumber,
141
160
  parallel_id: options.parallelId
142
161
  };
162
+ let metadata = buildUploadMetadata(options);
163
+ if (metadata) {
164
+ buildInfo.metadata = metadata;
165
+ }
166
+ return buildInfo;
143
167
  }
144
168
 
145
169
  // ============================================================================
@@ -316,17 +340,25 @@ export function buildUploadResult({
316
340
  * @returns {Object} Wait result
317
341
  */
318
342
  export function buildWaitResult(build) {
343
+ let totalComparisons = build.comparisonsTotal ?? build.total_comparisons ?? 0;
344
+ let passedComparisons = build.comparisonsPassed ?? build.passed_comparisons ?? build.identicalComparisons ?? build.identical_comparisons ?? 0;
345
+ let failedComparisons = build.comparisonsFailed ?? build.failed_comparisons ?? build.changedComparisons ?? build.changed_comparisons ?? 0;
346
+ let newComparisons = build.comparisonsNew ?? build.newComparisons ?? build.new_comparisons ?? 0;
347
+ let identicalComparisons = build.identicalComparisons ?? build.identical_comparisons ?? passedComparisons;
348
+ let approvalStatus = build.approvalStatus ?? build.approval_status ?? 'pending';
349
+ let hasComparisonCounts = build.comparisonsTotal !== undefined || build.total_comparisons !== undefined || build.comparisonsPassed !== undefined || build.passed_comparisons !== undefined || build.comparisonsFailed !== undefined || build.failed_comparisons !== undefined || build.comparisonsNew !== undefined || build.new_comparisons !== undefined || build.identicalComparisons !== undefined || build.identical_comparisons !== undefined;
319
350
  let result = {
320
351
  status: 'completed',
321
- build
352
+ build,
353
+ passedComparisons,
354
+ failedComparisons,
355
+ newComparisons,
356
+ identicalComparisons,
357
+ approvalStatus,
358
+ totalComparisons
322
359
  };
323
- if (typeof build.comparisonsTotal === 'number') {
324
- result.comparisons = build.comparisonsTotal;
325
- result.passedComparisons = build.comparisonsPassed || 0;
326
- result.failedComparisons = build.comparisonsFailed || 0;
327
- } else {
328
- result.passedComparisons = 0;
329
- result.failedComparisons = 0;
360
+ if (hasComparisonCounts) {
361
+ result.comparisons = totalComparisons;
330
362
  }
331
363
  if (build.url) {
332
364
  result.url = build.url;
@@ -386,23 +386,25 @@ export async function upload({
386
386
  let buildInfo = buildBuildInfo(uploadOptions, defaultBranch);
387
387
  let build = await createBuild(client, buildInfo);
388
388
  let buildId = build.id;
389
-
390
- // Check existing files
391
- let {
392
- toUpload,
393
- existing,
394
- screenshots
395
- } = await checkExistingFiles({
396
- fileMetadata,
397
- client,
398
- signal,
399
- buildId,
400
- deps: {
401
- checkShas,
402
- createError,
403
- output
404
- }
405
- });
389
+ let toUpload = fileMetadata;
390
+ let existing = [];
391
+ let screenshots = [];
392
+ if (!uploadOptions.uploadAll) {
393
+ let existingResult = await checkExistingFiles({
394
+ fileMetadata,
395
+ client,
396
+ signal,
397
+ buildId,
398
+ deps: {
399
+ checkShas,
400
+ createError,
401
+ output
402
+ }
403
+ });
404
+ toUpload = existingResult.toUpload;
405
+ existing = existingResult.existing;
406
+ screenshots = existingResult.screenshots;
407
+ }
406
408
  onProgress(buildDeduplicationProgress(toUpload.length, existing.length, files.length));
407
409
 
408
410
  // Upload files