@vizzly-testing/cli 0.16.4 → 0.18.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/README.md +4 -4
- package/claude-plugin/skills/debug-visual-regression/SKILL.md +2 -2
- package/dist/cli.js +84 -58
- package/dist/client/index.js +6 -6
- package/dist/commands/doctor.js +18 -17
- package/dist/commands/finalize.js +7 -7
- package/dist/commands/init.js +30 -30
- package/dist/commands/login.js +23 -23
- package/dist/commands/logout.js +4 -4
- package/dist/commands/project.js +36 -36
- package/dist/commands/run.js +33 -33
- package/dist/commands/status.js +14 -14
- package/dist/commands/tdd-daemon.js +43 -43
- package/dist/commands/tdd.js +27 -27
- package/dist/commands/upload.js +33 -33
- package/dist/commands/whoami.js +12 -12
- package/dist/index.js +9 -14
- package/dist/plugin-loader.js +28 -28
- package/dist/reporter/reporter-bundle.css +1 -1
- package/dist/reporter/reporter-bundle.iife.js +19 -19
- package/dist/sdk/index.js +33 -35
- package/dist/server/handlers/api-handler.js +4 -4
- package/dist/server/handlers/tdd-handler.js +12 -12
- package/dist/server/http-server.js +21 -22
- package/dist/server/middleware/json-parser.js +1 -1
- package/dist/server/routers/assets.js +14 -14
- package/dist/server/routers/auth.js +14 -14
- package/dist/server/routers/baseline.js +8 -8
- package/dist/server/routers/cloud-proxy.js +15 -15
- package/dist/server/routers/config.js +11 -11
- package/dist/server/routers/dashboard.js +11 -11
- package/dist/server/routers/health.js +4 -4
- package/dist/server/routers/projects.js +19 -19
- package/dist/server/routers/screenshot.js +9 -9
- package/dist/services/api-service.js +16 -16
- package/dist/services/auth-service.js +17 -17
- package/dist/services/build-manager.js +3 -3
- package/dist/services/config-service.js +33 -33
- package/dist/services/html-report-generator.js +8 -8
- package/dist/services/index.js +11 -11
- package/dist/services/project-service.js +19 -19
- package/dist/services/report-generator/report.css +3 -3
- package/dist/services/report-generator/viewer.js +25 -23
- package/dist/services/screenshot-server.js +1 -1
- package/dist/services/server-manager.js +5 -5
- package/dist/services/static-report-generator.js +14 -14
- package/dist/services/tdd-service.js +101 -95
- package/dist/services/test-runner.js +14 -4
- package/dist/services/uploader.js +10 -8
- package/dist/types/config.d.ts +2 -1
- package/dist/types/index.d.ts +11 -1
- package/dist/types/sdk.d.ts +1 -1
- package/dist/utils/browser.js +3 -3
- package/dist/utils/build-history.js +12 -12
- package/dist/utils/config-loader.js +19 -19
- package/dist/utils/config-schema.js +10 -9
- package/dist/utils/environment-config.js +11 -0
- package/dist/utils/fetch-utils.js +2 -2
- package/dist/utils/file-helpers.js +2 -2
- package/dist/utils/git.js +3 -6
- package/dist/utils/global-config.js +28 -25
- package/dist/utils/output.js +136 -28
- package/dist/utils/package-info.js +3 -3
- package/dist/utils/security.js +12 -12
- package/docs/api-reference.md +56 -27
- package/docs/doctor-command.md +1 -1
- package/docs/tdd-mode.md +3 -3
- package/package.json +9 -13
package/dist/commands/run.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { loadConfig } from '../utils/config-loader.js';
|
|
2
|
-
import * as output from '../utils/output.js';
|
|
3
1
|
import { createServices } from '../services/index.js';
|
|
2
|
+
import { loadConfig } from '../utils/config-loader.js';
|
|
4
3
|
import { detectBranch, detectCommit, detectCommitMessage, detectPullRequestNumber, generateBuildNameWithGit } from '../utils/git.js';
|
|
4
|
+
import * as output from '../utils/output.js';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Run command implementation
|
|
@@ -21,7 +21,7 @@ export async function runCommand(testCommand, options = {}, globalOptions = {})
|
|
|
21
21
|
let isTddMode = false;
|
|
22
22
|
|
|
23
23
|
// Ensure cleanup on exit
|
|
24
|
-
|
|
24
|
+
const cleanup = async () => {
|
|
25
25
|
output.cleanup();
|
|
26
26
|
|
|
27
27
|
// Cancel test runner (kills process and stops server)
|
|
@@ -36,40 +36,40 @@ export async function runCommand(testCommand, options = {}, globalOptions = {})
|
|
|
36
36
|
// Finalize build if we have one
|
|
37
37
|
if (testRunner && buildId) {
|
|
38
38
|
try {
|
|
39
|
-
|
|
39
|
+
const executionTime = Date.now() - (startTime || Date.now());
|
|
40
40
|
await testRunner.finalizeBuild(buildId, isTddMode, false, executionTime);
|
|
41
41
|
} catch {
|
|
42
42
|
// Silent fail on cleanup
|
|
43
43
|
}
|
|
44
44
|
}
|
|
45
45
|
};
|
|
46
|
-
|
|
46
|
+
const sigintHandler = async () => {
|
|
47
47
|
await cleanup();
|
|
48
48
|
process.exit(1);
|
|
49
49
|
};
|
|
50
|
-
|
|
50
|
+
const exitHandler = () => output.cleanup();
|
|
51
51
|
process.on('SIGINT', sigintHandler);
|
|
52
52
|
process.on('exit', exitHandler);
|
|
53
53
|
try {
|
|
54
54
|
// Load configuration with CLI overrides
|
|
55
|
-
|
|
55
|
+
const allOptions = {
|
|
56
56
|
...globalOptions,
|
|
57
57
|
...options
|
|
58
58
|
};
|
|
59
59
|
output.debug('[RUN] Loading config', {
|
|
60
60
|
hasToken: !!allOptions.token
|
|
61
61
|
});
|
|
62
|
-
|
|
62
|
+
const config = await loadConfig(globalOptions.config, allOptions);
|
|
63
63
|
output.debug('[RUN] Config loaded', {
|
|
64
64
|
hasApiKey: !!config.apiKey,
|
|
65
|
-
apiKeyPrefix: config.apiKey ? config.apiKey.substring(0, 8)
|
|
65
|
+
apiKeyPrefix: config.apiKey ? `${config.apiKey.substring(0, 8)}***` : 'NONE'
|
|
66
66
|
});
|
|
67
67
|
if (globalOptions.verbose) {
|
|
68
68
|
output.info('Token check:');
|
|
69
69
|
output.debug('Token details', {
|
|
70
70
|
hasApiKey: !!config.apiKey,
|
|
71
71
|
apiKeyType: typeof config.apiKey,
|
|
72
|
-
apiKeyPrefix: typeof config.apiKey === 'string' && config.apiKey ? config.apiKey.substring(0, 10)
|
|
72
|
+
apiKeyPrefix: typeof config.apiKey === 'string' && config.apiKey ? `${config.apiKey.substring(0, 10)}...` : 'none',
|
|
73
73
|
projectSlug: config.projectSlug || 'none',
|
|
74
74
|
organizationSlug: config.organizationSlug || 'none'
|
|
75
75
|
});
|
|
@@ -82,11 +82,11 @@ export async function runCommand(testCommand, options = {}, globalOptions = {})
|
|
|
82
82
|
}
|
|
83
83
|
|
|
84
84
|
// Collect git metadata and build info
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
85
|
+
const branch = await detectBranch(options.branch);
|
|
86
|
+
const commit = await detectCommit(options.commit);
|
|
87
|
+
const message = options.message || (await detectCommitMessage());
|
|
88
|
+
const buildName = await generateBuildNameWithGit(options.buildName);
|
|
89
|
+
const pullRequestNumber = detectPullRequestNumber();
|
|
90
90
|
if (globalOptions.verbose) {
|
|
91
91
|
output.info('Configuration loaded');
|
|
92
92
|
output.debug('Config details', {
|
|
@@ -104,7 +104,7 @@ export async function runCommand(testCommand, options = {}, globalOptions = {})
|
|
|
104
104
|
|
|
105
105
|
// Create service container and get test runner service
|
|
106
106
|
output.startSpinner('Initializing test runner...');
|
|
107
|
-
|
|
107
|
+
const configWithVerbose = {
|
|
108
108
|
...config,
|
|
109
109
|
verbose: globalOptions.verbose,
|
|
110
110
|
uploadAll: options.uploadAll || false
|
|
@@ -112,7 +112,7 @@ export async function runCommand(testCommand, options = {}, globalOptions = {})
|
|
|
112
112
|
output.debug('[RUN] Creating services', {
|
|
113
113
|
hasApiKey: !!configWithVerbose.apiKey
|
|
114
114
|
});
|
|
115
|
-
|
|
115
|
+
const services = createServices(configWithVerbose, 'run');
|
|
116
116
|
testRunner = services.testRunner;
|
|
117
117
|
output.stopSpinner();
|
|
118
118
|
|
|
@@ -121,7 +121,7 @@ export async function runCommand(testCommand, options = {}, globalOptions = {})
|
|
|
121
121
|
|
|
122
122
|
// Set up event handlers
|
|
123
123
|
testRunner.on('progress', progressData => {
|
|
124
|
-
|
|
124
|
+
const {
|
|
125
125
|
message: progressMessage
|
|
126
126
|
} = progressData;
|
|
127
127
|
output.progress(progressMessage || 'Running tests...');
|
|
@@ -164,7 +164,7 @@ export async function runCommand(testCommand, options = {}, globalOptions = {})
|
|
|
164
164
|
});
|
|
165
165
|
|
|
166
166
|
// Prepare run options
|
|
167
|
-
|
|
167
|
+
const runOptions = {
|
|
168
168
|
testCommand,
|
|
169
169
|
port: config.server.port,
|
|
170
170
|
timeout: config.server.timeout,
|
|
@@ -210,8 +210,8 @@ export async function runCommand(testCommand, options = {}, globalOptions = {})
|
|
|
210
210
|
// Check if it's a test command failure (as opposed to setup failure)
|
|
211
211
|
if (error.code === 'TEST_COMMAND_FAILED' || error.code === 'TEST_COMMAND_INTERRUPTED') {
|
|
212
212
|
// Extract exit code from error message if available
|
|
213
|
-
|
|
214
|
-
|
|
213
|
+
const exitCodeMatch = error.message.match(/exited with code (\d+)/);
|
|
214
|
+
const exitCode = exitCodeMatch ? parseInt(exitCodeMatch[1], 10) : 1;
|
|
215
215
|
output.error('Test run failed');
|
|
216
216
|
return {
|
|
217
217
|
success: false,
|
|
@@ -233,10 +233,10 @@ export async function runCommand(testCommand, options = {}, globalOptions = {})
|
|
|
233
233
|
if (runOptions.wait) {
|
|
234
234
|
output.info('Waiting for build completion...');
|
|
235
235
|
output.startSpinner('Processing comparisons...');
|
|
236
|
-
|
|
236
|
+
const {
|
|
237
237
|
uploader
|
|
238
238
|
} = services;
|
|
239
|
-
|
|
239
|
+
const buildResult = await uploader.waitForBuild(result.buildId);
|
|
240
240
|
output.success('Build processing completed');
|
|
241
241
|
|
|
242
242
|
// Exit with appropriate code based on comparison results
|
|
@@ -255,11 +255,11 @@ export async function runCommand(testCommand, options = {}, globalOptions = {})
|
|
|
255
255
|
|
|
256
256
|
// Provide more context about where the error occurred
|
|
257
257
|
let errorContext = 'Test run failed';
|
|
258
|
-
if (error.message
|
|
258
|
+
if (error.message?.includes('build')) {
|
|
259
259
|
errorContext = 'Build creation failed';
|
|
260
|
-
} else if (error.message
|
|
260
|
+
} else if (error.message?.includes('screenshot')) {
|
|
261
261
|
errorContext = 'Screenshot processing failed';
|
|
262
|
-
} else if (error.message
|
|
262
|
+
} else if (error.message?.includes('server')) {
|
|
263
263
|
errorContext = 'Server startup failed';
|
|
264
264
|
}
|
|
265
265
|
output.error(errorContext, error);
|
|
@@ -277,30 +277,30 @@ export async function runCommand(testCommand, options = {}, globalOptions = {})
|
|
|
277
277
|
* @param {Object} options - Command options
|
|
278
278
|
*/
|
|
279
279
|
export function validateRunOptions(testCommand, options) {
|
|
280
|
-
|
|
280
|
+
const errors = [];
|
|
281
281
|
if (!testCommand || testCommand.trim() === '') {
|
|
282
282
|
errors.push('Test command is required');
|
|
283
283
|
}
|
|
284
284
|
if (options.port) {
|
|
285
|
-
|
|
286
|
-
if (isNaN(port) || port < 1 || port > 65535) {
|
|
285
|
+
const port = parseInt(options.port, 10);
|
|
286
|
+
if (Number.isNaN(port) || port < 1 || port > 65535) {
|
|
287
287
|
errors.push('Port must be a valid number between 1 and 65535');
|
|
288
288
|
}
|
|
289
289
|
}
|
|
290
290
|
if (options.timeout) {
|
|
291
|
-
|
|
292
|
-
if (isNaN(timeout) || timeout < 1000) {
|
|
291
|
+
const timeout = parseInt(options.timeout, 10);
|
|
292
|
+
if (Number.isNaN(timeout) || timeout < 1000) {
|
|
293
293
|
errors.push('Timeout must be at least 1000 milliseconds');
|
|
294
294
|
}
|
|
295
295
|
}
|
|
296
296
|
if (options.batchSize !== undefined) {
|
|
297
|
-
|
|
297
|
+
const n = parseInt(options.batchSize, 10);
|
|
298
298
|
if (!Number.isFinite(n) || n <= 0) {
|
|
299
299
|
errors.push('Batch size must be a positive integer');
|
|
300
300
|
}
|
|
301
301
|
}
|
|
302
302
|
if (options.uploadTimeout !== undefined) {
|
|
303
|
-
|
|
303
|
+
const n = parseInt(options.uploadTimeout, 10);
|
|
304
304
|
if (!Number.isFinite(n) || n <= 0) {
|
|
305
305
|
errors.push('Upload timeout must be a positive integer (milliseconds)');
|
|
306
306
|
}
|
package/dist/commands/status.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { loadConfig } from '../utils/config-loader.js';
|
|
2
|
-
import * as output from '../utils/output.js';
|
|
3
1
|
import { createServices } from '../services/index.js';
|
|
2
|
+
import { loadConfig } from '../utils/config-loader.js';
|
|
4
3
|
import { getApiUrl } from '../utils/environment-config.js';
|
|
4
|
+
import * as output from '../utils/output.js';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Status command implementation
|
|
@@ -19,11 +19,11 @@ export async function statusCommand(buildId, options = {}, globalOptions = {}) {
|
|
|
19
19
|
output.info(`Checking status for build: ${buildId}`);
|
|
20
20
|
|
|
21
21
|
// Load configuration with CLI overrides
|
|
22
|
-
|
|
22
|
+
const allOptions = {
|
|
23
23
|
...globalOptions,
|
|
24
24
|
...options
|
|
25
25
|
};
|
|
26
|
-
|
|
26
|
+
const config = await loadConfig(globalOptions.config, allOptions);
|
|
27
27
|
|
|
28
28
|
// Validate API token
|
|
29
29
|
if (!config.apiKey) {
|
|
@@ -33,17 +33,17 @@ export async function statusCommand(buildId, options = {}, globalOptions = {}) {
|
|
|
33
33
|
|
|
34
34
|
// Get API service
|
|
35
35
|
output.startSpinner('Fetching build status...');
|
|
36
|
-
|
|
37
|
-
|
|
36
|
+
const services = createServices(config, 'status');
|
|
37
|
+
const {
|
|
38
38
|
apiService
|
|
39
39
|
} = services;
|
|
40
40
|
|
|
41
41
|
// Get build details via unified ApiService
|
|
42
|
-
|
|
42
|
+
const buildStatus = await apiService.getBuild(buildId);
|
|
43
43
|
output.stopSpinner();
|
|
44
44
|
|
|
45
45
|
// Extract build data from API response
|
|
46
|
-
|
|
46
|
+
const build = buildStatus.build || buildStatus;
|
|
47
47
|
|
|
48
48
|
// Display build summary
|
|
49
49
|
output.success(`Build: ${build.name || build.id}`);
|
|
@@ -77,15 +77,15 @@ export async function statusCommand(buildId, options = {}, globalOptions = {}) {
|
|
|
77
77
|
}
|
|
78
78
|
|
|
79
79
|
// Show build URL if we can construct it
|
|
80
|
-
|
|
80
|
+
const baseUrl = config.baseUrl || getApiUrl();
|
|
81
81
|
if (baseUrl && build.project_id) {
|
|
82
|
-
|
|
82
|
+
const buildUrl = baseUrl.replace('/api', '') + `/projects/${build.project_id}/builds/${build.id}`;
|
|
83
83
|
output.info(`View Build: ${buildUrl}`);
|
|
84
84
|
}
|
|
85
85
|
|
|
86
86
|
// Output JSON data for --json mode
|
|
87
87
|
if (globalOptions.json) {
|
|
88
|
-
|
|
88
|
+
const statusData = {
|
|
89
89
|
buildId: build.id,
|
|
90
90
|
status: build.status,
|
|
91
91
|
name: build.name,
|
|
@@ -131,9 +131,9 @@ export async function statusCommand(buildId, options = {}, globalOptions = {}) {
|
|
|
131
131
|
|
|
132
132
|
// Show progress if build is still processing
|
|
133
133
|
if (build.status === 'processing' || build.status === 'pending') {
|
|
134
|
-
|
|
134
|
+
const totalJobs = build.completed_jobs + build.failed_jobs + build.processing_screenshots;
|
|
135
135
|
if (totalJobs > 0) {
|
|
136
|
-
|
|
136
|
+
const progress = (build.completed_jobs + build.failed_jobs) / totalJobs;
|
|
137
137
|
output.info(`Progress: ${Math.round(progress * 100)}% complete`);
|
|
138
138
|
}
|
|
139
139
|
}
|
|
@@ -155,7 +155,7 @@ export async function statusCommand(buildId, options = {}, globalOptions = {}) {
|
|
|
155
155
|
* @param {Object} options - Command options
|
|
156
156
|
*/
|
|
157
157
|
export function validateStatusOptions(buildId) {
|
|
158
|
-
|
|
158
|
+
const errors = [];
|
|
159
159
|
if (!buildId || buildId.trim() === '') {
|
|
160
160
|
errors.push('Build ID is required');
|
|
161
161
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { homedir } from 'os';
|
|
4
|
-
import {
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
5
|
import * as output from '../utils/output.js';
|
|
6
6
|
import { tddCommand } from './tdd.js';
|
|
7
7
|
|
|
@@ -19,7 +19,7 @@ export async function tddStartCommand(options = {}, globalOptions = {}) {
|
|
|
19
19
|
|
|
20
20
|
// Check if server already running
|
|
21
21
|
if (await isServerRunning(options.port || 47392)) {
|
|
22
|
-
|
|
22
|
+
const port = options.port || 47392;
|
|
23
23
|
output.info(`TDD server already running at http://localhost:${port}`);
|
|
24
24
|
output.info(`Dashboard: http://localhost:${port}/dashboard`);
|
|
25
25
|
if (options.open) {
|
|
@@ -29,13 +29,13 @@ export async function tddStartCommand(options = {}, globalOptions = {}) {
|
|
|
29
29
|
}
|
|
30
30
|
try {
|
|
31
31
|
// Ensure .vizzly directory exists
|
|
32
|
-
|
|
32
|
+
const vizzlyDir = join(process.cwd(), '.vizzly');
|
|
33
33
|
if (!existsSync(vizzlyDir)) {
|
|
34
34
|
mkdirSync(vizzlyDir, {
|
|
35
35
|
recursive: true
|
|
36
36
|
});
|
|
37
37
|
}
|
|
38
|
-
|
|
38
|
+
const port = options.port || 47392;
|
|
39
39
|
|
|
40
40
|
// Show loading indicator if downloading baselines (but not in verbose mode since child shows progress)
|
|
41
41
|
if (options.baselineBuild && !globalOptions.verbose) {
|
|
@@ -43,7 +43,7 @@ export async function tddStartCommand(options = {}, globalOptions = {}) {
|
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
// Spawn child process with stdio inherited during init for direct error visibility
|
|
46
|
-
|
|
46
|
+
const child = spawn(process.execPath, [process.argv[1],
|
|
47
47
|
// CLI entry point
|
|
48
48
|
'tdd', 'start', '--daemon-child',
|
|
49
49
|
// Special flag for child process
|
|
@@ -72,7 +72,7 @@ export async function tddStartCommand(options = {}, globalOptions = {}) {
|
|
|
72
72
|
});
|
|
73
73
|
|
|
74
74
|
// Timeout after 30 seconds to prevent indefinite wait
|
|
75
|
-
|
|
75
|
+
const timeoutId = setTimeout(() => {
|
|
76
76
|
if (!initComplete && !initFailed) {
|
|
77
77
|
initFailed = true;
|
|
78
78
|
resolve();
|
|
@@ -95,8 +95,8 @@ export async function tddStartCommand(options = {}, globalOptions = {}) {
|
|
|
95
95
|
child.unref();
|
|
96
96
|
|
|
97
97
|
// Verify server started with retries
|
|
98
|
-
|
|
99
|
-
|
|
98
|
+
const maxRetries = 10;
|
|
99
|
+
const retryDelay = 200; // Start with 200ms
|
|
100
100
|
let running = false;
|
|
101
101
|
for (let i = 0; i < maxRetries && !running; i++) {
|
|
102
102
|
await new Promise(resolve => setTimeout(resolve, retryDelay * (i + 1)));
|
|
@@ -113,14 +113,14 @@ export async function tddStartCommand(options = {}, globalOptions = {}) {
|
|
|
113
113
|
|
|
114
114
|
// Write server info to global location for SDK discovery (iOS/Swift can read this)
|
|
115
115
|
try {
|
|
116
|
-
|
|
116
|
+
const globalVizzlyDir = join(homedir(), '.vizzly');
|
|
117
117
|
if (!existsSync(globalVizzlyDir)) {
|
|
118
118
|
mkdirSync(globalVizzlyDir, {
|
|
119
119
|
recursive: true
|
|
120
120
|
});
|
|
121
121
|
}
|
|
122
|
-
|
|
123
|
-
|
|
122
|
+
const globalServerFile = join(globalVizzlyDir, 'server.json');
|
|
123
|
+
const serverInfo = {
|
|
124
124
|
pid: child.pid,
|
|
125
125
|
port: port.toString(),
|
|
126
126
|
startTime: Date.now()
|
|
@@ -160,11 +160,11 @@ export async function tddStartCommand(options = {}, globalOptions = {}) {
|
|
|
160
160
|
* @private
|
|
161
161
|
*/
|
|
162
162
|
export async function runDaemonChild(options = {}, globalOptions = {}) {
|
|
163
|
-
|
|
164
|
-
|
|
163
|
+
const vizzlyDir = join(process.cwd(), '.vizzly');
|
|
164
|
+
const port = options.port || 47392;
|
|
165
165
|
try {
|
|
166
166
|
// Use existing tddCommand but with daemon mode
|
|
167
|
-
|
|
167
|
+
const {
|
|
168
168
|
cleanup
|
|
169
169
|
} = await tddCommand(null,
|
|
170
170
|
// No test command - server only
|
|
@@ -179,9 +179,9 @@ export async function runDaemonChild(options = {}, globalOptions = {}) {
|
|
|
179
179
|
}
|
|
180
180
|
|
|
181
181
|
// Store our PID for the stop command
|
|
182
|
-
|
|
182
|
+
const pidFile = join(vizzlyDir, 'server.pid');
|
|
183
183
|
writeFileSync(pidFile, process.pid.toString());
|
|
184
|
-
|
|
184
|
+
const serverInfo = {
|
|
185
185
|
pid: process.pid,
|
|
186
186
|
port: port,
|
|
187
187
|
startTime: Date.now()
|
|
@@ -189,16 +189,16 @@ export async function runDaemonChild(options = {}, globalOptions = {}) {
|
|
|
189
189
|
writeFileSync(join(vizzlyDir, 'server.json'), JSON.stringify(serverInfo, null, 2));
|
|
190
190
|
|
|
191
191
|
// Set up graceful shutdown
|
|
192
|
-
|
|
192
|
+
const handleShutdown = async () => {
|
|
193
193
|
try {
|
|
194
194
|
// Clean up PID files
|
|
195
195
|
if (existsSync(pidFile)) unlinkSync(pidFile);
|
|
196
|
-
|
|
196
|
+
const serverFile = join(vizzlyDir, 'server.json');
|
|
197
197
|
if (existsSync(serverFile)) unlinkSync(serverFile);
|
|
198
198
|
|
|
199
199
|
// Clean up global server file
|
|
200
200
|
try {
|
|
201
|
-
|
|
201
|
+
const globalServerFile = join(homedir(), '.vizzly', 'server.json');
|
|
202
202
|
if (existsSync(globalServerFile)) unlinkSync(globalServerFile);
|
|
203
203
|
} catch {
|
|
204
204
|
// Non-fatal
|
|
@@ -236,9 +236,9 @@ export async function tddStopCommand(options = {}, globalOptions = {}) {
|
|
|
236
236
|
verbose: globalOptions.verbose,
|
|
237
237
|
color: !globalOptions.noColor
|
|
238
238
|
});
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
239
|
+
const vizzlyDir = join(process.cwd(), '.vizzly');
|
|
240
|
+
const pidFile = join(vizzlyDir, 'server.pid');
|
|
241
|
+
const serverFile = join(vizzlyDir, 'server.json');
|
|
242
242
|
|
|
243
243
|
// First try to find process by PID file
|
|
244
244
|
let pid = null;
|
|
@@ -251,10 +251,10 @@ export async function tddStopCommand(options = {}, globalOptions = {}) {
|
|
|
251
251
|
}
|
|
252
252
|
|
|
253
253
|
// If no PID file or invalid, try to find by port using lsof
|
|
254
|
-
|
|
254
|
+
const port = options.port || 47392;
|
|
255
255
|
if (!pid) {
|
|
256
256
|
try {
|
|
257
|
-
|
|
257
|
+
const lsofProcess = spawn('lsof', ['-ti', `:${port}`], {
|
|
258
258
|
stdio: 'pipe'
|
|
259
259
|
});
|
|
260
260
|
let lsofOutput = '';
|
|
@@ -264,8 +264,8 @@ export async function tddStopCommand(options = {}, globalOptions = {}) {
|
|
|
264
264
|
await new Promise(resolve => {
|
|
265
265
|
lsofProcess.on('close', code => {
|
|
266
266
|
if (code === 0 && lsofOutput.trim()) {
|
|
267
|
-
|
|
268
|
-
if (foundPid && !isNaN(foundPid)) {
|
|
267
|
+
const foundPid = parseInt(lsofOutput.trim().split('\n')[0], 10);
|
|
268
|
+
if (foundPid && !Number.isNaN(foundPid)) {
|
|
269
269
|
pid = foundPid;
|
|
270
270
|
}
|
|
271
271
|
}
|
|
@@ -327,21 +327,21 @@ export async function tddStopCommand(options = {}, globalOptions = {}) {
|
|
|
327
327
|
* @param {Object} options - Command options
|
|
328
328
|
* @param {Object} globalOptions - Global CLI options
|
|
329
329
|
*/
|
|
330
|
-
export async function tddStatusCommand(
|
|
330
|
+
export async function tddStatusCommand(_options, globalOptions = {}) {
|
|
331
331
|
output.configure({
|
|
332
332
|
json: globalOptions.json,
|
|
333
333
|
verbose: globalOptions.verbose,
|
|
334
334
|
color: !globalOptions.noColor
|
|
335
335
|
});
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
336
|
+
const vizzlyDir = join(process.cwd(), '.vizzly');
|
|
337
|
+
const pidFile = join(vizzlyDir, 'server.pid');
|
|
338
|
+
const serverFile = join(vizzlyDir, 'server.json');
|
|
339
339
|
if (!existsSync(pidFile)) {
|
|
340
340
|
output.info('TDD server not running');
|
|
341
341
|
return;
|
|
342
342
|
}
|
|
343
343
|
try {
|
|
344
|
-
|
|
344
|
+
const pid = parseInt(readFileSync(pidFile, 'utf8').trim(), 10);
|
|
345
345
|
|
|
346
346
|
// Check if process is actually running
|
|
347
347
|
process.kill(pid, 0); // Signal 0 just checks if process exists
|
|
@@ -354,7 +354,7 @@ export async function tddStatusCommand(options, globalOptions = {}) {
|
|
|
354
354
|
}
|
|
355
355
|
|
|
356
356
|
// Try to check health endpoint
|
|
357
|
-
|
|
357
|
+
const health = await checkServerHealth(serverInfo.port);
|
|
358
358
|
if (health.running) {
|
|
359
359
|
output.success(`TDD server running (PID: ${pid})`);
|
|
360
360
|
output.info(`Dashboard: http://localhost:${serverInfo.port}/`);
|
|
@@ -365,10 +365,10 @@ export async function tddStatusCommand(options, globalOptions = {}) {
|
|
|
365
365
|
output.info(` Settings: http://localhost:${serverInfo.port}/settings`);
|
|
366
366
|
output.info(` Projects: http://localhost:${serverInfo.port}/projects`);
|
|
367
367
|
if (serverInfo.startTime) {
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
368
|
+
const uptime = Math.floor((Date.now() - serverInfo.startTime) / 1000);
|
|
369
|
+
const hours = Math.floor(uptime / 3600);
|
|
370
|
+
const minutes = Math.floor(uptime % 3600 / 60);
|
|
371
|
+
const seconds = uptime % 60;
|
|
372
372
|
let uptimeStr = '';
|
|
373
373
|
if (hours > 0) uptimeStr += `${hours}h `;
|
|
374
374
|
if (minutes > 0 || hours > 0) uptimeStr += `${minutes}m `;
|
|
@@ -398,7 +398,7 @@ export async function tddStatusCommand(options, globalOptions = {}) {
|
|
|
398
398
|
*/
|
|
399
399
|
async function isServerRunning(port = 47392) {
|
|
400
400
|
try {
|
|
401
|
-
|
|
401
|
+
const health = await checkServerHealth(port);
|
|
402
402
|
return health.running;
|
|
403
403
|
} catch {
|
|
404
404
|
return false;
|
|
@@ -411,8 +411,8 @@ async function isServerRunning(port = 47392) {
|
|
|
411
411
|
*/
|
|
412
412
|
async function checkServerHealth(port = 47392) {
|
|
413
413
|
try {
|
|
414
|
-
|
|
415
|
-
|
|
414
|
+
const response = await fetch(`http://localhost:${port}/health`);
|
|
415
|
+
const data = await response.json();
|
|
416
416
|
return {
|
|
417
417
|
running: response.ok,
|
|
418
418
|
port: data.port,
|
|
@@ -430,7 +430,7 @@ async function checkServerHealth(port = 47392) {
|
|
|
430
430
|
* @private
|
|
431
431
|
*/
|
|
432
432
|
function openDashboard(port = 47392) {
|
|
433
|
-
|
|
433
|
+
const url = `http://localhost:${port}/dashboard`;
|
|
434
434
|
|
|
435
435
|
// Cross-platform open command
|
|
436
436
|
let openCmd;
|
package/dist/commands/tdd.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { loadConfig } from '../utils/config-loader.js';
|
|
2
|
-
import * as output from '../utils/output.js';
|
|
3
1
|
import { createServices } from '../services/index.js';
|
|
2
|
+
import { loadConfig } from '../utils/config-loader.js';
|
|
4
3
|
import { detectBranch, detectCommit } from '../utils/git.js';
|
|
4
|
+
import * as output from '../utils/output.js';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* TDD command implementation
|
|
@@ -20,7 +20,7 @@ export async function tddCommand(testCommand, options = {}, globalOptions = {})
|
|
|
20
20
|
let isCleanedUp = false;
|
|
21
21
|
|
|
22
22
|
// Create cleanup function that can be called by the caller
|
|
23
|
-
|
|
23
|
+
const cleanup = async () => {
|
|
24
24
|
if (isCleanedUp) return;
|
|
25
25
|
isCleanedUp = true;
|
|
26
26
|
output.cleanup();
|
|
@@ -30,14 +30,14 @@ export async function tddCommand(testCommand, options = {}, globalOptions = {})
|
|
|
30
30
|
};
|
|
31
31
|
try {
|
|
32
32
|
// Load configuration with CLI overrides
|
|
33
|
-
|
|
33
|
+
const allOptions = {
|
|
34
34
|
...globalOptions,
|
|
35
35
|
...options
|
|
36
36
|
};
|
|
37
|
-
|
|
37
|
+
const config = await loadConfig(globalOptions.config, allOptions);
|
|
38
38
|
|
|
39
39
|
// Dev mode works locally by default - only needs token for baseline download
|
|
40
|
-
|
|
40
|
+
const needsToken = options.baselineBuild || options.baselineComparison;
|
|
41
41
|
if (!config.apiKey && needsToken) {
|
|
42
42
|
throw new Error('API token required when using --baseline-build or --baseline-comparison flags');
|
|
43
43
|
}
|
|
@@ -46,12 +46,12 @@ export async function tddCommand(testCommand, options = {}, globalOptions = {})
|
|
|
46
46
|
config.allowNoToken = true;
|
|
47
47
|
|
|
48
48
|
// Collect git metadata
|
|
49
|
-
|
|
50
|
-
|
|
49
|
+
const branch = await detectBranch(options.branch);
|
|
50
|
+
const commit = await detectCommit(options.commit);
|
|
51
51
|
|
|
52
52
|
// Show header (skip in daemon mode)
|
|
53
53
|
if (!options.daemon) {
|
|
54
|
-
|
|
54
|
+
const mode = config.apiKey ? 'local' : 'local';
|
|
55
55
|
output.header('tdd', mode);
|
|
56
56
|
|
|
57
57
|
// Show config in verbose mode
|
|
@@ -64,17 +64,17 @@ export async function tddCommand(testCommand, options = {}, globalOptions = {})
|
|
|
64
64
|
|
|
65
65
|
// Create services
|
|
66
66
|
output.startSpinner('Initializing TDD server...');
|
|
67
|
-
|
|
67
|
+
const configWithVerbose = {
|
|
68
68
|
...config,
|
|
69
69
|
verbose: globalOptions.verbose
|
|
70
70
|
};
|
|
71
|
-
|
|
71
|
+
const services = createServices(configWithVerbose, 'tdd');
|
|
72
72
|
testRunner = services.testRunner;
|
|
73
73
|
output.stopSpinner();
|
|
74
74
|
|
|
75
75
|
// Set up event handlers for user feedback
|
|
76
76
|
testRunner.on('progress', progressData => {
|
|
77
|
-
|
|
77
|
+
const {
|
|
78
78
|
message: progressMessage
|
|
79
79
|
} = progressData;
|
|
80
80
|
output.progress(progressMessage || 'Running tests...');
|
|
@@ -96,7 +96,7 @@ export async function tddCommand(testCommand, options = {}, globalOptions = {})
|
|
|
96
96
|
output.debug('capture', screenshotInfo.name);
|
|
97
97
|
});
|
|
98
98
|
testRunner.on('comparison-result', comparisonInfo => {
|
|
99
|
-
|
|
99
|
+
const {
|
|
100
100
|
name,
|
|
101
101
|
status,
|
|
102
102
|
pixelDifference
|
|
@@ -112,7 +112,7 @@ export async function tddCommand(testCommand, options = {}, globalOptions = {})
|
|
|
112
112
|
testRunner.on('error', error => {
|
|
113
113
|
output.error('Test runner error', error);
|
|
114
114
|
});
|
|
115
|
-
|
|
115
|
+
const runOptions = {
|
|
116
116
|
testCommand,
|
|
117
117
|
port: config.server.port,
|
|
118
118
|
timeout: config.server.timeout,
|
|
@@ -149,19 +149,19 @@ export async function tddCommand(testCommand, options = {}, globalOptions = {})
|
|
|
149
149
|
|
|
150
150
|
// Normal dev mode - run tests
|
|
151
151
|
output.debug('run', testCommand);
|
|
152
|
-
|
|
152
|
+
const runResult = await testRunner.run(runOptions);
|
|
153
153
|
|
|
154
154
|
// Show summary
|
|
155
|
-
|
|
155
|
+
const {
|
|
156
156
|
screenshotsCaptured,
|
|
157
157
|
comparisons
|
|
158
158
|
} = runResult;
|
|
159
159
|
|
|
160
160
|
// Determine success based on comparison results
|
|
161
|
-
|
|
161
|
+
const hasFailures = runResult.failed || runResult.comparisons?.some(c => c.status === 'failed');
|
|
162
162
|
if (comparisons && comparisons.length > 0) {
|
|
163
|
-
|
|
164
|
-
|
|
163
|
+
const passed = comparisons.filter(c => c.status === 'passed').length;
|
|
164
|
+
const failed = comparisons.filter(c => c.status === 'failed').length;
|
|
165
165
|
if (hasFailures) {
|
|
166
166
|
output.error(`${failed} visual difference${failed !== 1 ? 's' : ''} detected`);
|
|
167
167
|
output.info(`Check .vizzly/diffs/ for diff images`);
|
|
@@ -200,26 +200,26 @@ export async function tddCommand(testCommand, options = {}, globalOptions = {})
|
|
|
200
200
|
* @param {Object} options - Command options
|
|
201
201
|
*/
|
|
202
202
|
export function validateTddOptions(testCommand, options) {
|
|
203
|
-
|
|
203
|
+
const errors = [];
|
|
204
204
|
if (!testCommand || testCommand.trim() === '') {
|
|
205
205
|
errors.push('Test command is required');
|
|
206
206
|
}
|
|
207
207
|
if (options.port) {
|
|
208
|
-
|
|
209
|
-
if (isNaN(port) || port < 1 || port > 65535) {
|
|
208
|
+
const port = parseInt(options.port, 10);
|
|
209
|
+
if (Number.isNaN(port) || port < 1 || port > 65535) {
|
|
210
210
|
errors.push('Port must be a valid number between 1 and 65535');
|
|
211
211
|
}
|
|
212
212
|
}
|
|
213
213
|
if (options.timeout) {
|
|
214
|
-
|
|
215
|
-
if (isNaN(timeout) || timeout < 1000) {
|
|
214
|
+
const timeout = parseInt(options.timeout, 10);
|
|
215
|
+
if (Number.isNaN(timeout) || timeout < 1000) {
|
|
216
216
|
errors.push('Timeout must be at least 1000 milliseconds');
|
|
217
217
|
}
|
|
218
218
|
}
|
|
219
219
|
if (options.threshold !== undefined) {
|
|
220
|
-
|
|
221
|
-
if (isNaN(threshold) || threshold < 0
|
|
222
|
-
errors.push('Threshold must be a number
|
|
220
|
+
const threshold = parseFloat(options.threshold);
|
|
221
|
+
if (Number.isNaN(threshold) || threshold < 0) {
|
|
222
|
+
errors.push('Threshold must be a non-negative number (CIEDE2000 Delta E)');
|
|
223
223
|
}
|
|
224
224
|
}
|
|
225
225
|
return errors;
|