@vizzly-testing/cli 0.34.0 → 0.35.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/README.md +67 -7
  2. package/dist/api/core.js +18 -4
  3. package/dist/api/endpoints.js +38 -15
  4. package/dist/api/index.js +1 -1
  5. package/dist/cli.js +270 -194
  6. package/dist/client/index.js +62 -21
  7. package/dist/commands/api.js +9 -3
  8. package/dist/commands/preview.js +30 -26
  9. package/dist/commands/project.js +1 -1
  10. package/dist/commands/run.js +25 -10
  11. package/dist/commands/status.js +14 -1
  12. package/dist/commands/tdd-daemon-process.js +5 -1
  13. package/dist/commands/tdd-daemon.js +217 -139
  14. package/dist/commands/tdd.js +26 -5
  15. package/dist/commands/upload.js +31 -13
  16. package/dist/reporter/reporter-bundle.iife.js +6 -6
  17. package/dist/sdk/index.js +23 -9
  18. package/dist/server/handlers/api-handler.js +3 -2
  19. package/dist/server/handlers/tdd-handler.js +5 -1
  20. package/dist/server/routers/screenshot.js +11 -4
  21. package/dist/services/build-manager.js +6 -1
  22. package/dist/tdd/tdd-service.js +6 -2
  23. package/dist/test-runner/core.js +8 -3
  24. package/dist/test-runner/operations.js +19 -3
  25. package/dist/types/client.d.ts +32 -5
  26. package/dist/types/index.d.ts +33 -5
  27. package/dist/uploader/core.js +41 -9
  28. package/dist/uploader/operations.js +19 -17
  29. package/dist/utils/config-loader.js +13 -6
  30. package/dist/utils/context.js +46 -76
  31. package/dist/utils/environment-config.js +20 -0
  32. package/dist/utils/output.js +8 -5
  33. package/dist/utils/screenshot-options.js +60 -10
  34. package/package.json +1 -1
  35. package/skills/vizzly/SKILL.md +12 -4
  36. package/skills/vizzly/references/cli-context.md +8 -2
  37. package/skills/vizzly/references/setup-ci.md +3 -1
package/README.md CHANGED
@@ -35,28 +35,50 @@ 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
- Start the TDD server, run your tests, and open the dashboard at
41
- `http://localhost:47392`.
43
+ Start the TDD server, run your tests, and open the dashboard at the URL the
44
+ command prints. Add `--open` when you want Vizzly to open the dashboard for
45
+ you. If the default port is busy, Vizzly picks the next available port; use the
46
+ printed `--port` value with `vizzly tdd status` or `vizzly tdd stop`.
42
47
 
43
48
  ```bash
44
- vizzly tdd start
49
+ vizzly tdd start --open
45
50
  pnpm test -- --watch
46
51
  ```
47
52
 
48
53
  The dashboard shows screenshots, baselines, and diffs as they arrive. Accept or
49
54
  reject changes right from the UI.
50
55
 
56
+ For a one-off local check, wrap the test command with `tdd run`:
57
+
58
+ ```bash
59
+ vizzly tdd run "pnpm test" --no-open
60
+ vizzly context build current --source local --agent
61
+ ```
62
+
63
+ That run writes review data under `.vizzly/` and prints a context command you
64
+ can use for follow-up inspection. If screenshots were captured, Vizzly also
65
+ generates `.vizzly/report/index.html`; omit `--no-open` when you want that
66
+ report opened automatically.
67
+
51
68
  ### Run With Cloud Review
52
69
 
53
70
  Use cloud builds when you want shared baselines, team review, and CI status:
54
71
 
55
72
  ```bash
56
73
  vizzly login
74
+ vizzly project link your-org/your-project
57
75
  vizzly run "pnpm test" --wait
58
76
  ```
59
77
 
78
+ `vizzly login` authenticates your user account. `vizzly project link` creates a
79
+ project-scoped upload credential for this checkout, which `vizzly run` uses for
80
+ cloud uploads.
81
+
60
82
  `--wait` blocks until Vizzly finishes processing the build. It exits with code
61
83
  `1` when visual differences need review.
62
84
 
@@ -111,12 +133,23 @@ test('homepage looks correct', async ({ page }) => {
111
133
 
112
134
  let screenshot = await page.screenshot();
113
135
  await vizzlyScreenshot('homepage', screenshot, {
114
- browser: 'chrome',
115
- viewport: '1920x1080'
136
+ fullPage: true,
137
+ requestTimeout: 5000,
138
+ properties: {
139
+ browser: 'chrome',
140
+ viewport: { width: 1920, height: 1080 },
141
+ },
116
142
  });
117
143
  });
118
144
  ```
119
145
 
146
+ `properties` is your metadata bag for baseline grouping, filtering, and
147
+ debugging. SDK options such as `threshold`, `minClusterSize`, `fullPage`,
148
+ `requestTimeout`, and `buildId` stay at the top level. If one of those reserved
149
+ SDK options is accidentally placed inside `properties`, Vizzly removes it from
150
+ metadata, applies it as an option when possible, and warns so the call site can
151
+ be cleaned up.
152
+
120
153
  The client SDK is lightweight. It posts screenshots to the local Vizzly server
121
154
  or the cloud build wrapper. It works with any test runner.
122
155
 
@@ -136,9 +169,28 @@ await vizzlyScreenshot('homepage', './screenshots/homepage.png');
136
169
  Or upload an existing folder of screenshots:
137
170
 
138
171
  ```bash
139
- vizzly upload ./screenshots
172
+ vizzly upload ./screenshots --threshold 2 --min-cluster-size 4 --batch-size 10 --upload-timeout 60000
140
173
  ```
141
174
 
175
+ `--batch-size` controls how many screenshots are uploaded per request.
176
+ `--upload-timeout` controls the upload client's timeout, including how long
177
+ `--wait` polls for build processing.
178
+
179
+ When `vizzly run` sends screenshots to Vizzly, the CLI first asks the API to
180
+ resolve each screenshot by SHA. If Vizzly already has the image bytes, the CLI
181
+ creates the screenshot record without re-uploading the image. CI workflows can
182
+ force every screenshot through with `--upload-all`. Parallel jobs should use the
183
+ same stable ID, then finalize that ID after every shard completes:
184
+
185
+ ```bash
186
+ vizzly run "pnpm test" --upload-all --parallel-id "$GITHUB_RUN_ID"
187
+ vizzly finalize "$GITHUB_RUN_ID"
188
+ ```
189
+
190
+ For smoke jobs where cloud credentials are intentionally unavailable, add
191
+ `--allow-no-token` to `vizzly run` and Vizzly will keep the local screenshot
192
+ server path working without creating a cloud build.
193
+
142
194
  ## Configuration
143
195
 
144
196
  Generate a config file:
@@ -170,10 +222,18 @@ export default {
170
222
  | Command | What it does |
171
223
  | --- | --- |
172
224
  | `vizzly tdd start` | Start the local TDD server and dashboard. |
173
- | `vizzly tdd run "cmd"` | Run tests once and generate a static visual report. |
225
+ | `vizzly tdd status` | Check the local TDD server for this project. |
226
+ | `vizzly tdd list` | List running local TDD servers. |
227
+ | `vizzly tdd stop` | Stop the local TDD server for this project. |
228
+ | `vizzly tdd run "cmd"` | Run tests once and write local review data under `.vizzly/`. |
174
229
  | `vizzly run "cmd"` | Run tests with cloud build and review integration. |
175
230
  | `vizzly context ...` | Fetch visual context for builds, comparisons, screenshots, and review queues. |
176
231
  | `vizzly upload <dir>` | Upload an existing folder of screenshots. |
232
+ | `vizzly preview <dir>` | Upload static build output for in-context review. |
233
+ | `vizzly approve <comparison-id>` | Approve a visual comparison. |
234
+ | `vizzly reject <comparison-id>` | Reject a visual comparison with a reason. |
235
+ | `vizzly comment <build-id>` | Add a build comment. |
236
+ | `vizzly config [key]` | Inspect resolved configuration values. |
177
237
  | `vizzly login` | Authenticate through the browser. |
178
238
  | `vizzly doctor` | Validate your local setup. |
179
239
 
package/dist/api/core.js CHANGED
@@ -70,9 +70,9 @@ export function buildRequestHeaders({
70
70
  // ============================================================================
71
71
 
72
72
  /**
73
- * Build payload for screenshot upload
73
+ * Build payload for screenshot upload or SHA resolution
74
74
  * @param {string} name - Screenshot name
75
- * @param {Buffer} buffer - Image data
75
+ * @param {Buffer|null} buffer - Image data, or null when resolving by SHA
76
76
  * @param {Object} metadata - Screenshot metadata (viewport, browser, etc.)
77
77
  * @param {string|null} sha256 - Pre-computed SHA256 hash (optional)
78
78
  * @returns {Object} Screenshot upload payload
@@ -80,15 +80,28 @@ export function buildRequestHeaders({
80
80
  export function buildScreenshotPayload(name, buffer, metadata = {}, sha256 = null) {
81
81
  let payload = {
82
82
  name,
83
- image_data: buffer.toString('base64'),
84
83
  properties: metadata ?? {}
85
84
  };
85
+ if (buffer) {
86
+ payload.image_data = buffer.toString('base64');
87
+ }
86
88
  if (sha256) {
87
89
  payload.sha256 = sha256;
88
90
  }
89
91
  return payload;
90
92
  }
91
93
 
94
+ /**
95
+ * Build payload for server-side screenshot resolution without sending image bytes
96
+ * @param {string} name - Screenshot name
97
+ * @param {Object} metadata - Screenshot metadata (viewport, browser, etc.)
98
+ * @param {string} sha256 - Pre-computed SHA256 hash
99
+ * @returns {Object} Screenshot resolution payload
100
+ */
101
+ export function buildScreenshotResolvePayload(name, metadata = {}, sha256) {
102
+ return buildScreenshotPayload(name, null, metadata, sha256);
103
+ }
104
+
92
105
  /**
93
106
  * Build payload for build creation
94
107
  * @param {Object} options - Build options
@@ -172,7 +185,8 @@ export function buildScreenshotCheckObject(sha256, name, metadata = {}) {
172
185
  name,
173
186
  browser: meta.browser || 'chrome',
174
187
  viewport_width: meta.viewport?.width || meta.viewport_width || 1920,
175
- viewport_height: meta.viewport?.height || meta.viewport_height || 1080
188
+ viewport_height: meta.viewport?.height || meta.viewport_height || 1080,
189
+ properties: meta
176
190
  };
177
191
  }
178
192
 
@@ -7,7 +7,7 @@
7
7
 
8
8
  import { VizzlyError } from '../errors/vizzly-error.js';
9
9
  import * as output from '../utils/output.js';
10
- import { buildBuildPayload, buildEndpointWithParams, buildQueryParams, buildScreenshotCheckObject, buildScreenshotPayload, buildShaCheckPayload, computeSha256, findScreenshotBySha, shaExists } from './core.js';
10
+ import { buildBuildPayload, buildEndpointWithParams, buildQueryParams, buildScreenshotPayload, buildScreenshotResolvePayload, buildShaCheckPayload, computeSha256 } from './core.js';
11
11
 
12
12
  // ============================================================================
13
13
  // Build Endpoints
@@ -144,6 +144,16 @@ export async function checkShas(client, screenshots, buildId) {
144
144
  };
145
145
  }
146
146
  }
147
+ function canUploadBytesAfterShaResolveError(error) {
148
+ let status = error?.context?.status;
149
+
150
+ // Network failures, older servers, and server errors should degrade to the
151
+ // slower byte upload path. Auth and ownership errors must still surface.
152
+ if (!status) return true;
153
+ if (status >= 500) return true;
154
+ if (status === 400 && error.message?.includes('image_data')) return true;
155
+ return false;
156
+ }
147
157
 
148
158
  /**
149
159
  * Upload a screenshot with SHA deduplication
@@ -168,23 +178,36 @@ export async function uploadScreenshot(client, buildId, name, buffer, metadata =
168
178
  });
169
179
  }
170
180
 
171
- // Normal flow with SHA deduplication
181
+ // Normal flow with server-side SHA resolution.
172
182
  let sha256 = computeSha256(buffer);
173
- let checkObj = buildScreenshotCheckObject(sha256, name, metadata);
174
- let checkResult = await checkShas(client, [checkObj], buildId);
175
- if (shaExists(checkResult, sha256)) {
176
- // File already exists, screenshot record was automatically created
177
- let screenshot = findScreenshotBySha(checkResult, sha256);
178
- return {
179
- message: 'Screenshot already exists, skipped upload',
180
- sha256,
181
- skipped: true,
182
- screenshot,
183
- fromExisting: true
184
- };
183
+ let resolvePayload = buildScreenshotResolvePayload(name, metadata, sha256);
184
+ try {
185
+ let resolveResult = await client.request(`/api/sdk/builds/${buildId}/screenshots`, {
186
+ method: 'POST',
187
+ headers: {
188
+ 'Content-Type': 'application/json'
189
+ },
190
+ body: JSON.stringify(resolvePayload)
191
+ });
192
+ if (resolveResult?.upload_required === false) {
193
+ return {
194
+ message: 'Screenshot already exists, skipped upload',
195
+ sha256,
196
+ skipped: true,
197
+ screenshot: resolveResult,
198
+ fromExisting: true
199
+ };
200
+ }
201
+ } catch (error) {
202
+ if (!canUploadBytesAfterShaResolveError(error)) {
203
+ throw error;
204
+ }
205
+ output.debug('sha-resolve', 'failed, uploading screenshot bytes', {
206
+ error: error.message
207
+ });
185
208
  }
186
209
 
187
- // File doesn't exist, proceed with upload
210
+ // File doesn't exist, or resolve failed; proceed with upload.
188
211
  let payload = buildScreenshotPayload(name, buffer, metadata, sha256);
189
212
  return client.request(`/api/sdk/builds/${buildId}/screenshots`, {
190
213
  method: 'POST',
package/dist/api/index.js CHANGED
@@ -13,7 +13,7 @@
13
13
  // Client factory
14
14
  export { createApiClient, DEFAULT_API_URL } from './client.js';
15
15
  // Core pure functions
16
- export { buildApiUrl, buildAuthHeader, buildBuildPayload, buildEndpointWithParams, buildQueryParams, buildRequestHeaders, buildScreenshotCheckObject, buildScreenshotPayload, buildShaCheckPayload, buildUserAgent, computeSha256, extractErrorBody, findScreenshotBySha, isAuthError, isRateLimited, parseApiError, partitionByShaExistence, shaExists, shouldRetryWithRefresh } from './core.js';
16
+ export { buildApiUrl, buildAuthHeader, buildBuildPayload, buildEndpointWithParams, buildQueryParams, buildRequestHeaders, buildScreenshotCheckObject, buildScreenshotPayload, buildScreenshotResolvePayload, buildShaCheckPayload, buildUserAgent, computeSha256, extractErrorBody, findScreenshotBySha, isAuthError, isRateLimited, parseApiError, partitionByShaExistence, shaExists, shouldRetryWithRefresh } from './core.js';
17
17
 
18
18
  // Endpoint functions
19
19
  export { checkShas, createBuild, finalizeBuild, finalizeParallelBuild, getBatchHotspots, getBuild, getBuildContext, getBuilds, getComparison, getComparisonContext, getPreviewInfo, getReviewQueueContext, getScreenshotContext, getScreenshotHotspots, getSimilarFingerprintContext, getTddBaselines, getTokenContext, searchComparisons, updateBuildStatus, uploadPreviewZip, uploadScreenshot } from './endpoints.js';