@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.
Files changed (33) hide show
  1. package/README.md +55 -9
  2. package/dist/cli.js +15 -2
  3. package/dist/commands/finalize.js +72 -0
  4. package/dist/commands/run.js +59 -19
  5. package/dist/commands/tdd.js +6 -13
  6. package/dist/commands/upload.js +1 -0
  7. package/dist/server/handlers/tdd-handler.js +82 -8
  8. package/dist/services/api-service.js +14 -0
  9. package/dist/services/html-report-generator.js +377 -0
  10. package/dist/services/report-generator/report.css +355 -0
  11. package/dist/services/report-generator/viewer.js +100 -0
  12. package/dist/services/server-manager.js +3 -2
  13. package/dist/services/tdd-service.js +436 -66
  14. package/dist/services/test-runner.js +56 -28
  15. package/dist/services/uploader.js +3 -2
  16. package/dist/types/commands/finalize.d.ts +13 -0
  17. package/dist/types/server/handlers/tdd-handler.d.ts +18 -1
  18. package/dist/types/services/api-service.d.ts +6 -0
  19. package/dist/types/services/html-report-generator.d.ts +52 -0
  20. package/dist/types/services/report-generator/viewer.d.ts +0 -0
  21. package/dist/types/services/server-manager.d.ts +19 -1
  22. package/dist/types/services/tdd-service.d.ts +24 -3
  23. package/dist/types/services/uploader.d.ts +2 -1
  24. package/dist/types/utils/config-loader.d.ts +3 -0
  25. package/dist/types/utils/environment-config.d.ts +5 -0
  26. package/dist/types/utils/security.d.ts +29 -0
  27. package/dist/utils/config-loader.js +11 -1
  28. package/dist/utils/environment-config.js +9 -0
  29. package/dist/utils/security.js +154 -0
  30. package/docs/api-reference.md +27 -0
  31. package/docs/tdd-mode.md +58 -12
  32. package/docs/test-integration.md +69 -0
  33. package/package.json +3 -2
@@ -0,0 +1,154 @@
1
+ /**
2
+ * Security utilities for path sanitization and validation
3
+ * Protects against path traversal attacks and ensures safe file operations
4
+ */
5
+
6
+ import { resolve, normalize, isAbsolute, join } from 'path';
7
+ import { createServiceLogger } from './logger-factory.js';
8
+ const logger = createServiceLogger('SECURITY');
9
+
10
+ /**
11
+ * Sanitizes a screenshot name to prevent path traversal and ensure safe file naming
12
+ * @param {string} name - Original screenshot name
13
+ * @param {number} maxLength - Maximum allowed length (default: 255)
14
+ * @returns {string} Sanitized screenshot name
15
+ */
16
+ export function sanitizeScreenshotName(name, maxLength = 255) {
17
+ if (typeof name !== 'string' || name.length === 0) {
18
+ throw new Error('Screenshot name must be a non-empty string');
19
+ }
20
+ if (name.length > maxLength) {
21
+ throw new Error(`Screenshot name exceeds maximum length of ${maxLength} characters`);
22
+ }
23
+
24
+ // Block directory traversal patterns
25
+ if (name.includes('..') || name.includes('/') || name.includes('\\')) {
26
+ throw new Error('Screenshot name contains invalid path characters');
27
+ }
28
+
29
+ // Block absolute paths
30
+ if (isAbsolute(name)) {
31
+ throw new Error('Screenshot name cannot be an absolute path');
32
+ }
33
+
34
+ // Allow only safe characters: alphanumeric, hyphens, underscores, and dots
35
+ // Replace other characters with underscores
36
+ let sanitized = name.replace(/[^a-zA-Z0-9._-]/g, '_');
37
+
38
+ // Prevent names that start with dots (hidden files)
39
+ if (sanitized.startsWith('.')) {
40
+ sanitized = 'file_' + sanitized;
41
+ }
42
+
43
+ // Ensure we have a valid filename
44
+ if (sanitized.length === 0 || sanitized === '.' || sanitized === '..') {
45
+ sanitized = 'unnamed_screenshot';
46
+ }
47
+ return sanitized;
48
+ }
49
+
50
+ /**
51
+ * Validates that a path stays within the allowed working directory bounds
52
+ * @param {string} targetPath - Path to validate
53
+ * @param {string} workingDir - Working directory that serves as the security boundary
54
+ * @returns {string} Resolved and normalized path if valid
55
+ * @throws {Error} If path is invalid or outside bounds
56
+ */
57
+ export function validatePathSecurity(targetPath, workingDir) {
58
+ if (typeof targetPath !== 'string' || targetPath.length === 0) {
59
+ throw new Error('Path must be a non-empty string');
60
+ }
61
+ if (typeof workingDir !== 'string' || workingDir.length === 0) {
62
+ throw new Error('Working directory must be a non-empty string');
63
+ }
64
+
65
+ // Normalize and resolve both paths
66
+ let resolvedWorkingDir = resolve(normalize(workingDir));
67
+ let resolvedTargetPath = resolve(normalize(targetPath));
68
+
69
+ // Ensure the target path starts with the working directory
70
+ if (!resolvedTargetPath.startsWith(resolvedWorkingDir)) {
71
+ logger.warn(`Path traversal attempt blocked: ${targetPath} (resolved: ${resolvedTargetPath}) is outside working directory: ${resolvedWorkingDir}`);
72
+ throw new Error('Path is outside the allowed working directory');
73
+ }
74
+ return resolvedTargetPath;
75
+ }
76
+
77
+ /**
78
+ * Safely constructs a path within the working directory
79
+ * @param {string} workingDir - Base working directory
80
+ * @param {...string} pathSegments - Path segments to join
81
+ * @returns {string} Safely constructed path
82
+ * @throws {Error} If resulting path would be outside working directory
83
+ */
84
+ export function safePath(workingDir, ...pathSegments) {
85
+ if (pathSegments.length === 0) {
86
+ return validatePathSecurity(workingDir, workingDir);
87
+ }
88
+
89
+ // Sanitize each path segment
90
+ let sanitizedSegments = pathSegments.map(segment => {
91
+ if (typeof segment !== 'string') {
92
+ throw new Error('Path segment must be a string');
93
+ }
94
+
95
+ // Block directory traversal in segments
96
+ if (segment.includes('..')) {
97
+ throw new Error('Path segment contains directory traversal sequence');
98
+ }
99
+ return segment;
100
+ });
101
+ let targetPath = join(workingDir, ...sanitizedSegments);
102
+ return validatePathSecurity(targetPath, workingDir);
103
+ }
104
+
105
+ /**
106
+ * Validates screenshot properties object for safe values
107
+ * @param {Object} properties - Properties to validate
108
+ * @returns {Object} Validated properties object
109
+ */
110
+ export function validateScreenshotProperties(properties = {}) {
111
+ if (properties === null || typeof properties !== 'object') {
112
+ return {};
113
+ }
114
+ let validated = {};
115
+
116
+ // Validate common properties with safe constraints
117
+ if (properties.browser && typeof properties.browser === 'string') {
118
+ try {
119
+ validated.browser = sanitizeScreenshotName(properties.browser, 50);
120
+ } catch (error) {
121
+ // Skip invalid browser names, don't include them
122
+ logger.warn(`Invalid browser name '${properties.browser}': ${error.message}`);
123
+ }
124
+ }
125
+ if (properties.viewport && typeof properties.viewport === 'object') {
126
+ let viewport = {};
127
+ if (typeof properties.viewport.width === 'number' && properties.viewport.width > 0 && properties.viewport.width <= 10000) {
128
+ viewport.width = Math.floor(properties.viewport.width);
129
+ }
130
+ if (typeof properties.viewport.height === 'number' && properties.viewport.height > 0 && properties.viewport.height <= 10000) {
131
+ viewport.height = Math.floor(properties.viewport.height);
132
+ }
133
+ if (Object.keys(viewport).length > 0) {
134
+ validated.viewport = viewport;
135
+ }
136
+ }
137
+
138
+ // Allow other safe string properties but sanitize them
139
+ for (let [key, value] of Object.entries(properties)) {
140
+ if (key === 'browser' || key === 'viewport') continue; // Already handled
141
+
142
+ if (typeof key === 'string' && key.length <= 50 && /^[a-zA-Z0-9_-]+$/.test(key)) {
143
+ if (typeof value === 'string' && value.length <= 200) {
144
+ // Store sanitized version of string values
145
+ validated[key] = value.replace(/[<>&"']/g, ''); // Basic HTML entity prevention
146
+ } else if (typeof value === 'number' && !isNaN(value) && isFinite(value)) {
147
+ validated[key] = value;
148
+ } else if (typeof value === 'boolean') {
149
+ validated[key] = value;
150
+ }
151
+ }
152
+ }
153
+ return validated;
154
+ }
@@ -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/tdd-mode.md CHANGED
@@ -8,7 +8,7 @@ TDD Mode transforms your visual testing workflow by:
8
8
 
9
9
  - **Local comparison** - Compares screenshots on your machine using `odiff`
10
10
  - **Fast feedback** - No network uploads during development
11
- - **Immediate results** - Tests fail instantly when visual differences are detected
11
+ - **Immediate results** - Tests fail instantly when visual differences are detected
12
12
  - **Auto-baseline creation** - Creates baselines locally when none exist
13
13
  - **No token required** - Works entirely offline for local development
14
14
 
@@ -43,7 +43,7 @@ npx vizzly tdd "npm test"
43
43
  🐻 **Comparison behavior:**
44
44
  - Compares new screenshots against local baselines
45
45
  - **Tests fail immediately** when visual differences detected
46
- - Shows exact diff paths and percentages
46
+ - Generates interactive HTML report for visual analysis
47
47
  - Creates diff images in `.vizzly/diffs/`
48
48
 
49
49
  ### 3. Accept Changes (Update Baseline)
@@ -56,7 +56,7 @@ npx vizzly tdd "npm test" --set-baseline
56
56
 
57
57
  🐻 **Baseline update behavior:**
58
58
  - Skips all comparisons
59
- - Sets current screenshots as new baselines
59
+ - Sets current screenshots as new baselines
60
60
  - All tests pass (baseline accepted)
61
61
  - Future runs use updated baselines
62
62
 
@@ -73,7 +73,7 @@ npx vizzly run "npm test" --wait
73
73
  TDD Mode creates a local development environment:
74
74
 
75
75
  1. **Downloads baselines** - Gets approved screenshots from Vizzly
76
- 2. **Runs tests** - Executes your test suite normally
76
+ 2. **Runs tests** - Executes your test suite normally
77
77
  3. **Captures screenshots** - Collects new screenshots via `vizzlyScreenshot()`
78
78
  4. **Compares locally** - Uses `odiff` for pixel-perfect comparison
79
79
  5. **Fails immediately** - Tests fail when differences exceed threshold
@@ -89,9 +89,11 @@ TDD Mode creates a `.vizzly/` directory:
89
89
  │ ├── homepage.png
90
90
  │ ├── dashboard.png
91
91
  │ └── metadata.json # Baseline build information
92
- ├── current/ # Current test screenshots
92
+ ├── current/ # Current test screenshots
93
93
  │ ├── homepage.png
94
94
  │ └── dashboard.png
95
+ ├── report/ # Interactive HTML report
96
+ │ └── index.html # Visual comparison interface
95
97
  └── diffs/ # Visual diff images (when differences found)
96
98
  ├── homepage.png # Red overlay showing differences
97
99
  └── dashboard.png
@@ -99,6 +101,50 @@ TDD Mode creates a `.vizzly/` directory:
99
101
 
100
102
  **Important**: Add `.vizzly/` to your `.gitignore` file as it contains local development artifacts.
101
103
 
104
+ ## Interactive HTML Report
105
+
106
+ Each TDD run automatically generates a comprehensive HTML report at `.vizzly/report/index.html`. This report provides advanced visual comparison tools to analyze differences:
107
+
108
+ ### 🐻 **Viewing Modes**
109
+
110
+ **Overlay Mode** (Default)
111
+ - Shows current screenshot as base layer
112
+ - Click to toggle diff overlay on/off
113
+ - Perfect for spotting subtle changes
114
+
115
+ **Side-by-Side Mode**
116
+ - Displays baseline and current screenshots horizontally
117
+ - Easy to compare layout and content changes
118
+ - Great for reviewing larger modifications
119
+
120
+ **Onion Skin Mode**
121
+ - Drag across image to reveal baseline underneath
122
+ - Interactive reveal lets you control comparison area
123
+ - Ideal for precise change inspection
124
+
125
+ **Toggle Mode**
126
+ - Click image to switch between baseline and current
127
+ - Quick back-and-forth comparison
128
+ - Simple way to see before/after
129
+
130
+ ### 🐻 **Report Features**
131
+
132
+ - **Dark Theme** - Easy on the eyes during long debugging sessions
133
+ - **Mobile Responsive** - Works on any screen size
134
+ - **Clickable File Paths** - Click from terminal to open instantly
135
+ - **Clean Status Display** - Shows "Visual differences detected" instead of technical metrics
136
+ - **Test Summary** - Total, passed, failed counts and pass rate
137
+
138
+ ### 🐻 **Opening the Report**
139
+
140
+ ```bash
141
+ # Report path is shown after each run
142
+ 🐻 View detailed report: file:///path/to/.vizzly/report/index.html
143
+
144
+ # Click the link in your terminal, or open manually
145
+ open .vizzly/report/index.html # macOS
146
+ ```
147
+
102
148
  ## Command Options
103
149
 
104
150
  ### Basic TDD Mode
@@ -122,7 +168,7 @@ vizzly tdd "npm test" --set-baseline
122
168
  VIZZLY_TOKEN=your-token vizzly tdd "npm test" --baseline-build build-abc123
123
169
  ```
124
170
 
125
- **`--baseline-comparison <id>`** - Use specific comparison as baseline
171
+ **`--baseline-comparison <id>`** - Use specific comparison as baseline
126
172
  ```bash
127
173
  VIZZLY_TOKEN=your-token vizzly tdd "npm test" --baseline-comparison comparison-xyz789
128
174
  ```
@@ -272,14 +318,14 @@ jobs:
272
318
  - uses: actions/checkout@v4
273
319
  - uses: actions/setup-node@v4
274
320
  - run: npm ci
275
-
321
+
276
322
  # Use TDD mode for PR builds (faster, no uploads)
277
323
  - name: TDD Visual Tests (PR)
278
324
  if: github.event_name == 'pull_request'
279
325
  run: npx vizzly tdd "npm test"
280
326
  env:
281
327
  VIZZLY_TOKEN: ${{ secrets.VIZZLY_TOKEN }}
282
-
328
+
283
329
  # Upload full build for main branch
284
330
  - name: Full Visual Tests (main)
285
331
  if: github.ref == 'refs/heads/main'
@@ -295,7 +341,7 @@ jobs:
295
341
  - **Immediate feedback** - See results in seconds
296
342
  - **No API rate limits** - Test as often as needed
297
343
 
298
- ### Development Experience
344
+ ### Development Experience
299
345
  - **Fast iteration** - Make changes and test immediately
300
346
  - **Visual debugging** - See exact pixel differences
301
347
  - **Offline capable** - Works without internet (after initial baseline download)
@@ -354,7 +400,7 @@ npm install odiff-bin
354
400
 
355
401
  ### Use TDD Mode For
356
402
  - **Local development** - Fast iteration on UI changes
357
- - **Bug fixing** - Verify visual fixes immediately
403
+ - **Bug fixing** - Verify visual fixes immediately
358
404
  - **PR validation** - Quick checks without uploading
359
405
  - **Debugging** - Understand exactly what changed visually
360
406
 
@@ -372,5 +418,5 @@ npm install odiff-bin
372
418
  ## Next Steps
373
419
 
374
420
  - Learn about [Test Integration](./test-integration.md) for screenshot capture
375
- - Explore [Upload Command](./upload-command.md) for direct uploads
376
- - Check the [API Reference](./api-reference.md) for programmatic usage
421
+ - Explore [Upload Command](./upload-command.md) for direct uploads
422
+ - Check the [API Reference](./api-reference.md) for programmatic usage
@@ -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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vizzly-testing/cli",
3
- "version": "0.5.0",
3
+ "version": "0.7.0",
4
4
  "description": "Visual review platform for UI developers and designers",
5
5
  "keywords": [
6
6
  "visual-testing",
@@ -53,9 +53,10 @@
53
53
  ],
54
54
  "scripts": {
55
55
  "start": "node src/index.js",
56
- "build": "npm run clean && npm run compile && npm run types",
56
+ "build": "npm run clean && npm run compile && npm run copy-assets && npm run types",
57
57
  "clean": "rimraf dist",
58
58
  "compile": "babel src --out-dir dist --ignore '**/*.test.js'",
59
+ "copy-assets": "cp src/services/report-generator/report.css dist/services/report-generator/",
59
60
  "types": "tsc --emitDeclarationOnly --outDir dist/types",
60
61
  "prepublishOnly": "npm test && npm run build",
61
62
  "test": "vitest run",