@vizzly-testing/cli 0.6.0 → 0.7.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 +40 -0
- package/dist/cli.js +15 -2
- package/dist/commands/finalize.js +72 -0
- package/dist/commands/run.js +2 -1
- package/dist/commands/upload.js +1 -0
- package/dist/services/api-service.js +14 -0
- package/dist/services/tdd-service.js +81 -20
- package/dist/services/test-runner.js +2 -1
- package/dist/services/uploader.js +3 -2
- package/dist/types/commands/finalize.d.ts +13 -0
- package/dist/types/services/api-service.d.ts +6 -0
- package/dist/types/services/uploader.d.ts +2 -1
- package/dist/types/utils/environment-config.d.ts +5 -0
- package/dist/utils/config-loader.js +4 -1
- package/dist/utils/environment-config.js +9 -0
- package/docs/api-reference.md +27 -0
- package/docs/test-integration.md +69 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -82,6 +82,7 @@ await vizzlyScreenshot('homepage', screenshot, {
|
|
|
82
82
|
vizzly upload <directory> # Upload screenshots from directory
|
|
83
83
|
vizzly upload ./screenshots --wait # Wait for processing
|
|
84
84
|
vizzly upload ./screenshots --upload-all # Upload all without deduplication
|
|
85
|
+
vizzly upload ./screenshots --parallel-id "ci-run-123" # For parallel CI builds
|
|
85
86
|
```
|
|
86
87
|
|
|
87
88
|
### Run Tests with Integration
|
|
@@ -90,6 +91,7 @@ vizzly run "npm test" # Run with Vizzly integration
|
|
|
90
91
|
vizzly run "pytest" --port 3002 # Custom port
|
|
91
92
|
vizzly run "npm test" --wait # Wait for build completion
|
|
92
93
|
vizzly run "npm test" --allow-no-token # Run without API token
|
|
94
|
+
vizzly run "npm test" --parallel-id "ci-run-123" # For parallel CI builds
|
|
93
95
|
```
|
|
94
96
|
|
|
95
97
|
#### Run Command Options
|
|
@@ -111,6 +113,9 @@ vizzly run "npm test" --allow-no-token # Run without API token
|
|
|
111
113
|
- `--upload-timeout <ms>` - Upload wait timeout in ms
|
|
112
114
|
- `--upload-all` - Upload all screenshots without SHA deduplication
|
|
113
115
|
|
|
116
|
+
**Parallel Execution:**
|
|
117
|
+
- `--parallel-id <id>` - Unique identifier for parallel test execution (also via `VIZZLY_PARALLEL_ID`)
|
|
118
|
+
|
|
114
119
|
**Development & Testing:**
|
|
115
120
|
- `--allow-no-token` - Allow running without API token (useful for local development)
|
|
116
121
|
- `--token <token>` - API token override
|
|
@@ -150,6 +155,7 @@ vizzly init # Create vizzly.config.js with defaults
|
|
|
150
155
|
vizzly status <build-id> # Check build progress and results
|
|
151
156
|
vizzly status <build-id> --verbose # Detailed build information
|
|
152
157
|
vizzly status <build-id> --json # Machine-readable output
|
|
158
|
+
vizzly finalize <parallel-id> # Finalize parallel build after all shards complete
|
|
153
159
|
vizzly doctor # Fast local preflight (no network)
|
|
154
160
|
vizzly doctor --api # Include API connectivity checks
|
|
155
161
|
```
|
|
@@ -289,6 +295,37 @@ For CI/CD pipelines, use the `--wait` flag to wait for visual comparison results
|
|
|
289
295
|
VIZZLY_BRANCH: ${{ github.head_ref || github.ref_name }}
|
|
290
296
|
```
|
|
291
297
|
|
|
298
|
+
### Parallel Builds in CI
|
|
299
|
+
|
|
300
|
+
For parallel test execution, use `--parallel-id` to ensure all shards contribute to the same build:
|
|
301
|
+
|
|
302
|
+
```yaml
|
|
303
|
+
# GitHub Actions with parallel matrix
|
|
304
|
+
jobs:
|
|
305
|
+
e2e-tests:
|
|
306
|
+
strategy:
|
|
307
|
+
matrix:
|
|
308
|
+
shard: [1/4, 2/4, 3/4, 4/4]
|
|
309
|
+
steps:
|
|
310
|
+
- name: Run tests with Vizzly
|
|
311
|
+
run: |
|
|
312
|
+
npx vizzly run "npm test -- --shard=${{ matrix.shard }}" \
|
|
313
|
+
--parallel-id="${{ github.run_id }}-${{ github.run_attempt }}"
|
|
314
|
+
env:
|
|
315
|
+
VIZZLY_TOKEN: ${{ secrets.VIZZLY_TOKEN }}
|
|
316
|
+
|
|
317
|
+
finalize-e2e:
|
|
318
|
+
needs: e2e-tests
|
|
319
|
+
runs-on: ubuntu-latest
|
|
320
|
+
if: always() && needs.e2e-tests.result == 'success'
|
|
321
|
+
steps:
|
|
322
|
+
- name: Finalize parallel build
|
|
323
|
+
run: |
|
|
324
|
+
npx vizzly finalize "${{ github.run_id }}-${{ github.run_attempt }}"
|
|
325
|
+
env:
|
|
326
|
+
VIZZLY_TOKEN: ${{ secrets.VIZZLY_TOKEN }}
|
|
327
|
+
```
|
|
328
|
+
|
|
292
329
|
### GitLab CI
|
|
293
330
|
```yaml
|
|
294
331
|
visual-tests:
|
|
@@ -334,6 +371,9 @@ Check if Vizzly is enabled in the current environment.
|
|
|
334
371
|
- `VIZZLY_API_URL`: Override API base URL. Default: `https://vizzly.dev`.
|
|
335
372
|
- `VIZZLY_LOG_LEVEL`: Logger level. One of `debug`, `info`, `warn`, `error`. Example: `export VIZZLY_LOG_LEVEL=debug`.
|
|
336
373
|
|
|
374
|
+
### Parallel Builds
|
|
375
|
+
- `VIZZLY_PARALLEL_ID`: Unique identifier for parallel test execution. Example: `export VIZZLY_PARALLEL_ID=ci-run-123`.
|
|
376
|
+
|
|
337
377
|
### Git Information Override
|
|
338
378
|
For enhanced CI/CD integration, you can override git detection with these environment variables:
|
|
339
379
|
|
package/dist/cli.js
CHANGED
|
@@ -6,6 +6,7 @@ import { uploadCommand, validateUploadOptions } from './commands/upload.js';
|
|
|
6
6
|
import { runCommand, validateRunOptions } from './commands/run.js';
|
|
7
7
|
import { tddCommand, validateTddOptions } from './commands/tdd.js';
|
|
8
8
|
import { statusCommand, validateStatusOptions } from './commands/status.js';
|
|
9
|
+
import { finalizeCommand, validateFinalizeOptions } from './commands/finalize.js';
|
|
9
10
|
import { doctorCommand, validateDoctorOptions } from './commands/doctor.js';
|
|
10
11
|
import { getPackageVersion } from './utils/package-info.js';
|
|
11
12
|
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').option('--json', 'Machine-readable output').option('--no-color', 'Disable colored output');
|
|
@@ -16,7 +17,7 @@ program.command('init').description('Initialize Vizzly in your project').option(
|
|
|
16
17
|
...options
|
|
17
18
|
});
|
|
18
19
|
});
|
|
19
|
-
program.command('upload').description('Upload screenshots to Vizzly').argument('<path>', 'Path to screenshots directory or file').option('-b, --build-name <name>', 'Build name for grouping').option('-m, --metadata <json>', 'Additional metadata as JSON').option('--batch-size <n>', 'Upload batch size', v => parseInt(v, 10)).option('--upload-timeout <ms>', 'Upload timeout in milliseconds', v => parseInt(v, 10)).option('--branch <branch>', 'Git branch').option('--commit <sha>', 'Git commit SHA').option('--message <msg>', 'Commit message').option('--environment <env>', 'Environment name', 'test').option('--threshold <number>', 'Comparison threshold', parseFloat).option('--token <token>', 'API token override').option('--wait', 'Wait for build completion').option('--upload-all', 'Upload all screenshots without SHA deduplication').action(async (path, options) => {
|
|
20
|
+
program.command('upload').description('Upload screenshots to Vizzly').argument('<path>', 'Path to screenshots directory or file').option('-b, --build-name <name>', 'Build name for grouping').option('-m, --metadata <json>', 'Additional metadata as JSON').option('--batch-size <n>', 'Upload batch size', v => parseInt(v, 10)).option('--upload-timeout <ms>', 'Upload timeout in milliseconds', v => parseInt(v, 10)).option('--branch <branch>', 'Git branch').option('--commit <sha>', 'Git commit SHA').option('--message <msg>', 'Commit message').option('--environment <env>', 'Environment name', 'test').option('--threshold <number>', 'Comparison threshold', parseFloat).option('--token <token>', 'API token override').option('--wait', 'Wait for build completion').option('--upload-all', 'Upload all screenshots without SHA deduplication').option('--parallel-id <id>', 'Unique identifier for parallel test execution').action(async (path, options) => {
|
|
20
21
|
const globalOptions = program.opts();
|
|
21
22
|
|
|
22
23
|
// Validate options
|
|
@@ -59,7 +60,7 @@ program.command('tdd').description('Run tests in TDD mode with local visual comp
|
|
|
59
60
|
}
|
|
60
61
|
await cleanup();
|
|
61
62
|
});
|
|
62
|
-
program.command('run').description('Run tests with Vizzly integration').argument('<command>', 'Test command to run').option('--port <port>', 'Port for screenshot server', '47392').option('-b, --build-name <name>', 'Custom build name').option('--branch <branch>', 'Git branch override').option('--commit <sha>', 'Git commit SHA').option('--message <msg>', 'Commit message').option('--environment <env>', 'Environment name', 'test').option('--token <token>', 'API token override').option('--wait', 'Wait for build completion').option('--timeout <ms>', 'Server timeout in milliseconds', '30000').option('--allow-no-token', 'Allow running without API token').option('--upload-all', 'Upload all screenshots without SHA deduplication').action(async (command, options) => {
|
|
63
|
+
program.command('run').description('Run tests with Vizzly integration').argument('<command>', 'Test command to run').option('--port <port>', 'Port for screenshot server', '47392').option('-b, --build-name <name>', 'Custom build name').option('--branch <branch>', 'Git branch override').option('--commit <sha>', 'Git commit SHA').option('--message <msg>', 'Commit message').option('--environment <env>', 'Environment name', 'test').option('--token <token>', 'API token override').option('--wait', 'Wait for build completion').option('--timeout <ms>', 'Server timeout in milliseconds', '30000').option('--allow-no-token', 'Allow running without API token').option('--upload-all', 'Upload all screenshots without SHA deduplication').option('--parallel-id <id>', 'Unique identifier for parallel test execution').action(async (command, options) => {
|
|
63
64
|
const globalOptions = program.opts();
|
|
64
65
|
|
|
65
66
|
// Validate options
|
|
@@ -94,6 +95,18 @@ program.command('status').description('Check the status of a build').argument('<
|
|
|
94
95
|
}
|
|
95
96
|
await statusCommand(buildId, options, globalOptions);
|
|
96
97
|
});
|
|
98
|
+
program.command('finalize').description('Finalize a parallel build after all shards complete').argument('<parallel-id>', 'Parallel ID to finalize').action(async (parallelId, options) => {
|
|
99
|
+
const globalOptions = program.opts();
|
|
100
|
+
|
|
101
|
+
// Validate options
|
|
102
|
+
const validationErrors = validateFinalizeOptions(parallelId, options);
|
|
103
|
+
if (validationErrors.length > 0) {
|
|
104
|
+
console.error('Validation errors:');
|
|
105
|
+
validationErrors.forEach(error => console.error(` - ${error}`));
|
|
106
|
+
process.exit(1);
|
|
107
|
+
}
|
|
108
|
+
await finalizeCommand(parallelId, options, globalOptions);
|
|
109
|
+
});
|
|
97
110
|
program.command('doctor').description('Run diagnostics to check your environment and configuration').option('--api', 'Include API connectivity checks').action(async options => {
|
|
98
111
|
const globalOptions = program.opts();
|
|
99
112
|
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { loadConfig } from '../utils/config-loader.js';
|
|
2
|
+
import { ConsoleUI } from '../utils/console-ui.js';
|
|
3
|
+
import { createServiceContainer } from '../container/index.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Finalize command implementation
|
|
7
|
+
* @param {string} parallelId - Parallel ID to finalize
|
|
8
|
+
* @param {Object} options - Command options
|
|
9
|
+
* @param {Object} globalOptions - Global CLI options
|
|
10
|
+
*/
|
|
11
|
+
export async function finalizeCommand(parallelId, options = {}, globalOptions = {}) {
|
|
12
|
+
// Create UI handler
|
|
13
|
+
const ui = new ConsoleUI({
|
|
14
|
+
json: globalOptions.json,
|
|
15
|
+
verbose: globalOptions.verbose,
|
|
16
|
+
color: !globalOptions.noColor
|
|
17
|
+
});
|
|
18
|
+
try {
|
|
19
|
+
// Load configuration with CLI overrides
|
|
20
|
+
const allOptions = {
|
|
21
|
+
...globalOptions,
|
|
22
|
+
...options
|
|
23
|
+
};
|
|
24
|
+
const config = await loadConfig(globalOptions.config, allOptions);
|
|
25
|
+
|
|
26
|
+
// Validate API token
|
|
27
|
+
if (!config.apiKey) {
|
|
28
|
+
ui.error('API token required. Use --token or set VIZZLY_TOKEN environment variable');
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
if (globalOptions.verbose) {
|
|
32
|
+
ui.info('Configuration loaded', {
|
|
33
|
+
parallelId,
|
|
34
|
+
apiUrl: config.apiUrl
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Create service container and get API service
|
|
39
|
+
ui.startSpinner('Finalizing parallel build...');
|
|
40
|
+
const container = await createServiceContainer(config, 'finalize');
|
|
41
|
+
const apiService = await container.get('api');
|
|
42
|
+
ui.stopSpinner();
|
|
43
|
+
|
|
44
|
+
// Call finalize endpoint
|
|
45
|
+
const result = await apiService.finalizeParallelBuild(parallelId);
|
|
46
|
+
if (globalOptions.json) {
|
|
47
|
+
console.log(JSON.stringify(result, null, 2));
|
|
48
|
+
} else {
|
|
49
|
+
ui.success(`Parallel build ${result.build.id} finalized successfully`);
|
|
50
|
+
ui.info(`Status: ${result.build.status}`);
|
|
51
|
+
ui.info(`Parallel ID: ${result.build.parallel_id}`);
|
|
52
|
+
}
|
|
53
|
+
} catch (error) {
|
|
54
|
+
ui.stopSpinner();
|
|
55
|
+
ui.error('Failed to finalize parallel build', error);
|
|
56
|
+
} finally {
|
|
57
|
+
ui.cleanup();
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Validate finalize options
|
|
63
|
+
* @param {string} parallelId - Parallel ID to finalize
|
|
64
|
+
* @param {Object} options - Command options
|
|
65
|
+
*/
|
|
66
|
+
export function validateFinalizeOptions(parallelId, _options) {
|
|
67
|
+
const errors = [];
|
|
68
|
+
if (!parallelId || parallelId.trim() === '') {
|
|
69
|
+
errors.push('Parallel ID is required');
|
|
70
|
+
}
|
|
71
|
+
return errors;
|
|
72
|
+
}
|
package/dist/commands/run.js
CHANGED
|
@@ -162,7 +162,8 @@ export async function runCommand(testCommand, options = {}, globalOptions = {})
|
|
|
162
162
|
allowNoToken: config.allowNoToken || false,
|
|
163
163
|
wait: config.wait || options.wait || false,
|
|
164
164
|
uploadAll: options.uploadAll || false,
|
|
165
|
-
pullRequestNumber
|
|
165
|
+
pullRequestNumber,
|
|
166
|
+
parallelId: config.parallelId
|
|
166
167
|
};
|
|
167
168
|
|
|
168
169
|
// Start test run
|
package/dist/commands/upload.js
CHANGED
|
@@ -101,6 +101,7 @@ export async function uploadCommand(screenshotsPath, options = {}, globalOptions
|
|
|
101
101
|
uploadAll: options.uploadAll || false,
|
|
102
102
|
metadata: options.metadata ? JSON.parse(options.metadata) : {},
|
|
103
103
|
pullRequestNumber,
|
|
104
|
+
parallelId: config.parallelId,
|
|
104
105
|
onProgress: progressData => {
|
|
105
106
|
const {
|
|
106
107
|
message: progressMessage,
|
|
@@ -241,4 +241,18 @@ export class ApiService {
|
|
|
241
241
|
async getTokenContext() {
|
|
242
242
|
return this.request('/api/sdk/token/context');
|
|
243
243
|
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Finalize a parallel build
|
|
247
|
+
* @param {string} parallelId - Parallel ID to finalize
|
|
248
|
+
* @returns {Promise<Object>} Finalization result
|
|
249
|
+
*/
|
|
250
|
+
async finalizeParallelBuild(parallelId) {
|
|
251
|
+
return this.request(`/api/sdk/parallel/${parallelId}/finalize`, {
|
|
252
|
+
method: 'POST',
|
|
253
|
+
headers: {
|
|
254
|
+
'Content-Type': 'application/json'
|
|
255
|
+
}
|
|
256
|
+
});
|
|
257
|
+
}
|
|
244
258
|
}
|
|
@@ -159,9 +159,15 @@ export class TddService {
|
|
|
159
159
|
});
|
|
160
160
|
}
|
|
161
161
|
|
|
162
|
-
// Download
|
|
162
|
+
// Download screenshots in batches with progress indication
|
|
163
163
|
let downloadedCount = 0;
|
|
164
164
|
let skippedCount = 0;
|
|
165
|
+
let errorCount = 0;
|
|
166
|
+
const totalScreenshots = buildDetails.screenshots.length;
|
|
167
|
+
const batchSize = 5; // Download up to 5 screenshots concurrently
|
|
168
|
+
|
|
169
|
+
// Filter screenshots that need to be downloaded
|
|
170
|
+
const screenshotsToProcess = [];
|
|
165
171
|
for (const screenshot of buildDetails.screenshots) {
|
|
166
172
|
// Sanitize screenshot name for security
|
|
167
173
|
let sanitizedName;
|
|
@@ -169,6 +175,7 @@ export class TddService {
|
|
|
169
175
|
sanitizedName = sanitizeScreenshotName(screenshot.name);
|
|
170
176
|
} catch (error) {
|
|
171
177
|
logger.warn(`Skipping screenshot with invalid name '${screenshot.name}': ${error.message}`);
|
|
178
|
+
errorCount++;
|
|
172
179
|
continue;
|
|
173
180
|
}
|
|
174
181
|
const imagePath = safePath(this.baselinePath, `${sanitizedName}.png`);
|
|
@@ -190,28 +197,75 @@ export class TddService {
|
|
|
190
197
|
const downloadUrl = screenshot.original_url || screenshot.url;
|
|
191
198
|
if (!downloadUrl) {
|
|
192
199
|
logger.warn(`⚠️ Screenshot ${sanitizedName} has no download URL - skipping`);
|
|
193
|
-
|
|
200
|
+
errorCount++;
|
|
201
|
+
continue;
|
|
194
202
|
}
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
203
|
+
screenshotsToProcess.push({
|
|
204
|
+
screenshot,
|
|
205
|
+
sanitizedName,
|
|
206
|
+
imagePath,
|
|
207
|
+
downloadUrl
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Process downloads in batches
|
|
212
|
+
const actualDownloadsNeeded = screenshotsToProcess.length;
|
|
213
|
+
if (actualDownloadsNeeded > 0) {
|
|
214
|
+
logger.info(`📥 Downloading ${actualDownloadsNeeded} new/updated screenshots in batches of ${batchSize}...`);
|
|
215
|
+
for (let i = 0; i < screenshotsToProcess.length; i += batchSize) {
|
|
216
|
+
const batch = screenshotsToProcess.slice(i, i + batchSize);
|
|
217
|
+
const batchNum = Math.floor(i / batchSize) + 1;
|
|
218
|
+
const totalBatches = Math.ceil(screenshotsToProcess.length / batchSize);
|
|
219
|
+
logger.info(`📦 Processing batch ${batchNum}/${totalBatches} (${batch.length} screenshots)`);
|
|
220
|
+
|
|
221
|
+
// Download batch concurrently
|
|
222
|
+
const downloadPromises = batch.map(async ({
|
|
223
|
+
sanitizedName,
|
|
224
|
+
imagePath,
|
|
225
|
+
downloadUrl
|
|
226
|
+
}) => {
|
|
227
|
+
try {
|
|
228
|
+
logger.debug(`📥 Downloading: ${sanitizedName}`);
|
|
229
|
+
const response = await fetchWithTimeout(downloadUrl);
|
|
230
|
+
if (!response.ok) {
|
|
231
|
+
throw new NetworkError(`Failed to download ${sanitizedName}: ${response.statusText}`);
|
|
232
|
+
}
|
|
233
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
234
|
+
const imageBuffer = Buffer.from(arrayBuffer);
|
|
235
|
+
writeFileSync(imagePath, imageBuffer);
|
|
236
|
+
logger.debug(`✓ Downloaded ${sanitizedName}.png`);
|
|
237
|
+
return {
|
|
238
|
+
success: true,
|
|
239
|
+
name: sanitizedName
|
|
240
|
+
};
|
|
241
|
+
} catch (error) {
|
|
242
|
+
logger.warn(`⚠️ Failed to download ${sanitizedName}: ${error.message}`);
|
|
243
|
+
return {
|
|
244
|
+
success: false,
|
|
245
|
+
name: sanitizedName,
|
|
246
|
+
error: error.message
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
const batchResults = await Promise.all(downloadPromises);
|
|
251
|
+
const batchSuccesses = batchResults.filter(r => r.success).length;
|
|
252
|
+
const batchFailures = batchResults.filter(r => !r.success).length;
|
|
253
|
+
downloadedCount += batchSuccesses;
|
|
254
|
+
errorCount += batchFailures;
|
|
255
|
+
|
|
256
|
+
// Show progress
|
|
257
|
+
const totalProcessed = downloadedCount + skippedCount + errorCount;
|
|
258
|
+
const progressPercent = Math.round(totalProcessed / totalScreenshots * 100);
|
|
259
|
+
logger.info(`📊 Progress: ${totalProcessed}/${totalScreenshots} (${progressPercent}%) - ${batchSuccesses} downloaded, ${batchFailures} failed in this batch`);
|
|
209
260
|
}
|
|
210
261
|
}
|
|
211
262
|
|
|
212
263
|
// Check if we actually downloaded any screenshots
|
|
213
|
-
if (downloadedCount === 0) {
|
|
264
|
+
if (downloadedCount === 0 && skippedCount === 0) {
|
|
214
265
|
logger.error('❌ No screenshots were successfully downloaded from the baseline build');
|
|
266
|
+
if (errorCount > 0) {
|
|
267
|
+
logger.info(`💡 ${errorCount} screenshots had errors - check download URLs and network connection`);
|
|
268
|
+
}
|
|
215
269
|
logger.info('💡 This usually means the build failed or screenshots have no download URLs');
|
|
216
270
|
logger.info('💡 Try using a successful build ID, or run without --baseline-build to create local baselines');
|
|
217
271
|
return null;
|
|
@@ -258,9 +312,16 @@ export class TddService {
|
|
|
258
312
|
};
|
|
259
313
|
const metadataPath = join(this.baselinePath, 'metadata.json');
|
|
260
314
|
writeFileSync(metadataPath, JSON.stringify(this.baselineData, null, 2));
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
315
|
+
|
|
316
|
+
// Final summary
|
|
317
|
+
const actualDownloads = downloadedCount - skippedCount;
|
|
318
|
+
const totalAttempted = downloadedCount + errorCount;
|
|
319
|
+
if (skippedCount > 0 || errorCount > 0) {
|
|
320
|
+
let summaryParts = [];
|
|
321
|
+
if (actualDownloads > 0) summaryParts.push(`${actualDownloads} downloaded`);
|
|
322
|
+
if (skippedCount > 0) summaryParts.push(`${skippedCount} skipped (matching SHA)`);
|
|
323
|
+
if (errorCount > 0) summaryParts.push(`${errorCount} failed`);
|
|
324
|
+
logger.info(`✅ Baseline ready - ${summaryParts.join(', ')} - ${totalAttempted}/${buildDetails.screenshots.length} total`);
|
|
264
325
|
} else {
|
|
265
326
|
logger.info(`✅ Baseline downloaded successfully - ${downloadedCount}/${buildDetails.screenshots.length} screenshots`);
|
|
266
327
|
}
|
|
@@ -138,7 +138,8 @@ export class TestRunner extends BaseService {
|
|
|
138
138
|
environment: options.environment || 'test',
|
|
139
139
|
commit_sha: options.commit,
|
|
140
140
|
commit_message: options.message,
|
|
141
|
-
github_pull_request_number: options.pullRequestNumber
|
|
141
|
+
github_pull_request_number: options.pullRequestNumber,
|
|
142
|
+
parallel_id: options.parallelId
|
|
142
143
|
});
|
|
143
144
|
this.logger.debug(`Build created with ID: ${buildResult.id}`);
|
|
144
145
|
|
|
@@ -51,6 +51,7 @@ export function createUploader({
|
|
|
51
51
|
environment = 'production',
|
|
52
52
|
threshold,
|
|
53
53
|
pullRequestNumber,
|
|
54
|
+
parallelId,
|
|
54
55
|
onProgress = () => {}
|
|
55
56
|
}) {
|
|
56
57
|
try {
|
|
@@ -101,7 +102,8 @@ export function createUploader({
|
|
|
101
102
|
commit_message: message,
|
|
102
103
|
environment,
|
|
103
104
|
threshold,
|
|
104
|
-
github_pull_request_number: pullRequestNumber
|
|
105
|
+
github_pull_request_number: pullRequestNumber,
|
|
106
|
+
parallel_id: parallelId
|
|
105
107
|
};
|
|
106
108
|
const build = await api.createBuild(buildInfo);
|
|
107
109
|
const buildId = build.id;
|
|
@@ -219,7 +221,6 @@ export function createUploader({
|
|
|
219
221
|
if (build.status === 'failed') {
|
|
220
222
|
throw new UploadError(`Build failed: ${build.error || 'Unknown error'}`);
|
|
221
223
|
}
|
|
222
|
-
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
223
224
|
}
|
|
224
225
|
throw new TimeoutError(`Build timed out after ${timeout}ms`, {
|
|
225
226
|
buildId,
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Finalize command implementation
|
|
3
|
+
* @param {string} parallelId - Parallel ID to finalize
|
|
4
|
+
* @param {Object} options - Command options
|
|
5
|
+
* @param {Object} globalOptions - Global CLI options
|
|
6
|
+
*/
|
|
7
|
+
export function finalizeCommand(parallelId: string, options?: any, globalOptions?: any): Promise<void>;
|
|
8
|
+
/**
|
|
9
|
+
* Validate finalize options
|
|
10
|
+
* @param {string} parallelId - Parallel ID to finalize
|
|
11
|
+
* @param {Object} options - Command options
|
|
12
|
+
*/
|
|
13
|
+
export function validateFinalizeOptions(parallelId: string, _options: any): string[];
|
|
@@ -76,4 +76,10 @@ export class ApiService {
|
|
|
76
76
|
* @returns {Promise<Object>} Token context data
|
|
77
77
|
*/
|
|
78
78
|
getTokenContext(): Promise<any>;
|
|
79
|
+
/**
|
|
80
|
+
* Finalize a parallel build
|
|
81
|
+
* @param {string} parallelId - Parallel ID to finalize
|
|
82
|
+
* @returns {Promise<Object>} Finalization result
|
|
83
|
+
*/
|
|
84
|
+
finalizeParallelBuild(parallelId: string): Promise<any>;
|
|
79
85
|
}
|
|
@@ -8,7 +8,7 @@ export function createUploader({ apiKey, apiUrl, userAgent, command, upload: upl
|
|
|
8
8
|
command: any;
|
|
9
9
|
upload?: {};
|
|
10
10
|
}, options?: {}): {
|
|
11
|
-
upload: ({ screenshotsDir, buildName, branch, commit, message, environment, threshold, pullRequestNumber, onProgress, }: {
|
|
11
|
+
upload: ({ screenshotsDir, buildName, branch, commit, message, environment, threshold, pullRequestNumber, parallelId, onProgress, }: {
|
|
12
12
|
screenshotsDir: any;
|
|
13
13
|
buildName: any;
|
|
14
14
|
branch: any;
|
|
@@ -17,6 +17,7 @@ export function createUploader({ apiKey, apiUrl, userAgent, command, upload: upl
|
|
|
17
17
|
environment?: string;
|
|
18
18
|
threshold: any;
|
|
19
19
|
pullRequestNumber: any;
|
|
20
|
+
parallelId: any;
|
|
20
21
|
onProgress?: () => void;
|
|
21
22
|
}) => Promise<{
|
|
22
23
|
success: boolean;
|
|
@@ -37,6 +37,11 @@ export function getServerUrl(): string | undefined;
|
|
|
37
37
|
* @returns {string|undefined} Build ID
|
|
38
38
|
*/
|
|
39
39
|
export function getBuildId(): string | undefined;
|
|
40
|
+
/**
|
|
41
|
+
* Get parallel ID from environment
|
|
42
|
+
* @returns {string|undefined} Parallel ID
|
|
43
|
+
*/
|
|
44
|
+
export function getParallelId(): string | undefined;
|
|
40
45
|
/**
|
|
41
46
|
* Check if TDD mode is enabled
|
|
42
47
|
* @returns {boolean} Whether TDD mode is enabled
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { cosmiconfigSync } from 'cosmiconfig';
|
|
2
2
|
import { resolve } from 'path';
|
|
3
|
-
import { getApiToken, getApiUrl } from './environment-config.js';
|
|
3
|
+
import { getApiToken, getApiUrl, getParallelId } from './environment-config.js';
|
|
4
4
|
const DEFAULT_CONFIG = {
|
|
5
5
|
// API Configuration
|
|
6
6
|
apiKey: getApiToken(),
|
|
@@ -62,8 +62,10 @@ export async function loadConfig(configPath = null, cliOverrides = {}) {
|
|
|
62
62
|
// 2. Override with environment variables
|
|
63
63
|
const envApiKey = getApiToken();
|
|
64
64
|
const envApiUrl = getApiUrl();
|
|
65
|
+
const envParallelId = getParallelId();
|
|
65
66
|
if (envApiKey) config.apiKey = envApiKey;
|
|
66
67
|
if (envApiUrl !== 'https://vizzly.dev') config.apiUrl = envApiUrl;
|
|
68
|
+
if (envParallelId) config.parallelId = envParallelId;
|
|
67
69
|
|
|
68
70
|
// 3. Apply CLI overrides (highest priority)
|
|
69
71
|
applyCLIOverrides(config, cliOverrides);
|
|
@@ -85,6 +87,7 @@ function applyCLIOverrides(config, cliOverrides = {}) {
|
|
|
85
87
|
if (cliOverrides.branch) config.build.branch = cliOverrides.branch;
|
|
86
88
|
if (cliOverrides.commit) config.build.commit = cliOverrides.commit;
|
|
87
89
|
if (cliOverrides.message) config.build.message = cliOverrides.message;
|
|
90
|
+
if (cliOverrides.parallelId) config.parallelId = cliOverrides.parallelId;
|
|
88
91
|
|
|
89
92
|
// Server overrides
|
|
90
93
|
if (cliOverrides.port) config.server.port = parseInt(cliOverrides.port, 10);
|
|
@@ -59,6 +59,14 @@ export function getBuildId() {
|
|
|
59
59
|
return process.env.VIZZLY_BUILD_ID;
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
+
/**
|
|
63
|
+
* Get parallel ID from environment
|
|
64
|
+
* @returns {string|undefined} Parallel ID
|
|
65
|
+
*/
|
|
66
|
+
export function getParallelId() {
|
|
67
|
+
return process.env.VIZZLY_PARALLEL_ID;
|
|
68
|
+
}
|
|
69
|
+
|
|
62
70
|
/**
|
|
63
71
|
* Check if TDD mode is enabled
|
|
64
72
|
* @returns {boolean} Whether TDD mode is enabled
|
|
@@ -88,6 +96,7 @@ export function getAllEnvironmentConfig() {
|
|
|
88
96
|
enabled: isVizzlyEnabled(),
|
|
89
97
|
serverUrl: getServerUrl(),
|
|
90
98
|
buildId: getBuildId(),
|
|
99
|
+
parallelId: getParallelId(),
|
|
91
100
|
tddMode: isTddMode()
|
|
92
101
|
};
|
|
93
102
|
}
|
package/docs/api-reference.md
CHANGED
|
@@ -289,6 +289,7 @@ Upload screenshots from a directory.
|
|
|
289
289
|
- `--token <token>` - API token override
|
|
290
290
|
- `--wait` - Wait for build completion
|
|
291
291
|
- `--upload-all` - Upload all screenshots without SHA deduplication
|
|
292
|
+
- `--parallel-id <id>` - Unique identifier for parallel test execution
|
|
292
293
|
|
|
293
294
|
**Exit Codes:**
|
|
294
295
|
- `0` - Success (all approved or no changes)
|
|
@@ -321,6 +322,9 @@ Run tests with Vizzly integration.
|
|
|
321
322
|
- `--upload-timeout <ms>` - Upload wait timeout in ms (default: from config or 30000)
|
|
322
323
|
- `--upload-all` - Upload all screenshots without SHA deduplication
|
|
323
324
|
|
|
325
|
+
*Parallel Execution:*
|
|
326
|
+
- `--parallel-id <id>` - Unique identifier for parallel test execution
|
|
327
|
+
|
|
324
328
|
*Development & Testing:*
|
|
325
329
|
- `--allow-no-token` - Allow running without API token
|
|
326
330
|
- `--token <token>` - API token override
|
|
@@ -403,6 +407,26 @@ Check build status.
|
|
|
403
407
|
- `1` - Build has changes requiring review
|
|
404
408
|
- `2` - Build failed or error
|
|
405
409
|
|
|
410
|
+
### `vizzly finalize <parallel-id>`
|
|
411
|
+
|
|
412
|
+
Finalize a parallel build after all shards complete.
|
|
413
|
+
|
|
414
|
+
**Arguments:**
|
|
415
|
+
- `<parallel-id>` - Parallel ID to finalize
|
|
416
|
+
|
|
417
|
+
**Description:**
|
|
418
|
+
When using parallel execution with `--parallel-id`, all test shards contribute screenshots to the same shared build. After all shards complete successfully, use this command to finalize the build and trigger comparison processing.
|
|
419
|
+
|
|
420
|
+
**Example:**
|
|
421
|
+
```bash
|
|
422
|
+
# After all parallel shards complete
|
|
423
|
+
vizzly finalize "ci-run-123-attempt-1"
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
**Exit Codes:**
|
|
427
|
+
- `0` - Build finalized successfully
|
|
428
|
+
- `1` - Finalization failed or error
|
|
429
|
+
|
|
406
430
|
### `vizzly doctor`
|
|
407
431
|
|
|
408
432
|
Run environment diagnostics.
|
|
@@ -486,6 +510,9 @@ Configuration loaded via cosmiconfig in this order:
|
|
|
486
510
|
- `VIZZLY_API_URL` - API base URL override
|
|
487
511
|
- `VIZZLY_LOG_LEVEL` - Logger level (`debug`, `info`, `warn`, `error`)
|
|
488
512
|
|
|
513
|
+
**Parallel Builds:**
|
|
514
|
+
- `VIZZLY_PARALLEL_ID` - Unique identifier for parallel test execution
|
|
515
|
+
|
|
489
516
|
**Git Information Override (CI/CD Enhancement):**
|
|
490
517
|
- `VIZZLY_COMMIT_SHA` - Override detected commit SHA
|
|
491
518
|
- `VIZZLY_COMMIT_MESSAGE` - Override detected commit message
|
package/docs/test-integration.md
CHANGED
|
@@ -209,6 +209,15 @@ vizzly run "npm test" --upload-all
|
|
|
209
209
|
vizzly run "npm test" --threshold 0.02
|
|
210
210
|
```
|
|
211
211
|
|
|
212
|
+
### Parallel Execution
|
|
213
|
+
|
|
214
|
+
**`--parallel-id <id>`** - Unique identifier for parallel test execution
|
|
215
|
+
```bash
|
|
216
|
+
vizzly run "npm test" --parallel-id "ci-run-123"
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
When using parallel execution, multiple test runners can contribute screenshots to the same build. This is particularly useful for CI/CD pipelines with parallel jobs.
|
|
220
|
+
|
|
212
221
|
### Development Options
|
|
213
222
|
|
|
214
223
|
For TDD mode, use the dedicated `vizzly tdd` command. See [TDD Mode Guide](./tdd-mode.md) for details.
|
|
@@ -283,6 +292,43 @@ jobs:
|
|
|
283
292
|
|
|
284
293
|
**Enhanced Git Information:** The `VIZZLY_*` environment variables ensure accurate git metadata is captured in your builds, avoiding issues with merge commits that can occur in CI environments.
|
|
285
294
|
|
|
295
|
+
### Parallel Builds in CI
|
|
296
|
+
|
|
297
|
+
For parallel test execution, use `--parallel-id` to ensure all shards contribute to the same build:
|
|
298
|
+
|
|
299
|
+
```yaml
|
|
300
|
+
# GitHub Actions with parallel matrix
|
|
301
|
+
jobs:
|
|
302
|
+
e2e-tests:
|
|
303
|
+
strategy:
|
|
304
|
+
matrix:
|
|
305
|
+
shard: [1/4, 2/4, 3/4, 4/4]
|
|
306
|
+
steps:
|
|
307
|
+
- name: Run tests with Vizzly
|
|
308
|
+
run: |
|
|
309
|
+
npx vizzly run "npm test -- --shard=${{ matrix.shard }}" \
|
|
310
|
+
--parallel-id="${{ github.run_id }}-${{ github.run_attempt }}"
|
|
311
|
+
env:
|
|
312
|
+
VIZZLY_TOKEN: ${{ secrets.VIZZLY_TOKEN }}
|
|
313
|
+
|
|
314
|
+
finalize-e2e:
|
|
315
|
+
needs: e2e-tests
|
|
316
|
+
runs-on: ubuntu-latest
|
|
317
|
+
if: always() && needs.e2e-tests.result == 'success'
|
|
318
|
+
steps:
|
|
319
|
+
- name: Finalize parallel build
|
|
320
|
+
run: |
|
|
321
|
+
npx vizzly finalize "${{ github.run_id }}-${{ github.run_attempt }}"
|
|
322
|
+
env:
|
|
323
|
+
VIZZLY_TOKEN: ${{ secrets.VIZZLY_TOKEN }}
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
**How Parallel Builds Work:**
|
|
327
|
+
1. All shards with the same `--parallel-id` contribute to one shared build
|
|
328
|
+
2. First shard creates the build, subsequent shards add screenshots to it
|
|
329
|
+
3. After all shards complete, use `vizzly finalize` to trigger comparison processing
|
|
330
|
+
4. Use GitHub's run ID + attempt for uniqueness across workflow runs
|
|
331
|
+
|
|
286
332
|
### GitLab CI
|
|
287
333
|
|
|
288
334
|
```yaml
|
|
@@ -294,6 +340,29 @@ visual-tests:
|
|
|
294
340
|
- npx vizzly run "npm test" --wait
|
|
295
341
|
variables:
|
|
296
342
|
VIZZLY_TOKEN: $VIZZLY_TOKEN
|
|
343
|
+
|
|
344
|
+
# Parallel execution example
|
|
345
|
+
visual-tests-parallel:
|
|
346
|
+
stage: test
|
|
347
|
+
parallel:
|
|
348
|
+
matrix:
|
|
349
|
+
- SHARD: "1/4"
|
|
350
|
+
- SHARD: "2/4"
|
|
351
|
+
- SHARD: "3/4"
|
|
352
|
+
- SHARD: "4/4"
|
|
353
|
+
script:
|
|
354
|
+
- npm ci
|
|
355
|
+
- npx vizzly run "npm test -- --shard=$SHARD" --parallel-id "$CI_PIPELINE_ID-$CI_JOB_ID"
|
|
356
|
+
variables:
|
|
357
|
+
VIZZLY_TOKEN: $VIZZLY_TOKEN
|
|
358
|
+
|
|
359
|
+
finalize-visual-tests:
|
|
360
|
+
stage: finalize
|
|
361
|
+
needs: ["visual-tests-parallel"]
|
|
362
|
+
script:
|
|
363
|
+
- npx vizzly finalize "$CI_PIPELINE_ID-$CI_JOB_ID"
|
|
364
|
+
variables:
|
|
365
|
+
VIZZLY_TOKEN: $VIZZLY_TOKEN
|
|
297
366
|
```
|
|
298
367
|
|
|
299
368
|
## Advanced Usage
|