@vizzly-testing/cli 0.3.2 → 0.5.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 (60) hide show
  1. package/README.md +50 -28
  2. package/dist/cli.js +34 -30
  3. package/dist/client/index.js +1 -1
  4. package/dist/commands/run.js +38 -11
  5. package/dist/commands/tdd.js +30 -18
  6. package/dist/commands/upload.js +56 -5
  7. package/dist/server/handlers/api-handler.js +83 -0
  8. package/dist/server/handlers/tdd-handler.js +138 -0
  9. package/dist/server/http-server.js +132 -0
  10. package/dist/services/api-service.js +22 -2
  11. package/dist/services/server-manager.js +45 -29
  12. package/dist/services/test-runner.js +66 -69
  13. package/dist/services/uploader.js +11 -4
  14. package/dist/types/commands/run.d.ts +4 -1
  15. package/dist/types/commands/tdd.d.ts +5 -1
  16. package/dist/types/sdk/index.d.ts +6 -6
  17. package/dist/types/server/handlers/api-handler.d.ts +49 -0
  18. package/dist/types/server/handlers/tdd-handler.d.ts +85 -0
  19. package/dist/types/server/http-server.d.ts +5 -0
  20. package/dist/types/services/api-service.d.ts +1 -0
  21. package/dist/types/services/server-manager.d.ts +148 -3
  22. package/dist/types/services/test-runner.d.ts +1 -0
  23. package/dist/types/services/uploader.d.ts +2 -1
  24. package/dist/types/utils/ci-env.d.ts +55 -0
  25. package/dist/types/utils/config-helpers.d.ts +1 -1
  26. package/dist/types/utils/console-ui.d.ts +1 -1
  27. package/dist/types/utils/git.d.ts +12 -0
  28. package/dist/utils/ci-env.js +293 -0
  29. package/dist/utils/console-ui.js +4 -14
  30. package/dist/utils/git.js +38 -0
  31. package/docs/api-reference.md +17 -5
  32. package/docs/getting-started.md +1 -1
  33. package/docs/tdd-mode.md +9 -9
  34. package/docs/test-integration.md +9 -17
  35. package/docs/upload-command.md +7 -0
  36. package/package.json +4 -5
  37. package/dist/screenshot-wrapper.js +0 -68
  38. package/dist/server/index.js +0 -522
  39. package/dist/services/service-utils.js +0 -171
  40. package/dist/types/index.js +0 -153
  41. package/dist/types/screenshot-wrapper.d.ts +0 -27
  42. package/dist/types/server/index.d.ts +0 -38
  43. package/dist/types/services/service-utils.d.ts +0 -45
  44. package/dist/types/types/index.d.ts +0 -373
  45. package/dist/types/utils/diagnostics.d.ts +0 -69
  46. package/dist/types/utils/error-messages.d.ts +0 -42
  47. package/dist/types/utils/framework-detector.d.ts +0 -5
  48. package/dist/types/utils/help.d.ts +0 -11
  49. package/dist/types/utils/image-comparison.d.ts +0 -42
  50. package/dist/types/utils/package.d.ts +0 -1
  51. package/dist/types/utils/project-detection.d.ts +0 -19
  52. package/dist/types/utils/ui-helpers.d.ts +0 -23
  53. package/dist/utils/diagnostics.js +0 -184
  54. package/dist/utils/error-messages.js +0 -34
  55. package/dist/utils/framework-detector.js +0 -40
  56. package/dist/utils/help.js +0 -66
  57. package/dist/utils/image-comparison.js +0 -172
  58. package/dist/utils/package.js +0 -9
  59. package/dist/utils/project-detection.js +0 -145
  60. package/dist/utils/ui-helpers.js +0 -86
package/README.md CHANGED
@@ -55,7 +55,7 @@ vizzly upload ./screenshots --build-name "Release v1.2.3"
55
55
  vizzly run "npm test"
56
56
 
57
57
  # Use TDD mode for local development
58
- vizzly run "npm test" --tdd
58
+ vizzly tdd "npm test"
59
59
  ```
60
60
 
61
61
  ### In your test code
@@ -81,15 +81,14 @@ await vizzlyScreenshot('homepage', screenshot, {
81
81
  ```bash
82
82
  vizzly upload <directory> # Upload screenshots from directory
83
83
  vizzly upload ./screenshots --wait # Wait for processing
84
+ vizzly upload ./screenshots --upload-all # Upload all without deduplication
84
85
  ```
85
86
 
86
87
  ### Run Tests with Integration
87
88
  ```bash
88
89
  vizzly run "npm test" # Run with Vizzly integration
89
- vizzly run "npm test" --tdd # Local TDD mode
90
90
  vizzly run "pytest" --port 3002 # Custom port
91
91
  vizzly run "npm test" --wait # Wait for build completion
92
- vizzly run "npm test" --eager # Create build immediately
93
92
  vizzly run "npm test" --allow-no-token # Run without API token
94
93
  ```
95
94
 
@@ -108,19 +107,36 @@ vizzly run "npm test" --allow-no-token # Run without API token
108
107
 
109
108
  **Processing Options:**
110
109
  - `--wait` - Wait for build completion and exit with appropriate code
111
- - `--eager` - Create build immediately (default: lazy creation)
112
110
  - `--threshold <number>` - Comparison threshold (0-1, default: 0.01)
113
- - `--batch-size <n>` - Upload batch size used with `--wait`
114
111
  - `--upload-timeout <ms>` - Upload wait timeout in ms
112
+ - `--upload-all` - Upload all screenshots without SHA deduplication
115
113
 
116
114
  **Development & Testing:**
117
- - `--tdd` - Enable TDD mode with local comparisons
118
115
  - `--allow-no-token` - Allow running without API token (useful for local development)
119
116
  - `--token <token>` - API token override
120
117
 
121
- **Baseline Configuration:**
122
- - `--baseline-build <id>` - Use specific build as baseline for comparisons
123
- - `--baseline-comparison <id>` - Use specific comparison as baseline
118
+ ## TDD Command
119
+
120
+ For local visual testing with immediate feedback, use the dedicated `tdd` command:
121
+
122
+ ```bash
123
+ # First run - creates local baselines
124
+ vizzly tdd "npm test"
125
+
126
+ # Make changes and test - fails if visual differences detected
127
+ vizzly tdd "npm test"
128
+
129
+ # Accept changes as new baseline
130
+ vizzly tdd "npm test" --set-baseline
131
+ ```
132
+
133
+ **TDD Command Options:**
134
+ - `--set-baseline` - Accept current screenshots as new baseline
135
+ - `--baseline-build <id>` - Use specific build as baseline (requires API token)
136
+ - `--baseline-comparison <id>` - Use specific comparison as baseline (requires API token)
137
+ - `--threshold <number>` - Comparison threshold (0-1, default: 0.1)
138
+ - `--port <port>` - Server port (default: 47392)
139
+ - `--timeout <ms>` - Server timeout (default: 30000)
124
140
 
125
141
  ### Setup and Status Commands
126
142
  ```bash
@@ -176,25 +192,7 @@ VIZZLY_TOKEN=your-token vizzly doctor --api
176
192
  vizzly doctor --json
177
193
  ```
178
194
 
179
- ## TDD Mode
180
-
181
- TDD mode enables fast local development by comparing screenshots locally without uploading to Vizzly:
182
-
183
- ```bash
184
- # First run - creates local baselines (no token needed)
185
- npx vizzly tdd "npm test"
186
-
187
- # Make changes and test - fails if visual differences detected
188
- npx vizzly tdd "npm test"
189
-
190
- # Accept changes as new baseline
191
- npx vizzly tdd "npm test" --set-baseline
192
- ```
193
-
194
- - **🐻 Auto-baseline creation**: Creates baselines locally when none exist
195
- - **🐻 No token required**: Works entirely offline for local development
196
- - **🐻 Tests fail on differences**: Immediate feedback when visuals change
197
- - **🐻 Accept changes**: Use `--set-baseline` to update baselines
195
+ The dedicated `tdd` command provides fast local development with immediate visual feedback. See the [TDD Mode Guide](./docs/tdd-mode.md) for complete details on local visual testing.
198
196
 
199
197
  ## Configuration
200
198
 
@@ -279,6 +277,10 @@ For CI/CD pipelines, use the `--wait` flag to wait for visual comparison results
279
277
  run: npx vizzly run "npm test" --wait
280
278
  env:
281
279
  VIZZLY_TOKEN: ${{ secrets.VIZZLY_TOKEN }}
280
+ # 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 }}
283
+ VIZZLY_BRANCH: ${{ github.head_ref || github.ref_name }}
282
284
  ```
283
285
 
284
286
  ### GitLab CI
@@ -321,10 +323,30 @@ Check if Vizzly is enabled in the current environment.
321
323
 
322
324
  ## Environment Variables
323
325
 
326
+ ### Core Configuration
324
327
  - `VIZZLY_TOKEN`: API authentication token. Example: `export VIZZLY_TOKEN=your-token`.
325
328
  - `VIZZLY_API_URL`: Override API base URL. Default: `https://vizzly.dev`.
326
329
  - `VIZZLY_LOG_LEVEL`: Logger level. One of `debug`, `info`, `warn`, `error`. Example: `export VIZZLY_LOG_LEVEL=debug`.
327
330
 
331
+ ### Git Information Override
332
+ For enhanced CI/CD integration, you can override git detection with these environment variables:
333
+
334
+ - `VIZZLY_COMMIT_SHA`: Override detected commit SHA. Useful in CI environments.
335
+ - `VIZZLY_COMMIT_MESSAGE`: Override detected commit message. Useful in CI environments.
336
+ - `VIZZLY_BRANCH`: Override detected branch name. Useful in CI environments.
337
+ - `VIZZLY_PR_NUMBER`: Override detected pull request number. Useful for PR-specific builds.
338
+
339
+ **Example for GitHub Actions:**
340
+ ```yaml
341
+ env:
342
+ VIZZLY_COMMIT_MESSAGE: ${{ github.event.head_commit.message }}
343
+ VIZZLY_COMMIT_SHA: ${{ github.event.head_commit.id }}
344
+ VIZZLY_BRANCH: ${{ github.head_ref || github.ref_name }}
345
+ VIZZLY_PR_NUMBER: ${{ github.event.pull_request.number }}
346
+ ```
347
+
348
+ These variables take highest priority over both CLI arguments and automatic git detection.
349
+
328
350
  ## Contributing
329
351
 
330
352
  We welcome contributions! Whether you're fixing bugs, adding features, or improving documentation, your help makes Vizzly better for everyone.
package/dist/cli.js CHANGED
@@ -16,7 +16,7 @@ program.command('init').description('Initialize Vizzly in your project').option(
16
16
  ...options
17
17
  });
18
18
  });
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').action(async (path, options) => {
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
20
  const globalOptions = program.opts();
21
21
 
22
22
  // Validate options
@@ -38,36 +38,29 @@ program.command('tdd').description('Run tests in TDD mode with local visual comp
38
38
  validationErrors.forEach(error => console.error(` - ${error}`));
39
39
  process.exit(1);
40
40
  }
41
- await tddCommand(command, options, globalOptions);
42
- });
43
- program.command('run').description('Run tests with Vizzly integration').argument('<command>', 'Test command to run').option('--tdd', 'Enable TDD mode with auto-reload').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('--threshold <number>', 'Comparison threshold', parseFloat).option('--token <token>', 'API token override').option('--wait', 'Wait for build completion').option('--timeout <ms>', 'Server timeout in milliseconds', '30000').option('--eager', 'Create build immediately (default: lazy)').option('--allow-no-token', 'Allow running without API token').option('--baseline-build <id>', 'Use specific build as baseline').option('--baseline-comparison <id>', 'Use specific comparison as baseline').action(async (command, options) => {
44
- const globalOptions = program.opts();
45
-
46
- // Forward --tdd flag to TDD command (shortcut)
47
- if (options.tdd) {
48
- // Forward to tdd command with appropriate options
49
- const tddOptions = {
50
- port: options.port,
51
- branch: options.branch,
52
- environment: options.environment,
53
- threshold: options.threshold,
54
- token: options.token,
55
- timeout: options.timeout,
56
- baselineBuild: options.baselineBuild,
57
- baselineComparison: options.baselineComparison,
58
- allowNoToken: options.allowNoToken
59
- };
41
+ const {
42
+ result,
43
+ cleanup
44
+ } = await tddCommand(command, options, globalOptions);
60
45
 
61
- // Validate options using TDD validator
62
- const validationErrors = validateTddOptions(command, tddOptions);
63
- if (validationErrors.length > 0) {
64
- console.error('Validation errors:');
65
- validationErrors.forEach(error => console.error(` - ${error}`));
66
- process.exit(1);
67
- }
68
- await tddCommand(command, tddOptions, globalOptions);
69
- return;
46
+ // Set up cleanup on process signals
47
+ const handleCleanup = async () => {
48
+ await cleanup();
49
+ };
50
+ process.once('SIGINT', () => {
51
+ handleCleanup().then(() => process.exit(1));
52
+ });
53
+ process.once('SIGTERM', () => {
54
+ handleCleanup().then(() => process.exit(1));
55
+ });
56
+ if (result && !result.success && result.exitCode > 0) {
57
+ await cleanup();
58
+ process.exit(result.exitCode);
70
59
  }
60
+ await cleanup();
61
+ });
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
+ const globalOptions = program.opts();
71
64
 
72
65
  // Validate options
73
66
  const validationErrors = validateRunOptions(command, options);
@@ -76,7 +69,18 @@ program.command('run').description('Run tests with Vizzly integration').argument
76
69
  validationErrors.forEach(error => console.error(` - ${error}`));
77
70
  process.exit(1);
78
71
  }
79
- await runCommand(command, options, globalOptions);
72
+ try {
73
+ const result = await runCommand(command, options, globalOptions);
74
+ if (result && !result.success && result.exitCode > 0) {
75
+ process.exit(result.exitCode);
76
+ }
77
+ } catch (error) {
78
+ console.error('Command failed:', error.message);
79
+ if (globalOptions.verbose) {
80
+ console.error('Stack trace:', error.stack);
81
+ }
82
+ process.exit(1);
83
+ }
80
84
  });
81
85
  program.command('status').description('Check the status of a build').argument('<build-id>', 'Build ID to check status for').action(async (buildId, options) => {
82
86
  const globalOptions = program.opts();
@@ -66,7 +66,7 @@ function createSimpleClient(serverUrl) {
66
66
  buildId: getBuildId(),
67
67
  name,
68
68
  image: imageBuffer.toString('base64'),
69
- properties: options.properties || {},
69
+ properties: options,
70
70
  threshold: options.threshold || 0,
71
71
  variant: options.variant,
72
72
  fullPage: options.fullPage || false
@@ -1,7 +1,7 @@
1
1
  import { loadConfig } from '../utils/config-loader.js';
2
2
  import { ConsoleUI } from '../utils/console-ui.js';
3
3
  import { createServiceContainer } from '../container/index.js';
4
- import { detectBranch, detectCommit, getCommitMessage, generateBuildNameWithGit } from '../utils/git.js';
4
+ import { detectBranch, detectCommit, detectCommitMessage, detectPullRequestNumber, generateBuildNameWithGit } from '../utils/git.js';
5
5
 
6
6
  /**
7
7
  * Run command implementation
@@ -55,14 +55,14 @@ export async function runCommand(testCommand, options = {}, globalOptions = {})
55
55
  // Collect git metadata and build info
56
56
  const branch = await detectBranch(options.branch);
57
57
  const commit = await detectCommit(options.commit);
58
- const message = options.message || (await getCommitMessage());
58
+ const message = options.message || (await detectCommitMessage());
59
59
  const buildName = await generateBuildNameWithGit(options.buildName);
60
+ const pullRequestNumber = detectPullRequestNumber();
60
61
  if (globalOptions.verbose) {
61
62
  ui.info('Configuration loaded', {
62
63
  testCommand,
63
64
  port: config.server.port,
64
65
  timeout: config.server.timeout,
65
- tddMode: options.tdd || false,
66
66
  branch,
67
67
  commit: commit?.substring(0, 7),
68
68
  message,
@@ -76,9 +76,10 @@ export async function runCommand(testCommand, options = {}, globalOptions = {})
76
76
  ui.startSpinner('Initializing test runner...');
77
77
  const configWithVerbose = {
78
78
  ...config,
79
- verbose: globalOptions.verbose
79
+ verbose: globalOptions.verbose,
80
+ uploadAll: options.uploadAll || false
80
81
  };
81
- const command = options.tdd ? 'tdd' : 'run';
82
+ const command = 'run';
82
83
  const container = await createServiceContainer(configWithVerbose, command);
83
84
  testRunner = await container.get('testRunner'); // Assign to outer scope variable
84
85
  ui.stopSpinner();
@@ -112,21 +113,31 @@ export async function runCommand(testCommand, options = {}, globalOptions = {})
112
113
  });
113
114
  testRunner.on('build-created', buildInfo => {
114
115
  buildUrl = buildInfo.url;
116
+ // Debug: Log build creation details
117
+ if (globalOptions.verbose) {
118
+ ui.info(`Build created: ${buildInfo.buildId} - ${buildInfo.name}`);
119
+ }
115
120
  // Use UI for consistent formatting
116
121
  if (buildUrl) {
117
122
  ui.info(`Vizzly: ${buildUrl}`);
118
123
  }
119
124
  });
125
+ testRunner.on('build-failed', buildError => {
126
+ ui.error('Failed to create build', buildError);
127
+ });
120
128
  testRunner.on('error', error => {
129
+ ui.stopSpinner(); // Stop spinner to ensure error is visible
121
130
  ui.error('Test runner error occurred', error, 0); // Don't exit immediately, let runner handle it
122
131
  });
132
+ testRunner.on('build-finalize-failed', errorInfo => {
133
+ ui.warning(`Failed to finalize build ${errorInfo.buildId}: ${errorInfo.error}`);
134
+ });
123
135
 
124
136
  // Prepare run options
125
137
  const runOptions = {
126
138
  testCommand,
127
139
  port: config.server.port,
128
140
  timeout: config.server.timeout,
129
- tdd: options.tdd || false,
130
141
  buildName,
131
142
  branch,
132
143
  commit,
@@ -135,9 +146,9 @@ export async function runCommand(testCommand, options = {}, globalOptions = {})
135
146
  threshold: config.comparison.threshold,
136
147
  eager: config.eager || false,
137
148
  allowNoToken: config.allowNoToken || false,
138
- baselineBuildId: config.baselineBuildId,
139
- baselineComparisonId: config.baselineComparisonId,
140
- wait: config.wait || options.wait || false
149
+ wait: config.wait || options.wait || false,
150
+ uploadAll: options.uploadAll || false,
151
+ pullRequestNumber
141
152
  };
142
153
 
143
154
  // Start test run
@@ -172,13 +183,29 @@ export async function runCommand(testCommand, options = {}, globalOptions = {})
172
183
 
173
184
  // Exit with appropriate code based on comparison results
174
185
  if (buildResult.failedComparisons > 0) {
175
- ui.error(`${buildResult.failedComparisons} visual comparisons failed`, {}, 1);
186
+ ui.error(`${buildResult.failedComparisons} visual comparisons failed`, {}, 0);
187
+ // Return error status without calling process.exit in tests
188
+ return {
189
+ success: false,
190
+ exitCode: 1
191
+ };
176
192
  }
177
193
  }
178
194
  }
179
195
  ui.cleanup();
180
196
  } catch (error) {
181
- ui.error('Test run failed', error);
197
+ ui.stopSpinner(); // Ensure spinner is stopped before showing error
198
+
199
+ // Provide more context about where the error occurred
200
+ let errorContext = 'Test run failed';
201
+ if (error.message && error.message.includes('build')) {
202
+ errorContext = 'Build creation failed';
203
+ } else if (error.message && error.message.includes('screenshot')) {
204
+ errorContext = 'Screenshot processing failed';
205
+ } else if (error.message && error.message.includes('server')) {
206
+ errorContext = 'Server startup failed';
207
+ }
208
+ ui.error(errorContext, error);
182
209
  } finally {
183
210
  // Remove event listeners to prevent memory leaks
184
211
  process.removeListener('SIGINT', sigintHandler);
@@ -8,6 +8,7 @@ import { detectBranch, detectCommit } from '../utils/git.js';
8
8
  * @param {string} testCommand - Test command to execute
9
9
  * @param {Object} options - Command options
10
10
  * @param {Object} globalOptions - Global CLI options
11
+ * @returns {Promise<{result: Object, cleanup: Function}>} Result and cleanup function
11
12
  */
12
13
  export async function tddCommand(testCommand, options = {}, globalOptions = {}) {
13
14
  // Create UI handler
@@ -17,20 +18,17 @@ export async function tddCommand(testCommand, options = {}, globalOptions = {})
17
18
  color: !globalOptions.noColor
18
19
  });
19
20
  let testRunner = null;
21
+ let isCleanedUp = false;
20
22
 
21
- // Ensure cleanup on exit - store listeners for proper cleanup
23
+ // Create cleanup function that can be called by the caller
22
24
  const cleanup = async () => {
25
+ if (isCleanedUp) return;
26
+ isCleanedUp = true;
23
27
  ui.cleanup();
24
- // The test runner's finally block will handle server cleanup
25
- // We just need to ensure UI cleanup happens
26
- };
27
- const sigintHandler = async () => {
28
- await cleanup();
29
- process.exit(1);
28
+ if (testRunner?.cancel) {
29
+ await testRunner.cancel();
30
+ }
30
31
  };
31
- const exitHandler = () => ui.cleanup();
32
- process.on('SIGINT', sigintHandler);
33
- process.on('exit', exitHandler);
34
32
  try {
35
33
  // Load configuration with CLI overrides
36
34
  const allOptions = {
@@ -166,17 +164,31 @@ export async function tddCommand(testCommand, options = {}, globalOptions = {})
166
164
  }
167
165
  ui.success('TDD test run completed');
168
166
 
169
- // Exit with appropriate code based on comparison results
170
- if (result.failed || result.comparisons && result.comparisons.some(c => c.status === 'failed')) {
171
- ui.error('Visual differences detected in TDD mode', {}, 1);
167
+ // Determine success based on comparison results
168
+ const hasFailures = result.failed || result.comparisons && result.comparisons.some(c => c.status === 'failed');
169
+ if (hasFailures) {
170
+ ui.error('Visual differences detected in TDD mode', {}, 0);
172
171
  }
173
- ui.cleanup();
172
+
173
+ // Return result and cleanup function
174
+ return {
175
+ result: {
176
+ success: !hasFailures,
177
+ exitCode: hasFailures ? 1 : 0,
178
+ ...result
179
+ },
180
+ cleanup
181
+ };
174
182
  } catch (error) {
175
183
  ui.error('TDD test run failed', error);
176
- } finally {
177
- // Remove event listeners to prevent memory leaks
178
- process.removeListener('SIGINT', sigintHandler);
179
- process.removeListener('exit', exitHandler);
184
+ return {
185
+ result: {
186
+ success: false,
187
+ exitCode: 1,
188
+ error: error.message
189
+ },
190
+ cleanup
191
+ };
180
192
  }
181
193
  }
182
194
 
@@ -1,7 +1,7 @@
1
1
  import { loadConfig } from '../utils/config-loader.js';
2
2
  import { ConsoleUI } from '../utils/console-ui.js';
3
3
  import { createServiceContainer } from '../container/index.js';
4
- import { detectBranch, detectCommit, getCommitMessage, generateBuildNameWithGit } from '../utils/git.js';
4
+ import { detectBranch, detectCommit, detectCommitMessage, detectPullRequestNumber, generateBuildNameWithGit } from '../utils/git.js';
5
5
  import { ApiService } from '../services/api-service.js';
6
6
 
7
7
  /**
@@ -49,6 +49,9 @@ export async function uploadCommand(screenshotsPath, options = {}, globalOptions
49
49
 
50
50
  // Note: ConsoleUI handles cleanup via global process listeners
51
51
 
52
+ let buildId = null;
53
+ let config = null;
54
+ const uploadStartTime = Date.now();
52
55
  try {
53
56
  ui.info('Starting upload process...');
54
57
 
@@ -57,7 +60,7 @@ export async function uploadCommand(screenshotsPath, options = {}, globalOptions
57
60
  ...globalOptions,
58
61
  ...options
59
62
  };
60
- const config = await loadConfig(globalOptions.config, allOptions);
63
+ config = await loadConfig(globalOptions.config, allOptions);
61
64
 
62
65
  // Validate API token
63
66
  if (!config.apiKey) {
@@ -68,8 +71,9 @@ export async function uploadCommand(screenshotsPath, options = {}, globalOptions
68
71
  // Collect git metadata if not provided
69
72
  const branch = await detectBranch(options.branch);
70
73
  const commit = await detectCommit(options.commit);
71
- const message = options.message || (await getCommitMessage());
74
+ const message = options.message || (await detectCommitMessage());
72
75
  const buildName = await generateBuildNameWithGit(options.buildName);
76
+ const pullRequestNumber = detectPullRequestNumber();
73
77
  ui.info(`Uploading screenshots from: ${screenshotsPath}`);
74
78
  if (globalOptions.verbose) {
75
79
  ui.info('Configuration loaded', {
@@ -94,21 +98,54 @@ export async function uploadCommand(screenshotsPath, options = {}, globalOptions
94
98
  message,
95
99
  environment: config.build.environment,
96
100
  threshold: config.comparison.threshold,
101
+ uploadAll: options.uploadAll || false,
97
102
  metadata: options.metadata ? JSON.parse(options.metadata) : {},
103
+ pullRequestNumber,
98
104
  onProgress: progressData => {
99
105
  const {
100
106
  message: progressMessage,
101
107
  current,
102
108
  total,
103
- phase
109
+ phase,
110
+ buildId: progressBuildId
104
111
  } = progressData;
105
- ui.progress(progressMessage || `${phase || 'Processing'}: ${current || 0}/${total || 0}`, current, total);
112
+
113
+ // Track buildId when it becomes available
114
+ if (progressBuildId) {
115
+ buildId = progressBuildId;
116
+ }
117
+ let displayMessage = progressMessage;
118
+ if (!displayMessage && phase) {
119
+ if (current !== undefined && total !== undefined) {
120
+ displayMessage = `${phase}: ${current}/${total}`;
121
+ } else {
122
+ displayMessage = phase;
123
+ }
124
+ }
125
+ ui.progress(displayMessage || 'Processing...', current, total);
106
126
  }
107
127
  };
108
128
 
109
129
  // Start upload
110
130
  ui.progress('Starting upload...');
111
131
  const result = await uploader.upload(uploadOptions);
132
+ buildId = result.buildId; // Ensure we have the buildId
133
+
134
+ // Mark build as completed
135
+ if (result.buildId) {
136
+ ui.progress('Finalizing build...');
137
+ try {
138
+ const apiService = new ApiService({
139
+ baseUrl: config.apiUrl,
140
+ token: config.apiKey,
141
+ command: 'upload'
142
+ });
143
+ const executionTime = Date.now() - uploadStartTime;
144
+ await apiService.finalizeBuild(result.buildId, true, executionTime);
145
+ } catch (error) {
146
+ ui.warning(`Failed to finalize build: ${error.message}`);
147
+ }
148
+ }
112
149
  ui.success('Upload completed successfully');
113
150
 
114
151
  // Show Vizzly summary
@@ -138,6 +175,20 @@ export async function uploadCommand(screenshotsPath, options = {}, globalOptions
138
175
  }
139
176
  ui.cleanup();
140
177
  } catch (error) {
178
+ // Mark build as failed if we have a buildId and config
179
+ if (buildId && config) {
180
+ try {
181
+ const apiService = new ApiService({
182
+ baseUrl: config.apiUrl,
183
+ token: config.apiKey,
184
+ command: 'upload'
185
+ });
186
+ const executionTime = Date.now() - uploadStartTime;
187
+ await apiService.finalizeBuild(buildId, false, executionTime);
188
+ } catch {
189
+ // Silent fail on cleanup
190
+ }
191
+ }
141
192
  ui.error('Upload failed', error);
142
193
  }
143
194
  }
@@ -0,0 +1,83 @@
1
+ import { Buffer } from 'buffer';
2
+ import { createServiceLogger } from '../../utils/logger-factory.js';
3
+ const logger = createServiceLogger('API-HANDLER');
4
+ export const createApiHandler = apiService => {
5
+ let vizzlyDisabled = false;
6
+ let screenshotCount = 0;
7
+ const handleScreenshot = async (buildId, name, image, properties = {}) => {
8
+ if (vizzlyDisabled) {
9
+ logger.debug(`Screenshot captured (Vizzly disabled): ${name}`);
10
+ return {
11
+ statusCode: 200,
12
+ body: {
13
+ success: true,
14
+ disabled: true,
15
+ count: ++screenshotCount,
16
+ message: `Vizzly disabled - ${screenshotCount} screenshots captured but not uploaded`
17
+ }
18
+ };
19
+ }
20
+ if (!buildId) {
21
+ return {
22
+ statusCode: 400,
23
+ body: {
24
+ error: 'Build ID is required for screenshot upload'
25
+ }
26
+ };
27
+ }
28
+ if (!apiService) {
29
+ return {
30
+ statusCode: 500,
31
+ body: {
32
+ error: 'API service not available'
33
+ }
34
+ };
35
+ }
36
+ try {
37
+ const imageBuffer = Buffer.from(image, 'base64');
38
+ const result = await apiService.uploadScreenshot(buildId, name, imageBuffer, properties ?? {});
39
+ if (result.skipped) {
40
+ logger.debug(`Screenshot already exists, skipped: ${name}`);
41
+ } else {
42
+ logger.debug(`Screenshot uploaded: ${name}`);
43
+ }
44
+ if (!result.skipped) {
45
+ screenshotCount++;
46
+ }
47
+ return {
48
+ statusCode: 200,
49
+ body: {
50
+ success: true,
51
+ name,
52
+ skipped: result.skipped,
53
+ count: screenshotCount
54
+ }
55
+ };
56
+ } catch (uploadError) {
57
+ logger.error(`❌ Failed to upload screenshot ${name}:`, uploadError.message);
58
+ vizzlyDisabled = true;
59
+ const disabledMessage = '⚠️ Vizzly disabled due to upload error - continuing tests without visual testing';
60
+ logger.warn(disabledMessage);
61
+ return {
62
+ statusCode: 200,
63
+ body: {
64
+ success: true,
65
+ name,
66
+ disabled: true,
67
+ message: disabledMessage
68
+ }
69
+ };
70
+ }
71
+ };
72
+ const getScreenshotCount = () => screenshotCount;
73
+ const cleanup = () => {
74
+ vizzlyDisabled = false;
75
+ screenshotCount = 0;
76
+ logger.debug('API handler cleanup completed');
77
+ };
78
+ return {
79
+ handleScreenshot,
80
+ getScreenshotCount,
81
+ cleanup
82
+ };
83
+ };