@vizzly-testing/cli 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -123,13 +123,19 @@ For local visual testing with immediate feedback, use the dedicated `tdd` comman
123
123
  # First run - creates local baselines
124
124
  vizzly tdd "npm test"
125
125
 
126
- # Make changes and test - fails if visual differences detected
126
+ # Make changes and test - fails if visual differences detected
127
127
  vizzly tdd "npm test"
128
128
 
129
129
  # Accept changes as new baseline
130
130
  vizzly tdd "npm test" --set-baseline
131
131
  ```
132
132
 
133
+ **Interactive HTML Report:** Each TDD run generates a detailed HTML report with visual comparison tools:
134
+ - **Overlay mode** - Toggle between baseline and current screenshots
135
+ - **Side-by-side mode** - Compare baseline and current images horizontally
136
+ - **Onion skin mode** - Drag to reveal differences interactively
137
+ - **Toggle mode** - Click to switch between baseline and current
138
+
133
139
  **TDD Command Options:**
134
140
  - `--set-baseline` - Accept current screenshots as new baseline
135
141
  - `--baseline-build <id>` - Use specific build as baseline (requires API token)
@@ -203,25 +209,25 @@ export default {
203
209
  // API configuration
204
210
  // Set VIZZLY_TOKEN environment variable or uncomment and set here:
205
211
  // apiToken: 'your-token-here',
206
-
212
+
207
213
  // Screenshot configuration
208
214
  screenshots: {
209
215
  directory: './screenshots',
210
216
  formats: ['png']
211
217
  },
212
-
218
+
213
219
  // Server configuration
214
220
  server: {
215
221
  port: 47392,
216
222
  screenshotPath: '/screenshot'
217
223
  },
218
-
224
+
219
225
  // Comparison configuration
220
226
  comparison: {
221
227
  threshold: 0.1,
222
228
  ignoreAntialiasing: true
223
229
  },
224
-
230
+
225
231
  // Upload configuration
226
232
  upload: {
227
233
  concurrency: 5,
@@ -278,8 +284,8 @@ For CI/CD pipelines, use the `--wait` flag to wait for visual comparison results
278
284
  env:
279
285
  VIZZLY_TOKEN: ${{ secrets.VIZZLY_TOKEN }}
280
286
  # 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 }}
287
+ VIZZLY_COMMIT_MESSAGE: ${{ github.event.pull_request.head.commit.message || github.event.head_commit.message }}
288
+ VIZZLY_COMMIT_SHA: ${{ github.event.pull_request.head.sha || github.event.head_commit.id }}
283
289
  VIZZLY_BRANCH: ${{ github.head_ref || github.ref_name }}
284
290
  ```
285
291
 
@@ -339,8 +345,8 @@ For enhanced CI/CD integration, you can override git detection with these enviro
339
345
  **Example for GitHub Actions:**
340
346
  ```yaml
341
347
  env:
342
- VIZZLY_COMMIT_MESSAGE: ${{ github.event.head_commit.message }}
343
- VIZZLY_COMMIT_SHA: ${{ github.event.head_commit.id }}
348
+ VIZZLY_COMMIT_MESSAGE: ${{ github.event.pull_request.head.commit.message || github.event.head_commit.message }}
349
+ VIZZLY_COMMIT_SHA: ${{ github.event.pull_request.head.sha || github.event.head_commit.id }}
344
350
  VIZZLY_BRANCH: ${{ github.head_ref || github.ref_name }}
345
351
  VIZZLY_PR_NUMBER: ${{ github.event.pull_request.number }}
346
352
  ```
@@ -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 runResult = null;
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
- if (testRunner && runResult && runResult.buildId) {
27
+
28
+ // Cancel test runner (kills process and stops server)
29
+ if (testRunner) {
26
30
  try {
27
- // Try to finalize build on interruption
28
- await testRunner.finalizeBuild(runResult.buildId, false, false, Date.now() - (runResult.startTime || Date.now()));
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}`);
@@ -153,21 +167,46 @@ export async function runCommand(testCommand, options = {}, globalOptions = {})
153
167
 
154
168
  // Start test run
155
169
  ui.info('Starting test execution...');
156
- runResult = {
157
- startTime: Date.now()
158
- };
159
- const result = await testRunner.run(runOptions);
160
- runResult = {
161
- ...runResult,
162
- ...result
163
- };
164
- ui.success('Test run completed successfully');
170
+ startTime = Date.now();
171
+ isTddMode = runOptions.tdd || false;
172
+ let result;
173
+ try {
174
+ result = await testRunner.run(runOptions);
165
175
 
166
- // Show Vizzly summary
167
- if (result.buildId) {
168
- console.log(`🐻 Vizzly: Captured ${result.screenshotsCaptured} screenshots in build ${result.buildId}`);
169
- if (result.url) {
170
- console.log(`🔗 Vizzly: View results at ${result.url}`);
176
+ // Store buildId for cleanup purposes
177
+ if (result.buildId) {
178
+ buildId = result.buildId;
179
+ }
180
+ ui.success('Test run completed successfully');
181
+
182
+ // Show Vizzly summary
183
+ if (result.buildId) {
184
+ console.log(`🐻 Vizzly: Captured ${result.screenshotsCaptured} screenshots in build ${result.buildId}`);
185
+ if (result.url) {
186
+ console.log(`🔗 Vizzly: View results at ${result.url}`);
187
+ }
188
+ }
189
+ } catch (error) {
190
+ // Test execution failed - build should already be finalized by test runner
191
+ ui.stopSpinner();
192
+
193
+ // Check if it's a test command failure (as opposed to setup failure)
194
+ if (error.code === 'TEST_COMMAND_FAILED' || error.code === 'TEST_COMMAND_INTERRUPTED') {
195
+ // Extract exit code from error message if available
196
+ const exitCodeMatch = error.message.match(/exited with code (\d+)/);
197
+ const exitCode = exitCodeMatch ? parseInt(exitCodeMatch[1], 10) : 1;
198
+ ui.error('Test run failed');
199
+ return {
200
+ success: false,
201
+ exitCode
202
+ };
203
+ } else {
204
+ // Setup or other error
205
+ ui.error('Test run failed', error);
206
+ return {
207
+ success: false,
208
+ exitCode: 1
209
+ };
171
210
  }
172
211
  }
173
212
 
@@ -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 (config.apiKey) {
121
- ui.info('API token available - will fetch baselines for local comparison');
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
 
@@ -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(name, imageBuffer, properties);
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,