@vizzly-testing/cli 0.26.1 → 0.27.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.
- package/dist/cli.js +250 -5
- package/dist/commands/api.js +190 -0
- package/dist/commands/baselines.js +265 -0
- package/dist/commands/builds.js +274 -0
- package/dist/commands/comparisons.js +345 -0
- package/dist/commands/config-cmd.js +184 -0
- package/dist/commands/init.js +54 -12
- package/dist/commands/orgs.js +79 -0
- package/dist/commands/preview.js +13 -3
- package/dist/commands/project.js +69 -15
- package/dist/commands/projects.js +96 -0
- package/dist/commands/review.js +319 -0
- package/dist/commands/run.js +142 -2
- package/dist/commands/tdd-daemon.js +78 -12
- package/dist/commands/tdd.js +61 -2
- package/dist/commands/upload.js +94 -2
- package/dist/server/handlers/tdd-handler.js +23 -2
- package/dist/utils/config-loader.js +13 -1
- package/dist/utils/output.js +107 -4
- package/package.json +2 -1
package/dist/commands/run.js
CHANGED
|
@@ -271,6 +271,51 @@ export async function runCommand(testCommand, options = {}, globalOptions = {},
|
|
|
271
271
|
if (result.buildId) {
|
|
272
272
|
buildId = result.buildId;
|
|
273
273
|
}
|
|
274
|
+
|
|
275
|
+
// JSON output mode - output structured data and exit
|
|
276
|
+
if (globalOptions.json) {
|
|
277
|
+
let executionTimeMs = Date.now() - startTime;
|
|
278
|
+
|
|
279
|
+
// Get URL from result, or construct one as fallback
|
|
280
|
+
let displayUrl = result.url;
|
|
281
|
+
if (!displayUrl && config.apiKey) {
|
|
282
|
+
try {
|
|
283
|
+
let client = createApiClient({
|
|
284
|
+
baseUrl: config.apiUrl,
|
|
285
|
+
token: config.apiKey,
|
|
286
|
+
command: 'run'
|
|
287
|
+
});
|
|
288
|
+
let tokenContext = await getTokenContext(client);
|
|
289
|
+
let baseUrl = config.apiUrl.replace(/\/api.*$/, '');
|
|
290
|
+
if (tokenContext.organization?.slug && tokenContext.project?.slug) {
|
|
291
|
+
displayUrl = `${baseUrl}/${tokenContext.organization.slug}/${tokenContext.project.slug}/builds/${result.buildId}`;
|
|
292
|
+
}
|
|
293
|
+
} catch {
|
|
294
|
+
// Fallback to simple URL if context fetch fails
|
|
295
|
+
let baseUrl = config.apiUrl.replace(/\/api.*$/, '');
|
|
296
|
+
displayUrl = `${baseUrl}/builds/${result.buildId}`;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
let jsonResult = {
|
|
300
|
+
buildId: result.buildId,
|
|
301
|
+
status: 'completed',
|
|
302
|
+
url: displayUrl,
|
|
303
|
+
screenshotsCaptured: result.screenshotsCaptured || 0,
|
|
304
|
+
executionTimeMs,
|
|
305
|
+
git: {
|
|
306
|
+
branch,
|
|
307
|
+
commit,
|
|
308
|
+
message
|
|
309
|
+
},
|
|
310
|
+
exitCode: 0
|
|
311
|
+
};
|
|
312
|
+
output.data(jsonResult);
|
|
313
|
+
output.cleanup();
|
|
314
|
+
return {
|
|
315
|
+
success: true,
|
|
316
|
+
result
|
|
317
|
+
};
|
|
318
|
+
}
|
|
274
319
|
output.complete('Test run completed');
|
|
275
320
|
|
|
276
321
|
// Show Vizzly summary with link to results
|
|
@@ -314,14 +359,56 @@ export async function runCommand(testCommand, options = {}, globalOptions = {},
|
|
|
314
359
|
// Extract exit code from error message if available
|
|
315
360
|
let exitCodeMatch = error.message.match(/exited with code (\d+)/);
|
|
316
361
|
let exitCode = exitCodeMatch ? parseInt(exitCodeMatch[1], 10) : 1;
|
|
317
|
-
|
|
362
|
+
|
|
363
|
+
// JSON output for test command failure
|
|
364
|
+
if (globalOptions.json) {
|
|
365
|
+
let executionTimeMs = Date.now() - startTime;
|
|
366
|
+
output.data({
|
|
367
|
+
buildId: buildId || null,
|
|
368
|
+
status: 'failed',
|
|
369
|
+
error: {
|
|
370
|
+
code: error.code,
|
|
371
|
+
message: error.message
|
|
372
|
+
},
|
|
373
|
+
executionTimeMs,
|
|
374
|
+
git: {
|
|
375
|
+
branch,
|
|
376
|
+
commit,
|
|
377
|
+
message
|
|
378
|
+
},
|
|
379
|
+
exitCode
|
|
380
|
+
});
|
|
381
|
+
output.cleanup();
|
|
382
|
+
} else {
|
|
383
|
+
output.error('Test run failed');
|
|
384
|
+
}
|
|
318
385
|
return {
|
|
319
386
|
success: false,
|
|
320
387
|
exitCode
|
|
321
388
|
};
|
|
322
389
|
} else {
|
|
323
390
|
// Setup or other error - VizzlyError.getUserMessage() provides context
|
|
324
|
-
|
|
391
|
+
if (globalOptions.json) {
|
|
392
|
+
let executionTimeMs = Date.now() - (startTime || Date.now());
|
|
393
|
+
output.data({
|
|
394
|
+
buildId: buildId || null,
|
|
395
|
+
status: 'failed',
|
|
396
|
+
error: {
|
|
397
|
+
code: error.code || 'UNKNOWN_ERROR',
|
|
398
|
+
message: error.getUserMessage ? error.getUserMessage() : error.message
|
|
399
|
+
},
|
|
400
|
+
executionTimeMs,
|
|
401
|
+
git: {
|
|
402
|
+
branch,
|
|
403
|
+
commit,
|
|
404
|
+
message
|
|
405
|
+
},
|
|
406
|
+
exitCode: 1
|
|
407
|
+
});
|
|
408
|
+
output.cleanup();
|
|
409
|
+
} else {
|
|
410
|
+
output.error('Test run failed', error);
|
|
411
|
+
}
|
|
325
412
|
return {
|
|
326
413
|
success: false,
|
|
327
414
|
exitCode: 1
|
|
@@ -336,6 +423,59 @@ export async function runCommand(testCommand, options = {}, globalOptions = {},
|
|
|
336
423
|
output.info('Waiting for build completion...');
|
|
337
424
|
output.startSpinner('Processing comparisons...');
|
|
338
425
|
let buildResult = await uploader.waitForBuild(result.buildId);
|
|
426
|
+
|
|
427
|
+
// JSON output for --wait mode
|
|
428
|
+
if (globalOptions.json) {
|
|
429
|
+
let executionTimeMs = Date.now() - startTime;
|
|
430
|
+
|
|
431
|
+
// Get URL from result, or construct one as fallback
|
|
432
|
+
let displayUrl = result.url;
|
|
433
|
+
if (!displayUrl && config.apiKey) {
|
|
434
|
+
try {
|
|
435
|
+
let client = createApiClient({
|
|
436
|
+
baseUrl: config.apiUrl,
|
|
437
|
+
token: config.apiKey,
|
|
438
|
+
command: 'run'
|
|
439
|
+
});
|
|
440
|
+
let tokenContext = await getTokenContext(client);
|
|
441
|
+
let baseUrl = config.apiUrl.replace(/\/api.*$/, '');
|
|
442
|
+
if (tokenContext.organization?.slug && tokenContext.project?.slug) {
|
|
443
|
+
displayUrl = `${baseUrl}/${tokenContext.organization.slug}/${tokenContext.project.slug}/builds/${result.buildId}`;
|
|
444
|
+
}
|
|
445
|
+
} catch {
|
|
446
|
+
let baseUrl = config.apiUrl.replace(/\/api.*$/, '');
|
|
447
|
+
displayUrl = `${baseUrl}/builds/${result.buildId}`;
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
let exitCode = buildResult.failedComparisons > 0 ? 1 : 0;
|
|
451
|
+
let jsonResult = {
|
|
452
|
+
buildId: result.buildId,
|
|
453
|
+
status: buildResult.failedComparisons > 0 ? 'failed' : 'completed',
|
|
454
|
+
url: displayUrl,
|
|
455
|
+
screenshotsCaptured: result.screenshotsCaptured || 0,
|
|
456
|
+
executionTimeMs,
|
|
457
|
+
git: {
|
|
458
|
+
branch,
|
|
459
|
+
commit,
|
|
460
|
+
message
|
|
461
|
+
},
|
|
462
|
+
comparisons: {
|
|
463
|
+
total: buildResult.totalComparisons || 0,
|
|
464
|
+
new: buildResult.newComparisons || 0,
|
|
465
|
+
changed: buildResult.failedComparisons || 0,
|
|
466
|
+
identical: buildResult.identicalComparisons || 0
|
|
467
|
+
},
|
|
468
|
+
approvalStatus: buildResult.approvalStatus || 'pending',
|
|
469
|
+
exitCode
|
|
470
|
+
};
|
|
471
|
+
output.data(jsonResult);
|
|
472
|
+
output.cleanup();
|
|
473
|
+
return {
|
|
474
|
+
success: exitCode === 0,
|
|
475
|
+
exitCode,
|
|
476
|
+
result: jsonResult
|
|
477
|
+
};
|
|
478
|
+
}
|
|
339
479
|
output.success('Build processing completed');
|
|
340
480
|
|
|
341
481
|
// Exit with appropriate code based on comparison results
|
|
@@ -27,6 +27,16 @@ export async function tddStartCommand(options = {}, globalOptions = {}) {
|
|
|
27
27
|
if (existingServer) {
|
|
28
28
|
// Verify it's actually running
|
|
29
29
|
if (await isServerRunning(existingServer.port)) {
|
|
30
|
+
// JSON output for already running
|
|
31
|
+
if (globalOptions.json) {
|
|
32
|
+
output.data({
|
|
33
|
+
status: 'already_running',
|
|
34
|
+
port: existingServer.port,
|
|
35
|
+
pid: existingServer.pid,
|
|
36
|
+
dashboardUrl: `http://localhost:${existingServer.port}`
|
|
37
|
+
});
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
30
40
|
output.header('tdd', 'local');
|
|
31
41
|
output.print(` ${output.statusDot('success')} Already running`);
|
|
32
42
|
output.blank();
|
|
@@ -200,6 +210,21 @@ export async function tddStartCommand(options = {}, globalOptions = {}) {
|
|
|
200
210
|
// Non-fatal, SDK can still use health check
|
|
201
211
|
}
|
|
202
212
|
|
|
213
|
+
// JSON output for successful start
|
|
214
|
+
let dashboardUrl = `http://localhost:${port}`;
|
|
215
|
+
if (globalOptions.json) {
|
|
216
|
+
output.data({
|
|
217
|
+
status: 'started',
|
|
218
|
+
port,
|
|
219
|
+
pid: child.pid,
|
|
220
|
+
dashboardUrl
|
|
221
|
+
});
|
|
222
|
+
if (options.open) {
|
|
223
|
+
openDashboard(port);
|
|
224
|
+
}
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
|
|
203
228
|
// Show auto-allocated port message if applicable
|
|
204
229
|
if (autoAllocated) {
|
|
205
230
|
output.print(` ${output.statusDot('info')} Auto-assigned port ${colors.brand.textTertiary(`:${port}`)}`);
|
|
@@ -207,7 +232,6 @@ export async function tddStartCommand(options = {}, globalOptions = {}) {
|
|
|
207
232
|
}
|
|
208
233
|
|
|
209
234
|
// Show dashboard URL in a branded box
|
|
210
|
-
let dashboardUrl = `http://localhost:${port}`;
|
|
211
235
|
output.printBox(colors.brand.info(colors.underline(dashboardUrl)), {
|
|
212
236
|
title: 'Dashboard',
|
|
213
237
|
style: 'branded'
|
|
@@ -385,7 +409,15 @@ export async function tddStopCommand(options = {}, globalOptions = {}) {
|
|
|
385
409
|
}
|
|
386
410
|
}
|
|
387
411
|
if (!pid) {
|
|
388
|
-
output
|
|
412
|
+
// JSON output for not running
|
|
413
|
+
if (globalOptions.json) {
|
|
414
|
+
output.data({
|
|
415
|
+
status: 'not_running',
|
|
416
|
+
message: 'No TDD server running'
|
|
417
|
+
});
|
|
418
|
+
} else {
|
|
419
|
+
output.warn('No TDD server running');
|
|
420
|
+
}
|
|
389
421
|
|
|
390
422
|
// Clean up any stale files
|
|
391
423
|
if (existsSync(pidFile)) unlinkSync(pidFile);
|
|
@@ -428,6 +460,16 @@ export async function tddStopCommand(options = {}, globalOptions = {}) {
|
|
|
428
460
|
} catch {
|
|
429
461
|
// Non-fatal
|
|
430
462
|
}
|
|
463
|
+
|
|
464
|
+
// JSON output for successful stop
|
|
465
|
+
if (globalOptions.json) {
|
|
466
|
+
output.data({
|
|
467
|
+
status: 'stopped',
|
|
468
|
+
pid,
|
|
469
|
+
port
|
|
470
|
+
});
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
431
473
|
output.print(` ${output.statusDot('success')} Server stopped`);
|
|
432
474
|
} catch (error) {
|
|
433
475
|
if (error.code === 'ESRCH') {
|
|
@@ -467,6 +509,14 @@ export async function tddStatusCommand(_options, globalOptions = {}) {
|
|
|
467
509
|
const pidFile = join(vizzlyDir, 'server.pid');
|
|
468
510
|
const serverFile = join(vizzlyDir, 'server.json');
|
|
469
511
|
if (!existsSync(pidFile)) {
|
|
512
|
+
// JSON output for not running
|
|
513
|
+
if (globalOptions.json) {
|
|
514
|
+
output.data({
|
|
515
|
+
running: false,
|
|
516
|
+
message: 'TDD server not running'
|
|
517
|
+
});
|
|
518
|
+
return;
|
|
519
|
+
}
|
|
470
520
|
output.info('TDD server not running');
|
|
471
521
|
return;
|
|
472
522
|
}
|
|
@@ -486,15 +536,12 @@ export async function tddStatusCommand(_options, globalOptions = {}) {
|
|
|
486
536
|
// Try to check health endpoint
|
|
487
537
|
const health = await checkServerHealth(serverInfo.port);
|
|
488
538
|
if (health.running) {
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
// Show header
|
|
492
|
-
output.header('tdd', 'local');
|
|
493
|
-
|
|
494
|
-
// Show running status with uptime
|
|
539
|
+
// Calculate uptime
|
|
540
|
+
let uptimeMs = null;
|
|
495
541
|
let uptimeStr = '';
|
|
496
542
|
if (serverInfo.startTime) {
|
|
497
|
-
|
|
543
|
+
uptimeMs = Date.now() - serverInfo.startTime;
|
|
544
|
+
const uptime = Math.floor(uptimeMs / 1000);
|
|
498
545
|
const hours = Math.floor(uptime / 3600);
|
|
499
546
|
const minutes = Math.floor(uptime % 3600 / 60);
|
|
500
547
|
const seconds = uptime % 60;
|
|
@@ -502,11 +549,30 @@ export async function tddStatusCommand(_options, globalOptions = {}) {
|
|
|
502
549
|
if (minutes > 0 || hours > 0) uptimeStr += `${minutes}m `;
|
|
503
550
|
uptimeStr += `${seconds}s`;
|
|
504
551
|
}
|
|
552
|
+
let dashboardUrl = `http://localhost:${serverInfo.port}`;
|
|
553
|
+
|
|
554
|
+
// JSON output for running status
|
|
555
|
+
if (globalOptions.json) {
|
|
556
|
+
output.data({
|
|
557
|
+
running: true,
|
|
558
|
+
port: serverInfo.port,
|
|
559
|
+
pid,
|
|
560
|
+
uptimeMs,
|
|
561
|
+
uptime: uptimeStr || null,
|
|
562
|
+
dashboardUrl
|
|
563
|
+
});
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
566
|
+
let colors = output.getColors();
|
|
567
|
+
|
|
568
|
+
// Show header
|
|
569
|
+
output.header('tdd', 'local');
|
|
570
|
+
|
|
571
|
+
// Show running status with uptime
|
|
505
572
|
output.print(` ${output.statusDot('success')} Running ${uptimeStr ? colors.brand.textTertiary(`· ${uptimeStr}`) : ''}`);
|
|
506
573
|
output.blank();
|
|
507
574
|
|
|
508
575
|
// Show dashboard URL in a branded box
|
|
509
|
-
let dashboardUrl = `http://localhost:${serverInfo.port}`;
|
|
510
576
|
output.printBox(colors.brand.info(colors.underline(dashboardUrl)), {
|
|
511
577
|
title: 'Dashboard',
|
|
512
578
|
style: 'branded'
|
|
@@ -610,9 +676,9 @@ export async function tddListCommand(_options, globalOptions = {}) {
|
|
|
610
676
|
|
|
611
677
|
// JSON output
|
|
612
678
|
if (globalOptions.json) {
|
|
613
|
-
|
|
679
|
+
output.data({
|
|
614
680
|
servers
|
|
615
|
-
}
|
|
681
|
+
});
|
|
616
682
|
return;
|
|
617
683
|
}
|
|
618
684
|
|
package/dist/commands/tdd.js
CHANGED
|
@@ -196,16 +196,75 @@ export async function tddCommand(testCommand, options = {}, globalOptions = {},
|
|
|
196
196
|
// Determine success based on comparison results
|
|
197
197
|
// (Summary is printed by printResults() in tdd-service.js, called from getTddResults)
|
|
198
198
|
let hasFailures = runResult.failed || runResult.comparisons?.some(c => c.status === 'failed');
|
|
199
|
+
let exitCode = hasFailures ? 1 : 0;
|
|
200
|
+
|
|
201
|
+
// JSON output mode
|
|
202
|
+
if (globalOptions.json) {
|
|
203
|
+
// Build comparison data for JSON output
|
|
204
|
+
let comparisons = (runResult.comparisons || []).map(c => ({
|
|
205
|
+
name: c.name,
|
|
206
|
+
status: c.status,
|
|
207
|
+
signature: c.signature,
|
|
208
|
+
diffPercentage: c.diffPercentage ?? c.diff_percentage ?? null,
|
|
209
|
+
threshold: c.threshold ?? config.comparison.threshold,
|
|
210
|
+
paths: {
|
|
211
|
+
baseline: c.baselinePath || c.baseline_path || null,
|
|
212
|
+
current: c.currentPath || c.current_path || null,
|
|
213
|
+
diff: c.diffPath || c.diff_path || null
|
|
214
|
+
},
|
|
215
|
+
viewport: c.viewport || {
|
|
216
|
+
width: c.viewportWidth || c.viewport_width,
|
|
217
|
+
height: c.viewportHeight || c.viewport_height
|
|
218
|
+
},
|
|
219
|
+
browser: c.browser || null
|
|
220
|
+
}));
|
|
221
|
+
|
|
222
|
+
// Calculate summary
|
|
223
|
+
let summary = runResult.summary || {
|
|
224
|
+
total: comparisons.length,
|
|
225
|
+
passed: comparisons.filter(c => c.status === 'passed').length,
|
|
226
|
+
failed: comparisons.filter(c => c.status === 'failed').length,
|
|
227
|
+
new: comparisons.filter(c => c.status === 'new').length
|
|
228
|
+
};
|
|
229
|
+
output.data({
|
|
230
|
+
status: hasFailures ? 'failed' : 'completed',
|
|
231
|
+
exitCode,
|
|
232
|
+
comparisons,
|
|
233
|
+
summary,
|
|
234
|
+
reportPath: runResult.reportPath || '.vizzly/report/index.html'
|
|
235
|
+
});
|
|
236
|
+
output.cleanup();
|
|
237
|
+
}
|
|
199
238
|
return {
|
|
200
239
|
result: {
|
|
201
240
|
success: !hasFailures,
|
|
202
|
-
exitCode
|
|
241
|
+
exitCode,
|
|
203
242
|
...runResult
|
|
204
243
|
},
|
|
205
244
|
cleanup
|
|
206
245
|
};
|
|
207
246
|
} catch (error) {
|
|
208
|
-
output
|
|
247
|
+
// JSON output for errors
|
|
248
|
+
if (globalOptions.json) {
|
|
249
|
+
output.data({
|
|
250
|
+
status: 'failed',
|
|
251
|
+
exitCode: 1,
|
|
252
|
+
error: {
|
|
253
|
+
message: error.message,
|
|
254
|
+
code: error.code || 'UNKNOWN_ERROR'
|
|
255
|
+
},
|
|
256
|
+
comparisons: [],
|
|
257
|
+
summary: {
|
|
258
|
+
total: 0,
|
|
259
|
+
passed: 0,
|
|
260
|
+
failed: 0,
|
|
261
|
+
new: 0
|
|
262
|
+
}
|
|
263
|
+
});
|
|
264
|
+
output.cleanup();
|
|
265
|
+
} else {
|
|
266
|
+
output.error('Test failed', error);
|
|
267
|
+
}
|
|
209
268
|
return {
|
|
210
269
|
result: {
|
|
211
270
|
success: false,
|
package/dist/commands/upload.js
CHANGED
|
@@ -185,6 +185,33 @@ export async function uploadCommand(screenshotsPath, options = {}, globalOptions
|
|
|
185
185
|
output.warn(`Failed to finalize build: ${error.message}`);
|
|
186
186
|
}
|
|
187
187
|
}
|
|
188
|
+
|
|
189
|
+
// JSON output mode
|
|
190
|
+
if (globalOptions.json) {
|
|
191
|
+
let executionTimeMs = Date.now() - uploadStartTime;
|
|
192
|
+
let buildUrl = result.url || (result.buildId ? await buildUrlConstructor(result.buildId, config.apiUrl, config.apiKey, deps) : null);
|
|
193
|
+
output.data({
|
|
194
|
+
buildId: result.buildId,
|
|
195
|
+
url: buildUrl,
|
|
196
|
+
stats: {
|
|
197
|
+
total: result.stats?.total || 0,
|
|
198
|
+
uploaded: result.stats?.uploaded || 0,
|
|
199
|
+
skipped: result.stats?.skipped || 0,
|
|
200
|
+
bytes: result.stats?.bytes || 0
|
|
201
|
+
},
|
|
202
|
+
git: {
|
|
203
|
+
branch,
|
|
204
|
+
commit,
|
|
205
|
+
message
|
|
206
|
+
},
|
|
207
|
+
executionTimeMs
|
|
208
|
+
});
|
|
209
|
+
output.cleanup();
|
|
210
|
+
return {
|
|
211
|
+
success: true,
|
|
212
|
+
result
|
|
213
|
+
};
|
|
214
|
+
}
|
|
188
215
|
output.complete('Upload completed');
|
|
189
216
|
|
|
190
217
|
// Show Vizzly summary
|
|
@@ -205,6 +232,41 @@ export async function uploadCommand(screenshotsPath, options = {}, globalOptions
|
|
|
205
232
|
output.startSpinner('Processing comparisons...');
|
|
206
233
|
let buildResult = await uploader.waitForBuild(result.buildId);
|
|
207
234
|
output.stopSpinner();
|
|
235
|
+
|
|
236
|
+
// JSON output for --wait mode
|
|
237
|
+
if (globalOptions.json) {
|
|
238
|
+
let executionTimeMs = Date.now() - uploadStartTime;
|
|
239
|
+
let waitBuildUrl = buildResult.url || (await buildUrlConstructor(result.buildId, config.apiUrl, config.apiKey, deps));
|
|
240
|
+
output.data({
|
|
241
|
+
buildId: result.buildId,
|
|
242
|
+
status: buildResult.failedComparisons > 0 ? 'failed' : 'completed',
|
|
243
|
+
url: waitBuildUrl,
|
|
244
|
+
stats: {
|
|
245
|
+
total: result.stats?.total || 0,
|
|
246
|
+
uploaded: result.stats?.uploaded || 0,
|
|
247
|
+
skipped: result.stats?.skipped || 0,
|
|
248
|
+
bytes: result.stats?.bytes || 0
|
|
249
|
+
},
|
|
250
|
+
git: {
|
|
251
|
+
branch,
|
|
252
|
+
commit,
|
|
253
|
+
message
|
|
254
|
+
},
|
|
255
|
+
comparisons: {
|
|
256
|
+
total: buildResult.totalComparisons || 0,
|
|
257
|
+
passed: buildResult.passedComparisons || 0,
|
|
258
|
+
failed: buildResult.failedComparisons || 0,
|
|
259
|
+
new: buildResult.newComparisons || 0
|
|
260
|
+
},
|
|
261
|
+
approvalStatus: buildResult.approvalStatus || 'pending',
|
|
262
|
+
executionTimeMs
|
|
263
|
+
});
|
|
264
|
+
output.cleanup();
|
|
265
|
+
return {
|
|
266
|
+
success: buildResult.failedComparisons === 0,
|
|
267
|
+
result
|
|
268
|
+
};
|
|
269
|
+
}
|
|
208
270
|
output.complete('Build processing completed');
|
|
209
271
|
|
|
210
272
|
// Show build processing results
|
|
@@ -230,8 +292,18 @@ export async function uploadCommand(screenshotsPath, options = {}, globalOptions
|
|
|
230
292
|
// Don't fail CI for Vizzly infrastructure issues (5xx errors)
|
|
231
293
|
let status = error.context?.status;
|
|
232
294
|
if (status >= 500) {
|
|
233
|
-
|
|
234
|
-
|
|
295
|
+
if (globalOptions.json) {
|
|
296
|
+
output.data({
|
|
297
|
+
buildId: null,
|
|
298
|
+
status: 'skipped',
|
|
299
|
+
message: 'Vizzly API unavailable - upload skipped',
|
|
300
|
+
executionTimeMs: Date.now() - uploadStartTime
|
|
301
|
+
});
|
|
302
|
+
output.cleanup();
|
|
303
|
+
} else {
|
|
304
|
+
output.warn('Vizzly API unavailable - upload skipped. Your tests still ran.');
|
|
305
|
+
output.cleanup();
|
|
306
|
+
}
|
|
235
307
|
return {
|
|
236
308
|
success: true,
|
|
237
309
|
result: {
|
|
@@ -254,6 +326,26 @@ export async function uploadCommand(screenshotsPath, options = {}, globalOptions
|
|
|
254
326
|
// Silent fail on cleanup
|
|
255
327
|
}
|
|
256
328
|
}
|
|
329
|
+
|
|
330
|
+
// JSON output for errors
|
|
331
|
+
if (globalOptions.json) {
|
|
332
|
+
output.data({
|
|
333
|
+
buildId: buildId || null,
|
|
334
|
+
status: 'failed',
|
|
335
|
+
error: {
|
|
336
|
+
code: error.code || 'UPLOAD_FAILED',
|
|
337
|
+
message: error?.getUserMessage ? error.getUserMessage() : error.message
|
|
338
|
+
},
|
|
339
|
+
executionTimeMs: Date.now() - uploadStartTime
|
|
340
|
+
});
|
|
341
|
+
output.cleanup();
|
|
342
|
+
exit(1);
|
|
343
|
+
return {
|
|
344
|
+
success: false,
|
|
345
|
+
error
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
|
|
257
349
|
// Use user-friendly error message if available
|
|
258
350
|
let errorMessage = error?.getUserMessage ? error.getUserMessage() : error.message;
|
|
259
351
|
output.error(errorMessage || 'Upload failed', error);
|
|
@@ -405,6 +405,15 @@ export const createTddHandler = (config, workingDir, baselineBuild, baselineComp
|
|
|
405
405
|
// Update comparison in report data file
|
|
406
406
|
updateComparison(newComparison);
|
|
407
407
|
|
|
408
|
+
// Log screenshot event for menubar
|
|
409
|
+
// Normalize status to match HTTP response ('failed' -> 'diff')
|
|
410
|
+
let logStatus = comparison.status === 'failed' ? 'diff' : comparison.status;
|
|
411
|
+
output.info(`Screenshot: ${sanitizedName}`, {
|
|
412
|
+
screenshot: sanitizedName,
|
|
413
|
+
status: logStatus,
|
|
414
|
+
diffPercentage: comparison.diffPercentage || 0
|
|
415
|
+
});
|
|
416
|
+
|
|
408
417
|
// Visual diffs return 200 with status: 'diff' - they're not errors
|
|
409
418
|
// The SDK/user can decide whether to fail tests based on this
|
|
410
419
|
if (comparison.status === 'failed') {
|
|
@@ -486,7 +495,13 @@ export const createTddHandler = (config, workingDir, baselineBuild, baselineComp
|
|
|
486
495
|
diff: null
|
|
487
496
|
};
|
|
488
497
|
updateComparison(updatedComparison);
|
|
489
|
-
|
|
498
|
+
|
|
499
|
+
// Log screenshot event for menubar
|
|
500
|
+
output.info(`Screenshot: ${comparison.name}`, {
|
|
501
|
+
screenshot: comparison.name,
|
|
502
|
+
status: 'accepted',
|
|
503
|
+
diffPercentage: 0
|
|
504
|
+
});
|
|
490
505
|
return result;
|
|
491
506
|
} catch (error) {
|
|
492
507
|
output.error(`Failed to accept baseline for ${comparisonId}:`, error);
|
|
@@ -510,7 +525,13 @@ export const createTddHandler = (config, workingDir, baselineBuild, baselineComp
|
|
|
510
525
|
status: 'rejected'
|
|
511
526
|
};
|
|
512
527
|
updateComparison(updatedComparison);
|
|
513
|
-
|
|
528
|
+
|
|
529
|
+
// Log screenshot event for menubar
|
|
530
|
+
output.info(`Screenshot: ${comparison.name}`, {
|
|
531
|
+
screenshot: comparison.name,
|
|
532
|
+
status: 'rejected',
|
|
533
|
+
diffPercentage: comparison.diffPercentage || 0
|
|
534
|
+
});
|
|
514
535
|
return {
|
|
515
536
|
success: true,
|
|
516
537
|
id: comparisonId
|
|
@@ -2,7 +2,7 @@ import { resolve } from 'node:path';
|
|
|
2
2
|
import { cosmiconfigSync } from 'cosmiconfig';
|
|
3
3
|
import { validateVizzlyConfigWithDefaults } from './config-schema.js';
|
|
4
4
|
import { getApiToken, getApiUrl, getBuildName, getParallelId } from './environment-config.js';
|
|
5
|
-
import { getProjectMapping } from './global-config.js';
|
|
5
|
+
import { getAccessToken, getProjectMapping } from './global-config.js';
|
|
6
6
|
import * as output from './output.js';
|
|
7
7
|
const DEFAULT_CONFIG = {
|
|
8
8
|
// API Configuration
|
|
@@ -116,6 +116,18 @@ export async function loadConfig(configPath = null, cliOverrides = {}) {
|
|
|
116
116
|
output.debug('config', 'using token from --token flag');
|
|
117
117
|
}
|
|
118
118
|
applyCLIOverrides(config, cliOverrides);
|
|
119
|
+
|
|
120
|
+
// 6. Fall back to user auth token if no other token found
|
|
121
|
+
// This enables interactive commands (builds, comparisons, approve, etc.)
|
|
122
|
+
// to work without a project token when the user is logged in
|
|
123
|
+
if (!config.apiKey) {
|
|
124
|
+
let userToken = await getAccessToken();
|
|
125
|
+
if (userToken) {
|
|
126
|
+
config.apiKey = userToken;
|
|
127
|
+
config.isUserAuth = true; // Flag to indicate this is user auth, not project token
|
|
128
|
+
output.debug('config', 'using token from user login');
|
|
129
|
+
}
|
|
130
|
+
}
|
|
119
131
|
return config;
|
|
120
132
|
}
|
|
121
133
|
|