@vizzly-testing/cli 0.24.1 → 0.25.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.
- package/dist/cli.js +2 -2
- package/dist/commands/finalize.js +42 -2
- package/dist/commands/preview.js +56 -7
- package/dist/plugin-api.js +26 -0
- package/dist/reporter/reporter-bundle.iife.js +2 -2
- package/dist/utils/ci-env.js +1 -1
- package/dist/utils/config-loader.js +6 -1
- package/dist/utils/environment-config.js +9 -0
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -188,7 +188,7 @@ const formatHelp = (cmd, helper) => {
|
|
|
188
188
|
lines.push('');
|
|
189
189
|
return lines.join('\n');
|
|
190
190
|
};
|
|
191
|
-
program.name('vizzly').description('Vizzly CLI for visual regression testing').version(getPackageVersion()).option('-c, --config <path>', 'Config file path').option('--token <token>', 'Vizzly API token').option('-v, --verbose', 'Verbose output (shorthand for --log-level debug)').option('--log-level <level>', 'Log level: debug, info, warn, error (default: info, or VIZZLY_LOG_LEVEL env var)').option('--json', 'Machine-readable output').option('--color', 'Force colored output (even in non-TTY)').option('--no-color', 'Disable colored output').configureHelp({
|
|
191
|
+
program.name('vizzly').description('Vizzly CLI for visual regression testing').version(getPackageVersion()).option('-c, --config <path>', 'Config file path').option('--token <token>', 'Vizzly API token').option('-v, --verbose', 'Verbose output (shorthand for --log-level debug)').option('--log-level <level>', 'Log level: debug, info, warn, error (default: info, or VIZZLY_LOG_LEVEL env var)').option('--json', 'Machine-readable output').option('--color', 'Force colored output (even in non-TTY)').option('--no-color', 'Disable colored output').option('--strict', 'Fail on any error (default: be resilient, warn on non-critical issues)').configureHelp({
|
|
192
192
|
formatHelp
|
|
193
193
|
});
|
|
194
194
|
|
|
@@ -412,7 +412,7 @@ program.command('finalize').description('Finalize a parallel build after all sha
|
|
|
412
412
|
}
|
|
413
413
|
await finalizeCommand(parallelId, options, globalOptions);
|
|
414
414
|
});
|
|
415
|
-
program.command('preview').description('Upload static files as a preview for a build').argument('[path]', 'Path to static files (dist/, build/, out/)').option('-b, --build <id>', 'Build ID to attach preview to').option('-p, --parallel-id <id>', 'Look up build by parallel ID').option('--base <path>', 'Override auto-detected base path').option('--open', 'Open preview URL in browser after upload').option('--dry-run', 'Show what would be uploaded without uploading').option('-x, --exclude <pattern>', 'Exclude files/dirs (repeatable, e.g. -x "*.log" -x "temp/")', (val, prev) => prev ? [...prev, val] : [val]).option('-i, --include <pattern>', 'Override default exclusions (repeatable, e.g. -i package.json -i tests/)', (val, prev) => prev ? [...prev, val] : [val]).action(async (path, options) => {
|
|
415
|
+
program.command('preview').description('Upload static files as a preview for a build').argument('[path]', 'Path to static files (dist/, build/, out/)').option('-b, --build <id>', 'Build ID to attach preview to').option('-p, --parallel-id <id>', 'Look up build by parallel ID').option('--base <path>', 'Override auto-detected base path').option('--open', 'Open preview URL in browser after upload').option('--dry-run', 'Show what would be uploaded without uploading').option('--public-link', 'Acknowledge that preview URL grants access to anyone with the link (required for private projects)').option('-x, --exclude <pattern>', 'Exclude files/dirs (repeatable, e.g. -x "*.log" -x "temp/")', (val, prev) => prev ? [...prev, val] : [val]).option('-i, --include <pattern>', 'Override default exclusions (repeatable, e.g. -i package.json -i tests/)', (val, prev) => prev ? [...prev, val] : [val]).action(async (path, options) => {
|
|
416
416
|
const globalOptions = program.opts();
|
|
417
417
|
|
|
418
418
|
// Show helpful error if path is missing
|
|
@@ -7,6 +7,7 @@ import { createApiClient as defaultCreateApiClient, finalizeParallelBuild as def
|
|
|
7
7
|
import { loadConfig as defaultLoadConfig } from '../utils/config-loader.js';
|
|
8
8
|
import * as defaultOutput from '../utils/output.js';
|
|
9
9
|
import { writeSession as defaultWriteSession } from '../utils/session.js';
|
|
10
|
+
let MISSING_BUILD_HINTS = [' • No screenshots were uploaded with this parallel-id', ' • Tests were skipped or failed before capturing screenshots', ' • The parallel-id does not match what was used during test runs'];
|
|
10
11
|
|
|
11
12
|
/**
|
|
12
13
|
* Finalize command implementation
|
|
@@ -89,15 +90,54 @@ export async function finalizeCommand(parallelId, options = {}, globalOptions =
|
|
|
89
90
|
};
|
|
90
91
|
} catch (error) {
|
|
91
92
|
output.stopSpinner();
|
|
93
|
+
let status = error.context?.status;
|
|
92
94
|
|
|
93
95
|
// Don't fail CI for Vizzly infrastructure issues (5xx errors)
|
|
94
|
-
|
|
96
|
+
// Note: --strict does NOT affect 5xx handling - infrastructure issues are out of user's control
|
|
95
97
|
if (status >= 500) {
|
|
96
98
|
output.warn('Vizzly API unavailable - finalize skipped.');
|
|
97
99
|
return {
|
|
98
100
|
success: true,
|
|
99
101
|
result: {
|
|
100
|
-
skipped: true
|
|
102
|
+
skipped: true,
|
|
103
|
+
reason: 'api-unavailable'
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Handle missing builds gracefully (404 errors)
|
|
109
|
+
// This happens when: no screenshots were uploaded, tests were skipped, or parallel-id doesn't exist
|
|
110
|
+
if (status === 404) {
|
|
111
|
+
let isStrict = globalOptions.strict;
|
|
112
|
+
if (isStrict) {
|
|
113
|
+
output.error(`No build found for parallel ID: ${parallelId}`);
|
|
114
|
+
output.blank();
|
|
115
|
+
output.info('This can happen when:');
|
|
116
|
+
for (let hint of MISSING_BUILD_HINTS) {
|
|
117
|
+
output.info(hint);
|
|
118
|
+
}
|
|
119
|
+
exit(1);
|
|
120
|
+
return {
|
|
121
|
+
success: false,
|
|
122
|
+
reason: 'no-build-found',
|
|
123
|
+
error
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Non-strict mode: warn but don't fail CI
|
|
128
|
+
output.warn(`No build found for parallel ID: ${parallelId} - finalize skipped.`);
|
|
129
|
+
if (globalOptions.verbose) {
|
|
130
|
+
output.info('Possible reasons:');
|
|
131
|
+
for (let hint of MISSING_BUILD_HINTS) {
|
|
132
|
+
output.info(hint);
|
|
133
|
+
}
|
|
134
|
+
output.info('Use --strict flag to fail CI when no build is found.');
|
|
135
|
+
}
|
|
136
|
+
return {
|
|
137
|
+
success: true,
|
|
138
|
+
result: {
|
|
139
|
+
skipped: true,
|
|
140
|
+
reason: 'no-build-found'
|
|
101
141
|
}
|
|
102
142
|
};
|
|
103
143
|
}
|
package/dist/commands/preview.js
CHANGED
|
@@ -12,7 +12,7 @@ import { readFile, realpath, stat, unlink } from 'node:fs/promises';
|
|
|
12
12
|
import { tmpdir } from 'node:os';
|
|
13
13
|
import { join, resolve } from 'node:path';
|
|
14
14
|
import { promisify } from 'node:util';
|
|
15
|
-
import { createApiClient as defaultCreateApiClient, uploadPreviewZip as defaultUploadPreviewZip } from '../api/index.js';
|
|
15
|
+
import { createApiClient as defaultCreateApiClient, getBuild as defaultGetBuild, uploadPreviewZip as defaultUploadPreviewZip } from '../api/index.js';
|
|
16
16
|
import { openBrowser as defaultOpenBrowser } from '../utils/browser.js';
|
|
17
17
|
import { loadConfig as defaultLoadConfig } from '../utils/config-loader.js';
|
|
18
18
|
import { detectBranch as defaultDetectBranch } from '../utils/git.js';
|
|
@@ -273,6 +273,7 @@ export async function previewCommand(path, options = {}, globalOptions = {}, dep
|
|
|
273
273
|
let {
|
|
274
274
|
loadConfig = defaultLoadConfig,
|
|
275
275
|
createApiClient = defaultCreateApiClient,
|
|
276
|
+
getBuild = defaultGetBuild,
|
|
276
277
|
uploadPreviewZip = defaultUploadPreviewZip,
|
|
277
278
|
readSession = defaultReadSession,
|
|
278
279
|
formatSessionAge = defaultFormatSessionAge,
|
|
@@ -377,6 +378,59 @@ export async function previewCommand(path, options = {}, globalOptions = {}, dep
|
|
|
377
378
|
};
|
|
378
379
|
}
|
|
379
380
|
|
|
381
|
+
// Create API client for non-dry-run operations (reused for visibility check and upload)
|
|
382
|
+
let client;
|
|
383
|
+
if (!options.dryRun) {
|
|
384
|
+
client = createApiClient({
|
|
385
|
+
baseUrl: config.apiUrl,
|
|
386
|
+
token: config.apiKey,
|
|
387
|
+
command: 'preview'
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
// Check project visibility for private projects
|
|
391
|
+
let buildResponse;
|
|
392
|
+
try {
|
|
393
|
+
buildResponse = await getBuild(client, buildId);
|
|
394
|
+
} catch (error) {
|
|
395
|
+
if (error.status === 404) {
|
|
396
|
+
output.error(`Build not found: ${buildId}`);
|
|
397
|
+
} else {
|
|
398
|
+
output.error('Failed to verify project visibility', error);
|
|
399
|
+
}
|
|
400
|
+
exit(1);
|
|
401
|
+
// Return is for testing (exit is mocked in tests)
|
|
402
|
+
return {
|
|
403
|
+
success: false,
|
|
404
|
+
reason: 'build-fetch-failed',
|
|
405
|
+
error
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// Check if project is private and user hasn't acknowledged public link access
|
|
410
|
+
// Note: API returns { build, project } at top level, not nested
|
|
411
|
+
// Use === false to handle undefined/missing isPublic defensively
|
|
412
|
+
let project = buildResponse.project;
|
|
413
|
+
let isPrivate = project && project.isPublic === false;
|
|
414
|
+
if (isPrivate && !options.publicLink) {
|
|
415
|
+
output.error('This project is private.');
|
|
416
|
+
output.blank();
|
|
417
|
+
output.print(' Preview URLs grant access to anyone with the link (until expiration).');
|
|
418
|
+
output.blank();
|
|
419
|
+
output.print(' To proceed, acknowledge this by using:');
|
|
420
|
+
output.blank();
|
|
421
|
+
output.print(' vizzly preview ./dist --public-link');
|
|
422
|
+
output.blank();
|
|
423
|
+
output.print(' Or set your project to public in Vizzly settings.');
|
|
424
|
+
output.blank();
|
|
425
|
+
exit(1);
|
|
426
|
+
// Return is for testing (exit is mocked in tests)
|
|
427
|
+
return {
|
|
428
|
+
success: false,
|
|
429
|
+
reason: 'private-project-no-flag'
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
380
434
|
// Check for zip command availability (skip for dry-run)
|
|
381
435
|
if (!options.dryRun) {
|
|
382
436
|
let zipInfo = getZipCommand();
|
|
@@ -532,13 +586,8 @@ export async function previewCommand(path, options = {}, globalOptions = {}, dep
|
|
|
532
586
|
let compressionRatio = ((1 - zipBuffer.length / totalSize) * 100).toFixed(0);
|
|
533
587
|
output.updateSpinner(`Compressed to ${formatBytes(zipBuffer.length)} (${compressionRatio}% smaller)`);
|
|
534
588
|
|
|
535
|
-
// Upload
|
|
589
|
+
// Upload (reuse client created earlier)
|
|
536
590
|
output.updateSpinner('Uploading preview...');
|
|
537
|
-
let client = createApiClient({
|
|
538
|
-
baseUrl: config.apiUrl,
|
|
539
|
-
token: config.apiKey,
|
|
540
|
-
command: 'preview'
|
|
541
|
-
});
|
|
542
591
|
let result = await uploadPreviewZip(client, buildId, zipBuffer);
|
|
543
592
|
output.stopSpinner();
|
|
544
593
|
|
package/dist/plugin-api.js
CHANGED
|
@@ -9,10 +9,13 @@
|
|
|
9
9
|
* exposed to plugins to prevent coupling to implementation details.
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
+
import { detectBranch, detectCommit, detectCommitMessage, detectPullRequestNumber, generateBuildNameWithGit } from './utils/git.js';
|
|
13
|
+
|
|
12
14
|
/**
|
|
13
15
|
* Creates a stable plugin services object from the internal services
|
|
14
16
|
*
|
|
15
17
|
* Only exposes:
|
|
18
|
+
* - git: Git information detection (branch, commit, PR number, etc.)
|
|
16
19
|
* - testRunner: Build lifecycle management (createBuild, finalizeBuild, events)
|
|
17
20
|
* - serverManager: Screenshot server control (start, stop)
|
|
18
21
|
*
|
|
@@ -25,6 +28,29 @@ export function createPluginServices(services) {
|
|
|
25
28
|
serverManager
|
|
26
29
|
} = services;
|
|
27
30
|
return Object.freeze({
|
|
31
|
+
// Git detection utilities - provides correct git info from CI environments
|
|
32
|
+
git: Object.freeze({
|
|
33
|
+
/**
|
|
34
|
+
* Detect git information for build creation
|
|
35
|
+
* Handles CI environment variables correctly (GitHub Actions, GitLab, etc.)
|
|
36
|
+
*
|
|
37
|
+
* @param {Object} [options] - Detection options
|
|
38
|
+
* @param {string} [options.buildPrefix] - Prefix for generated build name
|
|
39
|
+
* @returns {Promise<Object>} Git info: { branch, commit, message, prNumber, buildName }
|
|
40
|
+
*/
|
|
41
|
+
async detect(options = {}) {
|
|
42
|
+
let [branch, commit, message] = await Promise.all([detectBranch(), detectCommit(), detectCommitMessage()]);
|
|
43
|
+
let prNumber = detectPullRequestNumber();
|
|
44
|
+
let buildName = await generateBuildNameWithGit(options.buildPrefix);
|
|
45
|
+
return {
|
|
46
|
+
branch,
|
|
47
|
+
commit,
|
|
48
|
+
message,
|
|
49
|
+
prNumber,
|
|
50
|
+
buildName
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
}),
|
|
28
54
|
testRunner: Object.freeze({
|
|
29
55
|
// EventEmitter methods for build lifecycle events
|
|
30
56
|
once: testRunner.once.bind(testRunner),
|