@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.
- package/README.md +67 -7
- package/dist/api/core.js +18 -4
- package/dist/api/endpoints.js +38 -15
- package/dist/api/index.js +1 -1
- package/dist/cli.js +270 -194
- package/dist/client/index.js +62 -21
- package/dist/commands/api.js +9 -3
- package/dist/commands/preview.js +30 -26
- package/dist/commands/project.js +1 -1
- package/dist/commands/run.js +25 -10
- package/dist/commands/status.js +14 -1
- package/dist/commands/tdd-daemon-process.js +5 -1
- package/dist/commands/tdd-daemon.js +217 -139
- package/dist/commands/tdd.js +26 -5
- package/dist/commands/upload.js +31 -13
- package/dist/reporter/reporter-bundle.iife.js +6 -6
- package/dist/sdk/index.js +23 -9
- package/dist/server/handlers/api-handler.js +3 -2
- package/dist/server/handlers/tdd-handler.js +5 -1
- package/dist/server/routers/screenshot.js +11 -4
- package/dist/services/build-manager.js +6 -1
- package/dist/tdd/tdd-service.js +6 -2
- package/dist/test-runner/core.js +8 -3
- package/dist/test-runner/operations.js +19 -3
- package/dist/types/client.d.ts +32 -5
- package/dist/types/index.d.ts +33 -5
- package/dist/uploader/core.js +41 -9
- package/dist/uploader/operations.js +19 -17
- package/dist/utils/config-loader.js +13 -6
- package/dist/utils/context.js +46 -76
- package/dist/utils/environment-config.js +20 -0
- package/dist/utils/output.js +8 -5
- package/dist/utils/screenshot-options.js +60 -10
- package/package.json +1 -1
- package/skills/vizzly/SKILL.md +12 -4
- package/skills/vizzly/references/cli-context.md +8 -2
- package/skills/vizzly/references/setup-ci.md +3 -1
package/dist/client/index.js
CHANGED
|
@@ -8,22 +8,27 @@ import { request as httpRequest } from 'node:http';
|
|
|
8
8
|
import { request as httpsRequest } from 'node:https';
|
|
9
9
|
import { dirname, join, parse } from 'node:path';
|
|
10
10
|
import { getBuildId, getServerUrl, isTddMode, setVizzlyEnabled } from '../utils/environment-config.js';
|
|
11
|
-
import {
|
|
11
|
+
import { normalizeScreenshotOptions } from '../utils/screenshot-options.js';
|
|
12
12
|
|
|
13
13
|
// Internal client state
|
|
14
14
|
let currentClient = null;
|
|
15
|
+
let currentServerUrl = null;
|
|
15
16
|
let isDisabled = false;
|
|
17
|
+
let currentFailOnDiff = false;
|
|
16
18
|
|
|
17
19
|
// Default timeout for screenshot requests (30 seconds)
|
|
18
20
|
let DEFAULT_TIMEOUT_MS = 30000;
|
|
19
21
|
|
|
20
22
|
// Log levels for client SDK output control
|
|
21
|
-
export let LOG_LEVELS = {
|
|
23
|
+
export let LOG_LEVELS = Object.freeze({
|
|
22
24
|
debug: 0,
|
|
23
25
|
info: 1,
|
|
24
26
|
warn: 2,
|
|
25
27
|
error: 3
|
|
26
|
-
};
|
|
28
|
+
});
|
|
29
|
+
function getEnvFailOnDiff(env = process.env) {
|
|
30
|
+
return env.VIZZLY_FAIL_ON_DIFF === 'true' || env.VIZZLY_FAIL_ON_DIFF === '1';
|
|
31
|
+
}
|
|
27
32
|
|
|
28
33
|
/**
|
|
29
34
|
* Check if client should log at the given level
|
|
@@ -56,6 +61,7 @@ function isVizzlyDisabled() {
|
|
|
56
61
|
function disableVizzly() {
|
|
57
62
|
isDisabled = true;
|
|
58
63
|
currentClient = null;
|
|
64
|
+
currentServerUrl = null;
|
|
59
65
|
}
|
|
60
66
|
|
|
61
67
|
/**
|
|
@@ -67,7 +73,8 @@ function disableVizzly() {
|
|
|
67
73
|
export function autoDiscoverTddServer(startDir, deps = {}) {
|
|
68
74
|
let {
|
|
69
75
|
exists = existsSync,
|
|
70
|
-
readFile = readFileSync
|
|
76
|
+
readFile = readFileSync,
|
|
77
|
+
env = process.env
|
|
71
78
|
} = deps;
|
|
72
79
|
try {
|
|
73
80
|
// Look for .vizzly/server.json in current directory and parent directories
|
|
@@ -80,6 +87,7 @@ export function autoDiscoverTddServer(startDir, deps = {}) {
|
|
|
80
87
|
let serverInfo = JSON.parse(readFile(serverJsonPath, 'utf8'));
|
|
81
88
|
if (serverInfo.port) {
|
|
82
89
|
let url = `http://localhost:${serverInfo.port}`;
|
|
90
|
+
currentFailOnDiff = getEnvFailOnDiff(env) || Boolean(serverInfo.failOnDiff);
|
|
83
91
|
return url;
|
|
84
92
|
}
|
|
85
93
|
} catch {
|
|
@@ -104,6 +112,7 @@ function getClient() {
|
|
|
104
112
|
}
|
|
105
113
|
if (!currentClient) {
|
|
106
114
|
let serverUrl = getServerUrl();
|
|
115
|
+
currentFailOnDiff = getEnvFailOnDiff();
|
|
107
116
|
|
|
108
117
|
// Auto-detect local TDD server and enable Vizzly if TDD server is found
|
|
109
118
|
if (!serverUrl) {
|
|
@@ -116,7 +125,10 @@ function getClient() {
|
|
|
116
125
|
|
|
117
126
|
// If we have a server URL, create the client (regardless of initial enabled state)
|
|
118
127
|
if (serverUrl) {
|
|
119
|
-
|
|
128
|
+
currentServerUrl = serverUrl;
|
|
129
|
+
currentClient = createSimpleClient(serverUrl, {
|
|
130
|
+
failOnDiff: currentFailOnDiff
|
|
131
|
+
});
|
|
120
132
|
}
|
|
121
133
|
}
|
|
122
134
|
return currentClient;
|
|
@@ -183,28 +195,38 @@ function httpPost(url, body, timeoutMs) {
|
|
|
183
195
|
* Create a simple HTTP client for screenshots
|
|
184
196
|
* @private
|
|
185
197
|
*/
|
|
186
|
-
function createSimpleClient(serverUrl) {
|
|
198
|
+
function createSimpleClient(serverUrl, clientOptions = {}) {
|
|
199
|
+
let {
|
|
200
|
+
failOnDiff = false
|
|
201
|
+
} = clientOptions;
|
|
187
202
|
return {
|
|
188
203
|
async screenshot(name, imageBuffer, options = {}) {
|
|
204
|
+
let normalizedOptions = normalizeScreenshotOptions(options);
|
|
205
|
+
let requestTimeout = normalizedOptions.requestTimeout || DEFAULT_TIMEOUT_MS;
|
|
206
|
+
for (let warning of normalizedOptions.warnings) {
|
|
207
|
+
console.warn(`[vizzly] ${warning.message}`);
|
|
208
|
+
}
|
|
189
209
|
try {
|
|
190
210
|
// If it's a string, assume it's a file path and send directly
|
|
191
211
|
// Otherwise it's a Buffer, so convert to base64
|
|
192
212
|
let isFilePath = typeof imageBuffer === 'string';
|
|
193
213
|
let image = isFilePath ? imageBuffer : imageBuffer.toString('base64');
|
|
194
214
|
let type = isFilePath ? 'file-path' : 'base64';
|
|
195
|
-
let
|
|
196
|
-
|
|
215
|
+
let screenshotData = {
|
|
216
|
+
buildId: normalizedOptions.buildId ?? getBuildId(),
|
|
217
|
+
name,
|
|
218
|
+
image,
|
|
219
|
+
type,
|
|
220
|
+
properties: normalizedOptions.properties
|
|
221
|
+
};
|
|
222
|
+
if (normalizedOptions.warnings.length > 0) {
|
|
223
|
+
screenshotData.warnings = normalizedOptions.warnings;
|
|
224
|
+
}
|
|
197
225
|
let httpStart = Date.now();
|
|
198
226
|
let {
|
|
199
227
|
status,
|
|
200
228
|
json
|
|
201
|
-
} = await httpPost(`${serverUrl}/screenshot`,
|
|
202
|
-
buildId: getBuildId(),
|
|
203
|
-
name,
|
|
204
|
-
image,
|
|
205
|
-
type,
|
|
206
|
-
properties
|
|
207
|
-
}, requestTimeout);
|
|
229
|
+
} = await httpPost(`${serverUrl}/screenshot`, screenshotData, requestTimeout);
|
|
208
230
|
let httpMs = Date.now() - httpStart;
|
|
209
231
|
if (shouldLogClient('debug')) {
|
|
210
232
|
console.debug(`[vizzly-client] ${name} HTTP completed in ${httpMs}ms`);
|
|
@@ -215,6 +237,9 @@ function createSimpleClient(serverUrl) {
|
|
|
215
237
|
// The summary will show all failures at the end
|
|
216
238
|
if (status === 422 && json.tddMode && json.comparison) {
|
|
217
239
|
let comp = json.comparison;
|
|
240
|
+
if (failOnDiff) {
|
|
241
|
+
throw new Error(`Visual diff detected for "${comp.name}" (${comp.diffPercentage ?? 0}% difference)`);
|
|
242
|
+
}
|
|
218
243
|
|
|
219
244
|
// Return success so test continues and captures remaining screenshots
|
|
220
245
|
// Visual diff details will be shown in the summary after tests complete
|
|
@@ -227,12 +252,15 @@ function createSimpleClient(serverUrl) {
|
|
|
227
252
|
}
|
|
228
253
|
throw new Error(`Screenshot failed: ${status} - ${json.error || 'Unknown error'}`);
|
|
229
254
|
}
|
|
255
|
+
if (failOnDiff && json?.tddMode && ['diff', 'failed'].includes(json.status)) {
|
|
256
|
+
throw new Error(`Visual diff detected for "${json.name || name}" (${json.diffPercentage ?? 0}% difference)`);
|
|
257
|
+
}
|
|
230
258
|
return json;
|
|
231
259
|
} catch (error) {
|
|
232
260
|
// Handle timeout
|
|
233
261
|
if (error.message === 'Request timeout') {
|
|
234
262
|
if (shouldLogClient('error')) {
|
|
235
|
-
console.error(`[vizzly] Screenshot timed out for "${name}" after ${
|
|
263
|
+
console.error(`[vizzly] Screenshot timed out for "${name}" after ${requestTimeout / 1000}s`);
|
|
236
264
|
}
|
|
237
265
|
disableVizzly();
|
|
238
266
|
return null;
|
|
@@ -308,7 +336,7 @@ function createSimpleClient(serverUrl) {
|
|
|
308
336
|
* await vizzlyScreenshot('checkout-form', screenshot, {
|
|
309
337
|
* properties: {
|
|
310
338
|
* browser: 'chrome',
|
|
311
|
-
* viewport:
|
|
339
|
+
* viewport: { width: 1920, height: 1080 }
|
|
312
340
|
* },
|
|
313
341
|
* threshold: 5
|
|
314
342
|
* });
|
|
@@ -366,8 +394,20 @@ export function isVizzlyReady() {
|
|
|
366
394
|
* @param {boolean} [config.enabled] - Enable/disable screenshots
|
|
367
395
|
*/
|
|
368
396
|
export function configure(config = {}) {
|
|
397
|
+
if ('failOnDiff' in config) {
|
|
398
|
+
currentFailOnDiff = config.failOnDiff === true;
|
|
399
|
+
} else if ('serverUrl' in config) {
|
|
400
|
+
currentFailOnDiff = getEnvFailOnDiff();
|
|
401
|
+
}
|
|
369
402
|
if ('serverUrl' in config) {
|
|
370
|
-
|
|
403
|
+
currentServerUrl = config.serverUrl || null;
|
|
404
|
+
currentClient = config.serverUrl ? createSimpleClient(config.serverUrl, {
|
|
405
|
+
failOnDiff: currentFailOnDiff
|
|
406
|
+
}) : null;
|
|
407
|
+
} else if ('failOnDiff' in config && currentClient && currentServerUrl) {
|
|
408
|
+
currentClient = createSimpleClient(currentServerUrl, {
|
|
409
|
+
failOnDiff: currentFailOnDiff
|
|
410
|
+
});
|
|
371
411
|
}
|
|
372
412
|
if (typeof config.enabled === 'boolean') {
|
|
373
413
|
setVizzlyEnabled(config.enabled);
|
|
@@ -397,10 +437,11 @@ export function getVizzlyInfo() {
|
|
|
397
437
|
let client = getClient();
|
|
398
438
|
return {
|
|
399
439
|
enabled: !isVizzlyDisabled(),
|
|
400
|
-
serverUrl: getServerUrl(),
|
|
440
|
+
serverUrl: currentServerUrl || getServerUrl() || null,
|
|
401
441
|
ready: !isVizzlyDisabled() && client !== null,
|
|
402
|
-
buildId: getBuildId(),
|
|
442
|
+
buildId: getBuildId() || null,
|
|
403
443
|
tddMode: isTddMode(),
|
|
404
|
-
disabled: isVizzlyDisabled()
|
|
444
|
+
disabled: isVizzlyDisabled(),
|
|
445
|
+
failOnDiff: currentFailOnDiff
|
|
405
446
|
};
|
|
406
447
|
}
|
package/dist/commands/api.js
CHANGED
|
@@ -65,7 +65,8 @@ export function appendApiQuery(endpoint, queryOption) {
|
|
|
65
65
|
}
|
|
66
66
|
export function validateApiRequest({
|
|
67
67
|
endpoint,
|
|
68
|
-
method
|
|
68
|
+
method,
|
|
69
|
+
hasData = false
|
|
69
70
|
}) {
|
|
70
71
|
let errors = [];
|
|
71
72
|
if (method !== 'GET' && method !== 'POST') {
|
|
@@ -75,6 +76,9 @@ export function validateApiRequest({
|
|
|
75
76
|
if (method === 'POST' && !isAllowedPostEndpoint(endpoint)) {
|
|
76
77
|
errors.push(`POST not allowed for ${endpoint}. Only approve, reject, and comment endpoints support POST.`);
|
|
77
78
|
}
|
|
79
|
+
if (hasData && method !== 'POST') {
|
|
80
|
+
errors.push('Request data requires --method POST.');
|
|
81
|
+
}
|
|
78
82
|
return errors;
|
|
79
83
|
}
|
|
80
84
|
export function buildApiRequest({
|
|
@@ -85,7 +89,8 @@ export function buildApiRequest({
|
|
|
85
89
|
let method = normalizeApiMethod(options.method || 'GET');
|
|
86
90
|
let errors = validateApiRequest({
|
|
87
91
|
endpoint: normalizedEndpoint,
|
|
88
|
-
method
|
|
92
|
+
method,
|
|
93
|
+
hasData: options.data !== undefined
|
|
89
94
|
});
|
|
90
95
|
if (errors.length > 0) {
|
|
91
96
|
return {
|
|
@@ -239,7 +244,8 @@ export function validateApiOptions(endpoint, options = {}) {
|
|
|
239
244
|
let method = normalizeApiMethod(options.method || 'GET');
|
|
240
245
|
errors.push(...validateApiRequest({
|
|
241
246
|
endpoint: normalizedEndpoint,
|
|
242
|
-
method
|
|
247
|
+
method,
|
|
248
|
+
hasData: options.data !== undefined
|
|
243
249
|
}));
|
|
244
250
|
return errors;
|
|
245
251
|
}
|
package/dist/commands/preview.js
CHANGED
|
@@ -259,6 +259,7 @@ export async function previewCommand(path, options = {}, globalOptions = {}, dep
|
|
|
259
259
|
formatSessionAge = defaultFormatSessionAge,
|
|
260
260
|
detectBranch = defaultDetectBranch,
|
|
261
261
|
openBrowser = defaultOpenBrowser,
|
|
262
|
+
createZipWithSystem: createZip = createZipWithSystem,
|
|
262
263
|
output = defaultOutput,
|
|
263
264
|
exit = code => process.exit(code)
|
|
264
265
|
} = deps;
|
|
@@ -268,39 +269,42 @@ export async function previewCommand(path, options = {}, globalOptions = {}, dep
|
|
|
268
269
|
color: !globalOptions.noColor
|
|
269
270
|
});
|
|
270
271
|
try {
|
|
271
|
-
|
|
272
|
-
let allOptions = {
|
|
273
|
-
...globalOptions,
|
|
274
|
-
...options
|
|
275
|
-
};
|
|
276
|
-
let config = await loadConfig(globalOptions.config, allOptions);
|
|
272
|
+
let uploadPath = options.base ? resolve(path, options.base) : path;
|
|
277
273
|
|
|
278
|
-
// Validate
|
|
279
|
-
|
|
280
|
-
|
|
274
|
+
// Validate path exists and is a directory before auth so local input
|
|
275
|
+
// mistakes are visible without requiring credentials.
|
|
276
|
+
if (!existsSync(uploadPath)) {
|
|
277
|
+
output.error(`Path does not exist: ${uploadPath}`);
|
|
281
278
|
exit(1);
|
|
282
279
|
return {
|
|
283
280
|
success: false,
|
|
284
|
-
reason: '
|
|
281
|
+
reason: 'path-not-found'
|
|
285
282
|
};
|
|
286
283
|
}
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
output.error(`Path does not exist: ${path}`);
|
|
284
|
+
let pathStat = statSync(uploadPath);
|
|
285
|
+
if (!pathStat.isDirectory()) {
|
|
286
|
+
output.error(`Path is not a directory: ${uploadPath}`);
|
|
291
287
|
exit(1);
|
|
292
288
|
return {
|
|
293
289
|
success: false,
|
|
294
|
-
reason: '
|
|
290
|
+
reason: 'not-a-directory'
|
|
295
291
|
};
|
|
296
292
|
}
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
293
|
+
|
|
294
|
+
// Load configuration
|
|
295
|
+
let allOptions = {
|
|
296
|
+
...globalOptions,
|
|
297
|
+
...options
|
|
298
|
+
};
|
|
299
|
+
let config = await loadConfig(globalOptions.config, allOptions);
|
|
300
|
+
|
|
301
|
+
// Validate API token (skip for dry-run)
|
|
302
|
+
if (!options.dryRun && !config.apiKey) {
|
|
303
|
+
output.error('API token required. Use --token or set VIZZLY_TOKEN environment variable');
|
|
300
304
|
exit(1);
|
|
301
305
|
return {
|
|
302
306
|
success: false,
|
|
303
|
-
reason: '
|
|
307
|
+
reason: 'no-api-key'
|
|
304
308
|
};
|
|
305
309
|
}
|
|
306
310
|
|
|
@@ -455,14 +459,14 @@ export async function previewCommand(path, options = {}, globalOptions = {}, dep
|
|
|
455
459
|
count: fileCount,
|
|
456
460
|
totalSize,
|
|
457
461
|
files
|
|
458
|
-
} = await countFiles(
|
|
462
|
+
} = await countFiles(uploadPath, {
|
|
459
463
|
collectPaths: options.dryRun,
|
|
460
464
|
excludedDirs,
|
|
461
465
|
excludedFiles
|
|
462
466
|
});
|
|
463
467
|
if (fileCount === 0) {
|
|
464
468
|
output.stopSpinner();
|
|
465
|
-
output.error(`No files found in ${
|
|
469
|
+
output.error(`No files found in ${uploadPath}`);
|
|
466
470
|
exit(1);
|
|
467
471
|
return {
|
|
468
472
|
success: false,
|
|
@@ -477,7 +481,7 @@ export async function previewCommand(path, options = {}, globalOptions = {}, dep
|
|
|
477
481
|
if (globalOptions.json) {
|
|
478
482
|
output.data({
|
|
479
483
|
dryRun: true,
|
|
480
|
-
path: resolve(
|
|
484
|
+
path: resolve(uploadPath),
|
|
481
485
|
fileCount,
|
|
482
486
|
totalSize,
|
|
483
487
|
excludedDirs,
|
|
@@ -490,7 +494,7 @@ export async function previewCommand(path, options = {}, globalOptions = {}, dep
|
|
|
490
494
|
} else {
|
|
491
495
|
output.info(`Dry run - would upload ${fileCount} files (${formatBytes(totalSize)})`);
|
|
492
496
|
output.blank();
|
|
493
|
-
output.print(` ${colors.brand.textTertiary('Source')} ${resolve(
|
|
497
|
+
output.print(` ${colors.brand.textTertiary('Source')} ${resolve(uploadPath)}`);
|
|
494
498
|
output.print(` ${colors.brand.textTertiary('Files')} ${fileCount}`);
|
|
495
499
|
output.print(` ${colors.brand.textTertiary('Total size')} ${formatBytes(totalSize)}`);
|
|
496
500
|
output.blank();
|
|
@@ -538,7 +542,7 @@ export async function previewCommand(path, options = {}, globalOptions = {}, dep
|
|
|
538
542
|
let zipPath = join(tmpdir(), `vizzly-preview-${Date.now()}-${randomSuffix}.zip`);
|
|
539
543
|
let zipBuffer;
|
|
540
544
|
try {
|
|
541
|
-
await
|
|
545
|
+
await createZip(uploadPath, zipPath, exclusions);
|
|
542
546
|
zipBuffer = await readFile(zipPath);
|
|
543
547
|
} catch (zipError) {
|
|
544
548
|
output.stopSpinner();
|
|
@@ -569,10 +573,10 @@ export async function previewCommand(path, options = {}, globalOptions = {}, dep
|
|
|
569
573
|
success: true,
|
|
570
574
|
buildId,
|
|
571
575
|
previewUrl: result.previewUrl,
|
|
572
|
-
files: result.uploaded
|
|
576
|
+
files: result.uploaded ?? fileCount,
|
|
573
577
|
bytes: totalSize,
|
|
574
578
|
compressedBytes: zipBuffer.length,
|
|
575
|
-
compressionRatio
|
|
579
|
+
compressionRatio: `${compressionPercent}%`,
|
|
576
580
|
newBytes: result.newBytes,
|
|
577
581
|
reusedBlobs: result.reusedBlobs || 0,
|
|
578
582
|
deduplicationRatio: result.deduplicationRatio,
|
package/dist/commands/project.js
CHANGED
|
@@ -56,7 +56,7 @@ export async function projectLinkCommand(selector, options = {}, globalOptions =
|
|
|
56
56
|
} = parseProjectSelector(selector, options);
|
|
57
57
|
try {
|
|
58
58
|
let config = await loadConfig(globalOptions.config, globalOptions);
|
|
59
|
-
let userToken = config.userToken || (await getAccessToken());
|
|
59
|
+
let userToken = config.userToken || globalOptions.token || (await getAccessToken());
|
|
60
60
|
if (!userToken) {
|
|
61
61
|
output.error('Login required before linking a project');
|
|
62
62
|
output.hint('Run "vizzly login" first, then try project link again');
|
package/dist/commands/run.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
import { spawn as defaultSpawn } from 'node:child_process';
|
|
7
7
|
import { createBuild as defaultCreateApiBuild, createApiClient as defaultCreateApiClient, finalizeBuild as defaultFinalizeApiBuild, getBuild as defaultGetBuild, getTokenContext as defaultGetTokenContext } from '../api/index.js';
|
|
8
|
+
import { CONFIG_DEFAULTS } from '../config/core.js';
|
|
8
9
|
import { VizzlyError } from '../errors/vizzly-error.js';
|
|
9
10
|
import { createServerManager as defaultCreateServerManager } from '../server-manager/index.js';
|
|
10
11
|
import { createBuildObject as defaultCreateBuildObject } from '../services/build-manager.js';
|
|
@@ -174,10 +175,11 @@ export async function runCommand(testCommand, options = {}, globalOptions = {},
|
|
|
174
175
|
}
|
|
175
176
|
|
|
176
177
|
// Collect git metadata and build info
|
|
177
|
-
let
|
|
178
|
-
let
|
|
179
|
-
let
|
|
180
|
-
let
|
|
178
|
+
let configuredBuildName = config.build.name && config.build.name !== CONFIG_DEFAULTS.build.name ? config.build.name : undefined;
|
|
179
|
+
let branch = await detectBranch(options.branch || config.build.branch);
|
|
180
|
+
let commit = await detectCommit(options.commit || config.build.commit);
|
|
181
|
+
let message = options.message || config.build.message || (await detectCommitMessage());
|
|
182
|
+
let buildName = await generateBuildNameWithGit(options.buildName || configuredBuildName);
|
|
181
183
|
let pullRequestNumber = detectPullRequestNumber();
|
|
182
184
|
if (globalOptions.verbose) {
|
|
183
185
|
output.info('Configuration loaded');
|
|
@@ -244,7 +246,8 @@ export async function runCommand(testCommand, options = {}, globalOptions = {},
|
|
|
244
246
|
wait: config.wait || options.wait || false,
|
|
245
247
|
uploadAll: options.uploadAll || false,
|
|
246
248
|
pullRequestNumber,
|
|
247
|
-
parallelId: config.parallelId
|
|
249
|
+
parallelId: config.parallelId,
|
|
250
|
+
json: globalOptions.json
|
|
248
251
|
};
|
|
249
252
|
|
|
250
253
|
// Start test run
|
|
@@ -305,7 +308,7 @@ export async function runCommand(testCommand, options = {}, globalOptions = {},
|
|
|
305
308
|
}
|
|
306
309
|
|
|
307
310
|
// JSON output mode - output structured data and exit
|
|
308
|
-
if (globalOptions.json) {
|
|
311
|
+
if (globalOptions.json && !runOptions.wait) {
|
|
309
312
|
let executionTimeMs = Date.now() - startTime;
|
|
310
313
|
let displayUrl = await resolveBuildDisplayUrl({
|
|
311
314
|
result,
|
|
@@ -334,10 +337,12 @@ export async function runCommand(testCommand, options = {}, globalOptions = {},
|
|
|
334
337
|
result
|
|
335
338
|
};
|
|
336
339
|
}
|
|
337
|
-
|
|
340
|
+
if (!globalOptions.json) {
|
|
341
|
+
output.complete('Test run completed');
|
|
342
|
+
}
|
|
338
343
|
|
|
339
344
|
// Show Vizzly summary with link to results
|
|
340
|
-
if (result.buildId) {
|
|
345
|
+
if (result.buildId && !globalOptions.json) {
|
|
341
346
|
output.blank();
|
|
342
347
|
let colors = output.getColors();
|
|
343
348
|
output.print(` ${colors.brand.textTertiary('Screenshots')} ${colors.white(result.screenshotsCaptured)}`);
|
|
@@ -384,7 +389,7 @@ export async function runCommand(testCommand, options = {}, globalOptions = {},
|
|
|
384
389
|
});
|
|
385
390
|
output.cleanup();
|
|
386
391
|
} else {
|
|
387
|
-
output.error('Test run failed');
|
|
392
|
+
output.error('Test run failed', error);
|
|
388
393
|
}
|
|
389
394
|
return {
|
|
390
395
|
success: false,
|
|
@@ -490,7 +495,17 @@ export async function runCommand(testCommand, options = {}, globalOptions = {},
|
|
|
490
495
|
// Don't fail CI for Vizzly infrastructure issues (5xx errors)
|
|
491
496
|
let status = error.context?.status;
|
|
492
497
|
if (status >= 500) {
|
|
493
|
-
|
|
498
|
+
if (globalOptions.json) {
|
|
499
|
+
output.data({
|
|
500
|
+
buildId: null,
|
|
501
|
+
status: 'skipped',
|
|
502
|
+
message: 'Vizzly API unavailable - visual tests skipped',
|
|
503
|
+
executionTimeMs: Date.now() - (startTime || Date.now())
|
|
504
|
+
});
|
|
505
|
+
} else {
|
|
506
|
+
output.warn('Vizzly API unavailable - visual tests skipped. Your tests still ran.');
|
|
507
|
+
}
|
|
508
|
+
output.cleanup();
|
|
494
509
|
output.debug('api', 'API error details:', {
|
|
495
510
|
error: error.message
|
|
496
511
|
});
|
package/dist/commands/status.js
CHANGED
|
@@ -36,6 +36,19 @@ function createStatusClient({
|
|
|
36
36
|
command: 'status'
|
|
37
37
|
});
|
|
38
38
|
}
|
|
39
|
+
function isMissingPreviewError(error) {
|
|
40
|
+
return error.context?.status === 404;
|
|
41
|
+
}
|
|
42
|
+
async function fetchOptionalPreviewInfo(getPreviewInfo, client, buildId) {
|
|
43
|
+
try {
|
|
44
|
+
return await getPreviewInfo(client, buildId);
|
|
45
|
+
} catch (error) {
|
|
46
|
+
if (isMissingPreviewError(error)) {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
throw error;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
39
52
|
export function normalizeBuildStatus(buildStatus) {
|
|
40
53
|
return buildStatus.build || buildStatus;
|
|
41
54
|
}
|
|
@@ -251,7 +264,7 @@ export async function statusCommand(buildId, options = {}, globalOptions = {}, d
|
|
|
251
264
|
let buildStatus = await getBuild(client, buildId);
|
|
252
265
|
|
|
253
266
|
// Also fetch preview info (if exists)
|
|
254
|
-
let previewInfo = await getPreviewInfo
|
|
267
|
+
let previewInfo = await fetchOptionalPreviewInfo(getPreviewInfo, client, buildId);
|
|
255
268
|
output.stopSpinner();
|
|
256
269
|
|
|
257
270
|
// Extract build data from API response
|
|
@@ -18,7 +18,7 @@ function isProcessRunning(pid) {
|
|
|
18
18
|
return false;
|
|
19
19
|
}
|
|
20
20
|
}
|
|
21
|
-
function parsePositiveInteger(value) {
|
|
21
|
+
export function parsePositiveInteger(value) {
|
|
22
22
|
let text = String(value).trim();
|
|
23
23
|
if (!/^\d+$/.test(text)) {
|
|
24
24
|
return null;
|
|
@@ -71,12 +71,16 @@ export async function resolveDaemonPid({
|
|
|
71
71
|
pidFile = join(process.cwd(), '.vizzly', 'server.pid'),
|
|
72
72
|
readPid = readDaemonPidFile,
|
|
73
73
|
findByPort = findDaemonPidByPort,
|
|
74
|
+
allowPortFallback = false,
|
|
74
75
|
fileDeps = {}
|
|
75
76
|
} = {}) {
|
|
76
77
|
let pid = readPid(pidFile, fileDeps);
|
|
77
78
|
if (pid) {
|
|
78
79
|
return pid;
|
|
79
80
|
}
|
|
81
|
+
if (!allowPortFallback) {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
80
84
|
return await findByPort(port);
|
|
81
85
|
}
|
|
82
86
|
export async function waitForProcessExit(pid, {
|