@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
|
@@ -102,7 +102,10 @@ export async function fetchBuildUrl({
|
|
|
102
102
|
}
|
|
103
103
|
|
|
104
104
|
/**
|
|
105
|
-
*
|
|
105
|
+
* Flush captures before saving the build's final status.
|
|
106
|
+
* Cloud builds fail when tests or uploads fail. Finalization errors are reported
|
|
107
|
+
* without replacing the test result; collected upload counts are still returned.
|
|
108
|
+
*
|
|
106
109
|
* @param {Object} options - Options
|
|
107
110
|
* @param {string} options.buildId - Build ID
|
|
108
111
|
* @param {boolean} options.tdd - Whether in TDD mode
|
|
@@ -115,6 +118,8 @@ export async function fetchBuildUrl({
|
|
|
115
118
|
* @param {Function} options.deps.finalizeApiBuild - API finalize function
|
|
116
119
|
* @param {Object} options.deps.output - Output utilities
|
|
117
120
|
* @param {Function} [options.deps.onFinalizeFailed] - Callback for finalize failure
|
|
121
|
+
* @returns {Promise<Object|null|undefined>} Cumulative cloud upload results when
|
|
122
|
+
* available; no upload results for local TDD or a run without a build.
|
|
118
123
|
*/
|
|
119
124
|
export async function finalizeBuild({
|
|
120
125
|
buildId,
|
|
@@ -134,6 +139,7 @@ export async function finalizeBuild({
|
|
|
134
139
|
if (!buildId) {
|
|
135
140
|
return;
|
|
136
141
|
}
|
|
142
|
+
let uploadStats = null;
|
|
137
143
|
try {
|
|
138
144
|
if (tdd) {
|
|
139
145
|
// TDD mode: use server handler to finalize (local-only)
|
|
@@ -146,16 +152,18 @@ export async function finalizeBuild({
|
|
|
146
152
|
} else {
|
|
147
153
|
// API mode: flush uploads first, then finalize build
|
|
148
154
|
if (serverManager.server?.finishBuild) {
|
|
149
|
-
await serverManager.server.finishBuild(buildId);
|
|
155
|
+
uploadStats = await serverManager.server.finishBuild(buildId);
|
|
150
156
|
}
|
|
151
157
|
|
|
152
158
|
// Then update build status via API
|
|
153
159
|
let clientOptions = buildClientOptions(config);
|
|
154
160
|
if (clientOptions) {
|
|
155
161
|
let client = createApiClient(clientOptions);
|
|
156
|
-
|
|
162
|
+
let uploadsFailed = uploadStats?.failed > 0;
|
|
163
|
+
let buildSucceeded = success && !uploadsFailed;
|
|
164
|
+
await finalizeApiBuild(client, buildId, buildSucceeded, executionTime, uploadsFailed ? `${uploadStats.failed} screenshot(s) failed to upload. Visual testing is incomplete.` : undefined);
|
|
157
165
|
output.debug('build', 'finalized via api', {
|
|
158
|
-
success
|
|
166
|
+
success: buildSucceeded
|
|
159
167
|
});
|
|
160
168
|
} else {
|
|
161
169
|
output.warn(`No API service available to finalize build ${buildId}`);
|
|
@@ -172,6 +180,7 @@ export async function finalizeBuild({
|
|
|
172
180
|
});
|
|
173
181
|
}
|
|
174
182
|
}
|
|
183
|
+
return uploadStats;
|
|
175
184
|
}
|
|
176
185
|
|
|
177
186
|
// ============================================================================
|
|
@@ -261,7 +270,10 @@ async function executeDisabledTestRun({
|
|
|
261
270
|
* @param {Object} options.runOptions - Run options (testCommand, tdd, etc.)
|
|
262
271
|
* @param {Object} options.config - Configuration object
|
|
263
272
|
* @param {Object} options.deps - Dependencies
|
|
264
|
-
* @returns {Promise<Object>}
|
|
273
|
+
* @returns {Promise<Object>} Test result with cumulative cloud upload results
|
|
274
|
+
* when available. Upload failures alone do not reject the run.
|
|
275
|
+
* @throws {Error} Test execution failure, with upload results attached when
|
|
276
|
+
* available so callers can report both without losing the test exit code.
|
|
265
277
|
*/
|
|
266
278
|
export async function runTests({
|
|
267
279
|
runOptions,
|
|
@@ -315,6 +327,7 @@ export async function runTests({
|
|
|
315
327
|
let screenshotCount = 0;
|
|
316
328
|
let testSuccess = false;
|
|
317
329
|
let testError = null;
|
|
330
|
+
let uploadStats = null;
|
|
318
331
|
let setBaseline;
|
|
319
332
|
try {
|
|
320
333
|
buildId = await createBuild({
|
|
@@ -425,7 +438,7 @@ export async function runTests({
|
|
|
425
438
|
let executionTime = Date.now() - startTime;
|
|
426
439
|
if (buildId) {
|
|
427
440
|
try {
|
|
428
|
-
await finalizeBuild({
|
|
441
|
+
uploadStats = await finalizeBuild({
|
|
429
442
|
buildId,
|
|
430
443
|
tdd,
|
|
431
444
|
success: testSuccess,
|
|
@@ -467,6 +480,7 @@ export async function runTests({
|
|
|
467
480
|
if (!runOptions.json) {
|
|
468
481
|
output.error('Test run failed:', testError);
|
|
469
482
|
}
|
|
483
|
+
if (uploadStats) testError.uploads = uploadStats;
|
|
470
484
|
throw testError;
|
|
471
485
|
}
|
|
472
486
|
return buildRunResult({
|
|
@@ -474,7 +488,8 @@ export async function runTests({
|
|
|
474
488
|
buildUrl,
|
|
475
489
|
testSuccess,
|
|
476
490
|
screenshotCount,
|
|
477
|
-
tddResults
|
|
491
|
+
tddResults,
|
|
492
|
+
uploadStats
|
|
478
493
|
});
|
|
479
494
|
}
|
|
480
495
|
|
package/dist/types/client.d.ts
CHANGED
|
@@ -33,6 +33,8 @@ export function autoDiscoverTddServer(
|
|
|
33
33
|
*/
|
|
34
34
|
export interface ScreenshotResult {
|
|
35
35
|
success: boolean;
|
|
36
|
+
/** Cloud capture accepted locally; upload completion is reported by flush. */
|
|
37
|
+
queued?: boolean;
|
|
36
38
|
status?:
|
|
37
39
|
| 'passed'
|
|
38
40
|
| 'failed'
|
|
@@ -67,16 +69,17 @@ export interface ScreenshotResult {
|
|
|
67
69
|
* @example
|
|
68
70
|
* // With properties and comparison settings
|
|
69
71
|
* await vizzlyScreenshot('checkout-form', screenshot, {
|
|
70
|
-
* properties: { browser: 'chrome',
|
|
72
|
+
* properties: { browser: 'chrome', theme: 'dark', locale: 'en-US' },
|
|
71
73
|
* threshold: 5,
|
|
72
74
|
* minClusterSize: 10,
|
|
73
75
|
* fullPage: true,
|
|
74
76
|
* requestTimeout: 5000
|
|
75
77
|
* });
|
|
76
78
|
*
|
|
77
|
-
* `properties` is the user metadata bag
|
|
78
|
-
*
|
|
79
|
-
*
|
|
79
|
+
* `properties` is the user metadata bag and every key is preserved. Vizzly
|
|
80
|
+
* reads width and height from the captured image. Comparison options are sent
|
|
81
|
+
* separately, while `requestTimeout` stays on the client request and `buildId`
|
|
82
|
+
* only routes the screenshot to a build.
|
|
80
83
|
*/
|
|
81
84
|
export function vizzlyScreenshot(
|
|
82
85
|
name: string,
|
|
@@ -94,11 +97,15 @@ export function vizzlyScreenshot(
|
|
|
94
97
|
): Promise<ScreenshotResult | null>;
|
|
95
98
|
|
|
96
99
|
/**
|
|
97
|
-
*
|
|
100
|
+
* Results returned by vizzlyFlush. Cloud counts cover the whole run, including
|
|
101
|
+
* earlier flushes; success is false if any capture failed. Uploaded and reused
|
|
102
|
+
* counts are separate. TDD mode reports local comparison results instead.
|
|
98
103
|
*/
|
|
99
104
|
export interface FlushResult {
|
|
100
105
|
success: boolean;
|
|
101
106
|
uploaded?: number;
|
|
107
|
+
reused?: number;
|
|
108
|
+
failures?: Array<{ name: string; error: string }>;
|
|
102
109
|
flushed?: boolean;
|
|
103
110
|
total?: number;
|
|
104
111
|
passed?: number;
|
package/dist/types/index.d.ts
CHANGED
|
@@ -401,7 +401,11 @@ export interface TddService {
|
|
|
401
401
|
compareScreenshot(
|
|
402
402
|
name: string,
|
|
403
403
|
imageBuffer: Buffer,
|
|
404
|
-
properties?: Record<string, unknown
|
|
404
|
+
properties?: Record<string, unknown>,
|
|
405
|
+
screenshotOptions?: Pick<
|
|
406
|
+
ScreenshotOptions,
|
|
407
|
+
'threshold' | 'minClusterSize' | 'fullPage'
|
|
408
|
+
>
|
|
405
409
|
): Promise<ComparisonResult>;
|
|
406
410
|
|
|
407
411
|
getResults(): TddResults;
|
package/dist/uploader/core.js
CHANGED
|
@@ -200,12 +200,13 @@ export function buildFileMetadata(filePath, buffer) {
|
|
|
200
200
|
* @returns {Object} Screenshot format for SHA check
|
|
201
201
|
*/
|
|
202
202
|
export function fileToScreenshotFormat(file) {
|
|
203
|
+
let browser = extractBrowserFromFilename(file.filename);
|
|
203
204
|
return {
|
|
204
205
|
sha256: file.sha256,
|
|
205
206
|
name: file.filename.replace(/\.png$/, ''),
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
207
|
+
properties: browser ? {
|
|
208
|
+
browser
|
|
209
|
+
} : {}
|
|
209
210
|
};
|
|
210
211
|
}
|
|
211
212
|
|
|
@@ -1,35 +1,10 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
*/
|
|
5
|
-
export let RESERVED_PROPERTY_OPTIONS = Object.freeze({
|
|
6
|
-
threshold: {
|
|
7
|
-
message: 'Move "threshold" out of properties; properties is only for user metadata.'
|
|
8
|
-
},
|
|
9
|
-
minClusterSize: {
|
|
10
|
-
message: 'Move "minClusterSize" out of properties; properties is only for user metadata.'
|
|
11
|
-
},
|
|
12
|
-
fullPage: {
|
|
13
|
-
message: 'Move "fullPage" out of properties; properties is only for user metadata.'
|
|
14
|
-
},
|
|
15
|
-
buildId: {
|
|
16
|
-
message: 'Move "buildId" out of properties; properties is only for user metadata.'
|
|
17
|
-
},
|
|
18
|
-
requestTimeout: {
|
|
19
|
-
message: 'Move "requestTimeout" out of properties; properties is only for user metadata.'
|
|
20
|
-
}
|
|
21
|
-
});
|
|
22
|
-
function createReservedPropertyWarning(option) {
|
|
23
|
-
return {
|
|
24
|
-
code: 'reserved-property-option',
|
|
25
|
-
option,
|
|
26
|
-
message: RESERVED_PROPERTY_OPTIONS[option].message
|
|
27
|
-
};
|
|
1
|
+
export let SCREENSHOT_OPTION_NAMES = Object.freeze(['threshold', 'minClusterSize', 'fullPage', 'captureMode', 'deviceScaleFactor', 'selector']);
|
|
2
|
+
export function getScreenshotOptionsPayload(options = {}) {
|
|
3
|
+
return Object.fromEntries(SCREENSHOT_OPTION_NAMES.filter(option => options[option] !== undefined).map(option => [option, options[option]]));
|
|
28
4
|
}
|
|
29
5
|
|
|
30
6
|
/**
|
|
31
|
-
*
|
|
32
|
-
* local TDD server and cloud-compatible comparison path.
|
|
7
|
+
* Keep user properties separate from the Vizzly options sent beside them.
|
|
33
8
|
*/
|
|
34
9
|
export function normalizeScreenshotOptions(options = {}) {
|
|
35
10
|
let {
|
|
@@ -38,40 +13,24 @@ export function normalizeScreenshotOptions(options = {}) {
|
|
|
38
13
|
requestTimeout,
|
|
39
14
|
threshold,
|
|
40
15
|
minClusterSize,
|
|
41
|
-
fullPage
|
|
16
|
+
fullPage,
|
|
17
|
+
captureMode,
|
|
18
|
+
deviceScaleFactor,
|
|
19
|
+
selector
|
|
42
20
|
} = options;
|
|
43
|
-
let
|
|
44
|
-
let normalizedProperties = {};
|
|
45
|
-
for (let [key, value] of Object.entries(properties || {})) {
|
|
46
|
-
if (RESERVED_PROPERTY_OPTIONS[key]) {
|
|
47
|
-
warnings.push(createReservedPropertyWarning(key));
|
|
48
|
-
if (key === 'threshold' && threshold === undefined) threshold = value;
|
|
49
|
-
if (key === 'minClusterSize' && minClusterSize === undefined) {
|
|
50
|
-
minClusterSize = value;
|
|
51
|
-
}
|
|
52
|
-
if (key === 'fullPage' && fullPage === undefined) fullPage = value;
|
|
53
|
-
if (key === 'buildId' && buildId === undefined) buildId = value;
|
|
54
|
-
if (key === 'requestTimeout' && requestTimeout === undefined) {
|
|
55
|
-
requestTimeout = value;
|
|
56
|
-
}
|
|
57
|
-
continue;
|
|
58
|
-
}
|
|
59
|
-
normalizedProperties[key] = value;
|
|
60
|
-
}
|
|
61
|
-
if (threshold !== undefined) {
|
|
62
|
-
normalizedProperties.threshold = threshold;
|
|
63
|
-
}
|
|
64
|
-
if (minClusterSize !== undefined) {
|
|
65
|
-
normalizedProperties.minClusterSize = minClusterSize;
|
|
66
|
-
}
|
|
67
|
-
if (fullPage !== undefined) {
|
|
68
|
-
normalizedProperties.fullPage = fullPage;
|
|
69
|
-
}
|
|
21
|
+
let sourceProperties = properties && typeof properties === 'object' && !Array.isArray(properties) ? properties : {};
|
|
70
22
|
return {
|
|
71
23
|
buildId,
|
|
72
24
|
requestTimeout,
|
|
73
|
-
|
|
74
|
-
|
|
25
|
+
threshold,
|
|
26
|
+
minClusterSize,
|
|
27
|
+
fullPage,
|
|
28
|
+
captureMode,
|
|
29
|
+
deviceScaleFactor,
|
|
30
|
+
selector,
|
|
31
|
+
properties: {
|
|
32
|
+
...sourceProperties
|
|
33
|
+
}
|
|
75
34
|
};
|
|
76
35
|
}
|
|
77
36
|
export function createScreenshotProperties(options = {}) {
|
package/dist/utils/security.js
CHANGED
|
@@ -166,55 +166,40 @@ export function safePath(workingDir, ...pathSegments) {
|
|
|
166
166
|
}
|
|
167
167
|
|
|
168
168
|
/**
|
|
169
|
-
*
|
|
170
|
-
*
|
|
171
|
-
*
|
|
169
|
+
* Validate user metadata without interpreting names as Vizzly options or
|
|
170
|
+
* rewriting values. Rendering code must escape strings for its output context.
|
|
171
|
+
* Reject unsafe object keys and values that cannot be represented as JSON.
|
|
172
|
+
*
|
|
173
|
+
* @param {Object} [properties={}] - User screenshot metadata.
|
|
174
|
+
* @returns {Object} A copy preserving the supplied JSON values.
|
|
175
|
+
* @throws {Error} Metadata contains unsafe keys, cycles, or non-JSON values.
|
|
172
176
|
*/
|
|
173
177
|
export function validateScreenshotProperties(properties = {}) {
|
|
174
|
-
if (properties
|
|
178
|
+
if (!properties || typeof properties !== 'object' || Array.isArray(properties)) {
|
|
175
179
|
return {};
|
|
176
180
|
}
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
try {
|
|
182
|
-
// Extract browser name without version (e.g., "Chrome/139.0.7258.138" -> "Chrome")
|
|
183
|
-
const browserName = properties.browser.split('/')[0];
|
|
184
|
-
validated.browser = sanitizeScreenshotName(browserName, 50);
|
|
185
|
-
} catch (error) {
|
|
186
|
-
// Skip invalid browser names, don't include them
|
|
187
|
-
output.warn(`Invalid browser name '${properties.browser}': ${error.message}`);
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
if (properties.viewport && typeof properties.viewport === 'object') {
|
|
191
|
-
const viewport = {};
|
|
192
|
-
if (typeof properties.viewport.width === 'number' && properties.viewport.width > 0 && properties.viewport.width <= 10000) {
|
|
193
|
-
viewport.width = Math.floor(properties.viewport.width);
|
|
194
|
-
}
|
|
195
|
-
if (typeof properties.viewport.height === 'number' && properties.viewport.height > 0 && properties.viewport.height <= 10000) {
|
|
196
|
-
viewport.height = Math.floor(properties.viewport.height);
|
|
181
|
+
let ancestors = new Set();
|
|
182
|
+
function copy(value) {
|
|
183
|
+
if (value === null || typeof value === 'string' || typeof value === 'boolean') {
|
|
184
|
+
return value;
|
|
197
185
|
}
|
|
198
|
-
if (
|
|
199
|
-
|
|
186
|
+
if (typeof value === 'number' && Number.isFinite(value)) return value;
|
|
187
|
+
if (typeof value !== 'object' || ancestors.has(value)) {
|
|
188
|
+
throw new Error('Screenshot properties must contain JSON values');
|
|
200
189
|
}
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
// Preserve safe URL/query characters like '&' in metadata values.
|
|
210
|
-
// Rendering layers should escape for HTML instead of mutating payload data here.
|
|
211
|
-
validated[key] = value.replace(/[<>"']/g, '');
|
|
212
|
-
} else if (typeof value === 'number' && !Number.isNaN(value) && Number.isFinite(value)) {
|
|
213
|
-
validated[key] = value;
|
|
214
|
-
} else if (typeof value === 'boolean') {
|
|
215
|
-
validated[key] = value;
|
|
190
|
+
ancestors.add(value);
|
|
191
|
+
let result;
|
|
192
|
+
if (Array.isArray(value)) {
|
|
193
|
+
result = value.map(copy);
|
|
194
|
+
} else {
|
|
195
|
+
let entries = Object.entries(value);
|
|
196
|
+
if (entries.some(([key]) => ['__proto__', 'constructor', 'prototype'].includes(key))) {
|
|
197
|
+
throw new Error('Screenshot properties contain an unsafe key');
|
|
216
198
|
}
|
|
199
|
+
result = Object.fromEntries(entries.map(([key, item]) => [key, copy(item)]));
|
|
217
200
|
}
|
|
201
|
+
ancestors.delete(value);
|
|
202
|
+
return result;
|
|
218
203
|
}
|
|
219
|
-
return
|
|
204
|
+
return copy(properties);
|
|
220
205
|
}
|