@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.
- package/LICENSE +21 -0
- package/README.md +363 -0
- package/bin/vizzly.js +3 -0
- package/dist/cli.js +104 -0
- package/dist/client/index.js +237 -0
- package/dist/commands/doctor.js +158 -0
- package/dist/commands/init.js +102 -0
- package/dist/commands/run.js +224 -0
- package/dist/commands/status.js +164 -0
- package/dist/commands/tdd.js +212 -0
- package/dist/commands/upload.js +181 -0
- package/dist/container/index.js +184 -0
- package/dist/errors/vizzly-error.js +149 -0
- package/dist/index.js +31 -0
- package/dist/screenshot-wrapper.js +68 -0
- package/dist/sdk/index.js +364 -0
- package/dist/server/index.js +522 -0
- package/dist/services/api-service.js +215 -0
- package/dist/services/base-service.js +154 -0
- package/dist/services/build-manager.js +214 -0
- package/dist/services/screenshot-server.js +96 -0
- package/dist/services/server-manager.js +61 -0
- package/dist/services/service-utils.js +171 -0
- package/dist/services/tdd-service.js +444 -0
- package/dist/services/test-runner.js +210 -0
- package/dist/services/uploader.js +413 -0
- package/dist/types/cli.d.ts +2 -0
- package/dist/types/client/index.d.ts +76 -0
- package/dist/types/commands/doctor.d.ts +11 -0
- package/dist/types/commands/init.d.ts +14 -0
- package/dist/types/commands/run.d.ts +13 -0
- package/dist/types/commands/status.d.ts +13 -0
- package/dist/types/commands/tdd.d.ts +13 -0
- package/dist/types/commands/upload.d.ts +13 -0
- package/dist/types/container/index.d.ts +61 -0
- package/dist/types/errors/vizzly-error.d.ts +75 -0
- package/dist/types/index.d.ts +10 -0
- package/dist/types/index.js +153 -0
- package/dist/types/screenshot-wrapper.d.ts +27 -0
- package/dist/types/sdk/index.d.ts +108 -0
- package/dist/types/server/index.d.ts +38 -0
- package/dist/types/services/api-service.d.ts +77 -0
- package/dist/types/services/base-service.d.ts +72 -0
- package/dist/types/services/build-manager.d.ts +68 -0
- package/dist/types/services/screenshot-server.d.ts +10 -0
- package/dist/types/services/server-manager.d.ts +8 -0
- package/dist/types/services/service-utils.d.ts +45 -0
- package/dist/types/services/tdd-service.d.ts +55 -0
- package/dist/types/services/test-runner.d.ts +25 -0
- package/dist/types/services/uploader.d.ts +34 -0
- package/dist/types/types/index.d.ts +373 -0
- package/dist/types/utils/colors.d.ts +12 -0
- package/dist/types/utils/config-helpers.d.ts +6 -0
- package/dist/types/utils/config-loader.d.ts +22 -0
- package/dist/types/utils/console-ui.d.ts +61 -0
- package/dist/types/utils/diagnostics.d.ts +69 -0
- package/dist/types/utils/environment-config.d.ts +54 -0
- package/dist/types/utils/environment.d.ts +36 -0
- package/dist/types/utils/error-messages.d.ts +42 -0
- package/dist/types/utils/fetch-utils.d.ts +1 -0
- package/dist/types/utils/framework-detector.d.ts +5 -0
- package/dist/types/utils/git.d.ts +44 -0
- package/dist/types/utils/help.d.ts +11 -0
- package/dist/types/utils/image-comparison.d.ts +42 -0
- package/dist/types/utils/logger-factory.d.ts +26 -0
- package/dist/types/utils/logger.d.ts +79 -0
- package/dist/types/utils/package-info.d.ts +15 -0
- package/dist/types/utils/package.d.ts +1 -0
- package/dist/types/utils/project-detection.d.ts +19 -0
- package/dist/types/utils/ui-helpers.d.ts +23 -0
- package/dist/utils/colors.js +66 -0
- package/dist/utils/config-helpers.js +8 -0
- package/dist/utils/config-loader.js +120 -0
- package/dist/utils/console-ui.js +226 -0
- package/dist/utils/diagnostics.js +184 -0
- package/dist/utils/environment-config.js +93 -0
- package/dist/utils/environment.js +109 -0
- package/dist/utils/error-messages.js +34 -0
- package/dist/utils/fetch-utils.js +9 -0
- package/dist/utils/framework-detector.js +40 -0
- package/dist/utils/git.js +226 -0
- package/dist/utils/help.js +66 -0
- package/dist/utils/image-comparison.js +172 -0
- package/dist/utils/logger-factory.js +76 -0
- package/dist/utils/logger.js +231 -0
- package/dist/utils/package-info.js +38 -0
- package/dist/utils/package.js +9 -0
- package/dist/utils/project-detection.js +145 -0
- package/dist/utils/ui-helpers.js +86 -0
- 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
|
+
}
|