@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
|
@@ -20,8 +20,14 @@ export async function tddStartCommand(options = {}, globalOptions = {}) {
|
|
|
20
20
|
// Check if server already running
|
|
21
21
|
if (await isServerRunning(options.port || 47392)) {
|
|
22
22
|
const port = options.port || 47392;
|
|
23
|
-
output.
|
|
24
|
-
output.
|
|
23
|
+
let colors = output.getColors();
|
|
24
|
+
output.header('tdd', 'local');
|
|
25
|
+
output.print(` ${output.statusDot('success')} Already running`);
|
|
26
|
+
output.blank();
|
|
27
|
+
output.printBox(colors.brand.info(colors.underline(`http://localhost:${port}`)), {
|
|
28
|
+
title: 'Dashboard',
|
|
29
|
+
style: 'branded'
|
|
30
|
+
});
|
|
25
31
|
if (options.open) {
|
|
26
32
|
openDashboard(port);
|
|
27
33
|
}
|
|
@@ -37,6 +43,9 @@ export async function tddStartCommand(options = {}, globalOptions = {}) {
|
|
|
37
43
|
}
|
|
38
44
|
const port = options.port || 47392;
|
|
39
45
|
|
|
46
|
+
// Show header first so debug messages appear below it
|
|
47
|
+
output.header('tdd', 'local');
|
|
48
|
+
|
|
40
49
|
// Show loading indicator if downloading baselines (but not in verbose mode since child shows progress)
|
|
41
50
|
if (options.baselineBuild && !globalOptions.verbose) {
|
|
42
51
|
output.startSpinner(`Downloading baselines from build ${options.baselineBuild}...`);
|
|
@@ -109,7 +118,6 @@ export async function tddStartCommand(options = {}, globalOptions = {}) {
|
|
|
109
118
|
output.error('Failed to start TDD server - server not responding to health checks');
|
|
110
119
|
process.exit(1);
|
|
111
120
|
}
|
|
112
|
-
output.success(`TDD server started at http://localhost:${port}`);
|
|
113
121
|
|
|
114
122
|
// Write server info to global location for SDK discovery (iOS/Swift can read this)
|
|
115
123
|
try {
|
|
@@ -129,22 +137,29 @@ export async function tddStartCommand(options = {}, globalOptions = {}) {
|
|
|
129
137
|
} catch {
|
|
130
138
|
// Non-fatal, SDK can still use health check
|
|
131
139
|
}
|
|
140
|
+
|
|
141
|
+
// Get colors for styled output
|
|
142
|
+
let colors = output.getColors();
|
|
143
|
+
|
|
144
|
+
// Show dashboard URL in a branded box
|
|
145
|
+
let dashboardUrl = `http://localhost:${port}`;
|
|
146
|
+
output.printBox(colors.brand.info(colors.underline(dashboardUrl)), {
|
|
147
|
+
title: 'Dashboard',
|
|
148
|
+
style: 'branded'
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
// Verbose mode: show next steps
|
|
152
|
+
if (globalOptions.verbose) {
|
|
153
|
+
output.blank();
|
|
154
|
+
output.print(` ${colors.brand.textTertiary('Next steps')}`);
|
|
155
|
+
output.print(` ${colors.brand.textMuted('1.')} Run tests in watch mode ${colors.brand.textMuted('(npm test -- --watch)')}`);
|
|
156
|
+
output.print(` ${colors.brand.textMuted('2.')} Review visual changes in the dashboard`);
|
|
157
|
+
output.print(` ${colors.brand.textMuted('3.')} Accept or reject baseline updates`);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Always show stop hint
|
|
132
161
|
output.blank();
|
|
133
|
-
output.
|
|
134
|
-
output.info(` http://localhost:${port}/`);
|
|
135
|
-
output.blank();
|
|
136
|
-
output.info('Available views:');
|
|
137
|
-
output.info(` Comparisons: http://localhost:${port}/`);
|
|
138
|
-
output.info(` Stats: http://localhost:${port}/stats`);
|
|
139
|
-
output.info(` Settings: http://localhost:${port}/settings`);
|
|
140
|
-
output.info(` Projects: http://localhost:${port}/projects`);
|
|
141
|
-
output.blank();
|
|
142
|
-
output.info('Next steps:');
|
|
143
|
-
output.info(' 1. Run your tests in watch mode (e.g., npm test -- --watch)');
|
|
144
|
-
output.info(' 2. View live visual comparisons in the dashboard');
|
|
145
|
-
output.info(' 3. Accept/reject baselines directly in the UI');
|
|
146
|
-
output.blank();
|
|
147
|
-
output.info('Stop server: npx vizzly dev stop');
|
|
162
|
+
output.hint('Stop with: vizzly tdd stop');
|
|
148
163
|
if (options.open) {
|
|
149
164
|
openDashboard(port);
|
|
150
165
|
}
|
|
@@ -289,9 +304,11 @@ export async function tddStopCommand(options = {}, globalOptions = {}) {
|
|
|
289
304
|
return;
|
|
290
305
|
}
|
|
291
306
|
try {
|
|
307
|
+
let _colors = output.getColors();
|
|
308
|
+
|
|
292
309
|
// Try to kill the process gracefully
|
|
293
310
|
process.kill(pid, 'SIGTERM');
|
|
294
|
-
output.
|
|
311
|
+
output.startSpinner('Stopping TDD server...');
|
|
295
312
|
|
|
296
313
|
// Give it a moment to shut down gracefully
|
|
297
314
|
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
@@ -301,15 +318,16 @@ export async function tddStopCommand(options = {}, globalOptions = {}) {
|
|
|
301
318
|
process.kill(pid, 0); // Just check if process exists
|
|
302
319
|
// If we get here, process is still running, force kill it
|
|
303
320
|
process.kill(pid, 'SIGKILL');
|
|
304
|
-
output.
|
|
321
|
+
output.stopSpinner();
|
|
322
|
+
output.debug('tdd', 'Force killed process');
|
|
305
323
|
} catch {
|
|
306
324
|
// Process is gone, which is what we want
|
|
325
|
+
output.stopSpinner();
|
|
307
326
|
}
|
|
308
327
|
|
|
309
328
|
// Clean up files
|
|
310
329
|
if (existsSync(pidFile)) unlinkSync(pidFile);
|
|
311
330
|
if (existsSync(serverFile)) unlinkSync(serverFile);
|
|
312
|
-
output.success('TDD server stopped');
|
|
313
331
|
} catch (error) {
|
|
314
332
|
if (error.code === 'ESRCH') {
|
|
315
333
|
// Process not found - clean up stale files
|
|
@@ -356,25 +374,36 @@ export async function tddStatusCommand(_options, globalOptions = {}) {
|
|
|
356
374
|
// Try to check health endpoint
|
|
357
375
|
const health = await checkServerHealth(serverInfo.port);
|
|
358
376
|
if (health.running) {
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
output.
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
output.info(` Projects: http://localhost:${serverInfo.port}/projects`);
|
|
377
|
+
let colors = output.getColors();
|
|
378
|
+
|
|
379
|
+
// Show header
|
|
380
|
+
output.header('tdd', 'local');
|
|
381
|
+
|
|
382
|
+
// Show running status with uptime
|
|
383
|
+
let uptimeStr = '';
|
|
367
384
|
if (serverInfo.startTime) {
|
|
368
385
|
const uptime = Math.floor((Date.now() - serverInfo.startTime) / 1000);
|
|
369
386
|
const hours = Math.floor(uptime / 3600);
|
|
370
387
|
const minutes = Math.floor(uptime % 3600 / 60);
|
|
371
388
|
const seconds = uptime % 60;
|
|
372
|
-
let uptimeStr = '';
|
|
373
389
|
if (hours > 0) uptimeStr += `${hours}h `;
|
|
374
390
|
if (minutes > 0 || hours > 0) uptimeStr += `${minutes}m `;
|
|
375
391
|
uptimeStr += `${seconds}s`;
|
|
392
|
+
}
|
|
393
|
+
output.print(` ${output.statusDot('success')} Running ${uptimeStr ? colors.brand.textTertiary(`· ${uptimeStr}`) : ''}`);
|
|
394
|
+
output.blank();
|
|
395
|
+
|
|
396
|
+
// Show dashboard URL in a branded box
|
|
397
|
+
let dashboardUrl = `http://localhost:${serverInfo.port}`;
|
|
398
|
+
output.printBox(colors.brand.info(colors.underline(dashboardUrl)), {
|
|
399
|
+
title: 'Dashboard',
|
|
400
|
+
style: 'branded'
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
// Verbose mode: show PID
|
|
404
|
+
if (globalOptions.verbose) {
|
|
376
405
|
output.blank();
|
|
377
|
-
output.
|
|
406
|
+
output.print(` ${colors.brand.textTertiary('PID:')} ${pid}`);
|
|
378
407
|
}
|
|
379
408
|
} else {
|
|
380
409
|
output.warn('TDD server process exists but not responding to health checks');
|
|
@@ -430,7 +459,7 @@ async function checkServerHealth(port = 47392) {
|
|
|
430
459
|
* @private
|
|
431
460
|
*/
|
|
432
461
|
function openDashboard(port = 47392) {
|
|
433
|
-
const url = `http://localhost:${port}
|
|
462
|
+
const url = `http://localhost:${port}`;
|
|
434
463
|
|
|
435
464
|
// Cross-platform open command
|
|
436
465
|
let openCmd;
|
package/dist/commands/tdd.js
CHANGED
|
@@ -1,43 +1,73 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
/**
|
|
2
|
+
* TDD 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 } 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 { initializeDaemon as defaultInitializeDaemon, runTests as defaultRunTests } from '../test-runner/index.js';
|
|
12
|
+
import { loadConfig as defaultLoadConfig } from '../utils/config-loader.js';
|
|
13
|
+
import { detectBranch as defaultDetectBranch, detectCommit as defaultDetectCommit } from '../utils/git.js';
|
|
14
|
+
import * as defaultOutput from '../utils/output.js';
|
|
5
15
|
|
|
6
16
|
/**
|
|
7
17
|
* TDD command implementation
|
|
8
18
|
* @param {string} testCommand - Test command to execute
|
|
9
19
|
* @param {Object} options - Command options
|
|
10
20
|
* @param {Object} globalOptions - Global CLI options
|
|
21
|
+
* @param {Object} deps - Dependencies for testing
|
|
11
22
|
* @returns {Promise<{result: Object, cleanup: Function}>} Result and cleanup function
|
|
12
23
|
*/
|
|
13
|
-
export async function tddCommand(testCommand, options = {}, globalOptions = {}) {
|
|
24
|
+
export async function tddCommand(testCommand, options = {}, globalOptions = {}, deps = {}) {
|
|
25
|
+
let {
|
|
26
|
+
loadConfig = defaultLoadConfig,
|
|
27
|
+
createApiClient = defaultCreateApiClient,
|
|
28
|
+
createApiBuild = defaultCreateApiBuild,
|
|
29
|
+
finalizeApiBuild = defaultFinalizeApiBuild,
|
|
30
|
+
getBuild = defaultGetBuild,
|
|
31
|
+
createServerManager = defaultCreateServerManager,
|
|
32
|
+
createBuildObject = defaultCreateBuildObject,
|
|
33
|
+
initializeDaemon = defaultInitializeDaemon,
|
|
34
|
+
runTests = defaultRunTests,
|
|
35
|
+
detectBranch = defaultDetectBranch,
|
|
36
|
+
detectCommit = defaultDetectCommit,
|
|
37
|
+
spawn = defaultSpawn,
|
|
38
|
+
output = defaultOutput
|
|
39
|
+
} = deps;
|
|
14
40
|
output.configure({
|
|
15
41
|
json: globalOptions.json,
|
|
16
42
|
verbose: globalOptions.verbose,
|
|
17
43
|
color: !globalOptions.noColor
|
|
18
44
|
});
|
|
19
|
-
let
|
|
45
|
+
let serverManager = null;
|
|
46
|
+
let testProcess = null;
|
|
20
47
|
let isCleanedUp = false;
|
|
21
48
|
|
|
22
49
|
// Create cleanup function that can be called by the caller
|
|
23
|
-
|
|
50
|
+
let cleanup = async () => {
|
|
24
51
|
if (isCleanedUp) return;
|
|
25
52
|
isCleanedUp = true;
|
|
26
53
|
output.cleanup();
|
|
27
|
-
if (
|
|
28
|
-
|
|
54
|
+
if (testProcess && !testProcess.killed) {
|
|
55
|
+
testProcess.kill('SIGKILL');
|
|
56
|
+
}
|
|
57
|
+
if (serverManager) {
|
|
58
|
+
await serverManager.stop();
|
|
29
59
|
}
|
|
30
60
|
};
|
|
31
61
|
try {
|
|
32
62
|
// Load configuration with CLI overrides
|
|
33
|
-
|
|
63
|
+
let allOptions = {
|
|
34
64
|
...globalOptions,
|
|
35
65
|
...options
|
|
36
66
|
};
|
|
37
|
-
|
|
67
|
+
let config = await loadConfig(globalOptions.config, allOptions);
|
|
38
68
|
|
|
39
69
|
// Dev mode works locally by default - only needs token for baseline download
|
|
40
|
-
|
|
70
|
+
let needsToken = options.baselineBuild || options.baselineComparison;
|
|
41
71
|
if (!config.apiKey && needsToken) {
|
|
42
72
|
throw new Error('API token required when using --baseline-build or --baseline-comparison flags');
|
|
43
73
|
}
|
|
@@ -46,97 +76,65 @@ export async function tddCommand(testCommand, options = {}, globalOptions = {})
|
|
|
46
76
|
config.allowNoToken = true;
|
|
47
77
|
|
|
48
78
|
// Collect git metadata
|
|
49
|
-
|
|
50
|
-
|
|
79
|
+
let branch = await detectBranch(options.branch);
|
|
80
|
+
let commit = await detectCommit(options.commit);
|
|
51
81
|
|
|
52
82
|
// Show header (skip in daemon mode)
|
|
53
83
|
if (!options.daemon) {
|
|
54
|
-
|
|
84
|
+
let mode = config.apiKey ? 'local' : 'local';
|
|
55
85
|
output.header('tdd', mode);
|
|
56
86
|
|
|
57
87
|
// Show config in verbose mode
|
|
58
|
-
output.debug('config',
|
|
59
|
-
port: config.server.port,
|
|
60
|
-
branch,
|
|
61
|
-
threshold: config.comparison.threshold
|
|
62
|
-
});
|
|
88
|
+
output.debug('config', `port=${config.server.port} threshold=${config.comparison.threshold}`);
|
|
63
89
|
}
|
|
64
90
|
|
|
65
|
-
// Create
|
|
91
|
+
// Create functional dependencies
|
|
66
92
|
output.startSpinner('Initializing TDD server...');
|
|
67
|
-
|
|
93
|
+
let configWithVerbose = {
|
|
68
94
|
...config,
|
|
69
95
|
verbose: globalOptions.verbose
|
|
70
96
|
};
|
|
71
|
-
const services = createServices(configWithVerbose, 'tdd');
|
|
72
|
-
testRunner = services.testRunner;
|
|
73
|
-
output.stopSpinner();
|
|
74
97
|
|
|
75
|
-
//
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
testRunner.on('test-output', data => {
|
|
83
|
-
// In non-JSON mode, show test output directly
|
|
84
|
-
if (!globalOptions.json) {
|
|
85
|
-
output.stopSpinner();
|
|
86
|
-
output.print(data.data);
|
|
87
|
-
}
|
|
88
|
-
});
|
|
89
|
-
testRunner.on('server-ready', serverInfo => {
|
|
90
|
-
// Only show in non-daemon mode (daemon shows its own startup message)
|
|
91
|
-
if (!options.daemon) {
|
|
92
|
-
output.debug('server', `listening on :${serverInfo.port}`);
|
|
93
|
-
}
|
|
94
|
-
});
|
|
95
|
-
testRunner.on('screenshot-captured', screenshotInfo => {
|
|
96
|
-
output.debug('capture', screenshotInfo.name);
|
|
97
|
-
});
|
|
98
|
-
testRunner.on('comparison-result', comparisonInfo => {
|
|
99
|
-
const {
|
|
100
|
-
name,
|
|
101
|
-
status,
|
|
102
|
-
pixelDifference
|
|
103
|
-
} = comparisonInfo;
|
|
104
|
-
if (status === 'passed') {
|
|
105
|
-
output.debug('compare', `${name} passed`);
|
|
106
|
-
} else if (status === 'failed') {
|
|
107
|
-
output.warn(`${name}: ${pixelDifference}% difference`);
|
|
108
|
-
} else if (status === 'new') {
|
|
109
|
-
output.debug('compare', `${name} (new baseline)`);
|
|
98
|
+
// Create server manager (functional object)
|
|
99
|
+
serverManager = createServerManager(configWithVerbose, {});
|
|
100
|
+
|
|
101
|
+
// Create build manager (functional object that provides the interface runTests expects)
|
|
102
|
+
let buildManager = {
|
|
103
|
+
async createBuild(buildOptions) {
|
|
104
|
+
return createBuildObject(buildOptions);
|
|
110
105
|
}
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
});
|
|
115
|
-
const runOptions = {
|
|
106
|
+
};
|
|
107
|
+
output.stopSpinner();
|
|
108
|
+
let runOptions = {
|
|
116
109
|
testCommand,
|
|
117
110
|
port: config.server.port,
|
|
118
111
|
timeout: config.server.timeout,
|
|
119
112
|
tdd: true,
|
|
120
113
|
daemon: options.daemon || false,
|
|
121
|
-
// Daemon mode flag
|
|
122
114
|
setBaseline: options.setBaseline || false,
|
|
123
|
-
// Pass through baseline update mode
|
|
124
115
|
branch,
|
|
125
116
|
commit,
|
|
126
117
|
environment: config.build.environment,
|
|
127
118
|
threshold: config.comparison.threshold,
|
|
128
119
|
allowNoToken: config.allowNoToken || false,
|
|
129
|
-
// Pass through the allow-no-token setting
|
|
130
120
|
baselineBuildId: config.baselineBuildId,
|
|
131
121
|
baselineComparisonId: config.baselineComparisonId,
|
|
132
|
-
wait: false
|
|
122
|
+
wait: false
|
|
133
123
|
};
|
|
134
124
|
|
|
135
125
|
// In daemon mode, just start the server without running tests
|
|
136
126
|
if (options.daemon) {
|
|
137
|
-
await
|
|
138
|
-
|
|
139
|
-
|
|
127
|
+
await initializeDaemon({
|
|
128
|
+
initOptions: runOptions,
|
|
129
|
+
deps: {
|
|
130
|
+
serverManager,
|
|
131
|
+
createError: (msg, code) => new VizzlyError(msg, code),
|
|
132
|
+
output,
|
|
133
|
+
onServerReady: data => {
|
|
134
|
+
output.debug('server', `ready on :${data.port}`);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
});
|
|
140
138
|
return {
|
|
141
139
|
result: {
|
|
142
140
|
success: true,
|
|
@@ -149,30 +147,38 @@ export async function tddCommand(testCommand, options = {}, globalOptions = {})
|
|
|
149
147
|
|
|
150
148
|
// Normal dev mode - run tests
|
|
151
149
|
output.debug('run', testCommand);
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
150
|
+
let runResult = await runTests({
|
|
151
|
+
runOptions,
|
|
152
|
+
config: configWithVerbose,
|
|
153
|
+
deps: {
|
|
154
|
+
serverManager,
|
|
155
|
+
buildManager,
|
|
156
|
+
spawn: (command, spawnOptions) => {
|
|
157
|
+
let proc = spawn(command, spawnOptions);
|
|
158
|
+
testProcess = proc;
|
|
159
|
+
return proc;
|
|
160
|
+
},
|
|
161
|
+
createApiClient,
|
|
162
|
+
createApiBuild,
|
|
163
|
+
getBuild,
|
|
164
|
+
finalizeApiBuild,
|
|
165
|
+
createError: (msg, code) => new VizzlyError(msg, code),
|
|
166
|
+
output,
|
|
167
|
+
onBuildCreated: data => {
|
|
168
|
+
output.debug('build', `created ${data.buildId?.substring(0, 8)}`);
|
|
169
|
+
},
|
|
170
|
+
onServerReady: data => {
|
|
171
|
+
output.debug('server', `ready on :${data.port}`);
|
|
172
|
+
},
|
|
173
|
+
onFinalizeFailed: data => {
|
|
174
|
+
output.warn(`Failed to finalize build: ${data.error}`);
|
|
175
|
+
}
|
|
170
176
|
}
|
|
171
|
-
}
|
|
172
|
-
output.result(`${screenshotsCaptured} screenshot${screenshotsCaptured !== 1 ? 's' : ''}`);
|
|
173
|
-
}
|
|
177
|
+
});
|
|
174
178
|
|
|
175
|
-
//
|
|
179
|
+
// Determine success based on comparison results
|
|
180
|
+
// (Summary is printed by printResults() in tdd-service.js, called from getTddResults)
|
|
181
|
+
let hasFailures = runResult.failed || runResult.comparisons?.some(c => c.status === 'failed');
|
|
176
182
|
return {
|
|
177
183
|
result: {
|
|
178
184
|
success: !hasFailures,
|
|
@@ -200,24 +206,24 @@ export async function tddCommand(testCommand, options = {}, globalOptions = {})
|
|
|
200
206
|
* @param {Object} options - Command options
|
|
201
207
|
*/
|
|
202
208
|
export function validateTddOptions(testCommand, options) {
|
|
203
|
-
|
|
209
|
+
let errors = [];
|
|
204
210
|
if (!testCommand || testCommand.trim() === '') {
|
|
205
211
|
errors.push('Test command is required');
|
|
206
212
|
}
|
|
207
213
|
if (options.port) {
|
|
208
|
-
|
|
214
|
+
let port = parseInt(options.port, 10);
|
|
209
215
|
if (Number.isNaN(port) || port < 1 || port > 65535) {
|
|
210
216
|
errors.push('Port must be a valid number between 1 and 65535');
|
|
211
217
|
}
|
|
212
218
|
}
|
|
213
219
|
if (options.timeout) {
|
|
214
|
-
|
|
220
|
+
let timeout = parseInt(options.timeout, 10);
|
|
215
221
|
if (Number.isNaN(timeout) || timeout < 1000) {
|
|
216
222
|
errors.push('Timeout must be at least 1000 milliseconds');
|
|
217
223
|
}
|
|
218
224
|
}
|
|
219
225
|
if (options.threshold !== undefined) {
|
|
220
|
-
|
|
226
|
+
let threshold = parseFloat(options.threshold);
|
|
221
227
|
if (Number.isNaN(threshold) || threshold < 0) {
|
|
222
228
|
errors.push('Threshold must be a non-negative number (CIEDE2000 Delta E)');
|
|
223
229
|
}
|
package/dist/commands/upload.js
CHANGED
|
@@ -1,25 +1,31 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { loadConfig } from '../utils/config-loader.js';
|
|
4
|
-
import { detectBranch, detectCommit, detectCommitMessage, detectPullRequestNumber, generateBuildNameWithGit } from '../utils/git.js';
|
|
5
|
-
import * as
|
|
1
|
+
import { createApiClient as defaultCreateApiClient, finalizeBuild as defaultFinalizeBuild, getTokenContext as defaultGetTokenContext } from '../api/index.js';
|
|
2
|
+
import { createUploader as defaultCreateUploader } from '../services/uploader.js';
|
|
3
|
+
import { loadConfig as defaultLoadConfig } from '../utils/config-loader.js';
|
|
4
|
+
import { detectBranch as defaultDetectBranch, detectCommit as defaultDetectCommit, detectCommitMessage as defaultDetectCommitMessage, detectPullRequestNumber as defaultDetectPullRequestNumber, generateBuildNameWithGit as defaultGenerateBuildNameWithGit } from '../utils/git.js';
|
|
5
|
+
import * as defaultOutput from '../utils/output.js';
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* Construct proper build URL with org/project context
|
|
9
9
|
* @param {string} buildId - Build ID
|
|
10
10
|
* @param {string} apiUrl - API base URL
|
|
11
11
|
* @param {string} apiToken - API token
|
|
12
|
+
* @param {Object} deps - Dependencies
|
|
12
13
|
* @returns {Promise<string>} Proper build URL
|
|
13
14
|
*/
|
|
14
|
-
async function constructBuildUrl(buildId, apiUrl, apiToken) {
|
|
15
|
+
export async function constructBuildUrl(buildId, apiUrl, apiToken, deps = {}) {
|
|
16
|
+
let {
|
|
17
|
+
createApiClient = defaultCreateApiClient,
|
|
18
|
+
getTokenContext = defaultGetTokenContext,
|
|
19
|
+
output = defaultOutput
|
|
20
|
+
} = deps;
|
|
15
21
|
try {
|
|
16
|
-
|
|
22
|
+
let client = createApiClient({
|
|
17
23
|
baseUrl: apiUrl,
|
|
18
24
|
token: apiToken,
|
|
19
25
|
command: 'upload'
|
|
20
26
|
});
|
|
21
|
-
|
|
22
|
-
|
|
27
|
+
let tokenContext = await getTokenContext(client);
|
|
28
|
+
let baseUrl = apiUrl.replace(/\/api.*$/, '');
|
|
23
29
|
if (tokenContext.organization?.slug && tokenContext.project?.slug) {
|
|
24
30
|
return `${baseUrl}/${tokenContext.organization.slug}/${tokenContext.project.slug}/builds/${buildId}`;
|
|
25
31
|
}
|
|
@@ -31,7 +37,7 @@ async function constructBuildUrl(buildId, apiUrl, apiToken) {
|
|
|
31
37
|
}
|
|
32
38
|
|
|
33
39
|
// Fallback URL construction
|
|
34
|
-
|
|
40
|
+
let baseUrl = apiUrl.replace(/\/api.*$/, '');
|
|
35
41
|
return `${baseUrl}/builds/${buildId}`;
|
|
36
42
|
}
|
|
37
43
|
|
|
@@ -40,8 +46,23 @@ async function constructBuildUrl(buildId, apiUrl, apiToken) {
|
|
|
40
46
|
* @param {string} screenshotsPath - Path to screenshots
|
|
41
47
|
* @param {Object} options - Command options
|
|
42
48
|
* @param {Object} globalOptions - Global CLI options
|
|
49
|
+
* @param {Object} deps - Dependencies for testing
|
|
43
50
|
*/
|
|
44
|
-
export async function uploadCommand(screenshotsPath, options = {}, globalOptions = {}) {
|
|
51
|
+
export async function uploadCommand(screenshotsPath, options = {}, globalOptions = {}, deps = {}) {
|
|
52
|
+
let {
|
|
53
|
+
loadConfig = defaultLoadConfig,
|
|
54
|
+
createApiClient = defaultCreateApiClient,
|
|
55
|
+
finalizeBuild = defaultFinalizeBuild,
|
|
56
|
+
createUploader = defaultCreateUploader,
|
|
57
|
+
detectBranch = defaultDetectBranch,
|
|
58
|
+
detectCommit = defaultDetectCommit,
|
|
59
|
+
detectCommitMessage = defaultDetectCommitMessage,
|
|
60
|
+
detectPullRequestNumber = defaultDetectPullRequestNumber,
|
|
61
|
+
generateBuildNameWithGit = defaultGenerateBuildNameWithGit,
|
|
62
|
+
output = defaultOutput,
|
|
63
|
+
exit = code => process.exit(code),
|
|
64
|
+
buildUrlConstructor = constructBuildUrl
|
|
65
|
+
} = deps;
|
|
45
66
|
output.configure({
|
|
46
67
|
json: globalOptions.json,
|
|
47
68
|
verbose: globalOptions.verbose,
|
|
@@ -63,7 +84,11 @@ export async function uploadCommand(screenshotsPath, options = {}, globalOptions
|
|
|
63
84
|
// Validate API token
|
|
64
85
|
if (!config.apiKey) {
|
|
65
86
|
output.error('API token required. Use --token or set VIZZLY_TOKEN environment variable');
|
|
66
|
-
|
|
87
|
+
exit(1);
|
|
88
|
+
return {
|
|
89
|
+
success: false,
|
|
90
|
+
reason: 'no-api-key'
|
|
91
|
+
};
|
|
67
92
|
}
|
|
68
93
|
|
|
69
94
|
// Collect git metadata if not provided
|
|
@@ -83,10 +108,12 @@ export async function uploadCommand(screenshotsPath, options = {}, globalOptions
|
|
|
83
108
|
});
|
|
84
109
|
}
|
|
85
110
|
|
|
86
|
-
//
|
|
111
|
+
// Create uploader
|
|
87
112
|
output.startSpinner('Initializing uploader...');
|
|
88
|
-
|
|
89
|
-
|
|
113
|
+
let uploader = createUploader({
|
|
114
|
+
...config,
|
|
115
|
+
command: 'upload'
|
|
116
|
+
});
|
|
90
117
|
|
|
91
118
|
// Prepare upload options with progress callback
|
|
92
119
|
const uploadOptions = {
|
|
@@ -135,64 +162,81 @@ export async function uploadCommand(screenshotsPath, options = {}, globalOptions
|
|
|
135
162
|
if (result.buildId) {
|
|
136
163
|
output.progress('Finalizing build...');
|
|
137
164
|
try {
|
|
138
|
-
|
|
165
|
+
let client = createApiClient({
|
|
139
166
|
baseUrl: config.apiUrl,
|
|
140
167
|
token: config.apiKey,
|
|
141
168
|
command: 'upload'
|
|
142
169
|
});
|
|
143
|
-
|
|
144
|
-
await
|
|
170
|
+
let executionTime = Date.now() - uploadStartTime;
|
|
171
|
+
await finalizeBuild(client, result.buildId, true, executionTime);
|
|
145
172
|
} catch (error) {
|
|
146
173
|
output.warn(`Failed to finalize build: ${error.message}`);
|
|
147
174
|
}
|
|
148
175
|
}
|
|
149
|
-
output.
|
|
176
|
+
output.complete('Upload completed');
|
|
150
177
|
|
|
151
178
|
// Show Vizzly summary
|
|
152
179
|
if (result.buildId) {
|
|
153
|
-
output.
|
|
180
|
+
output.blank();
|
|
181
|
+
output.keyValue({
|
|
182
|
+
Uploaded: `${result.stats.uploaded} of ${result.stats.total}`,
|
|
183
|
+
Build: result.buildId
|
|
184
|
+
});
|
|
154
185
|
// Use API-provided URL or construct proper URL with org/project context
|
|
155
|
-
|
|
156
|
-
output.
|
|
186
|
+
let buildUrl = result.url || (await buildUrlConstructor(result.buildId, config.apiUrl, config.apiKey, deps));
|
|
187
|
+
output.blank();
|
|
188
|
+
output.labelValue('View', output.link('Results', buildUrl));
|
|
157
189
|
}
|
|
158
190
|
|
|
159
191
|
// Wait for build completion if requested
|
|
160
192
|
if (options.wait && result.buildId) {
|
|
161
|
-
output.info('Waiting for build completion...');
|
|
162
193
|
output.startSpinner('Processing comparisons...');
|
|
163
|
-
|
|
164
|
-
output.
|
|
194
|
+
let buildResult = await uploader.waitForBuild(result.buildId);
|
|
195
|
+
output.stopSpinner();
|
|
196
|
+
output.complete('Build processing completed');
|
|
165
197
|
|
|
166
198
|
// Show build processing results
|
|
199
|
+
let colors = output.getColors();
|
|
167
200
|
if (buildResult.failedComparisons > 0) {
|
|
168
|
-
output.
|
|
201
|
+
output.blank();
|
|
202
|
+
output.print(` ${colors.brand.danger(buildResult.failedComparisons)} visual comparisons failed`);
|
|
169
203
|
} else {
|
|
170
|
-
output.
|
|
204
|
+
output.blank();
|
|
205
|
+
output.print(` ${colors.brand.success(buildResult.passedComparisons)} visual comparisons passed`);
|
|
171
206
|
}
|
|
172
207
|
// Use API-provided URL or construct proper URL with org/project context
|
|
173
|
-
|
|
174
|
-
output.
|
|
208
|
+
let waitBuildUrl = buildResult.url || (await buildUrlConstructor(result.buildId, config.apiUrl, config.apiKey, deps));
|
|
209
|
+
output.blank();
|
|
210
|
+
output.labelValue('View', output.link('Results', waitBuildUrl));
|
|
175
211
|
}
|
|
176
212
|
output.cleanup();
|
|
213
|
+
return {
|
|
214
|
+
success: true,
|
|
215
|
+
result
|
|
216
|
+
};
|
|
177
217
|
} catch (error) {
|
|
178
218
|
// Mark build as failed if we have a buildId and config
|
|
179
219
|
if (buildId && config) {
|
|
180
220
|
try {
|
|
181
|
-
|
|
221
|
+
let client = createApiClient({
|
|
182
222
|
baseUrl: config.apiUrl,
|
|
183
223
|
token: config.apiKey,
|
|
184
224
|
command: 'upload'
|
|
185
225
|
});
|
|
186
|
-
|
|
187
|
-
await
|
|
226
|
+
let executionTime = Date.now() - uploadStartTime;
|
|
227
|
+
await finalizeBuild(client, buildId, false, executionTime);
|
|
188
228
|
} catch {
|
|
189
229
|
// Silent fail on cleanup
|
|
190
230
|
}
|
|
191
231
|
}
|
|
192
232
|
// Use user-friendly error message if available
|
|
193
|
-
|
|
233
|
+
let errorMessage = error?.getUserMessage ? error.getUserMessage() : error.message;
|
|
194
234
|
output.error(errorMessage || 'Upload failed', error);
|
|
195
|
-
|
|
235
|
+
exit(1);
|
|
236
|
+
return {
|
|
237
|
+
success: false,
|
|
238
|
+
error
|
|
239
|
+
};
|
|
196
240
|
}
|
|
197
241
|
}
|
|
198
242
|
|