@vizzly-testing/cli 0.35.3-beta.4 → 0.37.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.
@@ -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', viewport: { width: 1920, height: 1080 } },
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. Comparison options are normalized
78
- * into the server metadata payload, while `requestTimeout` stays on the
79
- * client request and `buildId` only routes the screenshot to a build.
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
- * Flush result summary returned by vizzlyFlush
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;
@@ -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
- browser: extractBrowserFromFilename(file.filename) || 'chrome',
207
- viewport_width: 1920,
208
- viewport_height: 1080
207
+ properties: browser ? {
208
+ browser
209
+ } : {}
209
210
  };
210
211
  }
211
212
 
@@ -1,82 +1,10 @@
1
- /**
2
- * Screenshot option names that are part of the SDK/config contract, not the
3
- * user's arbitrary metadata bag.
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
- min_cluster_size: {
13
- message: 'Move "min_cluster_size" out of properties; properties is only for user metadata.'
14
- },
15
- fullPage: {
16
- message: 'Move "fullPage" out of properties; properties is only for user metadata.'
17
- },
18
- full_page: {
19
- message: 'Move "full_page" out of properties; properties is only for user metadata.'
20
- },
21
- captureMode: {
22
- message: 'Move "captureMode" out of properties; properties is only for user metadata.'
23
- },
24
- capture_mode: {
25
- message: 'Move "capture_mode" out of properties; properties is only for user metadata.'
26
- },
27
- deviceScaleFactor: {
28
- message: 'Move "deviceScaleFactor" out of properties; properties is only for user metadata.'
29
- },
30
- device_scale_factor: {
31
- message: 'Move "device_scale_factor" out of properties; properties is only for user metadata.'
32
- },
33
- pixelRatio: {
34
- message: 'Move "pixelRatio" out of properties; properties is only for user metadata.'
35
- },
36
- dpr: {
37
- message: 'Move "dpr" out of properties; properties is only for user metadata.'
38
- },
39
- selector: {
40
- message: 'Move "selector" out of properties; properties is only for user metadata.'
41
- },
42
- component: {
43
- message: 'Move "component" out of properties; properties is only for user metadata.'
44
- },
45
- elementSelector: {
46
- message: 'Move "elementSelector" out of properties; properties is only for user metadata.'
47
- },
48
- element_selector: {
49
- message: 'Move "element_selector" out of properties; properties is only for user metadata.'
50
- },
51
- buildId: {
52
- message: 'Move "buildId" out of properties; properties is only for user metadata.'
53
- },
54
- build_id: {
55
- message: 'Move "build_id" out of properties; properties is only for user metadata.'
56
- },
57
- requestTimeout: {
58
- message: 'Move "requestTimeout" out of properties; properties is only for user metadata.'
59
- },
60
- request_timeout: {
61
- message: 'Move "request_timeout" out of properties; properties is only for user metadata.'
62
- }
63
- });
64
1
  export let SCREENSHOT_OPTION_NAMES = Object.freeze(['threshold', 'minClusterSize', 'fullPage', 'captureMode', 'deviceScaleFactor', 'selector']);
65
2
  export function getScreenshotOptionsPayload(options = {}) {
66
3
  return Object.fromEntries(SCREENSHOT_OPTION_NAMES.filter(option => options[option] !== undefined).map(option => [option, options[option]]));
67
4
  }
68
- function createReservedPropertyWarning(option) {
69
- return {
70
- code: 'reserved-property-option',
71
- option,
72
- message: RESERVED_PROPERTY_OPTIONS[option].message
73
- };
74
- }
75
5
 
76
6
  /**
77
- * Normalize screenshot SDK options into the local and cloud upload payload.
78
- * Reserved names found inside properties are discarded instead of being promoted
79
- * into options.
7
+ * Keep user properties separate from the Vizzly options sent beside them.
80
8
  */
81
9
  export function normalizeScreenshotOptions(options = {}) {
82
10
  let {
@@ -90,16 +18,7 @@ export function normalizeScreenshotOptions(options = {}) {
90
18
  deviceScaleFactor,
91
19
  selector
92
20
  } = options;
93
- let warnings = [];
94
- let normalizedProperties = {};
95
21
  let sourceProperties = properties && typeof properties === 'object' && !Array.isArray(properties) ? properties : {};
96
- for (let [key, value] of Object.entries(sourceProperties)) {
97
- if (RESERVED_PROPERTY_OPTIONS[key]) {
98
- warnings.push(createReservedPropertyWarning(key));
99
- continue;
100
- }
101
- normalizedProperties[key] = value;
102
- }
103
22
  return {
104
23
  buildId,
105
24
  requestTimeout,
@@ -109,8 +28,9 @@ export function normalizeScreenshotOptions(options = {}) {
109
28
  captureMode,
110
29
  deviceScaleFactor,
111
30
  selector,
112
- properties: normalizedProperties,
113
- warnings
31
+ properties: {
32
+ ...sourceProperties
33
+ }
114
34
  };
115
35
  }
116
36
  export function createScreenshotProperties(options = {}) {
@@ -166,55 +166,40 @@ export function safePath(workingDir, ...pathSegments) {
166
166
  }
167
167
 
168
168
  /**
169
- * Validates screenshot properties object for safe values
170
- * @param {Object} properties - Properties to validate
171
- * @returns {Object} Validated properties object
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 === null || typeof properties !== 'object') {
178
+ if (!properties || typeof properties !== 'object' || Array.isArray(properties)) {
175
179
  return {};
176
180
  }
177
- const validated = {};
178
-
179
- // Validate common properties with safe constraints
180
- if (properties.browser && typeof properties.browser === 'string') {
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 (Object.keys(viewport).length > 0) {
199
- validated.viewport = viewport;
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
- // Allow other safe string properties but sanitize them
204
- for (const [key, value] of Object.entries(properties)) {
205
- if (key === 'browser' || key === 'viewport') continue; // Already handled
206
-
207
- if (typeof key === 'string' && key.length <= 50 && /^[a-zA-Z0-9_-]+$/.test(key)) {
208
- if (typeof value === 'string' && value.length <= 200) {
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 validated;
204
+ return copy(properties);
220
205
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vizzly-testing/cli",
3
- "version": "0.35.3-beta.4",
3
+ "version": "0.37.0",
4
4
  "description": "Visual regression testing from your terminal",
5
5
  "keywords": [
6
6
  "llm-context",
@@ -72,17 +72,18 @@
72
72
  "dev:reporter": "cd src/reporter && vite --config vite.dev.config.js",
73
73
  "test:types": "tsd",
74
74
  "prepublishOnly": "pnpm run build",
75
- "test": "node --experimental-test-coverage --test --test-concurrency=1 --test-reporter=spec $(find tests -name '*.test.js')",
75
+ "test": "node --experimental-test-coverage --test --test-concurrency=1 --test-reporter=spec $(find tests clients/swift/tests-js -name '*.test.js')",
76
76
  "test:watch": "node --test --test-reporter=spec --watch $(find tests -name '*.test.js')",
77
77
  "test:reporter": "playwright test --config=tests/reporter/playwright.config.js",
78
78
  "test:reporter:visual": "node bin/vizzly.js tdd run \"pnpm run test:reporter\" --no-open",
79
79
  "test:swift:e2e": "pnpm run build && node clients/swift/scripts/run-e2e.js",
80
+ "test:swift:previews:e2e": "node clients/swift/scripts/run-preview-e2e.js",
80
81
  "test:tui": "node --test --test-reporter=spec tests/tui/*.test.js",
81
82
  "test:tui:docker": "./tests/tui/run-tui-tests.sh",
82
- "lint": "biome check src tests clients/storybook/src clients/storybook/tests clients/static-site/src clients/static-site/tests clients/vitest/src clients/vitest/tests clients/ember/src clients/ember/tests clients/ember/bin",
83
- "lint:fix": "biome check --write --unsafe src tests clients/storybook/src clients/storybook/tests clients/static-site/src clients/static-site/tests clients/vitest/src clients/vitest/tests clients/ember/src clients/ember/tests clients/ember/bin",
84
- "format": "biome format --write src tests clients/storybook/src clients/storybook/tests clients/static-site/src clients/static-site/tests clients/vitest/src clients/vitest/tests clients/ember/src clients/ember/tests clients/ember/bin",
85
- "format:check": "biome format src tests clients/storybook/src clients/storybook/tests clients/static-site/src clients/static-site/tests clients/vitest/src clients/vitest/tests clients/ember/src clients/ember/tests clients/ember/bin",
83
+ "lint": "biome check src tests clients/storybook/src clients/storybook/tests clients/static-site/src clients/static-site/tests clients/vitest/src clients/vitest/tests clients/ember/src clients/ember/tests clients/ember/bin clients/swift/src clients/swift/tests-js clients/swift/scripts",
84
+ "lint:fix": "biome check --write --unsafe src tests clients/storybook/src clients/storybook/tests clients/static-site/src clients/static-site/tests clients/vitest/src clients/vitest/tests clients/ember/src clients/ember/tests clients/ember/bin clients/swift/src clients/swift/tests-js clients/swift/scripts",
85
+ "format": "biome format --write src tests clients/storybook/src clients/storybook/tests clients/static-site/src clients/static-site/tests clients/vitest/src clients/vitest/tests clients/ember/src clients/ember/tests clients/ember/bin clients/swift/src clients/swift/tests-js clients/swift/scripts",
86
+ "format:check": "biome format src tests clients/storybook/src clients/storybook/tests clients/static-site/src clients/static-site/tests clients/vitest/src clients/vitest/tests clients/ember/src clients/ember/tests clients/ember/bin clients/swift/src clients/swift/tests-js clients/swift/scripts",
86
87
  "fix": "pnpm run format && pnpm run lint:fix"
87
88
  },
88
89
  "engines": {
@@ -94,7 +95,7 @@
94
95
  },
95
96
  "dependencies": {
96
97
  "@vizzly-testing/bear-den": "0.1.4",
97
- "@vizzly-testing/honeydiff": "^0.16.0",
98
+ "@vizzly-testing/honeydiff": "^0.17.0",
98
99
  "ansis": "^4.2.0",
99
100
  "commander": "^15.0.0",
100
101
  "cosmiconfig": "^9.0.0",
@@ -20,21 +20,29 @@ test workflow in charge of how the UI is exercised.
20
20
 
21
21
  ## Inspect And Verify
22
22
 
23
+ For cloud API queries, selectable fields, or an authorized review decision,
24
+ start with `vizzly api schema --json` and follow
25
+ [schema discovery](references/cli-context.md#query-the-cloud-api).
26
+ Discover request details as needed instead of loading the entire OpenAPI document.
27
+ Use `vizzly context ... --json` for local evidence and human-readable `context`
28
+ output for guided inspection.
29
+
23
30
  1. Choose the supplied cloud build or comparison when one is named. Otherwise,
24
31
  use current local evidence or find the relevant cloud build.
25
- 2. Request bounded JSON:
32
+ 2. For cloud data, discover the relevant API operations. For locally saved data,
33
+ request JSON context:
26
34
 
27
35
  ```bash
28
- vizzly context build current --source local --agent --json
29
- vizzly context build <build-id> --source cloud --agent --json
36
+ vizzly api schema --json
37
+ vizzly context build current --source local --json
30
38
  ```
31
39
 
32
40
  3. Confirm the build, source, branch, timestamps, baseline, review state, and
33
- pagination before drawing conclusions. If `has_more` is true, run the
34
- returned next-page command before concluding. Missing fields remain unknown.
35
- 4. Follow `suggested_commands` to inspect a comparison. View its baseline,
36
- current, and diff images together. If an image cannot be opened, label the
37
- result metadata-only; do not call it visual verification.
41
+ pagination before drawing conclusions. Follow the schema's page instructions
42
+ for cloud data. Missing fields remain unknown.
43
+ 4. For cloud data, discover the comparison and image operations from the schema.
44
+ View the baseline, current, and diff images together. If an image cannot be
45
+ opened, label the result metadata-only; do not call it visual verification.
38
46
  5. Read image dimensions, viewport, browser, diff regions, fingerprint, and
39
47
  relevant history alongside the images. A prior approval is supporting
40
48
  evidence, not permission to approve the current comparison.
@@ -57,7 +65,7 @@ test workflow in charge of how the UI is exercised.
57
65
  ## Load A Reference When Needed
58
66
 
59
67
  - [CLI context](references/cli-context.md): local and cloud evidence, build
60
- discovery, drill-downs, images, and TDD lifecycle.
68
+ discovery, schema queries, review decisions, images, and TDD lifecycle.
61
69
  - [SDK capture](references/sdks.md): add or change screenshot capture code.
62
70
  - [Dynamic content](references/dynamic-content.md): investigate unstable
63
71
  content and screenshot-specific tolerances.
@@ -4,6 +4,41 @@ Use the repository's established CLI invocation and existing authentication.
4
4
  If cloud authentication is unavailable, report the blocker. Do not start an
5
5
  interactive login unless setup is in scope.
6
6
 
7
+ ## Query The Cloud API
8
+
9
+ Discover the live review API instead of guessing endpoints or adding command flags:
10
+
11
+ ```bash
12
+ vizzly api schema --json
13
+ vizzly api schema <operation-id> --json
14
+ vizzly api schema <operation-id> -q view=fields --json
15
+ vizzly api schema <operation-id> -q view=response --json
16
+ ```
17
+
18
+ The index lists available operations. The default operation view describes the
19
+ method, path, parameters, authentication, and example arguments. Request field
20
+ choices or response types only when needed. API JSON payloads are under
21
+ `data.response`.
22
+
23
+ Call the discovered path using `vizzly api <path>`, `-X` for its method, `-H`
24
+ for headers, and `-q` for query parameters. Send the discovered API version
25
+ header on data requests. Use `fields` to select just the evidence needed.
26
+ Follow the response's pagination values explicitly; the CLI fetches one page
27
+ per request. Keep cursors opaque and preserve the query they belong to.
28
+
29
+ For a review, discover projects/builds, then inspect the build's screenshots,
30
+ comparisons, and image endpoints. Download images with `--output <new-file>`
31
+ and view baseline, current, and diff together. Use file output for large analysis
32
+ responses too. Existing files are not overwritten. Export the full schema only
33
+ when needed: `vizzly api schema --full --output <new-file> --json`.
34
+
35
+ Review decisions require an authorized task, user credentials, and the documented
36
+ organization header. Discover the decision operation's body before sending it
37
+ with `-d @<file>` or `-d @-` for stdin. Generate a fresh `commandId` for each
38
+ intended decision, then read back the review state. Generic writes are not
39
+ automatically replayed; after an uncertain result, inspect state before retrying.
40
+ Treat schema examples as argument arrays, not shell scripts.
41
+
7
42
  ## Choose The Evidence
8
43
 
9
44
  Use an ID supplied by the task. If no cloud build is supplied, list recent
@@ -12,17 +47,18 @@ builds and select the one matching the branch, commit, or pull request:
12
47
  ```bash
13
48
  vizzly builds --branch <branch> --limit 5 --json
14
49
  vizzly status <build-id> --json
15
- vizzly context build <build-id> --source cloud --agent --json
50
+ vizzly api schema sdk.getBuildContext --json
16
51
  ```
17
52
 
18
- Use status for lifecycle facts and build context for visual evidence. Do not
19
- assume the first returned comparison is the most important; preserve API order
20
- and inspect the records relevant to the task.
53
+ Use status for lifecycle facts, then follow the discovered build-context
54
+ operation for visual evidence. Do not assume the first returned comparison is
55
+ the most important; preserve API order and inspect the records relevant to the
56
+ task.
21
57
 
22
58
  For saved local evidence:
23
59
 
24
60
  ```bash
25
- vizzly context build current --source local --agent --json
61
+ vizzly context build current --source local --json
26
62
  vizzly context screenshot "<screenshot-name>" --source local --json
27
63
  vizzly context review-queue --source local --json
28
64
  ```
@@ -54,16 +90,16 @@ When a cloud build is in scope:
54
90
 
55
91
  ```bash
56
92
  vizzly run "<existing visual test command>" --wait --json
57
- vizzly context build <build-id> --source cloud --agent --json
93
+ vizzly api schema --json
58
94
  ```
59
95
 
60
96
  ## Inspect A Comparison
61
97
 
62
- Follow the build response's `suggested_commands`. The direct form is:
98
+ Discover comparison and image operations from the schema:
63
99
 
64
100
  ```bash
65
- vizzly context comparison <comparison-id> --source <local-or-cloud> --agent --json
66
- vizzly context comparison <comparison-id> --source <local-or-cloud> --agent --include diffs --json
101
+ vizzly api schema getComparisonContext --json
102
+ vizzly api schema sdk.getComparisonImage --json
67
103
  ```
68
104
 
69
105
  Open all three images together. Prefer `original_url` and fall back to `url`:
@@ -86,12 +122,10 @@ vizzly context similar <fingerprint-hash> --source cloud --json
86
122
  vizzly context review-queue --source <local-or-cloud> --json
87
123
  ```
88
124
 
89
- Use `--include diffs` only when compact diagnostics are insufficient. Request
90
- comments only when human review context matters.
125
+ Use the documented response and pagination fields to request only the evidence
126
+ needed. Request comments only when human review context matters.
91
127
 
92
128
  ## Continue Without Guessing
93
129
 
94
- Run returned `suggested_commands` rather than reconstructing IDs, sources, or
95
- pagination. When more evidence exists, the next-page command carries the API's
96
- opaque `--cursor`; do not edit or interpret it. Keep follow-up commands pinned
97
- to the source that produced the evidence.
130
+ Use IDs and pagination values from API responses. Keep cursors opaque and
131
+ preserve the query they belong to when requesting the next page.
@@ -36,8 +36,8 @@ vizzly finalize "<shared-ci-run-id>" --json
36
36
  - `vizzly doctor`: local configuration
37
37
  - `vizzly tdd status --json`: a local daemon started by the task
38
38
  - `vizzly status <build-id> --json`: cloud lifecycle
39
- - `vizzly context build <build-id> --source cloud --agent --json`: visual
40
- evidence
39
+ - `vizzly api schema --json`: discover supported cloud review operations and
40
+ their request and response schemas.
41
41
 
42
42
  If screenshots are absent, verify the existing integration, the test path that
43
43
  should capture them, and the active session. If authentication is absent,