@vizzly-testing/cli 0.35.3-beta.4 → 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 CHANGED
@@ -144,18 +144,19 @@ test('homepage looks correct', async ({ page }) => {
144
144
  fullPage: true,
145
145
  requestTimeout: 5000,
146
146
  properties: {
147
- browser: 'chrome',
148
- viewport: { width: 1920, height: 1080 },
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, and
155
- debugging. SDK options such as `threshold`, `minClusterSize`, `fullPage`,
156
- `requestTimeout`, and `buildId` stay at the top level. Reserved SDK and capture
157
- fields inside `properties` are ignored and produce a warning; they are never
158
- interpreted as options or stored as user metadata.
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`.
159
160
 
160
161
  The client SDK is lightweight. It posts screenshots to the local Vizzly server
161
162
  or the cloud build wrapper. It works with any test runner.
package/dist/api/core.js CHANGED
@@ -30,14 +30,17 @@ export function buildAuthHeader(token) {
30
30
  * @param {string} version - CLI version
31
31
  * @param {string} command - Command being executed (run, upload, tdd, etc.)
32
32
  * @param {string|null} sdkUserAgent - Optional SDK user agent to append
33
+ * @param {string} nodeVersion - Node.js version to append
33
34
  * @returns {string} Complete User-Agent string
34
35
  */
35
- export function buildUserAgent(version, command, sdkUserAgent = null) {
36
+ export function buildUserAgent(version, command, sdkUserAgent = null, nodeVersion = process.versions.node) {
36
37
  let baseUserAgent = `vizzly-cli/${version} (${command})`;
38
+ let userAgentParts = [baseUserAgent];
37
39
  if (sdkUserAgent) {
38
- return `${baseUserAgent} ${sdkUserAgent}`;
40
+ userAgentParts.push(sdkUserAgent);
39
41
  }
40
- return baseUserAgent;
42
+ if (nodeVersion) userAgentParts.push(`node/${nodeVersion}`);
43
+ return userAgentParts.join(' ');
41
44
  }
42
45
 
43
46
  /**
@@ -74,7 +77,7 @@ export function buildRequestHeaders({
74
77
  * Build payload for screenshot upload or SHA resolution
75
78
  * @param {string} name - Screenshot name
76
79
  * @param {Buffer|null} buffer - Image data, or null when resolving by SHA
77
- * @param {Object} metadata - Screenshot metadata (viewport, browser, etc.)
80
+ * @param {Object} metadata - User screenshot properties
78
81
  * @param {string|null} sha256 - Pre-computed SHA256 hash (optional)
79
82
  * @returns {Object} Screenshot upload payload
80
83
  */
@@ -98,7 +101,7 @@ export function buildScreenshotPayload(name, buffer, metadata = {}, sha256 = nul
98
101
  /**
99
102
  * Build payload for server-side screenshot resolution without sending image bytes
100
103
  * @param {string} name - Screenshot name
101
- * @param {Object} metadata - Screenshot metadata (viewport, browser, etc.)
104
+ * @param {Object} metadata - User screenshot properties
102
105
  * @param {string} sha256 - Pre-computed SHA256 hash
103
106
  * @returns {Object} Screenshot resolution payload
104
107
  */
@@ -190,9 +193,6 @@ export function buildScreenshotCheckObject(sha256, name, metadata = {}) {
190
193
  return {
191
194
  sha256,
192
195
  name,
193
- browser: meta.browser || 'chrome',
194
- viewport_width: meta.viewport?.width || meta.viewport_width || 1920,
195
- viewport_height: meta.viewport?.height || meta.viewport_height || 1080,
196
196
  properties
197
197
  };
198
198
  }
@@ -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
- * Finalize a build (convenience wrapper for updateBuildStatus)
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 the build succeeded
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
  /**
@@ -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
@@ -220,9 +217,6 @@ function createSimpleClient(serverUrl, clientOptions = {}) {
220
217
  properties: normalizedOptions.properties
221
218
  };
222
219
  Object.assign(screenshotData, getScreenshotOptionsPayload(normalizedOptions));
223
- if (normalizedOptions.warnings.length > 0) {
224
- screenshotData.warnings = normalizedOptions.warnings;
225
- }
226
220
  let httpStart = Date.now();
227
221
  let {
228
222
  status,
@@ -1,5 +1,10 @@
1
1
  /**
2
- * Run command implementation
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 - Dependencies for testing
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
- output.print(` ${colors.brand.textTertiary('Screenshots')} ${colors.white(result.screenshotsCaptured)}`);
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,