@vizzly-testing/cli 0.1.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 (90) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +363 -0
  3. package/bin/vizzly.js +3 -0
  4. package/dist/cli.js +104 -0
  5. package/dist/client/index.js +237 -0
  6. package/dist/commands/doctor.js +158 -0
  7. package/dist/commands/init.js +102 -0
  8. package/dist/commands/run.js +224 -0
  9. package/dist/commands/status.js +164 -0
  10. package/dist/commands/tdd.js +212 -0
  11. package/dist/commands/upload.js +181 -0
  12. package/dist/container/index.js +184 -0
  13. package/dist/errors/vizzly-error.js +149 -0
  14. package/dist/index.js +31 -0
  15. package/dist/screenshot-wrapper.js +68 -0
  16. package/dist/sdk/index.js +364 -0
  17. package/dist/server/index.js +522 -0
  18. package/dist/services/api-service.js +215 -0
  19. package/dist/services/base-service.js +154 -0
  20. package/dist/services/build-manager.js +214 -0
  21. package/dist/services/screenshot-server.js +96 -0
  22. package/dist/services/server-manager.js +61 -0
  23. package/dist/services/service-utils.js +171 -0
  24. package/dist/services/tdd-service.js +444 -0
  25. package/dist/services/test-runner.js +210 -0
  26. package/dist/services/uploader.js +413 -0
  27. package/dist/types/cli.d.ts +2 -0
  28. package/dist/types/client/index.d.ts +76 -0
  29. package/dist/types/commands/doctor.d.ts +11 -0
  30. package/dist/types/commands/init.d.ts +14 -0
  31. package/dist/types/commands/run.d.ts +13 -0
  32. package/dist/types/commands/status.d.ts +13 -0
  33. package/dist/types/commands/tdd.d.ts +13 -0
  34. package/dist/types/commands/upload.d.ts +13 -0
  35. package/dist/types/container/index.d.ts +61 -0
  36. package/dist/types/errors/vizzly-error.d.ts +75 -0
  37. package/dist/types/index.d.ts +10 -0
  38. package/dist/types/index.js +153 -0
  39. package/dist/types/screenshot-wrapper.d.ts +27 -0
  40. package/dist/types/sdk/index.d.ts +108 -0
  41. package/dist/types/server/index.d.ts +38 -0
  42. package/dist/types/services/api-service.d.ts +77 -0
  43. package/dist/types/services/base-service.d.ts +72 -0
  44. package/dist/types/services/build-manager.d.ts +68 -0
  45. package/dist/types/services/screenshot-server.d.ts +10 -0
  46. package/dist/types/services/server-manager.d.ts +8 -0
  47. package/dist/types/services/service-utils.d.ts +45 -0
  48. package/dist/types/services/tdd-service.d.ts +55 -0
  49. package/dist/types/services/test-runner.d.ts +25 -0
  50. package/dist/types/services/uploader.d.ts +34 -0
  51. package/dist/types/types/index.d.ts +373 -0
  52. package/dist/types/utils/colors.d.ts +12 -0
  53. package/dist/types/utils/config-helpers.d.ts +6 -0
  54. package/dist/types/utils/config-loader.d.ts +22 -0
  55. package/dist/types/utils/console-ui.d.ts +61 -0
  56. package/dist/types/utils/diagnostics.d.ts +69 -0
  57. package/dist/types/utils/environment-config.d.ts +54 -0
  58. package/dist/types/utils/environment.d.ts +36 -0
  59. package/dist/types/utils/error-messages.d.ts +42 -0
  60. package/dist/types/utils/fetch-utils.d.ts +1 -0
  61. package/dist/types/utils/framework-detector.d.ts +5 -0
  62. package/dist/types/utils/git.d.ts +44 -0
  63. package/dist/types/utils/help.d.ts +11 -0
  64. package/dist/types/utils/image-comparison.d.ts +42 -0
  65. package/dist/types/utils/logger-factory.d.ts +26 -0
  66. package/dist/types/utils/logger.d.ts +79 -0
  67. package/dist/types/utils/package-info.d.ts +15 -0
  68. package/dist/types/utils/package.d.ts +1 -0
  69. package/dist/types/utils/project-detection.d.ts +19 -0
  70. package/dist/types/utils/ui-helpers.d.ts +23 -0
  71. package/dist/utils/colors.js +66 -0
  72. package/dist/utils/config-helpers.js +8 -0
  73. package/dist/utils/config-loader.js +120 -0
  74. package/dist/utils/console-ui.js +226 -0
  75. package/dist/utils/diagnostics.js +184 -0
  76. package/dist/utils/environment-config.js +93 -0
  77. package/dist/utils/environment.js +109 -0
  78. package/dist/utils/error-messages.js +34 -0
  79. package/dist/utils/fetch-utils.js +9 -0
  80. package/dist/utils/framework-detector.js +40 -0
  81. package/dist/utils/git.js +226 -0
  82. package/dist/utils/help.js +66 -0
  83. package/dist/utils/image-comparison.js +172 -0
  84. package/dist/utils/logger-factory.js +76 -0
  85. package/dist/utils/logger.js +231 -0
  86. package/dist/utils/package-info.js +38 -0
  87. package/dist/utils/package.js +9 -0
  88. package/dist/utils/project-detection.js +145 -0
  89. package/dist/utils/ui-helpers.js +86 -0
  90. package/package.json +103 -0
@@ -0,0 +1,224 @@
1
+ import { loadConfig } from '../utils/config-loader.js';
2
+ import { ConsoleUI } from '../utils/console-ui.js';
3
+ import { createServiceContainer } from '../container/index.js';
4
+ import { detectBranch, detectCommit, getCommitMessage, generateBuildNameWithGit } from '../utils/git.js';
5
+
6
+ /**
7
+ * Run command implementation
8
+ * @param {string} testCommand - Test command to execute
9
+ * @param {Object} options - Command options
10
+ * @param {Object} globalOptions - Global CLI options
11
+ */
12
+ export async function runCommand(testCommand, options = {}, globalOptions = {}) {
13
+ // Create UI handler
14
+ const ui = new ConsoleUI({
15
+ json: globalOptions.json,
16
+ verbose: globalOptions.verbose,
17
+ color: !globalOptions.noColor
18
+ });
19
+ let testRunner = null;
20
+ let runResult = null;
21
+
22
+ // Ensure cleanup on exit
23
+ const cleanup = async () => {
24
+ ui.cleanup();
25
+ if (testRunner && runResult && runResult.buildId) {
26
+ try {
27
+ // Try to finalize build on interruption
28
+ await testRunner.finalizeBuild(runResult.buildId, false, false, Date.now() - (runResult.startTime || Date.now()));
29
+ } catch {
30
+ // Silent fail on cleanup
31
+ }
32
+ }
33
+ };
34
+ const sigintHandler = async () => {
35
+ await cleanup();
36
+ process.exit(1);
37
+ };
38
+ const exitHandler = () => ui.cleanup();
39
+ process.on('SIGINT', sigintHandler);
40
+ process.on('exit', exitHandler);
41
+ try {
42
+ // Load configuration with CLI overrides
43
+ const allOptions = {
44
+ ...globalOptions,
45
+ ...options
46
+ };
47
+ const config = await loadConfig(globalOptions.config, allOptions);
48
+
49
+ // Validate API token (unless --allow-no-token is set)
50
+ if (!config.apiKey && !config.allowNoToken) {
51
+ ui.error('API token required. Use --token, set VIZZLY_TOKEN environment variable, or use --allow-no-token to run without uploading');
52
+ return;
53
+ }
54
+
55
+ // Collect git metadata and build info
56
+ const branch = await detectBranch(options.branch);
57
+ const commit = await detectCommit(options.commit);
58
+ const message = options.message || (await getCommitMessage());
59
+ const buildName = await generateBuildNameWithGit(options.buildName);
60
+ if (globalOptions.verbose) {
61
+ ui.info('Configuration loaded', {
62
+ testCommand,
63
+ port: config.server.port,
64
+ timeout: config.server.timeout,
65
+ tddMode: options.tdd || false,
66
+ branch,
67
+ commit: commit?.substring(0, 7),
68
+ message,
69
+ buildName,
70
+ environment: config.build.environment,
71
+ allowNoToken: config.allowNoToken || false
72
+ });
73
+ }
74
+
75
+ // Create service container and get test runner service
76
+ ui.startSpinner('Initializing test runner...');
77
+ const configWithVerbose = {
78
+ ...config,
79
+ verbose: globalOptions.verbose
80
+ };
81
+ const command = options.tdd ? 'tdd' : 'run';
82
+ const container = await createServiceContainer(configWithVerbose, command);
83
+ testRunner = await container.get('testRunner'); // Assign to outer scope variable
84
+ ui.stopSpinner();
85
+
86
+ // Track build URL for display
87
+ let buildUrl = null;
88
+
89
+ // Set up event handlers
90
+ testRunner.on('progress', progressData => {
91
+ const {
92
+ message: progressMessage
93
+ } = progressData;
94
+ ui.progress(progressMessage || 'Running tests...');
95
+ });
96
+ testRunner.on('test-output', output => {
97
+ // In non-JSON mode, show test output directly
98
+ if (!globalOptions.json) {
99
+ ui.stopSpinner();
100
+ console.log(output.data);
101
+ }
102
+ });
103
+ testRunner.on('server-ready', serverInfo => {
104
+ if (globalOptions.verbose) {
105
+ ui.info(`Screenshot server running on port ${serverInfo.port}`);
106
+ ui.info('Server details', serverInfo);
107
+ }
108
+ });
109
+ testRunner.on('screenshot-captured', screenshotInfo => {
110
+ // Use UI for consistent formatting
111
+ ui.info(`Vizzly: Screenshot captured - ${screenshotInfo.name}`);
112
+ });
113
+ testRunner.on('build-created', buildInfo => {
114
+ buildUrl = buildInfo.url;
115
+ // Use UI for consistent formatting
116
+ if (buildUrl) {
117
+ ui.info(`Vizzly: ${buildUrl}`);
118
+ }
119
+ });
120
+ testRunner.on('error', error => {
121
+ ui.error('Test runner error occurred', error, 0); // Don't exit immediately, let runner handle it
122
+ });
123
+
124
+ // Prepare run options
125
+ const runOptions = {
126
+ testCommand,
127
+ port: config.server.port,
128
+ timeout: config.server.timeout,
129
+ tdd: options.tdd || false,
130
+ buildName,
131
+ branch,
132
+ commit,
133
+ message,
134
+ environment: config.build.environment,
135
+ threshold: config.comparison.threshold,
136
+ eager: config.eager || false,
137
+ allowNoToken: config.allowNoToken || false,
138
+ baselineBuildId: config.baselineBuildId,
139
+ baselineComparisonId: config.baselineComparisonId,
140
+ wait: config.wait || options.wait || false
141
+ };
142
+
143
+ // Start test run
144
+ ui.info('Starting test execution...');
145
+ runResult = {
146
+ startTime: Date.now()
147
+ };
148
+ const result = await testRunner.run(runOptions);
149
+ runResult = {
150
+ ...runResult,
151
+ ...result
152
+ };
153
+ ui.success('Test run completed successfully');
154
+
155
+ // Show Vizzly summary
156
+ if (result.buildId) {
157
+ console.log(`🐻 Vizzly: Captured ${result.screenshotsCaptured} screenshots in build ${result.buildId}`);
158
+ if (result.url) {
159
+ console.log(`🔗 Vizzly: View results at ${result.url}`);
160
+ }
161
+ }
162
+
163
+ // Output results
164
+ if (result.buildId) {
165
+ // Wait for build completion if requested
166
+ if (runOptions.wait) {
167
+ ui.info('Waiting for build completion...');
168
+ ui.startSpinner('Processing comparisons...');
169
+ const uploader = await container.get('uploader');
170
+ const buildResult = await uploader.waitForBuild(result.buildId);
171
+ ui.success('Build processing completed');
172
+
173
+ // Exit with appropriate code based on comparison results
174
+ if (buildResult.failedComparisons > 0) {
175
+ ui.error(`${buildResult.failedComparisons} visual comparisons failed`, {}, 1);
176
+ }
177
+ }
178
+ }
179
+ ui.cleanup();
180
+ } catch (error) {
181
+ ui.error('Test run failed', error);
182
+ } finally {
183
+ // Remove event listeners to prevent memory leaks
184
+ process.removeListener('SIGINT', sigintHandler);
185
+ process.removeListener('exit', exitHandler);
186
+ }
187
+ }
188
+
189
+ /**
190
+ * Validate run options
191
+ * @param {string} testCommand - Test command to execute
192
+ * @param {Object} options - Command options
193
+ */
194
+ export function validateRunOptions(testCommand, options) {
195
+ const errors = [];
196
+ if (!testCommand || testCommand.trim() === '') {
197
+ errors.push('Test command is required');
198
+ }
199
+ if (options.port) {
200
+ const port = parseInt(options.port, 10);
201
+ if (isNaN(port) || port < 1 || port > 65535) {
202
+ errors.push('Port must be a valid number between 1 and 65535');
203
+ }
204
+ }
205
+ if (options.timeout) {
206
+ const timeout = parseInt(options.timeout, 10);
207
+ if (isNaN(timeout) || timeout < 1000) {
208
+ errors.push('Timeout must be at least 1000 milliseconds');
209
+ }
210
+ }
211
+ if (options.batchSize !== undefined) {
212
+ const n = parseInt(options.batchSize, 10);
213
+ if (!Number.isFinite(n) || n <= 0) {
214
+ errors.push('Batch size must be a positive integer');
215
+ }
216
+ }
217
+ if (options.uploadTimeout !== undefined) {
218
+ const n = parseInt(options.uploadTimeout, 10);
219
+ if (!Number.isFinite(n) || n <= 0) {
220
+ errors.push('Upload timeout must be a positive integer (milliseconds)');
221
+ }
222
+ }
223
+ return errors;
224
+ }
@@ -0,0 +1,164 @@
1
+ import { loadConfig } from '../utils/config-loader.js';
2
+ import { ConsoleUI } from '../utils/console-ui.js';
3
+ import { createServiceContainer } from '../container/index.js';
4
+ import { getApiUrl } from '../utils/environment-config.js';
5
+
6
+ /**
7
+ * Status command implementation
8
+ * @param {string} buildId - Build ID to check status for
9
+ * @param {Object} options - Command options
10
+ * @param {Object} globalOptions - Global CLI options
11
+ */
12
+ export async function statusCommand(buildId, options = {}, globalOptions = {}) {
13
+ // Create UI handler
14
+ const ui = new ConsoleUI({
15
+ json: globalOptions.json,
16
+ verbose: globalOptions.verbose,
17
+ color: !globalOptions.noColor
18
+ });
19
+
20
+ // Note: ConsoleUI handles cleanup via global process listeners
21
+
22
+ try {
23
+ ui.info(`Checking status for build: ${buildId}`);
24
+
25
+ // Load configuration with CLI overrides
26
+ const allOptions = {
27
+ ...globalOptions,
28
+ ...options
29
+ };
30
+ const config = await loadConfig(globalOptions.config, allOptions);
31
+
32
+ // Validate API token
33
+ if (!config.apiKey) {
34
+ ui.error('API token required. Use --token or set VIZZLY_TOKEN environment variable');
35
+ return;
36
+ }
37
+
38
+ // Get API service
39
+ ui.startSpinner('Fetching build status...');
40
+ const container = await createServiceContainer(config, 'status');
41
+ const apiService = await container.get('apiService');
42
+
43
+ // Get build details via unified ApiService
44
+ const buildStatus = await apiService.getBuild(buildId);
45
+ ui.stopSpinner();
46
+
47
+ // Extract build data from API response
48
+ const build = buildStatus.build || buildStatus;
49
+
50
+ // Display build summary
51
+ ui.success(`Build: ${build.name || build.id}`);
52
+ ui.info(`Status: ${build.status.toUpperCase()}`);
53
+ ui.info(`Environment: ${build.environment}`);
54
+ if (build.branch) {
55
+ ui.info(`Branch: ${build.branch}`);
56
+ }
57
+ if (build.commit_sha) {
58
+ ui.info(`Commit: ${build.commit_sha.substring(0, 8)} - ${build.commit_message || 'No message'}`);
59
+ }
60
+
61
+ // Show screenshot and comparison stats
62
+ ui.info(`Screenshots: ${build.screenshot_count || 0} total`);
63
+ ui.info(`Comparisons: ${build.total_comparisons || 0} total (${build.new_comparisons || 0} new, ${build.changed_comparisons || 0} changed, ${build.identical_comparisons || 0} identical)`);
64
+ if (build.approval_status) {
65
+ ui.info(`Approval Status: ${build.approval_status}`);
66
+ }
67
+
68
+ // Show timing information
69
+ if (build.created_at) {
70
+ ui.info(`Created: ${new Date(build.created_at).toLocaleString()}`);
71
+ }
72
+ if (build.completed_at) {
73
+ ui.info(`Completed: ${new Date(build.completed_at).toLocaleString()}`);
74
+ } else if (build.status !== 'completed' && build.status !== 'failed') {
75
+ ui.info(`Started: ${new Date(build.started_at || build.created_at).toLocaleString()}`);
76
+ }
77
+ if (build.execution_time_ms) {
78
+ ui.info(`Execution Time: ${Math.round(build.execution_time_ms / 1000)}s`);
79
+ }
80
+
81
+ // Show build URL if we can construct it
82
+ const baseUrl = config.baseUrl || getApiUrl();
83
+ if (baseUrl && build.project_id) {
84
+ const buildUrl = baseUrl.replace('/api', '') + `/projects/${build.project_id}/builds/${build.id}`;
85
+ ui.info(`View Build: ${buildUrl}`);
86
+ }
87
+
88
+ // Output JSON data for --json mode
89
+ if (globalOptions.json) {
90
+ const statusData = {
91
+ buildId: build.id,
92
+ status: build.status,
93
+ name: build.name,
94
+ createdAt: build.created_at,
95
+ updatedAt: build.updated_at,
96
+ completedAt: build.completed_at,
97
+ environment: build.environment,
98
+ branch: build.branch,
99
+ commit: build.commit_sha,
100
+ commitMessage: build.commit_message,
101
+ screenshotsTotal: build.screenshot_count || 0,
102
+ comparisonsTotal: build.total_comparisons || 0,
103
+ newComparisons: build.new_comparisons || 0,
104
+ changedComparisons: build.changed_comparisons || 0,
105
+ identicalComparisons: build.identical_comparisons || 0,
106
+ approvalStatus: build.approval_status,
107
+ executionTime: build.execution_time_ms,
108
+ isBaseline: build.is_baseline,
109
+ userAgent: build.user_agent
110
+ };
111
+ ui.data(statusData);
112
+ }
113
+
114
+ // Show additional info in verbose mode
115
+ if (globalOptions.verbose) {
116
+ ui.info('\n--- Additional Details ---');
117
+ if (build.approved_screenshots > 0 || build.rejected_screenshots > 0 || build.pending_screenshots > 0) {
118
+ ui.info(`Screenshot Approvals: ${build.approved_screenshots || 0} approved, ${build.rejected_screenshots || 0} rejected, ${build.pending_screenshots || 0} pending`);
119
+ }
120
+ if (build.avg_diff_percentage !== null) {
121
+ ui.info(`Average Diff: ${(build.avg_diff_percentage * 100).toFixed(2)}%`);
122
+ }
123
+ if (build.github_pull_request_number) {
124
+ ui.info(`GitHub PR: #${build.github_pull_request_number}`);
125
+ }
126
+ if (build.is_baseline) {
127
+ ui.info('This build is marked as a baseline');
128
+ }
129
+ ui.info(`User Agent: ${build.user_agent || 'Unknown'}`);
130
+ ui.info(`Build ID: ${build.id}`);
131
+ ui.info(`Project ID: ${build.project_id}`);
132
+ }
133
+
134
+ // Show progress if build is still processing
135
+ if (build.status === 'processing' || build.status === 'pending') {
136
+ const totalJobs = build.completed_jobs + build.failed_jobs + build.processing_screenshots;
137
+ if (totalJobs > 0) {
138
+ const progress = (build.completed_jobs + build.failed_jobs) / totalJobs;
139
+ ui.info(`Progress: ${Math.round(progress * 100)}% complete`);
140
+ }
141
+ }
142
+ ui.cleanup();
143
+
144
+ // Exit with appropriate code based on build status
145
+ if (build.status === 'failed' || build.failed_jobs > 0) {
146
+ process.exit(1);
147
+ }
148
+ } catch (error) {
149
+ ui.error('Failed to get build status', error);
150
+ }
151
+ }
152
+
153
+ /**
154
+ * Validate status options
155
+ * @param {string} buildId - Build ID to check
156
+ * @param {Object} options - Command options
157
+ */
158
+ export function validateStatusOptions(buildId) {
159
+ const errors = [];
160
+ if (!buildId || buildId.trim() === '') {
161
+ errors.push('Build ID is required');
162
+ }
163
+ return errors;
164
+ }
@@ -0,0 +1,212 @@
1
+ import { loadConfig } from '../utils/config-loader.js';
2
+ import { ConsoleUI } from '../utils/console-ui.js';
3
+ import { createServiceContainer } from '../container/index.js';
4
+ import { detectBranch, detectCommit } from '../utils/git.js';
5
+
6
+ /**
7
+ * TDD command implementation
8
+ * @param {string} testCommand - Test command to execute
9
+ * @param {Object} options - Command options
10
+ * @param {Object} globalOptions - Global CLI options
11
+ */
12
+ export async function tddCommand(testCommand, options = {}, globalOptions = {}) {
13
+ // Create UI handler
14
+ const ui = new ConsoleUI({
15
+ json: globalOptions.json,
16
+ verbose: globalOptions.verbose,
17
+ color: !globalOptions.noColor
18
+ });
19
+ let testRunner = null;
20
+
21
+ // Ensure cleanup on exit - store listeners for proper cleanup
22
+ const cleanup = async () => {
23
+ 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);
30
+ };
31
+ const exitHandler = () => ui.cleanup();
32
+ process.on('SIGINT', sigintHandler);
33
+ process.on('exit', exitHandler);
34
+ try {
35
+ // Load configuration with CLI overrides
36
+ const allOptions = {
37
+ ...globalOptions,
38
+ ...options
39
+ };
40
+ const config = await loadConfig(globalOptions.config, allOptions);
41
+
42
+ // Auto-detect missing token and allow no-token mode for TDD
43
+ if (!config.apiKey) {
44
+ config.allowNoToken = true;
45
+ ui.warning('No API token detected - running in local-only mode');
46
+ }
47
+
48
+ // Handle --set-baseline flag
49
+ if (options.setBaseline) {
50
+ ui.info('🐻 Baseline update mode - current screenshots will become new baselines');
51
+ }
52
+
53
+ // Collect git metadata
54
+ const branch = await detectBranch(options.branch);
55
+ const commit = await detectCommit(options.commit);
56
+ if (globalOptions.verbose) {
57
+ ui.info('TDD Configuration loaded', {
58
+ testCommand,
59
+ port: config.server.port,
60
+ timeout: config.server.timeout,
61
+ branch,
62
+ commit: commit?.substring(0, 7),
63
+ environment: config.build.environment,
64
+ threshold: config.comparison.threshold,
65
+ baselineBuildId: config.baselineBuildId,
66
+ baselineComparisonId: config.baselineComparisonId
67
+ });
68
+ }
69
+
70
+ // Create service container and get services
71
+ ui.startSpinner('Initializing TDD mode...');
72
+ const configWithVerbose = {
73
+ ...config,
74
+ verbose: globalOptions.verbose
75
+ };
76
+ const container = await createServiceContainer(configWithVerbose, 'tdd');
77
+ testRunner = await container.get('testRunner');
78
+ ui.stopSpinner();
79
+
80
+ // Set up event handlers for user feedback
81
+ testRunner.on('progress', progressData => {
82
+ const {
83
+ message: progressMessage
84
+ } = progressData;
85
+ ui.progress(progressMessage || 'Running TDD tests...');
86
+ });
87
+ testRunner.on('test-output', output => {
88
+ // In non-JSON mode, show test output directly
89
+ if (!globalOptions.json) {
90
+ ui.stopSpinner();
91
+ console.log(output.data);
92
+ }
93
+ });
94
+ testRunner.on('server-ready', serverInfo => {
95
+ if (globalOptions.verbose) {
96
+ ui.info(`TDD screenshot server running on port ${serverInfo.port}`);
97
+ ui.info('Server details', serverInfo);
98
+ }
99
+ });
100
+ testRunner.on('screenshot-captured', screenshotInfo => {
101
+ ui.info(`Vizzly TDD: Screenshot captured - ${screenshotInfo.name}`);
102
+ });
103
+ testRunner.on('comparison-result', comparisonInfo => {
104
+ const {
105
+ name,
106
+ status,
107
+ pixelDifference
108
+ } = comparisonInfo;
109
+ if (status === 'passed') {
110
+ ui.info(`✅ ${name}: Visual comparison passed`);
111
+ } else if (status === 'failed') {
112
+ ui.warning(`❌ ${name}: Visual comparison failed (${pixelDifference}% difference)`);
113
+ } else if (status === 'new') {
114
+ ui.warning(`🆕 ${name}: New screenshot (no baseline)`);
115
+ }
116
+ });
117
+ testRunner.on('error', error => {
118
+ ui.error('TDD test runner error occurred', error, 0); // Don't exit immediately
119
+ });
120
+
121
+ // Show informational messages about baseline behavior
122
+ if (config.apiKey) {
123
+ ui.info('API token available - will fetch baselines for local comparison');
124
+ } else {
125
+ ui.warning('Running without API token - all screenshots will be marked as new');
126
+ }
127
+
128
+ // Prepare TDD run options (no uploads, local comparisons only)
129
+ const runOptions = {
130
+ testCommand,
131
+ port: config.server.port,
132
+ timeout: config.server.timeout,
133
+ tdd: true,
134
+ // Enable TDD mode
135
+ setBaseline: options.setBaseline || false,
136
+ // Pass through baseline update mode
137
+ branch,
138
+ commit,
139
+ environment: config.build.environment,
140
+ threshold: config.comparison.threshold,
141
+ allowNoToken: config.allowNoToken || false,
142
+ // Pass through the allow-no-token setting
143
+ baselineBuildId: config.baselineBuildId,
144
+ baselineComparisonId: config.baselineComparisonId,
145
+ wait: false // No build to wait for in TDD mode
146
+ };
147
+
148
+ // Start TDD test run (local comparisons only)
149
+ ui.info('Starting TDD test execution...');
150
+ const result = await testRunner.run(runOptions);
151
+
152
+ // Show TDD summary
153
+ const {
154
+ screenshotsCaptured,
155
+ comparisons
156
+ } = result;
157
+ console.log(`🐻 Vizzly TDD: Processed ${screenshotsCaptured} screenshots`);
158
+ if (comparisons && comparisons.length > 0) {
159
+ const passed = comparisons.filter(c => c.status === 'passed').length;
160
+ const failed = comparisons.filter(c => c.status === 'failed').length;
161
+ const newScreenshots = comparisons.filter(c => c.status === 'new').length;
162
+ console.log(`📊 Results: ${passed} passed, ${failed} failed, ${newScreenshots} new`);
163
+ if (failed > 0) {
164
+ console.log(`🔍 Check diff images in .vizzly/diffs/ directory`);
165
+ }
166
+ }
167
+ ui.success('TDD test run completed');
168
+
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);
172
+ }
173
+ ui.cleanup();
174
+ } catch (error) {
175
+ 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);
180
+ }
181
+ }
182
+
183
+ /**
184
+ * Validate TDD options
185
+ * @param {string} testCommand - Test command to execute
186
+ * @param {Object} options - Command options
187
+ */
188
+ export function validateTddOptions(testCommand, options) {
189
+ const errors = [];
190
+ if (!testCommand || testCommand.trim() === '') {
191
+ errors.push('Test command is required');
192
+ }
193
+ if (options.port) {
194
+ const port = parseInt(options.port, 10);
195
+ if (isNaN(port) || port < 1 || port > 65535) {
196
+ errors.push('Port must be a valid number between 1 and 65535');
197
+ }
198
+ }
199
+ if (options.timeout) {
200
+ const timeout = parseInt(options.timeout, 10);
201
+ if (isNaN(timeout) || timeout < 1000) {
202
+ errors.push('Timeout must be at least 1000 milliseconds');
203
+ }
204
+ }
205
+ if (options.threshold !== undefined) {
206
+ const threshold = parseFloat(options.threshold);
207
+ if (isNaN(threshold) || threshold < 0 || threshold > 1) {
208
+ errors.push('Threshold must be a number between 0 and 1');
209
+ }
210
+ }
211
+ return errors;
212
+ }