@vizzly-testing/cli 0.35.3-beta.3 → 0.36.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 +8 -8
- package/dist/api/core.js +20 -13
- package/dist/api/endpoints.js +14 -11
- package/dist/client/index.js +2 -7
- package/dist/commands/run.js +55 -3
- package/dist/reporter/reporter-bundle.iife.js +51 -51
- package/dist/screenshot-server/core.js +5 -1
- package/dist/sdk/index.js +2 -11
- package/dist/server/handlers/api-handler.js +117 -191
- package/dist/server/handlers/tdd-handler.js +15 -50
- package/dist/server/routers/screenshot.js +30 -4
- package/dist/tdd/tdd-service.js +3 -3
- package/dist/test-runner/core.js +7 -1
- package/dist/test-runner/operations.js +22 -7
- package/dist/types/client.d.ts +12 -5
- package/dist/types/index.d.ts +5 -1
- package/dist/uploader/core.js +4 -3
- package/dist/utils/screenshot-options.js +18 -59
- package/dist/utils/security.js +27 -42
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -144,19 +144,19 @@ test('homepage looks correct', async ({ page }) => {
|
|
|
144
144
|
fullPage: true,
|
|
145
145
|
requestTimeout: 5000,
|
|
146
146
|
properties: {
|
|
147
|
-
|
|
148
|
-
|
|
147
|
+
theme: 'dark',
|
|
148
|
+
locale: 'en-US',
|
|
149
149
|
},
|
|
150
150
|
});
|
|
151
151
|
});
|
|
152
152
|
```
|
|
153
153
|
|
|
154
|
-
`properties` is your metadata bag for baseline grouping, filtering,
|
|
155
|
-
debugging.
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
be
|
|
154
|
+
`properties` is your key/value metadata bag for baseline grouping, filtering,
|
|
155
|
+
and debugging. Vizzly passes every property through as user metadata; property
|
|
156
|
+
names are never interpreted as options. The supported top-level options are
|
|
157
|
+
`threshold`, `minClusterSize`, `fullPage`, `requestTimeout`, and `buildId`.
|
|
158
|
+
Vizzly reads width and height from the captured image, so viewport dimensions do
|
|
159
|
+
not need to be included in `properties`.
|
|
160
160
|
|
|
161
161
|
The client SDK is lightweight. It posts screenshots to the local Vizzly server
|
|
162
162
|
or the cloud build wrapper. It works with any test runner.
|
package/dist/api/core.js
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
import crypto from 'node:crypto';
|
|
9
9
|
import { URLSearchParams } from 'node:url';
|
|
10
|
+
import { createScreenshotProperties, getScreenshotOptionsPayload } from '../utils/screenshot-options.js';
|
|
10
11
|
|
|
11
12
|
// ============================================================================
|
|
12
13
|
// Header Building
|
|
@@ -29,14 +30,17 @@ export function buildAuthHeader(token) {
|
|
|
29
30
|
* @param {string} version - CLI version
|
|
30
31
|
* @param {string} command - Command being executed (run, upload, tdd, etc.)
|
|
31
32
|
* @param {string|null} sdkUserAgent - Optional SDK user agent to append
|
|
33
|
+
* @param {string} nodeVersion - Node.js version to append
|
|
32
34
|
* @returns {string} Complete User-Agent string
|
|
33
35
|
*/
|
|
34
|
-
export function buildUserAgent(version, command, sdkUserAgent = null) {
|
|
36
|
+
export function buildUserAgent(version, command, sdkUserAgent = null, nodeVersion = process.versions.node) {
|
|
35
37
|
let baseUserAgent = `vizzly-cli/${version} (${command})`;
|
|
38
|
+
let userAgentParts = [baseUserAgent];
|
|
36
39
|
if (sdkUserAgent) {
|
|
37
|
-
|
|
40
|
+
userAgentParts.push(sdkUserAgent);
|
|
38
41
|
}
|
|
39
|
-
|
|
42
|
+
if (nodeVersion) userAgentParts.push(`node/${nodeVersion}`);
|
|
43
|
+
return userAgentParts.join(' ');
|
|
40
44
|
}
|
|
41
45
|
|
|
42
46
|
/**
|
|
@@ -73,15 +77,18 @@ export function buildRequestHeaders({
|
|
|
73
77
|
* Build payload for screenshot upload or SHA resolution
|
|
74
78
|
* @param {string} name - Screenshot name
|
|
75
79
|
* @param {Buffer|null} buffer - Image data, or null when resolving by SHA
|
|
76
|
-
* @param {Object} metadata -
|
|
80
|
+
* @param {Object} metadata - User screenshot properties
|
|
77
81
|
* @param {string|null} sha256 - Pre-computed SHA256 hash (optional)
|
|
78
82
|
* @returns {Object} Screenshot upload payload
|
|
79
83
|
*/
|
|
80
|
-
export function buildScreenshotPayload(name, buffer, metadata = {}, sha256 = null) {
|
|
84
|
+
export function buildScreenshotPayload(name, buffer, metadata = {}, sha256 = null, screenshotOptions = {}) {
|
|
81
85
|
let payload = {
|
|
82
86
|
name,
|
|
83
|
-
properties:
|
|
87
|
+
properties: createScreenshotProperties({
|
|
88
|
+
properties: metadata
|
|
89
|
+
})
|
|
84
90
|
};
|
|
91
|
+
Object.assign(payload, getScreenshotOptionsPayload(screenshotOptions));
|
|
85
92
|
if (buffer) {
|
|
86
93
|
payload.image_data = buffer.toString('base64');
|
|
87
94
|
}
|
|
@@ -94,12 +101,12 @@ export function buildScreenshotPayload(name, buffer, metadata = {}, sha256 = nul
|
|
|
94
101
|
/**
|
|
95
102
|
* Build payload for server-side screenshot resolution without sending image bytes
|
|
96
103
|
* @param {string} name - Screenshot name
|
|
97
|
-
* @param {Object} metadata -
|
|
104
|
+
* @param {Object} metadata - User screenshot properties
|
|
98
105
|
* @param {string} sha256 - Pre-computed SHA256 hash
|
|
99
106
|
* @returns {Object} Screenshot resolution payload
|
|
100
107
|
*/
|
|
101
|
-
export function buildScreenshotResolvePayload(name, metadata = {}, sha256) {
|
|
102
|
-
return buildScreenshotPayload(name, null, metadata, sha256);
|
|
108
|
+
export function buildScreenshotResolvePayload(name, metadata = {}, sha256, screenshotOptions = {}) {
|
|
109
|
+
return buildScreenshotPayload(name, null, metadata, sha256, screenshotOptions);
|
|
103
110
|
}
|
|
104
111
|
|
|
105
112
|
/**
|
|
@@ -180,13 +187,13 @@ export function buildShaCheckPayload(screenshots, buildId) {
|
|
|
180
187
|
*/
|
|
181
188
|
export function buildScreenshotCheckObject(sha256, name, metadata = {}) {
|
|
182
189
|
let meta = metadata || {};
|
|
190
|
+
let properties = createScreenshotProperties({
|
|
191
|
+
properties: meta
|
|
192
|
+
});
|
|
183
193
|
return {
|
|
184
194
|
sha256,
|
|
185
195
|
name,
|
|
186
|
-
|
|
187
|
-
viewport_width: meta.viewport?.width || meta.viewport_width || 1920,
|
|
188
|
-
viewport_height: meta.viewport?.height || meta.viewport_height || 1080,
|
|
189
|
-
properties: meta
|
|
196
|
+
properties
|
|
190
197
|
};
|
|
191
198
|
}
|
|
192
199
|
|
package/dist/api/endpoints.js
CHANGED
|
@@ -80,13 +80,15 @@ export async function createBuild(client, metadata) {
|
|
|
80
80
|
* @param {Object} client - API client
|
|
81
81
|
* @param {string} buildId - Build ID
|
|
82
82
|
* @param {string} status - Build status (pending|running|completed|failed)
|
|
83
|
-
* @param {number|null} executionTimeMs - Execution time in milliseconds
|
|
83
|
+
* @param {number|null} [executionTimeMs=null] - Execution time in milliseconds
|
|
84
|
+
* @param {string} [failureReason] - Explanation sent only for a failed build
|
|
84
85
|
* @returns {Promise<Object>} Updated build data
|
|
85
86
|
*/
|
|
86
|
-
export async function updateBuildStatus(client, buildId, status, executionTimeMs = null) {
|
|
87
|
+
export async function updateBuildStatus(client, buildId, status, executionTimeMs = null, failureReason) {
|
|
87
88
|
let body = {
|
|
88
89
|
status
|
|
89
90
|
};
|
|
91
|
+
if (status === 'failed' && failureReason) body.failureReason = failureReason;
|
|
90
92
|
if (executionTimeMs != null) {
|
|
91
93
|
body.executionTimeMs = executionTimeMs;
|
|
92
94
|
}
|
|
@@ -100,16 +102,17 @@ export async function updateBuildStatus(client, buildId, status, executionTimeMs
|
|
|
100
102
|
}
|
|
101
103
|
|
|
102
104
|
/**
|
|
103
|
-
*
|
|
105
|
+
* Mark a build completed or failed, with an optional failure explanation.
|
|
104
106
|
* @param {Object} client - API client
|
|
105
107
|
* @param {string} buildId - Build ID
|
|
106
|
-
* @param {boolean} success - Whether
|
|
107
|
-
* @param {number|null} executionTimeMs - Execution time in milliseconds
|
|
108
|
+
* @param {boolean} [success=true] - Whether tests and uploads succeeded
|
|
109
|
+
* @param {number|null} [executionTimeMs=null] - Execution time in milliseconds
|
|
110
|
+
* @param {string} [failureReason] - Explanation sent only for a failed build
|
|
108
111
|
* @returns {Promise<Object>} Finalized build data
|
|
109
112
|
*/
|
|
110
|
-
export async function finalizeBuild(client, buildId, success = true, executionTimeMs = null) {
|
|
113
|
+
export async function finalizeBuild(client, buildId, success = true, executionTimeMs = null, failureReason) {
|
|
111
114
|
let status = success ? 'completed' : 'failed';
|
|
112
|
-
return updateBuildStatus(client, buildId, status, executionTimeMs);
|
|
115
|
+
return updateBuildStatus(client, buildId, status, executionTimeMs, failureReason);
|
|
113
116
|
}
|
|
114
117
|
|
|
115
118
|
/**
|
|
@@ -179,10 +182,10 @@ function canUploadBytesAfterShaResolveError(error) {
|
|
|
179
182
|
* @param {boolean} skipDedup - Skip SHA deduplication (uploadAll mode)
|
|
180
183
|
* @returns {Promise<Object>} Upload result
|
|
181
184
|
*/
|
|
182
|
-
export async function uploadScreenshot(client, buildId, name, buffer, metadata = {}, skipDedup = false) {
|
|
185
|
+
export async function uploadScreenshot(client, buildId, name, buffer, metadata = {}, skipDedup = false, screenshotOptions = {}) {
|
|
183
186
|
// Skip SHA deduplication if requested
|
|
184
187
|
if (skipDedup) {
|
|
185
|
-
let payload = buildScreenshotPayload(name, buffer, metadata);
|
|
188
|
+
let payload = buildScreenshotPayload(name, buffer, metadata, null, screenshotOptions);
|
|
186
189
|
return client.request(`/api/sdk/builds/${buildId}/screenshots`, {
|
|
187
190
|
method: 'POST',
|
|
188
191
|
headers: {
|
|
@@ -194,7 +197,7 @@ export async function uploadScreenshot(client, buildId, name, buffer, metadata =
|
|
|
194
197
|
|
|
195
198
|
// Normal flow with server-side SHA resolution.
|
|
196
199
|
let sha256 = computeSha256(buffer);
|
|
197
|
-
let resolvePayload = buildScreenshotResolvePayload(name, metadata, sha256);
|
|
200
|
+
let resolvePayload = buildScreenshotResolvePayload(name, metadata, sha256, screenshotOptions);
|
|
198
201
|
try {
|
|
199
202
|
let resolveResult = await client.request(`/api/sdk/builds/${buildId}/screenshots`, {
|
|
200
203
|
method: 'POST',
|
|
@@ -222,7 +225,7 @@ export async function uploadScreenshot(client, buildId, name, buffer, metadata =
|
|
|
222
225
|
}
|
|
223
226
|
|
|
224
227
|
// File doesn't exist, or resolve failed; proceed with upload.
|
|
225
|
-
let payload = buildScreenshotPayload(name, buffer, metadata, sha256);
|
|
228
|
+
let payload = buildScreenshotPayload(name, buffer, metadata, sha256, screenshotOptions);
|
|
226
229
|
return client.request(`/api/sdk/builds/${buildId}/screenshots`, {
|
|
227
230
|
method: 'POST',
|
|
228
231
|
headers: {
|
package/dist/client/index.js
CHANGED
|
@@ -8,7 +8,7 @@ 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 { normalizeScreenshotOptions } from '../utils/screenshot-options.js';
|
|
11
|
+
import { getScreenshotOptionsPayload, normalizeScreenshotOptions } from '../utils/screenshot-options.js';
|
|
12
12
|
|
|
13
13
|
// Internal client state
|
|
14
14
|
let currentClient = null;
|
|
@@ -203,9 +203,6 @@ function createSimpleClient(serverUrl, clientOptions = {}) {
|
|
|
203
203
|
async screenshot(name, imageBuffer, options = {}) {
|
|
204
204
|
let normalizedOptions = normalizeScreenshotOptions(options);
|
|
205
205
|
let requestTimeout = normalizedOptions.requestTimeout || DEFAULT_TIMEOUT_MS;
|
|
206
|
-
for (let warning of normalizedOptions.warnings) {
|
|
207
|
-
console.warn(`[vizzly] ${warning.message}`);
|
|
208
|
-
}
|
|
209
206
|
try {
|
|
210
207
|
// If it's a string, assume it's a file path and send directly
|
|
211
208
|
// Otherwise it's a Buffer, so convert to base64
|
|
@@ -219,9 +216,7 @@ function createSimpleClient(serverUrl, clientOptions = {}) {
|
|
|
219
216
|
type,
|
|
220
217
|
properties: normalizedOptions.properties
|
|
221
218
|
};
|
|
222
|
-
|
|
223
|
-
screenshotData.warnings = normalizedOptions.warnings;
|
|
224
|
-
}
|
|
219
|
+
Object.assign(screenshotData, getScreenshotOptionsPayload(normalizedOptions));
|
|
225
220
|
let httpStart = Date.now();
|
|
226
221
|
let {
|
|
227
222
|
status,
|
package/dist/commands/run.js
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Run
|
|
2
|
+
* Run tests and report their visual results in terminal or JSON output.
|
|
3
|
+
*
|
|
4
|
+
* Incomplete uploads preserve the test exit code and skip comparison waiting.
|
|
5
|
+
* After successful uploads, --wait can still fail for visual comparison results.
|
|
6
|
+
* The returned success flag describes command success, not upload completeness.
|
|
7
|
+
*
|
|
3
8
|
* Uses functional operations directly - no class wrappers needed
|
|
4
9
|
*/
|
|
5
10
|
|
|
@@ -16,6 +21,14 @@ import { loadConfig as defaultLoadConfig } from '../utils/config-loader.js';
|
|
|
16
21
|
import { detectBranch as defaultDetectBranch, detectCommit as defaultDetectCommit, detectCommitMessage as defaultDetectCommitMessage, detectPullRequestNumber as defaultDetectPullRequestNumber, generateBuildNameWithGit as defaultGenerateBuildNameWithGit } from '../utils/git.js';
|
|
17
22
|
import * as defaultOutput from '../utils/output.js';
|
|
18
23
|
import { writeSession as defaultWriteSession } from '../utils/session.js';
|
|
24
|
+
function formatUploadCounts({
|
|
25
|
+
total,
|
|
26
|
+
uploaded,
|
|
27
|
+
reused,
|
|
28
|
+
failed
|
|
29
|
+
}) {
|
|
30
|
+
return `${total} captured, ${uploaded} uploaded, ${reused} reused, ${failed} failed`;
|
|
31
|
+
}
|
|
19
32
|
export async function resolveBuildDisplayUrl({
|
|
20
33
|
result,
|
|
21
34
|
config,
|
|
@@ -68,7 +81,9 @@ function buildContextCommand(buildId, {
|
|
|
68
81
|
* @param {string} testCommand - Test command to execute
|
|
69
82
|
* @param {Object} options - Command options
|
|
70
83
|
* @param {Object} globalOptions - Global CLI options
|
|
71
|
-
* @param {Object} deps -
|
|
84
|
+
* @param {Object} deps - Command services and process hooks.
|
|
85
|
+
* @returns {Promise<Object>} Command outcome with success, an optional exitCode,
|
|
86
|
+
* and run results when available.
|
|
72
87
|
*/
|
|
73
88
|
export async function runCommand(testCommand, options = {}, globalOptions = {}, deps = {}) {
|
|
74
89
|
let {
|
|
@@ -321,6 +336,33 @@ export async function runCommand(testCommand, options = {}, globalOptions = {},
|
|
|
321
336
|
if (result.buildId) {
|
|
322
337
|
buildId = result.buildId;
|
|
323
338
|
}
|
|
339
|
+
if (result.uploads?.failed > 0) {
|
|
340
|
+
let message = `${result.uploads.failed} screenshot(s) failed to upload. Visual testing is incomplete.`;
|
|
341
|
+
if (globalOptions.json) {
|
|
342
|
+
output.data({
|
|
343
|
+
buildId: result.buildId,
|
|
344
|
+
status: 'incomplete',
|
|
345
|
+
screenshotsCaptured: result.screenshotsCaptured,
|
|
346
|
+
uploads: result.uploads,
|
|
347
|
+
error: {
|
|
348
|
+
code: 'SCREENSHOT_UPLOAD_FAILED',
|
|
349
|
+
message
|
|
350
|
+
},
|
|
351
|
+
url: result.url,
|
|
352
|
+
exitCode: 0
|
|
353
|
+
});
|
|
354
|
+
} else {
|
|
355
|
+
output.warn(message);
|
|
356
|
+
output.print(`Screenshots: ${formatUploadCounts(result.uploads)}`);
|
|
357
|
+
if (result.url) output.info(`Results: ${result.url}`);
|
|
358
|
+
}
|
|
359
|
+
output.cleanup();
|
|
360
|
+
return {
|
|
361
|
+
success: true,
|
|
362
|
+
exitCode: 0,
|
|
363
|
+
result
|
|
364
|
+
};
|
|
365
|
+
}
|
|
324
366
|
|
|
325
367
|
// JSON output mode - output structured data and exit
|
|
326
368
|
if (globalOptions.json && !runOptions.wait) {
|
|
@@ -336,6 +378,9 @@ export async function runCommand(testCommand, options = {}, globalOptions = {},
|
|
|
336
378
|
status: 'completed',
|
|
337
379
|
url: displayUrl,
|
|
338
380
|
screenshotsCaptured: result.screenshotsCaptured || 0,
|
|
381
|
+
...(result.uploads ? {
|
|
382
|
+
uploads: result.uploads
|
|
383
|
+
} : {}),
|
|
339
384
|
executionTimeMs,
|
|
340
385
|
git: {
|
|
341
386
|
branch,
|
|
@@ -362,7 +407,8 @@ export async function runCommand(testCommand, options = {}, globalOptions = {},
|
|
|
362
407
|
if (result.buildId && !globalOptions.json) {
|
|
363
408
|
output.blank();
|
|
364
409
|
let colors = output.getColors();
|
|
365
|
-
|
|
410
|
+
let screenshotSummary = result.uploads ? formatUploadCounts(result.uploads) : result.screenshotsCaptured;
|
|
411
|
+
output.print(` ${colors.brand.textTertiary('Screenshots')} ${colors.white(screenshotSummary)}`);
|
|
366
412
|
let displayUrl = await resolveBuildDisplayUrl({
|
|
367
413
|
result,
|
|
368
414
|
config,
|
|
@@ -392,6 +438,9 @@ export async function runCommand(testCommand, options = {}, globalOptions = {},
|
|
|
392
438
|
output.data({
|
|
393
439
|
buildId: buildId || null,
|
|
394
440
|
status: 'failed',
|
|
441
|
+
...(error.uploads ? {
|
|
442
|
+
uploads: error.uploads
|
|
443
|
+
} : {}),
|
|
395
444
|
error: {
|
|
396
445
|
code: error.code,
|
|
397
446
|
message: error.message
|
|
@@ -479,6 +528,9 @@ export async function runCommand(testCommand, options = {}, globalOptions = {},
|
|
|
479
528
|
status: buildResult.status,
|
|
480
529
|
url: displayUrl,
|
|
481
530
|
screenshotsCaptured: result.screenshotsCaptured || 0,
|
|
531
|
+
...(result.uploads ? {
|
|
532
|
+
uploads: result.uploads
|
|
533
|
+
} : {}),
|
|
482
534
|
executionTimeMs,
|
|
483
535
|
git: {
|
|
484
536
|
branch,
|