@vizzly-testing/cli 0.3.1 → 0.4.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 (55) hide show
  1. package/README.md +26 -28
  2. package/dist/cli.js +18 -30
  3. package/dist/client/index.js +1 -1
  4. package/dist/commands/run.js +34 -9
  5. package/dist/commands/tdd.js +6 -1
  6. package/dist/commands/upload.js +52 -3
  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 +40 -11
  11. package/dist/services/server-manager.js +45 -29
  12. package/dist/services/test-runner.js +64 -69
  13. package/dist/services/uploader.js +47 -82
  14. package/dist/types/commands/run.d.ts +4 -1
  15. package/dist/types/commands/tdd.d.ts +4 -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 +4 -2
  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/utils/config-helpers.d.ts +1 -1
  24. package/dist/types/utils/console-ui.d.ts +1 -1
  25. package/dist/utils/console-ui.js +4 -14
  26. package/docs/api-reference.md +2 -5
  27. package/docs/getting-started.md +1 -1
  28. package/docs/tdd-mode.md +9 -9
  29. package/docs/test-integration.md +3 -17
  30. package/docs/upload-command.md +7 -0
  31. package/package.json +1 -1
  32. package/dist/screenshot-wrapper.js +0 -68
  33. package/dist/server/index.js +0 -522
  34. package/dist/services/service-utils.js +0 -171
  35. package/dist/types/index.js +0 -153
  36. package/dist/types/screenshot-wrapper.d.ts +0 -27
  37. package/dist/types/server/index.d.ts +0 -38
  38. package/dist/types/services/service-utils.d.ts +0 -45
  39. package/dist/types/types/index.d.ts +0 -373
  40. package/dist/types/utils/diagnostics.d.ts +0 -69
  41. package/dist/types/utils/error-messages.d.ts +0 -42
  42. package/dist/types/utils/framework-detector.d.ts +0 -5
  43. package/dist/types/utils/help.d.ts +0 -11
  44. package/dist/types/utils/image-comparison.d.ts +0 -42
  45. package/dist/types/utils/package.d.ts +0 -1
  46. package/dist/types/utils/project-detection.d.ts +0 -19
  47. package/dist/types/utils/ui-helpers.d.ts +0 -23
  48. package/dist/utils/diagnostics.js +0 -184
  49. package/dist/utils/error-messages.js +0 -34
  50. package/dist/utils/framework-detector.js +0 -40
  51. package/dist/utils/help.js +0 -66
  52. package/dist/utils/image-comparison.js +0 -172
  53. package/dist/utils/package.js +0 -9
  54. package/dist/utils/project-detection.js +0 -145
  55. 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
 
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,37 +38,14 @@ 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);
41
+ const result = await tddCommand(command, options, globalOptions);
42
+ if (result && !result.success && result.exitCode > 0) {
43
+ process.exit(result.exitCode);
44
+ }
42
45
  });
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) => {
46
+ 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) => {
44
47
  const globalOptions = program.opts();
45
48
 
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
- };
60
-
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;
70
- }
71
-
72
49
  // Validate options
73
50
  const validationErrors = validateRunOptions(command, options);
74
51
  if (validationErrors.length > 0) {
@@ -76,7 +53,18 @@ program.command('run').description('Run tests with Vizzly integration').argument
76
53
  validationErrors.forEach(error => console.error(` - ${error}`));
77
54
  process.exit(1);
78
55
  }
79
- await runCommand(command, options, globalOptions);
56
+ try {
57
+ const result = await runCommand(command, options, globalOptions);
58
+ if (result && !result.success && result.exitCode > 0) {
59
+ process.exit(result.exitCode);
60
+ }
61
+ } catch (error) {
62
+ console.error('Command failed:', error.message);
63
+ if (globalOptions.verbose) {
64
+ console.error('Stack trace:', error.stack);
65
+ }
66
+ process.exit(1);
67
+ }
80
68
  });
81
69
  program.command('status').description('Check the status of a build').argument('<build-id>', 'Build ID to check status for').action(async (buildId, options) => {
82
70
  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
@@ -62,7 +62,6 @@ export async function runCommand(testCommand, options = {}, globalOptions = {})
62
62
  testCommand,
63
63
  port: config.server.port,
64
64
  timeout: config.server.timeout,
65
- tddMode: options.tdd || false,
66
65
  branch,
67
66
  commit: commit?.substring(0, 7),
68
67
  message,
@@ -76,9 +75,10 @@ export async function runCommand(testCommand, options = {}, globalOptions = {})
76
75
  ui.startSpinner('Initializing test runner...');
77
76
  const configWithVerbose = {
78
77
  ...config,
79
- verbose: globalOptions.verbose
78
+ verbose: globalOptions.verbose,
79
+ uploadAll: options.uploadAll || false
80
80
  };
81
- const command = options.tdd ? 'tdd' : 'run';
81
+ const command = 'run';
82
82
  const container = await createServiceContainer(configWithVerbose, command);
83
83
  testRunner = await container.get('testRunner'); // Assign to outer scope variable
84
84
  ui.stopSpinner();
@@ -112,21 +112,31 @@ export async function runCommand(testCommand, options = {}, globalOptions = {})
112
112
  });
113
113
  testRunner.on('build-created', buildInfo => {
114
114
  buildUrl = buildInfo.url;
115
+ // Debug: Log build creation details
116
+ if (globalOptions.verbose) {
117
+ ui.info(`Build created: ${buildInfo.buildId} - ${buildInfo.name}`);
118
+ }
115
119
  // Use UI for consistent formatting
116
120
  if (buildUrl) {
117
121
  ui.info(`Vizzly: ${buildUrl}`);
118
122
  }
119
123
  });
124
+ testRunner.on('build-failed', buildError => {
125
+ ui.error('Failed to create build', buildError);
126
+ });
120
127
  testRunner.on('error', error => {
128
+ ui.stopSpinner(); // Stop spinner to ensure error is visible
121
129
  ui.error('Test runner error occurred', error, 0); // Don't exit immediately, let runner handle it
122
130
  });
131
+ testRunner.on('build-finalize-failed', errorInfo => {
132
+ ui.warning(`Failed to finalize build ${errorInfo.buildId}: ${errorInfo.error}`);
133
+ });
123
134
 
124
135
  // Prepare run options
125
136
  const runOptions = {
126
137
  testCommand,
127
138
  port: config.server.port,
128
139
  timeout: config.server.timeout,
129
- tdd: options.tdd || false,
130
140
  buildName,
131
141
  branch,
132
142
  commit,
@@ -135,9 +145,8 @@ export async function runCommand(testCommand, options = {}, globalOptions = {})
135
145
  threshold: config.comparison.threshold,
136
146
  eager: config.eager || false,
137
147
  allowNoToken: config.allowNoToken || false,
138
- baselineBuildId: config.baselineBuildId,
139
- baselineComparisonId: config.baselineComparisonId,
140
- wait: config.wait || options.wait || false
148
+ wait: config.wait || options.wait || false,
149
+ uploadAll: options.uploadAll || false
141
150
  };
142
151
 
143
152
  // Start test run
@@ -172,13 +181,29 @@ export async function runCommand(testCommand, options = {}, globalOptions = {})
172
181
 
173
182
  // Exit with appropriate code based on comparison results
174
183
  if (buildResult.failedComparisons > 0) {
175
- ui.error(`${buildResult.failedComparisons} visual comparisons failed`, {}, 1);
184
+ ui.error(`${buildResult.failedComparisons} visual comparisons failed`, {}, 0);
185
+ // Return error status without calling process.exit in tests
186
+ return {
187
+ success: false,
188
+ exitCode: 1
189
+ };
176
190
  }
177
191
  }
178
192
  }
179
193
  ui.cleanup();
180
194
  } catch (error) {
181
- ui.error('Test run failed', error);
195
+ ui.stopSpinner(); // Ensure spinner is stopped before showing error
196
+
197
+ // Provide more context about where the error occurred
198
+ let errorContext = 'Test run failed';
199
+ if (error.message && error.message.includes('build')) {
200
+ errorContext = 'Build creation failed';
201
+ } else if (error.message && error.message.includes('screenshot')) {
202
+ errorContext = 'Screenshot processing failed';
203
+ } else if (error.message && error.message.includes('server')) {
204
+ errorContext = 'Server startup failed';
205
+ }
206
+ ui.error(errorContext, error);
182
207
  } finally {
183
208
  // Remove event listeners to prevent memory leaks
184
209
  process.removeListener('SIGINT', sigintHandler);
@@ -168,7 +168,12 @@ export async function tddCommand(testCommand, options = {}, globalOptions = {})
168
168
 
169
169
  // Exit with appropriate code based on comparison results
170
170
  if (result.failed || result.comparisons && result.comparisons.some(c => c.status === 'failed')) {
171
- ui.error('Visual differences detected in TDD mode', {}, 1);
171
+ ui.error('Visual differences detected in TDD mode', {}, 0);
172
+ // Return error status without calling process.exit in tests
173
+ return {
174
+ success: false,
175
+ exitCode: 1
176
+ };
172
177
  }
173
178
  ui.cleanup();
174
179
  } catch (error) {
@@ -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) {
@@ -94,21 +97,53 @@ export async function uploadCommand(screenshotsPath, options = {}, globalOptions
94
97
  message,
95
98
  environment: config.build.environment,
96
99
  threshold: config.comparison.threshold,
100
+ uploadAll: options.uploadAll || false,
97
101
  metadata: options.metadata ? JSON.parse(options.metadata) : {},
98
102
  onProgress: progressData => {
99
103
  const {
100
104
  message: progressMessage,
101
105
  current,
102
106
  total,
103
- phase
107
+ phase,
108
+ buildId: progressBuildId
104
109
  } = progressData;
105
- ui.progress(progressMessage || `${phase || 'Processing'}: ${current || 0}/${total || 0}`, current, total);
110
+
111
+ // Track buildId when it becomes available
112
+ if (progressBuildId) {
113
+ buildId = progressBuildId;
114
+ }
115
+ let displayMessage = progressMessage;
116
+ if (!displayMessage && phase) {
117
+ if (current !== undefined && total !== undefined) {
118
+ displayMessage = `${phase}: ${current}/${total}`;
119
+ } else {
120
+ displayMessage = phase;
121
+ }
122
+ }
123
+ ui.progress(displayMessage || 'Processing...', current, total);
106
124
  }
107
125
  };
108
126
 
109
127
  // Start upload
110
128
  ui.progress('Starting upload...');
111
129
  const result = await uploader.upload(uploadOptions);
130
+ buildId = result.buildId; // Ensure we have the buildId
131
+
132
+ // Mark build as completed
133
+ if (result.buildId) {
134
+ ui.progress('Finalizing build...');
135
+ try {
136
+ const apiService = new ApiService({
137
+ baseUrl: config.apiUrl,
138
+ token: config.apiKey,
139
+ command: 'upload'
140
+ });
141
+ const executionTime = Date.now() - uploadStartTime;
142
+ await apiService.finalizeBuild(result.buildId, true, executionTime);
143
+ } catch (error) {
144
+ ui.warning(`Failed to finalize build: ${error.message}`);
145
+ }
146
+ }
112
147
  ui.success('Upload completed successfully');
113
148
 
114
149
  // Show Vizzly summary
@@ -138,6 +173,20 @@ export async function uploadCommand(screenshotsPath, options = {}, globalOptions
138
173
  }
139
174
  ui.cleanup();
140
175
  } catch (error) {
176
+ // Mark build as failed if we have a buildId and config
177
+ if (buildId && config) {
178
+ try {
179
+ const apiService = new ApiService({
180
+ baseUrl: config.apiUrl,
181
+ token: config.apiKey,
182
+ command: 'upload'
183
+ });
184
+ const executionTime = Date.now() - uploadStartTime;
185
+ await apiService.finalizeBuild(buildId, false, executionTime);
186
+ } catch {
187
+ // Silent fail on cleanup
188
+ }
189
+ }
141
190
  ui.error('Upload failed', error);
142
191
  }
143
192
  }
@@ -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
+ };
@@ -0,0 +1,138 @@
1
+ import { Buffer } from 'buffer';
2
+ import { createServiceLogger } from '../../utils/logger-factory.js';
3
+ import { TddService } from '../../services/tdd-service.js';
4
+ import { colors } from '../../utils/colors.js';
5
+ const logger = createServiceLogger('TDD-HANDLER');
6
+ export const createTddHandler = (config, workingDir, baselineBuild, baselineComparison) => {
7
+ const tddService = new TddService(config, workingDir);
8
+ const builds = new Map();
9
+ const initialize = async () => {
10
+ logger.info('🔄 TDD mode enabled - setting up local comparison...');
11
+ const baseline = await tddService.loadBaseline();
12
+ if (!baseline) {
13
+ if (config.apiKey) {
14
+ logger.info('📥 No local baseline found, downloading from Vizzly...');
15
+ await tddService.downloadBaselines(baselineBuild, baselineComparison);
16
+ } else {
17
+ logger.info('📝 No local baseline found and no API token - all screenshots will be marked as new');
18
+ }
19
+ } else {
20
+ logger.info(`✅ Using existing baseline: ${colors.cyan(baseline.buildName)}`);
21
+ }
22
+ };
23
+ const registerBuild = buildId => {
24
+ builds.set(buildId, {
25
+ id: buildId,
26
+ name: `TDD Build ${buildId}`,
27
+ branch: 'current',
28
+ environment: 'test',
29
+ screenshots: [],
30
+ createdAt: Date.now()
31
+ });
32
+ logger.debug(`Registered TDD build: ${buildId}`);
33
+ };
34
+ const handleScreenshot = async (buildId, name, image, properties = {}) => {
35
+ const build = builds.get(buildId);
36
+ if (!build) {
37
+ throw new Error(`Build ${buildId} not found`);
38
+ }
39
+ const screenshot = {
40
+ name,
41
+ imageData: image,
42
+ properties,
43
+ timestamp: Date.now()
44
+ };
45
+ build.screenshots.push(screenshot);
46
+ const imageBuffer = Buffer.from(image, 'base64');
47
+ const comparison = await tddService.compareScreenshot(name, imageBuffer, properties);
48
+ if (comparison.status === 'failed') {
49
+ return {
50
+ statusCode: 422,
51
+ body: {
52
+ error: 'Visual difference detected',
53
+ details: `Screenshot '${name}' differs from baseline`,
54
+ comparison: {
55
+ name: comparison.name,
56
+ status: comparison.status,
57
+ baseline: comparison.baseline,
58
+ current: comparison.current,
59
+ diff: comparison.diff
60
+ },
61
+ tddMode: true
62
+ }
63
+ };
64
+ }
65
+ if (comparison.status === 'baseline-updated') {
66
+ return {
67
+ statusCode: 200,
68
+ body: {
69
+ status: 'success',
70
+ message: `Baseline updated for ${name}`,
71
+ comparison: {
72
+ name: comparison.name,
73
+ status: comparison.status,
74
+ baseline: comparison.baseline,
75
+ current: comparison.current
76
+ },
77
+ tddMode: true
78
+ }
79
+ };
80
+ }
81
+ if (comparison.status === 'error') {
82
+ return {
83
+ statusCode: 500,
84
+ body: {
85
+ error: `Comparison failed: ${comparison.error}`,
86
+ tddMode: true
87
+ }
88
+ };
89
+ }
90
+ logger.debug(`✅ TDD: ${comparison.status.toUpperCase()} ${name}`);
91
+ return {
92
+ statusCode: 200,
93
+ body: {
94
+ success: true,
95
+ comparison: {
96
+ name: comparison.name,
97
+ status: comparison.status
98
+ },
99
+ tddMode: true
100
+ }
101
+ };
102
+ };
103
+ const getScreenshotCount = buildId => {
104
+ const build = builds.get(buildId);
105
+ return build ? build.screenshots.length : 0;
106
+ };
107
+ const finishBuild = async buildId => {
108
+ const build = builds.get(buildId);
109
+ if (!build) {
110
+ throw new Error(`Build ${buildId} not found`);
111
+ }
112
+ if (build.screenshots.length === 0) {
113
+ throw new Error('No screenshots to process. Make sure your tests are calling the Vizzly screenshot function.');
114
+ }
115
+ const results = tddService.printResults();
116
+ builds.delete(buildId);
117
+ return {
118
+ id: buildId,
119
+ name: build.name,
120
+ tddMode: true,
121
+ results,
122
+ url: null,
123
+ passed: results.failed === 0 && results.errors === 0
124
+ };
125
+ };
126
+ const cleanup = () => {
127
+ builds.clear();
128
+ logger.debug('TDD handler cleanup completed');
129
+ };
130
+ return {
131
+ initialize,
132
+ registerBuild,
133
+ handleScreenshot,
134
+ getScreenshotCount,
135
+ finishBuild,
136
+ cleanup
137
+ };
138
+ };