@vizzly-testing/cli 0.34.0 → 0.35.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/README.md +39 -3
- package/dist/api/core.js +2 -1
- package/dist/cli.js +19 -6
- package/dist/client/index.js +62 -21
- package/dist/commands/api.js +9 -3
- package/dist/commands/preview.js +13 -11
- package/dist/commands/project.js +1 -1
- package/dist/commands/run.js +18 -6
- package/dist/commands/tdd-daemon.js +8 -3
- package/dist/commands/tdd.js +24 -4
- package/dist/commands/upload.js +31 -13
- package/dist/reporter/reporter-bundle.iife.js +1 -1
- 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/test-runner/core.js +4 -1
- package/dist/test-runner/operations.js +2 -1
- 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 +12 -6
- package/dist/utils/context.js +46 -76
- package/dist/utils/environment-config.js +20 -0
- package/dist/utils/output.js +5 -5
- package/dist/utils/screenshot-options.js +60 -10
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -35,6 +35,9 @@ For agent-friendly repos, install the Vizzly skill and add a short project
|
|
|
35
35
|
vizzly init --agent-guidance
|
|
36
36
|
```
|
|
37
37
|
|
|
38
|
+
Use `vizzly init --agent-skill` to install only the local skill, or
|
|
39
|
+
`vizzly init --skip-agent-skill` when you want config without the agent prompt.
|
|
40
|
+
|
|
38
41
|
### Start Local TDD
|
|
39
42
|
|
|
40
43
|
Start the TDD server, run your tests, and open the dashboard at
|
|
@@ -54,9 +57,14 @@ Use cloud builds when you want shared baselines, team review, and CI status:
|
|
|
54
57
|
|
|
55
58
|
```bash
|
|
56
59
|
vizzly login
|
|
60
|
+
vizzly project link your-org/your-project
|
|
57
61
|
vizzly run "pnpm test" --wait
|
|
58
62
|
```
|
|
59
63
|
|
|
64
|
+
`vizzly login` authenticates your user account. `vizzly project link` creates a
|
|
65
|
+
project-scoped upload credential for this checkout, which `vizzly run` uses for
|
|
66
|
+
cloud uploads.
|
|
67
|
+
|
|
60
68
|
`--wait` blocks until Vizzly finishes processing the build. It exits with code
|
|
61
69
|
`1` when visual differences need review.
|
|
62
70
|
|
|
@@ -111,12 +119,23 @@ test('homepage looks correct', async ({ page }) => {
|
|
|
111
119
|
|
|
112
120
|
let screenshot = await page.screenshot();
|
|
113
121
|
await vizzlyScreenshot('homepage', screenshot, {
|
|
114
|
-
|
|
115
|
-
|
|
122
|
+
fullPage: true,
|
|
123
|
+
requestTimeout: 5000,
|
|
124
|
+
properties: {
|
|
125
|
+
browser: 'chrome',
|
|
126
|
+
viewport: { width: 1920, height: 1080 },
|
|
127
|
+
},
|
|
116
128
|
});
|
|
117
129
|
});
|
|
118
130
|
```
|
|
119
131
|
|
|
132
|
+
`properties` is your metadata bag for baseline grouping, filtering, and
|
|
133
|
+
debugging. SDK options such as `threshold`, `minClusterSize`, `fullPage`,
|
|
134
|
+
`requestTimeout`, and `buildId` stay at the top level. If one of those reserved
|
|
135
|
+
SDK options is accidentally placed inside `properties`, Vizzly removes it from
|
|
136
|
+
metadata, applies it as an option when possible, and warns so the call site can
|
|
137
|
+
be cleaned up.
|
|
138
|
+
|
|
120
139
|
The client SDK is lightweight. It posts screenshots to the local Vizzly server
|
|
121
140
|
or the cloud build wrapper. It works with any test runner.
|
|
122
141
|
|
|
@@ -136,9 +155,26 @@ await vizzlyScreenshot('homepage', './screenshots/homepage.png');
|
|
|
136
155
|
Or upload an existing folder of screenshots:
|
|
137
156
|
|
|
138
157
|
```bash
|
|
139
|
-
vizzly upload ./screenshots
|
|
158
|
+
vizzly upload ./screenshots --threshold 2 --min-cluster-size 4 --batch-size 10 --upload-timeout 60000
|
|
140
159
|
```
|
|
141
160
|
|
|
161
|
+
`--batch-size` controls how many screenshots are uploaded per request.
|
|
162
|
+
`--upload-timeout` controls the upload client's timeout, including how long
|
|
163
|
+
`--wait` polls for build processing.
|
|
164
|
+
|
|
165
|
+
CI workflows can force every screenshot through even when the SHA cache says it
|
|
166
|
+
already uploaded. Parallel jobs should use the same stable ID, then finalize
|
|
167
|
+
that ID after every shard completes:
|
|
168
|
+
|
|
169
|
+
```bash
|
|
170
|
+
vizzly run "pnpm test" --upload-all --parallel-id "$GITHUB_RUN_ID"
|
|
171
|
+
vizzly finalize "$GITHUB_RUN_ID"
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
For smoke jobs where cloud credentials are intentionally unavailable, add
|
|
175
|
+
`--allow-no-token` to `vizzly run` and Vizzly will keep the local screenshot
|
|
176
|
+
server path working without creating a cloud build.
|
|
177
|
+
|
|
142
178
|
## Configuration
|
|
143
179
|
|
|
144
180
|
Generate a config file:
|
package/dist/api/core.js
CHANGED
|
@@ -172,7 +172,8 @@ export function buildScreenshotCheckObject(sha256, name, metadata = {}) {
|
|
|
172
172
|
name,
|
|
173
173
|
browser: meta.browser || 'chrome',
|
|
174
174
|
viewport_width: meta.viewport?.width || meta.viewport_width || 1920,
|
|
175
|
-
viewport_height: meta.viewport?.height || meta.viewport_height || 1080
|
|
175
|
+
viewport_height: meta.viewport?.height || meta.viewport_height || 1080,
|
|
176
|
+
properties: meta
|
|
176
177
|
};
|
|
177
178
|
}
|
|
178
179
|
|
package/dist/cli.js
CHANGED
|
@@ -207,6 +207,7 @@ const formatHelp = (cmd, helper) => {
|
|
|
207
207
|
};
|
|
208
208
|
function extractGlobalOptionsFromArgv(argv, commandNames = null) {
|
|
209
209
|
let configPath = null;
|
|
210
|
+
let tokenArg = null;
|
|
210
211
|
let verboseMode = false;
|
|
211
212
|
let logLevelArg = null;
|
|
212
213
|
let jsonArg = null;
|
|
@@ -214,6 +215,11 @@ function extractGlobalOptionsFromArgv(argv, commandNames = null) {
|
|
|
214
215
|
if ((argv[i] === '-c' || argv[i] === '--config') && argv[i + 1]) {
|
|
215
216
|
configPath = argv[i + 1];
|
|
216
217
|
}
|
|
218
|
+
if (argv[i] === '--token' && argv[i + 1]) {
|
|
219
|
+
tokenArg = argv[i + 1];
|
|
220
|
+
} else if (argv[i].startsWith('--token=')) {
|
|
221
|
+
tokenArg = argv[i].substring('--token='.length);
|
|
222
|
+
}
|
|
217
223
|
if (argv[i] === '-v' || argv[i] === '--verbose') {
|
|
218
224
|
verboseMode = true;
|
|
219
225
|
}
|
|
@@ -234,6 +240,7 @@ function extractGlobalOptionsFromArgv(argv, commandNames = null) {
|
|
|
234
240
|
}
|
|
235
241
|
return {
|
|
236
242
|
configPath,
|
|
243
|
+
tokenArg,
|
|
237
244
|
verboseMode,
|
|
238
245
|
logLevelArg,
|
|
239
246
|
jsonArg
|
|
@@ -260,6 +267,7 @@ program.name('vizzly').description('Vizzly CLI for visual regression testing').v
|
|
|
260
267
|
// We need to manually parse to get the config option early
|
|
261
268
|
let {
|
|
262
269
|
configPath,
|
|
270
|
+
tokenArg,
|
|
263
271
|
verboseMode,
|
|
264
272
|
logLevelArg,
|
|
265
273
|
jsonArg
|
|
@@ -280,9 +288,11 @@ output.configure({
|
|
|
280
288
|
color: colorOverride,
|
|
281
289
|
json: jsonArg
|
|
282
290
|
});
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
291
|
+
let config = await loadConfig(configPath, {
|
|
292
|
+
token: tokenArg
|
|
293
|
+
});
|
|
294
|
+
let services = createServices(config);
|
|
295
|
+
let pluginServices = createPluginServices(services);
|
|
286
296
|
let plugins = [];
|
|
287
297
|
try {
|
|
288
298
|
plugins = await loadPlugins(configPath, config);
|
|
@@ -311,8 +321,8 @@ program.command('init').description('Initialize Vizzly in your project').option(
|
|
|
311
321
|
plugins
|
|
312
322
|
});
|
|
313
323
|
});
|
|
314
|
-
program.command('upload').description('Upload screenshots to Vizzly').argument('<path>', 'Path to screenshots directory
|
|
315
|
-
|
|
324
|
+
program.command('upload').description('Upload screenshots to Vizzly').argument('<path>', 'Path to screenshots directory').option('-b, --build-name <name>', 'Build name for grouping').option('-m, --metadata <json>', 'Additional metadata as JSON').option('--batch-size <n>', 'Upload batch size', Number).option('--upload-timeout <ms>', 'Upload timeout in milliseconds', Number).option('--branch <branch>', 'Git branch').option('--commit <sha>', 'Git commit SHA').option('--message <msg>', 'Commit message').option('--environment <env>', 'Environment name', 'test').option('--threshold <number>', 'Comparison threshold', Number).option('--min-cluster-size <pixels>', 'Minimum changed-pixel cluster size', Number).option('--token <token>', 'API token override').option('--wait', 'Wait for build completion').option('--upload-all', 'Upload all screenshots without SHA deduplication').option('--parallel-id <id>', 'Unique identifier for parallel test execution').action(async (path, options) => {
|
|
325
|
+
let globalOptions = program.opts();
|
|
316
326
|
|
|
317
327
|
// Validate options
|
|
318
328
|
const validationErrors = validateUploadOptions(path, options);
|
|
@@ -323,7 +333,10 @@ program.command('upload').description('Upload screenshots to Vizzly').argument('
|
|
|
323
333
|
}
|
|
324
334
|
process.exit(1);
|
|
325
335
|
}
|
|
326
|
-
await uploadCommand(path, options, globalOptions);
|
|
336
|
+
let result = await uploadCommand(path, options, globalOptions);
|
|
337
|
+
if (result?.exitCode) {
|
|
338
|
+
process.exit(result.exitCode);
|
|
339
|
+
}
|
|
327
340
|
});
|
|
328
341
|
|
|
329
342
|
// TDD command with subcommands - local visual review with an interactive dashboard
|
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;
|
|
@@ -284,19 +285,20 @@ export async function previewCommand(path, options = {}, globalOptions = {}, dep
|
|
|
284
285
|
reason: 'no-api-key'
|
|
285
286
|
};
|
|
286
287
|
}
|
|
288
|
+
let uploadPath = options.base ? resolve(path, options.base) : path;
|
|
287
289
|
|
|
288
290
|
// Validate path exists and is a directory
|
|
289
|
-
if (!existsSync(
|
|
290
|
-
output.error(`Path does not exist: ${
|
|
291
|
+
if (!existsSync(uploadPath)) {
|
|
292
|
+
output.error(`Path does not exist: ${uploadPath}`);
|
|
291
293
|
exit(1);
|
|
292
294
|
return {
|
|
293
295
|
success: false,
|
|
294
296
|
reason: 'path-not-found'
|
|
295
297
|
};
|
|
296
298
|
}
|
|
297
|
-
let pathStat = statSync(
|
|
299
|
+
let pathStat = statSync(uploadPath);
|
|
298
300
|
if (!pathStat.isDirectory()) {
|
|
299
|
-
output.error(`Path is not a directory: ${
|
|
301
|
+
output.error(`Path is not a directory: ${uploadPath}`);
|
|
300
302
|
exit(1);
|
|
301
303
|
return {
|
|
302
304
|
success: false,
|
|
@@ -455,14 +457,14 @@ export async function previewCommand(path, options = {}, globalOptions = {}, dep
|
|
|
455
457
|
count: fileCount,
|
|
456
458
|
totalSize,
|
|
457
459
|
files
|
|
458
|
-
} = await countFiles(
|
|
460
|
+
} = await countFiles(uploadPath, {
|
|
459
461
|
collectPaths: options.dryRun,
|
|
460
462
|
excludedDirs,
|
|
461
463
|
excludedFiles
|
|
462
464
|
});
|
|
463
465
|
if (fileCount === 0) {
|
|
464
466
|
output.stopSpinner();
|
|
465
|
-
output.error(`No files found in ${
|
|
467
|
+
output.error(`No files found in ${uploadPath}`);
|
|
466
468
|
exit(1);
|
|
467
469
|
return {
|
|
468
470
|
success: false,
|
|
@@ -477,7 +479,7 @@ export async function previewCommand(path, options = {}, globalOptions = {}, dep
|
|
|
477
479
|
if (globalOptions.json) {
|
|
478
480
|
output.data({
|
|
479
481
|
dryRun: true,
|
|
480
|
-
path: resolve(
|
|
482
|
+
path: resolve(uploadPath),
|
|
481
483
|
fileCount,
|
|
482
484
|
totalSize,
|
|
483
485
|
excludedDirs,
|
|
@@ -490,7 +492,7 @@ export async function previewCommand(path, options = {}, globalOptions = {}, dep
|
|
|
490
492
|
} else {
|
|
491
493
|
output.info(`Dry run - would upload ${fileCount} files (${formatBytes(totalSize)})`);
|
|
492
494
|
output.blank();
|
|
493
|
-
output.print(` ${colors.brand.textTertiary('Source')} ${resolve(
|
|
495
|
+
output.print(` ${colors.brand.textTertiary('Source')} ${resolve(uploadPath)}`);
|
|
494
496
|
output.print(` ${colors.brand.textTertiary('Files')} ${fileCount}`);
|
|
495
497
|
output.print(` ${colors.brand.textTertiary('Total size')} ${formatBytes(totalSize)}`);
|
|
496
498
|
output.blank();
|
|
@@ -538,7 +540,7 @@ export async function previewCommand(path, options = {}, globalOptions = {}, dep
|
|
|
538
540
|
let zipPath = join(tmpdir(), `vizzly-preview-${Date.now()}-${randomSuffix}.zip`);
|
|
539
541
|
let zipBuffer;
|
|
540
542
|
try {
|
|
541
|
-
await
|
|
543
|
+
await createZip(uploadPath, zipPath, exclusions);
|
|
542
544
|
zipBuffer = await readFile(zipPath);
|
|
543
545
|
} catch (zipError) {
|
|
544
546
|
output.stopSpinner();
|
|
@@ -569,10 +571,10 @@ export async function previewCommand(path, options = {}, globalOptions = {}, dep
|
|
|
569
571
|
success: true,
|
|
570
572
|
buildId,
|
|
571
573
|
previewUrl: result.previewUrl,
|
|
572
|
-
files: result.uploaded
|
|
574
|
+
files: result.uploaded ?? fileCount,
|
|
573
575
|
bytes: totalSize,
|
|
574
576
|
compressedBytes: zipBuffer.length,
|
|
575
|
-
compressionRatio
|
|
577
|
+
compressionRatio: `${compressionPercent}%`,
|
|
576
578
|
newBytes: result.newBytes,
|
|
577
579
|
reusedBlobs: result.reusedBlobs || 0,
|
|
578
580
|
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');
|
|
@@ -305,7 +307,7 @@ export async function runCommand(testCommand, options = {}, globalOptions = {},
|
|
|
305
307
|
}
|
|
306
308
|
|
|
307
309
|
// JSON output mode - output structured data and exit
|
|
308
|
-
if (globalOptions.json) {
|
|
310
|
+
if (globalOptions.json && !runOptions.wait) {
|
|
309
311
|
let executionTimeMs = Date.now() - startTime;
|
|
310
312
|
let displayUrl = await resolveBuildDisplayUrl({
|
|
311
313
|
result,
|
|
@@ -490,7 +492,17 @@ export async function runCommand(testCommand, options = {}, globalOptions = {},
|
|
|
490
492
|
// Don't fail CI for Vizzly infrastructure issues (5xx errors)
|
|
491
493
|
let status = error.context?.status;
|
|
492
494
|
if (status >= 500) {
|
|
493
|
-
|
|
495
|
+
if (globalOptions.json) {
|
|
496
|
+
output.data({
|
|
497
|
+
buildId: null,
|
|
498
|
+
status: 'skipped',
|
|
499
|
+
message: 'Vizzly API unavailable - visual tests skipped',
|
|
500
|
+
executionTimeMs: Date.now() - (startTime || Date.now())
|
|
501
|
+
});
|
|
502
|
+
} else {
|
|
503
|
+
output.warn('Vizzly API unavailable - visual tests skipped. Your tests still ran.');
|
|
504
|
+
}
|
|
505
|
+
output.cleanup();
|
|
494
506
|
output.debug('api', 'API error details:', {
|
|
495
507
|
error: error.message
|
|
496
508
|
});
|
|
@@ -43,17 +43,20 @@ export function cleanupLocalDaemonFiles(directory = process.cwd(), deps = {}) {
|
|
|
43
43
|
export function buildLegacyServerInfo({
|
|
44
44
|
pid,
|
|
45
45
|
port,
|
|
46
|
+
failOnDiff = false,
|
|
46
47
|
now = Date.now
|
|
47
48
|
}) {
|
|
48
49
|
return {
|
|
49
50
|
pid,
|
|
50
51
|
port: port.toString(),
|
|
51
|
-
startTime: now()
|
|
52
|
+
startTime: now(),
|
|
53
|
+
failOnDiff
|
|
52
54
|
};
|
|
53
55
|
}
|
|
54
56
|
export function writeLegacyGlobalServerFile({
|
|
55
57
|
pid,
|
|
56
|
-
port
|
|
58
|
+
port,
|
|
59
|
+
failOnDiff = false
|
|
57
60
|
}, {
|
|
58
61
|
home = homedir,
|
|
59
62
|
exists = existsSync,
|
|
@@ -71,6 +74,7 @@ export function writeLegacyGlobalServerFile({
|
|
|
71
74
|
let serverInfo = buildLegacyServerInfo({
|
|
72
75
|
pid,
|
|
73
76
|
port,
|
|
77
|
+
failOnDiff,
|
|
74
78
|
now
|
|
75
79
|
});
|
|
76
80
|
writeFile(globalServerFile, JSON.stringify(serverInfo, null, 2));
|
|
@@ -398,7 +402,8 @@ export async function tddStartCommand(options = {}, globalOptions = {}) {
|
|
|
398
402
|
try {
|
|
399
403
|
writeLegacyGlobalServerFile({
|
|
400
404
|
pid: child.pid,
|
|
401
|
-
port
|
|
405
|
+
port,
|
|
406
|
+
failOnDiff: options.failOnDiff || false
|
|
402
407
|
});
|
|
403
408
|
} catch {
|
|
404
409
|
// Non-fatal, SDK can still use health check
|
package/dist/commands/tdd.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 } 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 { createAuthService as defaultCreateAuthService } from '../services/auth-service.js';
|
|
@@ -13,7 +14,7 @@ import { createConfigService as defaultCreateConfigService } from '../services/c
|
|
|
13
14
|
import { createProjectService as defaultCreateProjectService } from '../services/project-service.js';
|
|
14
15
|
import { initializeDaemon as defaultInitializeDaemon, runTests as defaultRunTests } from '../test-runner/index.js';
|
|
15
16
|
import { loadConfig as defaultLoadConfig } from '../utils/config-loader.js';
|
|
16
|
-
import { detectBranch as defaultDetectBranch, detectCommit as defaultDetectCommit } from '../utils/git.js';
|
|
17
|
+
import { detectBranch as defaultDetectBranch, detectCommit as defaultDetectCommit, detectCommitMessage as defaultDetectCommitMessage, detectPullRequestNumber as defaultDetectPullRequestNumber, generateBuildNameWithGit as defaultGenerateBuildNameWithGit } from '../utils/git.js';
|
|
17
18
|
import * as defaultOutput from '../utils/output.js';
|
|
18
19
|
function buildLocalContextCommand() {
|
|
19
20
|
return 'vizzly context build current --source local --agent';
|
|
@@ -43,6 +44,9 @@ export async function tddCommand(testCommand, options = {}, globalOptions = {},
|
|
|
43
44
|
runTests = defaultRunTests,
|
|
44
45
|
detectBranch = defaultDetectBranch,
|
|
45
46
|
detectCommit = defaultDetectCommit,
|
|
47
|
+
detectCommitMessage = defaultDetectCommitMessage,
|
|
48
|
+
detectPullRequestNumber = defaultDetectPullRequestNumber,
|
|
49
|
+
generateBuildNameWithGit = defaultGenerateBuildNameWithGit,
|
|
46
50
|
spawn = defaultSpawn,
|
|
47
51
|
output = defaultOutput
|
|
48
52
|
} = deps;
|
|
@@ -76,7 +80,7 @@ export async function tddCommand(testCommand, options = {}, globalOptions = {},
|
|
|
76
80
|
let config = await loadConfig(globalOptions.config, allOptions);
|
|
77
81
|
|
|
78
82
|
// Dev mode works locally by default - only needs token for baseline download
|
|
79
|
-
let needsToken = options.baselineBuild || options.baselineComparison;
|
|
83
|
+
let needsToken = options.baselineBuild || options.baselineComparison || config.baselineBuildId || config.baselineComparisonId;
|
|
80
84
|
if (!config.apiKey && needsToken) {
|
|
81
85
|
throw new Error('API token required when using --baseline-build or --baseline-comparison flags');
|
|
82
86
|
}
|
|
@@ -85,8 +89,12 @@ export async function tddCommand(testCommand, options = {}, globalOptions = {},
|
|
|
85
89
|
config.allowNoToken = true;
|
|
86
90
|
|
|
87
91
|
// Collect git metadata
|
|
88
|
-
let
|
|
89
|
-
let
|
|
92
|
+
let configuredBuildName = config.build.name && config.build.name !== CONFIG_DEFAULTS.build.name ? config.build.name : undefined;
|
|
93
|
+
let branch = await detectBranch(options.branch || config.build.branch);
|
|
94
|
+
let commit = await detectCommit(options.commit || config.build.commit);
|
|
95
|
+
let message = options.message || config.build.message || (await detectCommitMessage());
|
|
96
|
+
let buildName = await generateBuildNameWithGit(options.buildName || configuredBuildName);
|
|
97
|
+
let pullRequestNumber = detectPullRequestNumber();
|
|
90
98
|
|
|
91
99
|
// Show header (skip in daemon mode)
|
|
92
100
|
if (!options.daemon) {
|
|
@@ -132,9 +140,21 @@ export async function tddCommand(testCommand, options = {}, globalOptions = {},
|
|
|
132
140
|
tdd: true,
|
|
133
141
|
daemon: options.daemon || false,
|
|
134
142
|
setBaseline: options.setBaseline || false,
|
|
143
|
+
failOnDiff: options.failOnDiff || false,
|
|
144
|
+
name: buildName,
|
|
145
|
+
buildName,
|
|
135
146
|
branch,
|
|
136
147
|
commit,
|
|
148
|
+
message,
|
|
137
149
|
environment: config.build.environment,
|
|
150
|
+
pullRequestNumber,
|
|
151
|
+
parallelId: config.parallelId,
|
|
152
|
+
metadata: {
|
|
153
|
+
comparison: {
|
|
154
|
+
threshold: config.comparison.threshold,
|
|
155
|
+
minClusterSize: config.comparison.minClusterSize
|
|
156
|
+
}
|
|
157
|
+
},
|
|
138
158
|
threshold: config.comparison.threshold,
|
|
139
159
|
minClusterSize: config.comparison.minClusterSize,
|
|
140
160
|
allowNoToken: config.allowNoToken || false,
|