@vizzly-testing/cli 0.5.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 +55 -9
- package/dist/cli.js +15 -2
- package/dist/commands/finalize.js +72 -0
- package/dist/commands/run.js +59 -19
- package/dist/commands/tdd.js +6 -13
- package/dist/commands/upload.js +1 -0
- package/dist/server/handlers/tdd-handler.js +82 -8
- package/dist/services/api-service.js +14 -0
- package/dist/services/html-report-generator.js +377 -0
- package/dist/services/report-generator/report.css +355 -0
- package/dist/services/report-generator/viewer.js +100 -0
- package/dist/services/server-manager.js +3 -2
- package/dist/services/tdd-service.js +436 -66
- package/dist/services/test-runner.js +56 -28
- package/dist/services/uploader.js +3 -2
- package/dist/types/commands/finalize.d.ts +13 -0
- package/dist/types/server/handlers/tdd-handler.d.ts +18 -1
- package/dist/types/services/api-service.d.ts +6 -0
- package/dist/types/services/html-report-generator.d.ts +52 -0
- package/dist/types/services/report-generator/viewer.d.ts +0 -0
- package/dist/types/services/server-manager.d.ts +19 -1
- package/dist/types/services/tdd-service.d.ts +24 -3
- package/dist/types/services/uploader.d.ts +2 -1
- package/dist/types/utils/config-loader.d.ts +3 -0
- package/dist/types/utils/environment-config.d.ts +5 -0
- package/dist/types/utils/security.d.ts +29 -0
- package/dist/utils/config-loader.js +11 -1
- package/dist/utils/environment-config.js +9 -0
- package/dist/utils/security.js +154 -0
- package/docs/api-reference.md +27 -0
- package/docs/tdd-mode.md +58 -12
- package/docs/test-integration.md +69 -0
- package/package.json +3 -2
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
|
|
@@ -123,13 +128,19 @@ For local visual testing with immediate feedback, use the dedicated `tdd` comman
|
|
|
123
128
|
# First run - creates local baselines
|
|
124
129
|
vizzly tdd "npm test"
|
|
125
130
|
|
|
126
|
-
# Make changes and test - fails if visual differences detected
|
|
131
|
+
# Make changes and test - fails if visual differences detected
|
|
127
132
|
vizzly tdd "npm test"
|
|
128
133
|
|
|
129
134
|
# Accept changes as new baseline
|
|
130
135
|
vizzly tdd "npm test" --set-baseline
|
|
131
136
|
```
|
|
132
137
|
|
|
138
|
+
**Interactive HTML Report:** Each TDD run generates a detailed HTML report with visual comparison tools:
|
|
139
|
+
- **Overlay mode** - Toggle between baseline and current screenshots
|
|
140
|
+
- **Side-by-side mode** - Compare baseline and current images horizontally
|
|
141
|
+
- **Onion skin mode** - Drag to reveal differences interactively
|
|
142
|
+
- **Toggle mode** - Click to switch between baseline and current
|
|
143
|
+
|
|
133
144
|
**TDD Command Options:**
|
|
134
145
|
- `--set-baseline` - Accept current screenshots as new baseline
|
|
135
146
|
- `--baseline-build <id>` - Use specific build as baseline (requires API token)
|
|
@@ -144,6 +155,7 @@ vizzly init # Create vizzly.config.js with defaults
|
|
|
144
155
|
vizzly status <build-id> # Check build progress and results
|
|
145
156
|
vizzly status <build-id> --verbose # Detailed build information
|
|
146
157
|
vizzly status <build-id> --json # Machine-readable output
|
|
158
|
+
vizzly finalize <parallel-id> # Finalize parallel build after all shards complete
|
|
147
159
|
vizzly doctor # Fast local preflight (no network)
|
|
148
160
|
vizzly doctor --api # Include API connectivity checks
|
|
149
161
|
```
|
|
@@ -203,25 +215,25 @@ export default {
|
|
|
203
215
|
// API configuration
|
|
204
216
|
// Set VIZZLY_TOKEN environment variable or uncomment and set here:
|
|
205
217
|
// apiToken: 'your-token-here',
|
|
206
|
-
|
|
218
|
+
|
|
207
219
|
// Screenshot configuration
|
|
208
220
|
screenshots: {
|
|
209
221
|
directory: './screenshots',
|
|
210
222
|
formats: ['png']
|
|
211
223
|
},
|
|
212
|
-
|
|
224
|
+
|
|
213
225
|
// Server configuration
|
|
214
226
|
server: {
|
|
215
227
|
port: 47392,
|
|
216
228
|
screenshotPath: '/screenshot'
|
|
217
229
|
},
|
|
218
|
-
|
|
230
|
+
|
|
219
231
|
// Comparison configuration
|
|
220
232
|
comparison: {
|
|
221
233
|
threshold: 0.1,
|
|
222
234
|
ignoreAntialiasing: true
|
|
223
235
|
},
|
|
224
|
-
|
|
236
|
+
|
|
225
237
|
// Upload configuration
|
|
226
238
|
upload: {
|
|
227
239
|
concurrency: 5,
|
|
@@ -278,11 +290,42 @@ For CI/CD pipelines, use the `--wait` flag to wait for visual comparison results
|
|
|
278
290
|
env:
|
|
279
291
|
VIZZLY_TOKEN: ${{ secrets.VIZZLY_TOKEN }}
|
|
280
292
|
# Optional: Provide correct git information from GitHub context
|
|
281
|
-
VIZZLY_COMMIT_MESSAGE: ${{ github.event.head_commit.message }}
|
|
282
|
-
VIZZLY_COMMIT_SHA: ${{ github.event.head_commit.id }}
|
|
293
|
+
VIZZLY_COMMIT_MESSAGE: ${{ github.event.pull_request.head.commit.message || github.event.head_commit.message }}
|
|
294
|
+
VIZZLY_COMMIT_SHA: ${{ github.event.pull_request.head.sha || github.event.head_commit.id }}
|
|
283
295
|
VIZZLY_BRANCH: ${{ github.head_ref || github.ref_name }}
|
|
284
296
|
```
|
|
285
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
|
+
|
|
286
329
|
### GitLab CI
|
|
287
330
|
```yaml
|
|
288
331
|
visual-tests:
|
|
@@ -328,6 +371,9 @@ Check if Vizzly is enabled in the current environment.
|
|
|
328
371
|
- `VIZZLY_API_URL`: Override API base URL. Default: `https://vizzly.dev`.
|
|
329
372
|
- `VIZZLY_LOG_LEVEL`: Logger level. One of `debug`, `info`, `warn`, `error`. Example: `export VIZZLY_LOG_LEVEL=debug`.
|
|
330
373
|
|
|
374
|
+
### Parallel Builds
|
|
375
|
+
- `VIZZLY_PARALLEL_ID`: Unique identifier for parallel test execution. Example: `export VIZZLY_PARALLEL_ID=ci-run-123`.
|
|
376
|
+
|
|
331
377
|
### Git Information Override
|
|
332
378
|
For enhanced CI/CD integration, you can override git detection with these environment variables:
|
|
333
379
|
|
|
@@ -339,8 +385,8 @@ For enhanced CI/CD integration, you can override git detection with these enviro
|
|
|
339
385
|
**Example for GitHub Actions:**
|
|
340
386
|
```yaml
|
|
341
387
|
env:
|
|
342
|
-
VIZZLY_COMMIT_MESSAGE: ${{ github.event.head_commit.message }}
|
|
343
|
-
VIZZLY_COMMIT_SHA: ${{ github.event.head_commit.id }}
|
|
388
|
+
VIZZLY_COMMIT_MESSAGE: ${{ github.event.pull_request.head.commit.message || github.event.head_commit.message }}
|
|
389
|
+
VIZZLY_COMMIT_SHA: ${{ github.event.pull_request.head.sha || github.event.head_commit.id }}
|
|
344
390
|
VIZZLY_BRANCH: ${{ github.head_ref || github.ref_name }}
|
|
345
391
|
VIZZLY_PR_NUMBER: ${{ github.event.pull_request.number }}
|
|
346
392
|
```
|
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
|
@@ -17,15 +17,28 @@ export async function runCommand(testCommand, options = {}, globalOptions = {})
|
|
|
17
17
|
color: !globalOptions.noColor
|
|
18
18
|
});
|
|
19
19
|
let testRunner = null;
|
|
20
|
-
let
|
|
20
|
+
let buildId = null;
|
|
21
|
+
let startTime = null;
|
|
22
|
+
let isTddMode = false;
|
|
21
23
|
|
|
22
24
|
// Ensure cleanup on exit
|
|
23
25
|
const cleanup = async () => {
|
|
24
26
|
ui.cleanup();
|
|
25
|
-
|
|
27
|
+
|
|
28
|
+
// Cancel test runner (kills process and stops server)
|
|
29
|
+
if (testRunner) {
|
|
26
30
|
try {
|
|
27
|
-
|
|
28
|
-
|
|
31
|
+
await testRunner.cancel();
|
|
32
|
+
} catch {
|
|
33
|
+
// Silent fail
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Finalize build if we have one
|
|
38
|
+
if (testRunner && buildId) {
|
|
39
|
+
try {
|
|
40
|
+
const executionTime = Date.now() - (startTime || Date.now());
|
|
41
|
+
await testRunner.finalizeBuild(buildId, isTddMode, false, executionTime);
|
|
29
42
|
} catch {
|
|
30
43
|
// Silent fail on cleanup
|
|
31
44
|
}
|
|
@@ -113,6 +126,7 @@ export async function runCommand(testCommand, options = {}, globalOptions = {})
|
|
|
113
126
|
});
|
|
114
127
|
testRunner.on('build-created', buildInfo => {
|
|
115
128
|
buildUrl = buildInfo.url;
|
|
129
|
+
buildId = buildInfo.buildId;
|
|
116
130
|
// Debug: Log build creation details
|
|
117
131
|
if (globalOptions.verbose) {
|
|
118
132
|
ui.info(`Build created: ${buildInfo.buildId} - ${buildInfo.name}`);
|
|
@@ -148,26 +162,52 @@ export async function runCommand(testCommand, options = {}, globalOptions = {})
|
|
|
148
162
|
allowNoToken: config.allowNoToken || false,
|
|
149
163
|
wait: config.wait || options.wait || false,
|
|
150
164
|
uploadAll: options.uploadAll || false,
|
|
151
|
-
pullRequestNumber
|
|
165
|
+
pullRequestNumber,
|
|
166
|
+
parallelId: config.parallelId
|
|
152
167
|
};
|
|
153
168
|
|
|
154
169
|
// Start test run
|
|
155
170
|
ui.info('Starting test execution...');
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
...runResult,
|
|
162
|
-
...result
|
|
163
|
-
};
|
|
164
|
-
ui.success('Test run completed successfully');
|
|
171
|
+
startTime = Date.now();
|
|
172
|
+
isTddMode = runOptions.tdd || false;
|
|
173
|
+
let result;
|
|
174
|
+
try {
|
|
175
|
+
result = await testRunner.run(runOptions);
|
|
165
176
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
177
|
+
// Store buildId for cleanup purposes
|
|
178
|
+
if (result.buildId) {
|
|
179
|
+
buildId = result.buildId;
|
|
180
|
+
}
|
|
181
|
+
ui.success('Test run completed successfully');
|
|
182
|
+
|
|
183
|
+
// Show Vizzly summary
|
|
184
|
+
if (result.buildId) {
|
|
185
|
+
console.log(`🐻 Vizzly: Captured ${result.screenshotsCaptured} screenshots in build ${result.buildId}`);
|
|
186
|
+
if (result.url) {
|
|
187
|
+
console.log(`🔗 Vizzly: View results at ${result.url}`);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
} catch (error) {
|
|
191
|
+
// Test execution failed - build should already be finalized by test runner
|
|
192
|
+
ui.stopSpinner();
|
|
193
|
+
|
|
194
|
+
// Check if it's a test command failure (as opposed to setup failure)
|
|
195
|
+
if (error.code === 'TEST_COMMAND_FAILED' || error.code === 'TEST_COMMAND_INTERRUPTED') {
|
|
196
|
+
// Extract exit code from error message if available
|
|
197
|
+
const exitCodeMatch = error.message.match(/exited with code (\d+)/);
|
|
198
|
+
const exitCode = exitCodeMatch ? parseInt(exitCodeMatch[1], 10) : 1;
|
|
199
|
+
ui.error('Test run failed');
|
|
200
|
+
return {
|
|
201
|
+
success: false,
|
|
202
|
+
exitCode
|
|
203
|
+
};
|
|
204
|
+
} else {
|
|
205
|
+
// Setup or other error
|
|
206
|
+
ui.error('Test run failed', error);
|
|
207
|
+
return {
|
|
208
|
+
success: false,
|
|
209
|
+
exitCode: 1
|
|
210
|
+
};
|
|
171
211
|
}
|
|
172
212
|
}
|
|
173
213
|
|
package/dist/commands/tdd.js
CHANGED
|
@@ -11,7 +11,6 @@ import { detectBranch, detectCommit } from '../utils/git.js';
|
|
|
11
11
|
* @returns {Promise<{result: Object, cleanup: Function}>} Result and cleanup function
|
|
12
12
|
*/
|
|
13
13
|
export async function tddCommand(testCommand, options = {}, globalOptions = {}) {
|
|
14
|
-
// Create UI handler
|
|
15
14
|
const ui = new ConsoleUI({
|
|
16
15
|
json: globalOptions.json,
|
|
17
16
|
verbose: globalOptions.verbose,
|
|
@@ -43,11 +42,6 @@ export async function tddCommand(testCommand, options = {}, globalOptions = {})
|
|
|
43
42
|
ui.warning('No API token detected - running in local-only mode');
|
|
44
43
|
}
|
|
45
44
|
|
|
46
|
-
// Handle --set-baseline flag
|
|
47
|
-
if (options.setBaseline) {
|
|
48
|
-
ui.info('🐻 Baseline update mode - current screenshots will become new baselines');
|
|
49
|
-
}
|
|
50
|
-
|
|
51
45
|
// Collect git metadata
|
|
52
46
|
const branch = await detectBranch(options.branch);
|
|
53
47
|
const commit = await detectCommit(options.commit);
|
|
@@ -117,19 +111,20 @@ export async function tddCommand(testCommand, options = {}, globalOptions = {})
|
|
|
117
111
|
});
|
|
118
112
|
|
|
119
113
|
// Show informational messages about baseline behavior
|
|
120
|
-
if (
|
|
121
|
-
ui.info('
|
|
114
|
+
if (options.setBaseline) {
|
|
115
|
+
ui.info('🐻 Baseline update mode - will ignore existing baselines and create new ones');
|
|
116
|
+
} else if (config.baselineBuildId || config.baselineComparisonId) {
|
|
117
|
+
ui.info('API token available - will fetch remote baselines for local comparison');
|
|
118
|
+
} else if (config.apiKey) {
|
|
119
|
+
ui.info('API token available - will use existing local baselines or create new ones');
|
|
122
120
|
} else {
|
|
123
121
|
ui.warning('Running without API token - all screenshots will be marked as new');
|
|
124
122
|
}
|
|
125
|
-
|
|
126
|
-
// Prepare TDD run options (no uploads, local comparisons only)
|
|
127
123
|
const runOptions = {
|
|
128
124
|
testCommand,
|
|
129
125
|
port: config.server.port,
|
|
130
126
|
timeout: config.server.timeout,
|
|
131
127
|
tdd: true,
|
|
132
|
-
// Enable TDD mode
|
|
133
128
|
setBaseline: options.setBaseline || false,
|
|
134
129
|
// Pass through baseline update mode
|
|
135
130
|
branch,
|
|
@@ -142,8 +137,6 @@ export async function tddCommand(testCommand, options = {}, globalOptions = {})
|
|
|
142
137
|
baselineComparisonId: config.baselineComparisonId,
|
|
143
138
|
wait: false // No build to wait for in TDD mode
|
|
144
139
|
};
|
|
145
|
-
|
|
146
|
-
// Start TDD test run (local comparisons only)
|
|
147
140
|
ui.info('Starting TDD test execution...');
|
|
148
141
|
const result = await testRunner.run(runOptions);
|
|
149
142
|
|
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,
|
|
@@ -2,17 +2,32 @@ import { Buffer } from 'buffer';
|
|
|
2
2
|
import { createServiceLogger } from '../../utils/logger-factory.js';
|
|
3
3
|
import { TddService } from '../../services/tdd-service.js';
|
|
4
4
|
import { colors } from '../../utils/colors.js';
|
|
5
|
+
import { sanitizeScreenshotName, validateScreenshotProperties } from '../../utils/security.js';
|
|
5
6
|
const logger = createServiceLogger('TDD-HANDLER');
|
|
6
|
-
export const createTddHandler = (config, workingDir, baselineBuild, baselineComparison) => {
|
|
7
|
-
const tddService = new TddService(config, workingDir);
|
|
7
|
+
export const createTddHandler = (config, workingDir, baselineBuild, baselineComparison, setBaseline = false) => {
|
|
8
|
+
const tddService = new TddService(config, workingDir, setBaseline);
|
|
8
9
|
const builds = new Map();
|
|
9
10
|
const initialize = async () => {
|
|
10
11
|
logger.info('🔄 TDD mode enabled - setting up local comparison...');
|
|
12
|
+
|
|
13
|
+
// In baseline update mode, skip all baseline loading/downloading
|
|
14
|
+
if (setBaseline) {
|
|
15
|
+
logger.info('📁 Ready for new baseline creation - all screenshots will be treated as new baselines');
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Check if we have baseline override flags that should force a fresh download
|
|
20
|
+
const shouldForceDownload = (baselineBuild || baselineComparison) && config.apiKey;
|
|
21
|
+
if (shouldForceDownload) {
|
|
22
|
+
logger.info('📥 Baseline override specified, downloading fresh baselines from Vizzly...');
|
|
23
|
+
await tddService.downloadBaselines(config.build?.environment || 'test', config.build?.branch || null, baselineBuild, baselineComparison);
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
11
26
|
const baseline = await tddService.loadBaseline();
|
|
12
27
|
if (!baseline) {
|
|
13
28
|
if (config.apiKey) {
|
|
14
29
|
logger.info('📥 No local baseline found, downloading from Vizzly...');
|
|
15
|
-
await tddService.downloadBaselines(baselineBuild, baselineComparison);
|
|
30
|
+
await tddService.downloadBaselines(config.build?.environment || 'test', config.build?.branch || null, baselineBuild, baselineComparison);
|
|
16
31
|
} else {
|
|
17
32
|
logger.info('📝 No local baseline found and no API token - all screenshots will be marked as new');
|
|
18
33
|
}
|
|
@@ -36,15 +51,72 @@ export const createTddHandler = (config, workingDir, baselineBuild, baselineComp
|
|
|
36
51
|
if (!build) {
|
|
37
52
|
throw new Error(`Build ${buildId} not found`);
|
|
38
53
|
}
|
|
54
|
+
|
|
55
|
+
// Validate and sanitize screenshot name
|
|
56
|
+
let sanitizedName;
|
|
57
|
+
try {
|
|
58
|
+
sanitizedName = sanitizeScreenshotName(name);
|
|
59
|
+
} catch (error) {
|
|
60
|
+
return {
|
|
61
|
+
statusCode: 400,
|
|
62
|
+
body: {
|
|
63
|
+
error: 'Invalid screenshot name',
|
|
64
|
+
details: error.message,
|
|
65
|
+
tddMode: true
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Validate and sanitize properties
|
|
71
|
+
let validatedProperties;
|
|
72
|
+
try {
|
|
73
|
+
validatedProperties = validateScreenshotProperties(properties);
|
|
74
|
+
} catch (error) {
|
|
75
|
+
return {
|
|
76
|
+
statusCode: 400,
|
|
77
|
+
body: {
|
|
78
|
+
error: 'Invalid screenshot properties',
|
|
79
|
+
details: error.message,
|
|
80
|
+
tddMode: true
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Create unique screenshot name based on properties
|
|
86
|
+
let uniqueName = sanitizedName;
|
|
87
|
+
const relevantProps = [];
|
|
88
|
+
|
|
89
|
+
// Add browser to name if provided (already validated)
|
|
90
|
+
if (validatedProperties.browser) {
|
|
91
|
+
relevantProps.push(validatedProperties.browser);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Add viewport info if provided (already validated)
|
|
95
|
+
if (validatedProperties.viewport && validatedProperties.viewport.width && validatedProperties.viewport.height) {
|
|
96
|
+
relevantProps.push(`${validatedProperties.viewport.width}x${validatedProperties.viewport.height}`);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Combine base name with relevant properties and sanitize the result
|
|
100
|
+
if (relevantProps.length > 0) {
|
|
101
|
+
let proposedUniqueName = `${sanitizedName}-${relevantProps.join('-')}`;
|
|
102
|
+
try {
|
|
103
|
+
uniqueName = sanitizeScreenshotName(proposedUniqueName);
|
|
104
|
+
} catch (error) {
|
|
105
|
+
// If the combined name is invalid, fall back to the base sanitized name
|
|
106
|
+
uniqueName = sanitizedName;
|
|
107
|
+
logger.warn(`Combined screenshot name invalid (${error.message}), using base name: ${uniqueName}`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
39
110
|
const screenshot = {
|
|
40
|
-
name,
|
|
111
|
+
name: uniqueName,
|
|
112
|
+
originalName: name,
|
|
41
113
|
imageData: image,
|
|
42
|
-
properties,
|
|
114
|
+
properties: validatedProperties,
|
|
43
115
|
timestamp: Date.now()
|
|
44
116
|
};
|
|
45
117
|
build.screenshots.push(screenshot);
|
|
46
118
|
const imageBuffer = Buffer.from(image, 'base64');
|
|
47
|
-
const comparison = await tddService.compareScreenshot(
|
|
119
|
+
const comparison = await tddService.compareScreenshot(uniqueName, imageBuffer, validatedProperties);
|
|
48
120
|
if (comparison.status === 'failed') {
|
|
49
121
|
return {
|
|
50
122
|
statusCode: 422,
|
|
@@ -56,7 +128,9 @@ export const createTddHandler = (config, workingDir, baselineBuild, baselineComp
|
|
|
56
128
|
status: comparison.status,
|
|
57
129
|
baseline: comparison.baseline,
|
|
58
130
|
current: comparison.current,
|
|
59
|
-
diff: comparison.diff
|
|
131
|
+
diff: comparison.diff,
|
|
132
|
+
diffPercentage: comparison.diffPercentage,
|
|
133
|
+
threshold: comparison.threshold
|
|
60
134
|
},
|
|
61
135
|
tddMode: true
|
|
62
136
|
}
|
|
@@ -112,7 +186,7 @@ export const createTddHandler = (config, workingDir, baselineBuild, baselineComp
|
|
|
112
186
|
if (build.screenshots.length === 0) {
|
|
113
187
|
throw new Error('No screenshots to process. Make sure your tests are calling the Vizzly screenshot function.');
|
|
114
188
|
}
|
|
115
|
-
const results = tddService.printResults();
|
|
189
|
+
const results = await tddService.printResults();
|
|
116
190
|
builds.delete(buildId);
|
|
117
191
|
return {
|
|
118
192
|
id: buildId,
|
|
@@ -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
|
}
|