@vizzly-testing/cli 0.20.0 → 0.20.1-beta.1
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/dist/api/client.js +134 -0
- package/dist/api/core.js +341 -0
- package/dist/api/endpoints.js +314 -0
- package/dist/api/index.js +19 -0
- package/dist/auth/client.js +91 -0
- package/dist/auth/core.js +176 -0
- package/dist/auth/index.js +30 -0
- package/dist/auth/operations.js +148 -0
- package/dist/cli.js +178 -3
- package/dist/client/index.js +144 -77
- package/dist/commands/doctor.js +121 -36
- package/dist/commands/finalize.js +49 -18
- package/dist/commands/init.js +13 -18
- package/dist/commands/login.js +49 -55
- package/dist/commands/logout.js +17 -9
- package/dist/commands/project.js +100 -71
- package/dist/commands/run.js +189 -95
- package/dist/commands/status.js +101 -66
- package/dist/commands/tdd-daemon.js +61 -32
- package/dist/commands/tdd.js +104 -98
- package/dist/commands/upload.js +78 -34
- package/dist/commands/whoami.js +44 -42
- package/dist/config/core.js +438 -0
- package/dist/config/index.js +13 -0
- package/dist/config/operations.js +327 -0
- package/dist/index.js +1 -1
- package/dist/project/core.js +295 -0
- package/dist/project/index.js +13 -0
- package/dist/project/operations.js +393 -0
- package/dist/reporter/reporter-bundle.css +1 -1
- package/dist/reporter/reporter-bundle.iife.js +16 -16
- package/dist/screenshot-server/core.js +157 -0
- package/dist/screenshot-server/index.js +11 -0
- package/dist/screenshot-server/operations.js +183 -0
- package/dist/sdk/index.js +3 -2
- package/dist/server/handlers/api-handler.js +14 -5
- package/dist/server/handlers/tdd-handler.js +191 -53
- package/dist/server/http-server.js +9 -3
- package/dist/server/routers/baseline.js +58 -0
- package/dist/server/routers/dashboard.js +10 -6
- package/dist/server/routers/screenshot.js +32 -0
- package/dist/server-manager/core.js +186 -0
- package/dist/server-manager/index.js +81 -0
- package/dist/server-manager/operations.js +209 -0
- package/dist/services/build-manager.js +2 -69
- package/dist/services/index.js +21 -48
- package/dist/services/screenshot-server.js +40 -74
- package/dist/services/server-manager.js +45 -80
- package/dist/services/test-runner.js +90 -250
- package/dist/services/uploader.js +56 -358
- package/dist/tdd/core/hotspot-coverage.js +112 -0
- package/dist/tdd/core/signature.js +101 -0
- package/dist/tdd/index.js +19 -0
- package/dist/tdd/metadata/baseline-metadata.js +103 -0
- package/dist/tdd/metadata/hotspot-metadata.js +93 -0
- package/dist/tdd/services/baseline-downloader.js +151 -0
- package/dist/tdd/services/baseline-manager.js +166 -0
- package/dist/tdd/services/comparison-service.js +230 -0
- package/dist/tdd/services/hotspot-service.js +71 -0
- package/dist/tdd/services/result-service.js +123 -0
- package/dist/tdd/tdd-service.js +1145 -0
- package/dist/test-runner/core.js +255 -0
- package/dist/test-runner/index.js +13 -0
- package/dist/test-runner/operations.js +483 -0
- package/dist/types/client.d.ts +25 -2
- package/dist/uploader/core.js +396 -0
- package/dist/uploader/index.js +11 -0
- package/dist/uploader/operations.js +412 -0
- package/dist/utils/colors.js +187 -39
- package/dist/utils/config-loader.js +3 -6
- package/dist/utils/context.js +228 -0
- package/dist/utils/output.js +449 -14
- package/docs/api-reference.md +173 -8
- package/docs/tui-elements.md +560 -0
- package/package.json +13 -13
- package/dist/services/api-service.js +0 -412
- package/dist/services/auth-service.js +0 -226
- package/dist/services/config-service.js +0 -369
- package/dist/services/html-report-generator.js +0 -455
- package/dist/services/project-service.js +0 -326
- package/dist/services/report-generator/report.css +0 -411
- package/dist/services/report-generator/viewer.js +0 -102
- package/dist/services/static-report-generator.js +0 -207
- package/dist/services/tdd-service.js +0 -1437
package/dist/commands/run.js
CHANGED
|
@@ -1,65 +1,119 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Run command implementation
|
|
3
|
+
* Uses functional operations directly - no class wrappers needed
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { spawn as defaultSpawn } from 'node:child_process';
|
|
7
|
+
import { createBuild as defaultCreateApiBuild, createApiClient as defaultCreateApiClient, finalizeBuild as defaultFinalizeApiBuild, getBuild as defaultGetBuild, getTokenContext as defaultGetTokenContext } from '../api/index.js';
|
|
8
|
+
import { VizzlyError } from '../errors/vizzly-error.js';
|
|
9
|
+
import { createServerManager as defaultCreateServerManager } from '../server-manager/index.js';
|
|
10
|
+
import { createBuildObject as defaultCreateBuildObject } from '../services/build-manager.js';
|
|
11
|
+
import { createUploader as defaultCreateUploader } from '../services/uploader.js';
|
|
12
|
+
import { finalizeBuild as defaultFinalizeBuild, runTests as defaultRunTests } from '../test-runner/index.js';
|
|
13
|
+
import { loadConfig as defaultLoadConfig } from '../utils/config-loader.js';
|
|
14
|
+
import { detectBranch as defaultDetectBranch, detectCommit as defaultDetectCommit, detectCommitMessage as defaultDetectCommitMessage, detectPullRequestNumber as defaultDetectPullRequestNumber, generateBuildNameWithGit as defaultGenerateBuildNameWithGit } from '../utils/git.js';
|
|
15
|
+
import * as defaultOutput from '../utils/output.js';
|
|
5
16
|
|
|
6
17
|
/**
|
|
7
18
|
* Run command implementation
|
|
8
19
|
* @param {string} testCommand - Test command to execute
|
|
9
20
|
* @param {Object} options - Command options
|
|
10
21
|
* @param {Object} globalOptions - Global CLI options
|
|
22
|
+
* @param {Object} deps - Dependencies for testing
|
|
11
23
|
*/
|
|
12
|
-
export async function runCommand(testCommand, options = {}, globalOptions = {}) {
|
|
24
|
+
export async function runCommand(testCommand, options = {}, globalOptions = {}, deps = {}) {
|
|
25
|
+
let {
|
|
26
|
+
loadConfig = defaultLoadConfig,
|
|
27
|
+
createApiClient = defaultCreateApiClient,
|
|
28
|
+
createApiBuild = defaultCreateApiBuild,
|
|
29
|
+
finalizeApiBuild = defaultFinalizeApiBuild,
|
|
30
|
+
getBuild = defaultGetBuild,
|
|
31
|
+
getTokenContext = defaultGetTokenContext,
|
|
32
|
+
createServerManager = defaultCreateServerManager,
|
|
33
|
+
createBuildObject = defaultCreateBuildObject,
|
|
34
|
+
createUploader = defaultCreateUploader,
|
|
35
|
+
finalizeBuild = defaultFinalizeBuild,
|
|
36
|
+
runTests = defaultRunTests,
|
|
37
|
+
detectBranch = defaultDetectBranch,
|
|
38
|
+
detectCommit = defaultDetectCommit,
|
|
39
|
+
detectCommitMessage = defaultDetectCommitMessage,
|
|
40
|
+
detectPullRequestNumber = defaultDetectPullRequestNumber,
|
|
41
|
+
generateBuildNameWithGit = defaultGenerateBuildNameWithGit,
|
|
42
|
+
spawn = defaultSpawn,
|
|
43
|
+
output = defaultOutput,
|
|
44
|
+
exit = code => process.exit(code),
|
|
45
|
+
processOn = (event, handler) => process.on(event, handler),
|
|
46
|
+
processRemoveListener = (event, handler) => process.removeListener(event, handler)
|
|
47
|
+
} = deps;
|
|
13
48
|
output.configure({
|
|
14
49
|
json: globalOptions.json,
|
|
15
50
|
verbose: globalOptions.verbose,
|
|
16
51
|
color: !globalOptions.noColor
|
|
17
52
|
});
|
|
18
|
-
let
|
|
53
|
+
let serverManager = null;
|
|
54
|
+
let testProcess = null;
|
|
19
55
|
let buildId = null;
|
|
20
56
|
let startTime = null;
|
|
21
57
|
let isTddMode = false;
|
|
58
|
+
let config = null;
|
|
22
59
|
|
|
23
60
|
// Ensure cleanup on exit
|
|
24
|
-
|
|
61
|
+
let cleanup = async () => {
|
|
25
62
|
output.cleanup();
|
|
26
63
|
|
|
27
|
-
//
|
|
28
|
-
if (
|
|
64
|
+
// Kill test process if running
|
|
65
|
+
if (testProcess && !testProcess.killed) {
|
|
66
|
+
testProcess.kill('SIGKILL');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Stop server
|
|
70
|
+
if (serverManager) {
|
|
29
71
|
try {
|
|
30
|
-
await
|
|
72
|
+
await serverManager.stop();
|
|
31
73
|
} catch {
|
|
32
74
|
// Silent fail
|
|
33
75
|
}
|
|
34
76
|
}
|
|
35
77
|
|
|
36
78
|
// Finalize build if we have one
|
|
37
|
-
if (
|
|
79
|
+
if (buildId && config) {
|
|
38
80
|
try {
|
|
39
|
-
|
|
40
|
-
await
|
|
81
|
+
let executionTime = Date.now() - (startTime || Date.now());
|
|
82
|
+
await finalizeBuild({
|
|
83
|
+
buildId,
|
|
84
|
+
tdd: isTddMode,
|
|
85
|
+
success: false,
|
|
86
|
+
executionTime,
|
|
87
|
+
config,
|
|
88
|
+
deps: {
|
|
89
|
+
serverManager,
|
|
90
|
+
createApiClient,
|
|
91
|
+
finalizeApiBuild,
|
|
92
|
+
output
|
|
93
|
+
}
|
|
94
|
+
});
|
|
41
95
|
} catch {
|
|
42
96
|
// Silent fail on cleanup
|
|
43
97
|
}
|
|
44
98
|
}
|
|
45
99
|
};
|
|
46
|
-
|
|
100
|
+
let sigintHandler = async () => {
|
|
47
101
|
await cleanup();
|
|
48
|
-
|
|
102
|
+
exit(1);
|
|
49
103
|
};
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
104
|
+
let exitHandler = () => output.cleanup();
|
|
105
|
+
processOn('SIGINT', sigintHandler);
|
|
106
|
+
processOn('exit', exitHandler);
|
|
53
107
|
try {
|
|
54
108
|
// Load configuration with CLI overrides
|
|
55
|
-
|
|
109
|
+
let allOptions = {
|
|
56
110
|
...globalOptions,
|
|
57
111
|
...options
|
|
58
112
|
};
|
|
59
113
|
output.debug('[RUN] Loading config', {
|
|
60
114
|
hasToken: !!allOptions.token
|
|
61
115
|
});
|
|
62
|
-
|
|
116
|
+
config = await loadConfig(globalOptions.config, allOptions);
|
|
63
117
|
output.debug('[RUN] Config loaded', {
|
|
64
118
|
hasApiKey: !!config.apiKey,
|
|
65
119
|
apiKeyPrefix: config.apiKey ? `${config.apiKey.substring(0, 8)}***` : 'NONE'
|
|
@@ -78,15 +132,19 @@ export async function runCommand(testCommand, options = {}, globalOptions = {})
|
|
|
78
132
|
// Validate API token (unless --allow-no-token is set)
|
|
79
133
|
if (!config.apiKey && !config.allowNoToken) {
|
|
80
134
|
output.error('API token required. Use --token, set VIZZLY_TOKEN environment variable, or use --allow-no-token to run without uploading');
|
|
81
|
-
|
|
135
|
+
exit(1);
|
|
136
|
+
return {
|
|
137
|
+
success: false,
|
|
138
|
+
reason: 'no-api-key'
|
|
139
|
+
};
|
|
82
140
|
}
|
|
83
141
|
|
|
84
142
|
// Collect git metadata and build info
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
143
|
+
let branch = await detectBranch(options.branch);
|
|
144
|
+
let commit = await detectCommit(options.commit);
|
|
145
|
+
let message = options.message || (await detectCommitMessage());
|
|
146
|
+
let buildName = await generateBuildNameWithGit(options.buildName);
|
|
147
|
+
let pullRequestNumber = detectPullRequestNumber();
|
|
90
148
|
if (globalOptions.verbose) {
|
|
91
149
|
output.info('Configuration loaded');
|
|
92
150
|
output.debug('Config details', {
|
|
@@ -102,9 +160,9 @@ export async function runCommand(testCommand, options = {}, globalOptions = {})
|
|
|
102
160
|
});
|
|
103
161
|
}
|
|
104
162
|
|
|
105
|
-
// Create
|
|
163
|
+
// Create functional dependencies
|
|
106
164
|
output.startSpinner('Initializing test runner...');
|
|
107
|
-
|
|
165
|
+
let configWithVerbose = {
|
|
108
166
|
...config,
|
|
109
167
|
verbose: globalOptions.verbose,
|
|
110
168
|
uploadAll: options.uploadAll || false
|
|
@@ -112,59 +170,29 @@ export async function runCommand(testCommand, options = {}, globalOptions = {})
|
|
|
112
170
|
output.debug('[RUN] Creating services', {
|
|
113
171
|
hasApiKey: !!configWithVerbose.apiKey
|
|
114
172
|
});
|
|
115
|
-
const services = createServices(configWithVerbose, 'run');
|
|
116
|
-
testRunner = services.testRunner;
|
|
117
|
-
output.stopSpinner();
|
|
118
173
|
|
|
119
|
-
//
|
|
120
|
-
|
|
174
|
+
// Create server manager (functional object)
|
|
175
|
+
serverManager = createServerManager(configWithVerbose, {});
|
|
121
176
|
|
|
122
|
-
//
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
} = progressData;
|
|
127
|
-
output.progress(progressMessage || 'Running tests...');
|
|
128
|
-
});
|
|
129
|
-
testRunner.on('test-output', data => {
|
|
130
|
-
// In non-JSON mode, show test output directly
|
|
131
|
-
if (!globalOptions.json) {
|
|
132
|
-
output.stopSpinner();
|
|
133
|
-
output.print(data.data);
|
|
134
|
-
}
|
|
135
|
-
});
|
|
136
|
-
testRunner.on('server-ready', serverInfo => {
|
|
137
|
-
if (globalOptions.verbose) {
|
|
138
|
-
output.info(`Screenshot server running on port ${serverInfo.port}`);
|
|
139
|
-
output.debug('Server details', serverInfo);
|
|
140
|
-
}
|
|
141
|
-
});
|
|
142
|
-
testRunner.on('screenshot-captured', screenshotInfo => {
|
|
143
|
-
output.info(`Vizzly: Screenshot captured - ${screenshotInfo.name}`);
|
|
144
|
-
});
|
|
145
|
-
testRunner.on('build-created', buildInfo => {
|
|
146
|
-
buildUrl = buildInfo.url;
|
|
147
|
-
buildId = buildInfo.buildId;
|
|
148
|
-
if (globalOptions.verbose) {
|
|
149
|
-
output.info(`Build created: ${buildInfo.buildId} - ${buildInfo.name}`);
|
|
150
|
-
}
|
|
151
|
-
if (buildUrl) {
|
|
152
|
-
output.info(`Vizzly: ${buildUrl}`);
|
|
177
|
+
// Create build manager (functional object)
|
|
178
|
+
let buildManager = {
|
|
179
|
+
async createBuild(buildOptions) {
|
|
180
|
+
return createBuildObject(buildOptions);
|
|
153
181
|
}
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
// Create uploader for --wait functionality
|
|
185
|
+
let uploader = createUploader({
|
|
186
|
+
...configWithVerbose,
|
|
187
|
+
command: 'run'
|
|
154
188
|
});
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
output.stopSpinner();
|
|
160
|
-
output.error('Test runner error occurred', error);
|
|
161
|
-
});
|
|
162
|
-
testRunner.on('build-finalize-failed', errorInfo => {
|
|
163
|
-
output.warn(`Failed to finalize build ${errorInfo.buildId}: ${errorInfo.error}`);
|
|
164
|
-
});
|
|
189
|
+
output.stopSpinner();
|
|
190
|
+
|
|
191
|
+
// Track build URL for display
|
|
192
|
+
let buildUrl = null;
|
|
165
193
|
|
|
166
194
|
// Prepare run options
|
|
167
|
-
|
|
195
|
+
let runOptions = {
|
|
168
196
|
testCommand,
|
|
169
197
|
port: config.server.port,
|
|
170
198
|
timeout: config.server.timeout,
|
|
@@ -188,19 +216,80 @@ export async function runCommand(testCommand, options = {}, globalOptions = {})
|
|
|
188
216
|
isTddMode = runOptions.tdd || false;
|
|
189
217
|
let result;
|
|
190
218
|
try {
|
|
191
|
-
result = await
|
|
219
|
+
result = await runTests({
|
|
220
|
+
runOptions,
|
|
221
|
+
config: configWithVerbose,
|
|
222
|
+
deps: {
|
|
223
|
+
serverManager,
|
|
224
|
+
buildManager,
|
|
225
|
+
spawn: (command, spawnOptions) => {
|
|
226
|
+
let proc = spawn(command, spawnOptions);
|
|
227
|
+
testProcess = proc;
|
|
228
|
+
return proc;
|
|
229
|
+
},
|
|
230
|
+
createApiClient,
|
|
231
|
+
createApiBuild,
|
|
232
|
+
getBuild,
|
|
233
|
+
finalizeApiBuild,
|
|
234
|
+
createError: (msg, code) => new VizzlyError(msg, code),
|
|
235
|
+
output,
|
|
236
|
+
onBuildCreated: data => {
|
|
237
|
+
buildUrl = data.url;
|
|
238
|
+
buildId = data.buildId;
|
|
239
|
+
if (globalOptions.verbose) {
|
|
240
|
+
output.info(`Build created: ${data.buildId}`);
|
|
241
|
+
}
|
|
242
|
+
if (buildUrl) {
|
|
243
|
+
output.info(`Vizzly: ${buildUrl}`);
|
|
244
|
+
}
|
|
245
|
+
},
|
|
246
|
+
onServerReady: data => {
|
|
247
|
+
if (globalOptions.verbose) {
|
|
248
|
+
output.info(`Screenshot server running on port ${data.port}`);
|
|
249
|
+
}
|
|
250
|
+
},
|
|
251
|
+
onFinalizeFailed: data => {
|
|
252
|
+
output.warn(`Failed to finalize build ${data.buildId}: ${data.error}`);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
});
|
|
192
256
|
|
|
193
257
|
// Store buildId for cleanup purposes
|
|
194
258
|
if (result.buildId) {
|
|
195
259
|
buildId = result.buildId;
|
|
196
260
|
}
|
|
197
|
-
output.
|
|
261
|
+
output.complete('Test run completed');
|
|
198
262
|
|
|
199
|
-
// Show Vizzly summary
|
|
263
|
+
// Show Vizzly summary with link to results
|
|
200
264
|
if (result.buildId) {
|
|
201
|
-
output.
|
|
202
|
-
|
|
203
|
-
|
|
265
|
+
output.blank();
|
|
266
|
+
let colors = output.getColors();
|
|
267
|
+
output.print(` ${colors.brand.textTertiary('Screenshots')} ${colors.white(result.screenshotsCaptured)}`);
|
|
268
|
+
|
|
269
|
+
// Get URL from result, or construct one as fallback
|
|
270
|
+
let displayUrl = result.url;
|
|
271
|
+
if (!displayUrl && config.apiKey) {
|
|
272
|
+
try {
|
|
273
|
+
let client = createApiClient({
|
|
274
|
+
baseUrl: config.apiUrl,
|
|
275
|
+
token: config.apiKey,
|
|
276
|
+
command: 'run'
|
|
277
|
+
});
|
|
278
|
+
let tokenContext = await getTokenContext(client);
|
|
279
|
+
let baseUrl = config.apiUrl.replace(/\/api.*$/, '');
|
|
280
|
+
if (tokenContext.organization?.slug && tokenContext.project?.slug) {
|
|
281
|
+
displayUrl = `${baseUrl}/${tokenContext.organization.slug}/${tokenContext.project.slug}/builds/${result.buildId}`;
|
|
282
|
+
}
|
|
283
|
+
} catch {
|
|
284
|
+
// Fallback to simple URL if context fetch fails
|
|
285
|
+
let baseUrl = config.apiUrl.replace(/\/api.*$/, '');
|
|
286
|
+
displayUrl = `${baseUrl}/builds/${result.buildId}`;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
if (displayUrl) {
|
|
290
|
+
output.print(` ${colors.brand.textTertiary('Results')} ${colors.cyan(colors.underline(displayUrl))}`);
|
|
291
|
+
} else {
|
|
292
|
+
output.print(` ${colors.brand.textTertiary('Build')} ${colors.dim(result.buildId)}`);
|
|
204
293
|
}
|
|
205
294
|
}
|
|
206
295
|
} catch (error) {
|
|
@@ -210,8 +299,8 @@ export async function runCommand(testCommand, options = {}, globalOptions = {})
|
|
|
210
299
|
// Check if it's a test command failure (as opposed to setup failure)
|
|
211
300
|
if (error.code === 'TEST_COMMAND_FAILED' || error.code === 'TEST_COMMAND_INTERRUPTED') {
|
|
212
301
|
// Extract exit code from error message if available
|
|
213
|
-
|
|
214
|
-
|
|
302
|
+
let exitCodeMatch = error.message.match(/exited with code (\d+)/);
|
|
303
|
+
let exitCode = exitCodeMatch ? parseInt(exitCodeMatch[1], 10) : 1;
|
|
215
304
|
output.error('Test run failed');
|
|
216
305
|
return {
|
|
217
306
|
success: false,
|
|
@@ -233,10 +322,7 @@ export async function runCommand(testCommand, options = {}, globalOptions = {})
|
|
|
233
322
|
if (runOptions.wait) {
|
|
234
323
|
output.info('Waiting for build completion...');
|
|
235
324
|
output.startSpinner('Processing comparisons...');
|
|
236
|
-
|
|
237
|
-
uploader
|
|
238
|
-
} = services;
|
|
239
|
-
const buildResult = await uploader.waitForBuild(result.buildId);
|
|
325
|
+
let buildResult = await uploader.waitForBuild(result.buildId);
|
|
240
326
|
output.success('Build processing completed');
|
|
241
327
|
|
|
242
328
|
// Exit with appropriate code based on comparison results
|
|
@@ -250,6 +336,10 @@ export async function runCommand(testCommand, options = {}, globalOptions = {})
|
|
|
250
336
|
}
|
|
251
337
|
}
|
|
252
338
|
output.cleanup();
|
|
339
|
+
return {
|
|
340
|
+
success: true,
|
|
341
|
+
result
|
|
342
|
+
};
|
|
253
343
|
} catch (error) {
|
|
254
344
|
output.stopSpinner();
|
|
255
345
|
|
|
@@ -263,11 +353,15 @@ export async function runCommand(testCommand, options = {}, globalOptions = {})
|
|
|
263
353
|
errorContext = 'Server startup failed';
|
|
264
354
|
}
|
|
265
355
|
output.error(errorContext, error);
|
|
266
|
-
|
|
356
|
+
exit(1);
|
|
357
|
+
return {
|
|
358
|
+
success: false,
|
|
359
|
+
error
|
|
360
|
+
};
|
|
267
361
|
} finally {
|
|
268
362
|
// Remove event listeners to prevent memory leaks
|
|
269
|
-
|
|
270
|
-
|
|
363
|
+
processRemoveListener('SIGINT', sigintHandler);
|
|
364
|
+
processRemoveListener('exit', exitHandler);
|
|
271
365
|
}
|
|
272
366
|
}
|
|
273
367
|
|
|
@@ -277,30 +371,30 @@ export async function runCommand(testCommand, options = {}, globalOptions = {})
|
|
|
277
371
|
* @param {Object} options - Command options
|
|
278
372
|
*/
|
|
279
373
|
export function validateRunOptions(testCommand, options) {
|
|
280
|
-
|
|
374
|
+
let errors = [];
|
|
281
375
|
if (!testCommand || testCommand.trim() === '') {
|
|
282
376
|
errors.push('Test command is required');
|
|
283
377
|
}
|
|
284
378
|
if (options.port) {
|
|
285
|
-
|
|
379
|
+
let port = parseInt(options.port, 10);
|
|
286
380
|
if (Number.isNaN(port) || port < 1 || port > 65535) {
|
|
287
381
|
errors.push('Port must be a valid number between 1 and 65535');
|
|
288
382
|
}
|
|
289
383
|
}
|
|
290
384
|
if (options.timeout) {
|
|
291
|
-
|
|
385
|
+
let timeout = parseInt(options.timeout, 10);
|
|
292
386
|
if (Number.isNaN(timeout) || timeout < 1000) {
|
|
293
387
|
errors.push('Timeout must be at least 1000 milliseconds');
|
|
294
388
|
}
|
|
295
389
|
}
|
|
296
390
|
if (options.batchSize !== undefined) {
|
|
297
|
-
|
|
391
|
+
let n = parseInt(options.batchSize, 10);
|
|
298
392
|
if (!Number.isFinite(n) || n <= 0) {
|
|
299
393
|
errors.push('Batch size must be a positive integer');
|
|
300
394
|
}
|
|
301
395
|
}
|
|
302
396
|
if (options.uploadTimeout !== undefined) {
|
|
303
|
-
|
|
397
|
+
let n = parseInt(options.uploadTimeout, 10);
|
|
304
398
|
if (!Number.isFinite(n) || n <= 0) {
|
|
305
399
|
errors.push('Upload timeout must be a positive integer (milliseconds)');
|
|
306
400
|
}
|
package/dist/commands/status.js
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Status command implementation
|
|
3
|
+
* Uses functional API operations directly
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { createApiClient, getBuild } from '../api/index.js';
|
|
2
7
|
import { loadConfig } from '../utils/config-loader.js';
|
|
3
8
|
import { getApiUrl } from '../utils/environment-config.js';
|
|
4
9
|
import * as output from '../utils/output.js';
|
|
@@ -16,14 +21,12 @@ export async function statusCommand(buildId, options = {}, globalOptions = {}) {
|
|
|
16
21
|
color: !globalOptions.noColor
|
|
17
22
|
});
|
|
18
23
|
try {
|
|
19
|
-
output.info(`Checking status for build: ${buildId}`);
|
|
20
|
-
|
|
21
24
|
// Load configuration with CLI overrides
|
|
22
|
-
|
|
25
|
+
let allOptions = {
|
|
23
26
|
...globalOptions,
|
|
24
27
|
...options
|
|
25
28
|
};
|
|
26
|
-
|
|
29
|
+
let config = await loadConfig(globalOptions.config, allOptions);
|
|
27
30
|
|
|
28
31
|
// Validate API token
|
|
29
32
|
if (!config.apiKey) {
|
|
@@ -31,61 +34,22 @@ export async function statusCommand(buildId, options = {}, globalOptions = {}) {
|
|
|
31
34
|
process.exit(1);
|
|
32
35
|
}
|
|
33
36
|
|
|
34
|
-
// Get API
|
|
37
|
+
// Get build details via functional API
|
|
35
38
|
output.startSpinner('Fetching build status...');
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
const buildStatus = await apiService.getBuild(buildId);
|
|
39
|
+
let client = createApiClient({
|
|
40
|
+
baseUrl: config.apiUrl,
|
|
41
|
+
token: config.apiKey,
|
|
42
|
+
command: 'status'
|
|
43
|
+
});
|
|
44
|
+
let buildStatus = await getBuild(client, buildId);
|
|
43
45
|
output.stopSpinner();
|
|
44
46
|
|
|
45
47
|
// Extract build data from API response
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
// Display build summary
|
|
49
|
-
output.success(`Build: ${build.name || build.id}`);
|
|
50
|
-
output.info(`Status: ${build.status.toUpperCase()}`);
|
|
51
|
-
output.info(`Environment: ${build.environment}`);
|
|
52
|
-
if (build.branch) {
|
|
53
|
-
output.info(`Branch: ${build.branch}`);
|
|
54
|
-
}
|
|
55
|
-
if (build.commit_sha) {
|
|
56
|
-
output.info(`Commit: ${build.commit_sha.substring(0, 8)} - ${build.commit_message || 'No message'}`);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
// Show screenshot and comparison stats
|
|
60
|
-
output.info(`Screenshots: ${build.screenshot_count || 0} total`);
|
|
61
|
-
output.info(`Comparisons: ${build.total_comparisons || 0} total (${build.new_comparisons || 0} new, ${build.changed_comparisons || 0} changed, ${build.identical_comparisons || 0} identical)`);
|
|
62
|
-
if (build.approval_status) {
|
|
63
|
-
output.info(`Approval Status: ${build.approval_status}`);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
// Show timing information
|
|
67
|
-
if (build.created_at) {
|
|
68
|
-
output.info(`Created: ${new Date(build.created_at).toLocaleString()}`);
|
|
69
|
-
}
|
|
70
|
-
if (build.completed_at) {
|
|
71
|
-
output.info(`Completed: ${new Date(build.completed_at).toLocaleString()}`);
|
|
72
|
-
} else if (build.status !== 'completed' && build.status !== 'failed') {
|
|
73
|
-
output.info(`Started: ${new Date(build.started_at || build.created_at).toLocaleString()}`);
|
|
74
|
-
}
|
|
75
|
-
if (build.execution_time_ms) {
|
|
76
|
-
output.info(`Execution Time: ${Math.round(build.execution_time_ms / 1000)}s`);
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
// Show build URL if we can construct it
|
|
80
|
-
const baseUrl = config.baseUrl || getApiUrl();
|
|
81
|
-
if (baseUrl && build.project_id) {
|
|
82
|
-
const buildUrl = baseUrl.replace('/api', '') + `/projects/${build.project_id}/builds/${build.id}`;
|
|
83
|
-
output.info(`View Build: ${buildUrl}`);
|
|
84
|
-
}
|
|
48
|
+
let build = buildStatus.build || buildStatus;
|
|
85
49
|
|
|
86
|
-
// Output JSON
|
|
50
|
+
// Output in JSON mode
|
|
87
51
|
if (globalOptions.json) {
|
|
88
|
-
|
|
52
|
+
let statusData = {
|
|
89
53
|
buildId: build.id,
|
|
90
54
|
status: build.status,
|
|
91
55
|
name: build.name,
|
|
@@ -107,34 +71,105 @@ export async function statusCommand(buildId, options = {}, globalOptions = {}) {
|
|
|
107
71
|
userAgent: build.user_agent
|
|
108
72
|
};
|
|
109
73
|
output.data(statusData);
|
|
74
|
+
output.cleanup();
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Human-readable output
|
|
79
|
+
output.header('status', build.status);
|
|
80
|
+
|
|
81
|
+
// Build info section
|
|
82
|
+
let buildInfo = {
|
|
83
|
+
Name: build.name || build.id,
|
|
84
|
+
Status: build.status.toUpperCase(),
|
|
85
|
+
Environment: build.environment
|
|
86
|
+
};
|
|
87
|
+
if (build.branch) {
|
|
88
|
+
buildInfo.Branch = build.branch;
|
|
89
|
+
}
|
|
90
|
+
if (build.commit_sha) {
|
|
91
|
+
buildInfo.Commit = `${build.commit_sha.substring(0, 8)} - ${build.commit_message || 'No message'}`;
|
|
92
|
+
}
|
|
93
|
+
output.keyValue(buildInfo);
|
|
94
|
+
output.blank();
|
|
95
|
+
|
|
96
|
+
// Comparison stats with visual indicators
|
|
97
|
+
let colors = output.getColors();
|
|
98
|
+
let stats = [];
|
|
99
|
+
let newCount = build.new_comparisons || 0;
|
|
100
|
+
let changedCount = build.changed_comparisons || 0;
|
|
101
|
+
let identicalCount = build.identical_comparisons || 0;
|
|
102
|
+
let screenshotCount = build.screenshot_count || 0;
|
|
103
|
+
output.labelValue('Screenshots', String(screenshotCount));
|
|
104
|
+
if (newCount > 0) {
|
|
105
|
+
stats.push(`${colors.brand.info(newCount)} new`);
|
|
106
|
+
}
|
|
107
|
+
if (changedCount > 0) {
|
|
108
|
+
stats.push(`${colors.brand.warning(changedCount)} changed`);
|
|
109
|
+
}
|
|
110
|
+
if (identicalCount > 0) {
|
|
111
|
+
stats.push(`${colors.brand.success(identicalCount)} identical`);
|
|
112
|
+
}
|
|
113
|
+
if (stats.length > 0) {
|
|
114
|
+
output.labelValue('Comparisons', stats.join(colors.brand.textMuted(' · ')));
|
|
115
|
+
}
|
|
116
|
+
if (build.approval_status) {
|
|
117
|
+
output.labelValue('Approval', build.approval_status);
|
|
118
|
+
}
|
|
119
|
+
output.blank();
|
|
120
|
+
|
|
121
|
+
// Timing info
|
|
122
|
+
if (build.created_at) {
|
|
123
|
+
output.hint(`Created ${new Date(build.created_at).toLocaleString()}`);
|
|
124
|
+
}
|
|
125
|
+
if (build.completed_at) {
|
|
126
|
+
output.hint(`Completed ${new Date(build.completed_at).toLocaleString()}`);
|
|
127
|
+
} else if (build.status !== 'completed' && build.status !== 'failed') {
|
|
128
|
+
output.hint(`Started ${new Date(build.started_at || build.created_at).toLocaleString()}`);
|
|
129
|
+
}
|
|
130
|
+
if (build.execution_time_ms) {
|
|
131
|
+
output.hint(`Took ${Math.round(build.execution_time_ms / 1000)}s`);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Show build URL if we can construct it
|
|
135
|
+
let baseUrl = config.baseUrl || getApiUrl();
|
|
136
|
+
if (baseUrl && build.project_id) {
|
|
137
|
+
let buildUrl = baseUrl.replace('/api', '') + `/projects/${build.project_id}/builds/${build.id}`;
|
|
138
|
+
output.blank();
|
|
139
|
+
output.labelValue('View', output.link('Build', buildUrl));
|
|
110
140
|
}
|
|
111
141
|
|
|
112
142
|
// Show additional info in verbose mode
|
|
113
143
|
if (globalOptions.verbose) {
|
|
114
|
-
output.
|
|
144
|
+
output.blank();
|
|
145
|
+
output.divider();
|
|
146
|
+
output.blank();
|
|
147
|
+
let verboseInfo = {};
|
|
115
148
|
if (build.approved_screenshots > 0 || build.rejected_screenshots > 0 || build.pending_screenshots > 0) {
|
|
116
|
-
|
|
149
|
+
verboseInfo.Approvals = `${build.approved_screenshots || 0} approved, ${build.rejected_screenshots || 0} rejected, ${build.pending_screenshots || 0} pending`;
|
|
117
150
|
}
|
|
118
151
|
if (build.avg_diff_percentage !== null) {
|
|
119
|
-
|
|
152
|
+
verboseInfo['Avg Diff'] = `${(build.avg_diff_percentage * 100).toFixed(2)}%`;
|
|
120
153
|
}
|
|
121
154
|
if (build.github_pull_request_number) {
|
|
122
|
-
|
|
155
|
+
verboseInfo['GitHub PR'] = `#${build.github_pull_request_number}`;
|
|
123
156
|
}
|
|
124
157
|
if (build.is_baseline) {
|
|
125
|
-
|
|
158
|
+
verboseInfo.Baseline = 'Yes';
|
|
126
159
|
}
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
160
|
+
verboseInfo['User Agent'] = build.user_agent || 'Unknown';
|
|
161
|
+
verboseInfo['Build ID'] = build.id;
|
|
162
|
+
verboseInfo['Project ID'] = build.project_id;
|
|
163
|
+
output.keyValue(verboseInfo);
|
|
130
164
|
}
|
|
131
165
|
|
|
132
166
|
// Show progress if build is still processing
|
|
133
167
|
if (build.status === 'processing' || build.status === 'pending') {
|
|
134
|
-
|
|
168
|
+
let totalJobs = build.completed_jobs + build.failed_jobs + build.processing_screenshots;
|
|
135
169
|
if (totalJobs > 0) {
|
|
136
|
-
|
|
137
|
-
output.
|
|
170
|
+
let progress = (build.completed_jobs + build.failed_jobs) / totalJobs;
|
|
171
|
+
output.blank();
|
|
172
|
+
output.print(` ${output.progressBar(progress * 100, 100)} ${Math.round(progress * 100)}%`);
|
|
138
173
|
}
|
|
139
174
|
}
|
|
140
175
|
output.cleanup();
|
|
@@ -155,7 +190,7 @@ export async function statusCommand(buildId, options = {}, globalOptions = {}) {
|
|
|
155
190
|
* @param {Object} options - Command options
|
|
156
191
|
*/
|
|
157
192
|
export function validateStatusOptions(buildId) {
|
|
158
|
-
|
|
193
|
+
let errors = [];
|
|
159
194
|
if (!buildId || buildId.trim() === '') {
|
|
160
195
|
errors.push('Build ID is required');
|
|
161
196
|
}
|