@vizzly-testing/cli 0.8.0 → 0.9.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 +26 -13
- package/dist/cli.js +25 -1
- package/dist/client/index.js +77 -11
- package/dist/commands/init.js +23 -17
- package/dist/commands/tdd-daemon.js +312 -0
- package/dist/commands/tdd.js +45 -14
- package/dist/reporter/reporter-bundle.css +1 -0
- package/dist/reporter/reporter-bundle.iife.js +57 -0
- package/dist/server/handlers/api-handler.js +98 -30
- package/dist/server/handlers/tdd-handler.js +264 -77
- package/dist/server/http-server.js +358 -15
- package/dist/services/html-report-generator.js +77 -0
- package/dist/services/report-generator/report.css +56 -0
- package/dist/services/screenshot-server.js +6 -3
- package/dist/services/server-manager.js +2 -9
- package/dist/services/tdd-service.js +188 -25
- package/dist/services/test-runner.js +43 -1
- package/dist/types/commands/tdd-daemon.d.ts +18 -0
- package/dist/types/container/index.d.ts +1 -3
- package/dist/types/reporter/src/components/app-router.d.ts +3 -0
- package/dist/types/reporter/src/components/comparison/comparison-actions.d.ts +5 -0
- package/dist/types/reporter/src/components/comparison/comparison-card.d.ts +6 -0
- package/dist/types/reporter/src/components/comparison/comparison-list.d.ts +6 -0
- package/dist/types/reporter/src/components/comparison/comparison-viewer.d.ts +4 -0
- package/dist/types/reporter/src/components/comparison/view-mode-selector.d.ts +4 -0
- package/dist/types/reporter/src/components/comparison/viewer-modes/onion-viewer.d.ts +3 -0
- package/dist/types/reporter/src/components/comparison/viewer-modes/overlay-viewer.d.ts +3 -0
- package/dist/types/reporter/src/components/comparison/viewer-modes/side-by-side-viewer.d.ts +3 -0
- package/dist/types/reporter/src/components/comparison/viewer-modes/toggle-viewer.d.ts +3 -0
- package/dist/types/reporter/src/components/dashboard/dashboard-filters.d.ts +16 -0
- package/dist/types/reporter/src/components/dashboard/dashboard-header.d.ts +5 -0
- package/dist/types/reporter/src/components/dashboard/dashboard-stats.d.ts +4 -0
- package/dist/types/reporter/src/components/dashboard/empty-state.d.ts +8 -0
- package/dist/types/reporter/src/components/ui/smart-image.d.ts +7 -0
- package/dist/types/reporter/src/components/ui/status-badge.d.ts +5 -0
- package/dist/types/reporter/src/components/ui/toast.d.ts +4 -0
- package/dist/types/reporter/src/components/views/comparisons-view.d.ts +6 -0
- package/dist/types/reporter/src/components/views/stats-view.d.ts +6 -0
- package/dist/types/reporter/src/hooks/use-baseline-actions.d.ts +5 -0
- package/dist/types/reporter/src/hooks/use-comparison-filters.d.ts +20 -0
- package/dist/types/reporter/src/hooks/use-image-loader.d.ts +1 -0
- package/dist/types/reporter/src/hooks/use-report-data.d.ts +7 -0
- package/dist/types/reporter/src/hooks/use-vizzly-api.d.ts +9 -0
- package/dist/types/reporter/src/main.d.ts +1 -0
- package/dist/types/reporter/src/services/api-client.d.ts +4 -0
- package/dist/types/reporter/src/utils/comparison-helpers.d.ts +16 -0
- package/dist/types/reporter/src/utils/constants.d.ts +37 -0
- package/dist/types/reporter/vite.config.d.ts +2 -0
- package/dist/types/reporter/vite.dev.config.d.ts +2 -0
- package/dist/types/sdk/index.d.ts +1 -2
- package/dist/types/server/handlers/api-handler.d.ts +5 -14
- package/dist/types/server/handlers/tdd-handler.d.ts +18 -17
- package/dist/types/server/http-server.d.ts +2 -1
- package/dist/types/services/base-service.d.ts +1 -2
- package/dist/types/services/html-report-generator.d.ts +3 -3
- package/dist/types/services/screenshot-server.d.ts +1 -1
- package/dist/types/services/server-manager.d.ts +25 -35
- package/dist/types/services/tdd-service.d.ts +7 -1
- package/dist/types/services/test-runner.d.ts +6 -1
- package/dist/types/utils/build-history.d.ts +16 -0
- package/dist/types/utils/config-loader.d.ts +1 -1
- package/dist/types/utils/console-ui.d.ts +1 -1
- package/dist/types/utils/git.d.ts +4 -4
- package/dist/types/utils/security.d.ts +2 -1
- package/dist/utils/build-history.js +103 -0
- package/dist/utils/security.js +14 -5
- package/docs/api-reference.md +1 -3
- package/docs/getting-started.md +1 -1
- package/docs/tdd-mode.md +176 -112
- package/package.json +17 -4
package/README.md
CHANGED
|
@@ -55,7 +55,7 @@ vizzly upload ./screenshots --build-name "Release v1.2.3"
|
|
|
55
55
|
vizzly run "npm test"
|
|
56
56
|
|
|
57
57
|
# Use TDD mode for local development
|
|
58
|
-
vizzly tdd "npm test"
|
|
58
|
+
vizzly tdd run "npm test"
|
|
59
59
|
```
|
|
60
60
|
|
|
61
61
|
### In your test code
|
|
@@ -125,29 +125,42 @@ vizzly run "npm test" --parallel-id "ci-run-123" # For parallel CI builds
|
|
|
125
125
|
For local visual testing with immediate feedback, use the dedicated `tdd` command:
|
|
126
126
|
|
|
127
127
|
```bash
|
|
128
|
-
#
|
|
129
|
-
vizzly tdd
|
|
128
|
+
# Start interactive TDD dashboard
|
|
129
|
+
vizzly tdd start
|
|
130
130
|
|
|
131
|
-
#
|
|
132
|
-
|
|
131
|
+
# Run your tests in watch mode
|
|
132
|
+
npm test -- --watch
|
|
133
133
|
|
|
134
|
-
#
|
|
135
|
-
vizzly tdd "npm test" --set-baseline
|
|
134
|
+
# View the dashboard at http://localhost:47392
|
|
136
135
|
```
|
|
137
136
|
|
|
138
|
-
**Interactive
|
|
139
|
-
- **
|
|
140
|
-
- **
|
|
141
|
-
- **
|
|
142
|
-
- **
|
|
137
|
+
**Interactive Dashboard:** The TDD dashboard provides real-time visual feedback:
|
|
138
|
+
- **Live Updates** - See comparisons as tests run
|
|
139
|
+
- **Visual Diff Modes** - Overlay, side-by-side, onion skin, and toggle views
|
|
140
|
+
- **Baseline Management** - Accept/reject changes directly from the UI
|
|
141
|
+
- **Test Statistics** - Real-time pass/fail metrics
|
|
142
|
+
- **Dark Theme** - Easy on the eyes during long sessions
|
|
143
|
+
|
|
144
|
+
**TDD Subcommands:**
|
|
145
|
+
|
|
146
|
+
```bash
|
|
147
|
+
# Start the TDD dashboard server
|
|
148
|
+
vizzly tdd start [options]
|
|
149
|
+
|
|
150
|
+
# Run tests in single-shot mode
|
|
151
|
+
vizzly tdd run "npm test" [options]
|
|
152
|
+
|
|
153
|
+
# Stop a running TDD server
|
|
154
|
+
vizzly tdd stop
|
|
155
|
+
```
|
|
143
156
|
|
|
144
157
|
**TDD Command Options:**
|
|
145
158
|
- `--set-baseline` - Accept current screenshots as new baseline
|
|
146
159
|
- `--baseline-build <id>` - Use specific build as baseline (requires API token)
|
|
147
|
-
- `--baseline-comparison <id>` - Use specific comparison as baseline (requires API token)
|
|
148
160
|
- `--threshold <number>` - Comparison threshold (0-1, default: 0.1)
|
|
149
161
|
- `--port <port>` - Server port (default: 47392)
|
|
150
162
|
- `--timeout <ms>` - Server timeout (default: 30000)
|
|
163
|
+
- `--daemon` - Run server in background (start command only)
|
|
151
164
|
|
|
152
165
|
### Setup and Status Commands
|
|
153
166
|
```bash
|
package/dist/cli.js
CHANGED
|
@@ -5,6 +5,7 @@ import { init } from './commands/init.js';
|
|
|
5
5
|
import { uploadCommand, validateUploadOptions } from './commands/upload.js';
|
|
6
6
|
import { runCommand, validateRunOptions } from './commands/run.js';
|
|
7
7
|
import { tddCommand, validateTddOptions } from './commands/tdd.js';
|
|
8
|
+
import { tddStartCommand, tddStopCommand, tddStatusCommand } from './commands/tdd-daemon.js';
|
|
8
9
|
import { statusCommand, validateStatusOptions } from './commands/status.js';
|
|
9
10
|
import { finalizeCommand, validateFinalizeOptions } from './commands/finalize.js';
|
|
10
11
|
import { doctorCommand, validateDoctorOptions } from './commands/doctor.js';
|
|
@@ -29,7 +30,30 @@ program.command('upload').description('Upload screenshots to Vizzly').argument('
|
|
|
29
30
|
}
|
|
30
31
|
await uploadCommand(path, options, globalOptions);
|
|
31
32
|
});
|
|
32
|
-
|
|
33
|
+
|
|
34
|
+
// TDD command with subcommands
|
|
35
|
+
const tddCmd = program.command('tdd').description('Run tests in TDD mode with local visual comparisons');
|
|
36
|
+
|
|
37
|
+
// TDD Start - Background server
|
|
38
|
+
tddCmd.command('start').description('Start background TDD server').option('--port <port>', 'Port for screenshot server', '47392').option('--open', 'Open dashboard in browser').option('--baseline-build <id>', 'Use specific build as baseline').option('--baseline-comparison <id>', 'Use specific comparison as baseline').option('--environment <env>', 'Environment name', 'test').option('--threshold <number>', 'Comparison threshold', parseFloat).option('--timeout <ms>', 'Server timeout in milliseconds', '30000').option('--token <token>', 'API token override').action(async options => {
|
|
39
|
+
const globalOptions = program.opts();
|
|
40
|
+
await tddStartCommand(options, globalOptions);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
// TDD Stop - Kill background server
|
|
44
|
+
tddCmd.command('stop').description('Stop background TDD server').action(async options => {
|
|
45
|
+
const globalOptions = program.opts();
|
|
46
|
+
await tddStopCommand(options, globalOptions);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
// TDD Status - Check server status
|
|
50
|
+
tddCmd.command('status').description('Check TDD server status').action(async options => {
|
|
51
|
+
const globalOptions = program.opts();
|
|
52
|
+
await tddStatusCommand(options, globalOptions);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// TDD Run - One-off test run (primary workflow)
|
|
56
|
+
tddCmd.command('run <command>').description('Run tests once in TDD mode with local visual comparisons').option('--port <port>', 'Port for screenshot server', '47392').option('--branch <branch>', 'Git branch override').option('--environment <env>', 'Environment name', 'test').option('--threshold <number>', 'Comparison threshold', parseFloat).option('--token <token>', 'API token override').option('--timeout <ms>', 'Server timeout in milliseconds', '30000').option('--baseline-build <id>', 'Use specific build as baseline').option('--baseline-comparison <id>', 'Use specific comparison as baseline').option('--set-baseline', 'Accept current screenshots as new baseline (overwrites existing)').action(async (command, options) => {
|
|
33
57
|
const globalOptions = program.opts();
|
|
34
58
|
|
|
35
59
|
// Validate options
|
package/dist/client/index.js
CHANGED
|
@@ -3,11 +3,14 @@
|
|
|
3
3
|
* @description Thin client for test runners - minimal API for taking screenshots
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import {
|
|
6
|
+
import { getServerUrl, getBuildId, isTddMode, setVizzlyEnabled } from '../utils/environment-config.js';
|
|
7
|
+
import { existsSync, readFileSync } from 'fs';
|
|
8
|
+
import { join, parse, dirname } from 'path';
|
|
7
9
|
|
|
8
10
|
// Internal client state
|
|
9
11
|
let currentClient = null;
|
|
10
12
|
let isDisabled = false;
|
|
13
|
+
let hasLoggedWarning = false;
|
|
11
14
|
|
|
12
15
|
/**
|
|
13
16
|
* Check if Vizzly is currently disabled
|
|
@@ -15,7 +18,8 @@ let isDisabled = false;
|
|
|
15
18
|
* @returns {boolean} True if disabled via environment variable or auto-disabled due to failure
|
|
16
19
|
*/
|
|
17
20
|
function isVizzlyDisabled() {
|
|
18
|
-
|
|
21
|
+
// Don't check isVizzlyEnabled() here - let auto-discovery happen first
|
|
22
|
+
return isDisabled;
|
|
19
23
|
}
|
|
20
24
|
|
|
21
25
|
/**
|
|
@@ -31,6 +35,37 @@ function disableVizzly(reason = 'disabled') {
|
|
|
31
35
|
}
|
|
32
36
|
}
|
|
33
37
|
|
|
38
|
+
/**
|
|
39
|
+
* Auto-discover local TDD server by checking for server.json
|
|
40
|
+
* @private
|
|
41
|
+
* @returns {string|null} Server URL if found
|
|
42
|
+
*/
|
|
43
|
+
function autoDiscoverTddServer() {
|
|
44
|
+
try {
|
|
45
|
+
// Look for .vizzly/server.json in current directory and parent directories
|
|
46
|
+
let currentDir = process.cwd();
|
|
47
|
+
const root = parse(currentDir).root;
|
|
48
|
+
while (currentDir !== root) {
|
|
49
|
+
const serverJsonPath = join(currentDir, '.vizzly', 'server.json');
|
|
50
|
+
if (existsSync(serverJsonPath)) {
|
|
51
|
+
try {
|
|
52
|
+
const serverInfo = JSON.parse(readFileSync(serverJsonPath, 'utf8'));
|
|
53
|
+
if (serverInfo.port) {
|
|
54
|
+
const url = `http://localhost:${serverInfo.port}`;
|
|
55
|
+
return url;
|
|
56
|
+
}
|
|
57
|
+
} catch {
|
|
58
|
+
// Invalid JSON, continue searching
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
currentDir = dirname(currentDir);
|
|
62
|
+
}
|
|
63
|
+
return null;
|
|
64
|
+
} catch {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
34
69
|
/**
|
|
35
70
|
* Get the current client instance
|
|
36
71
|
* @private
|
|
@@ -40,9 +75,19 @@ function getClient() {
|
|
|
40
75
|
return null;
|
|
41
76
|
}
|
|
42
77
|
if (!currentClient) {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
if
|
|
78
|
+
let serverUrl = getServerUrl();
|
|
79
|
+
|
|
80
|
+
// Auto-detect local TDD server and enable Vizzly if TDD server is found
|
|
81
|
+
if (!serverUrl) {
|
|
82
|
+
serverUrl = autoDiscoverTddServer();
|
|
83
|
+
if (serverUrl) {
|
|
84
|
+
// Automatically enable Vizzly when TDD server is detected
|
|
85
|
+
setVizzlyEnabled(true);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// If we have a server URL, create the client (regardless of initial enabled state)
|
|
90
|
+
if (serverUrl) {
|
|
46
91
|
currentClient = createSimpleClient(serverUrl);
|
|
47
92
|
}
|
|
48
93
|
}
|
|
@@ -80,22 +125,39 @@ function createSimpleClient(serverUrl) {
|
|
|
80
125
|
};
|
|
81
126
|
});
|
|
82
127
|
|
|
83
|
-
// In TDD mode, if we get 422 (visual difference),
|
|
128
|
+
// In TDD mode, if we get 422 (visual difference), log but DON'T throw
|
|
129
|
+
// This allows all screenshots in the test to be captured and compared
|
|
84
130
|
if (response.status === 422 && errorData.tddMode && errorData.comparison) {
|
|
85
131
|
const comp = errorData.comparison;
|
|
86
|
-
|
|
132
|
+
const diffPercent = comp.diffPercentage ? comp.diffPercentage.toFixed(2) : '0.00';
|
|
133
|
+
|
|
134
|
+
// Extract port from serverUrl (e.g., "http://localhost:47392" -> "47392")
|
|
135
|
+
const urlMatch = serverUrl.match(/:(\d+)/);
|
|
136
|
+
const port = urlMatch ? urlMatch[1] : '47392';
|
|
137
|
+
const dashboardUrl = `http://localhost:${port}/dashboard`;
|
|
138
|
+
|
|
139
|
+
// Just log warning - don't throw by default in TDD mode
|
|
140
|
+
// This allows all screenshots to be captured
|
|
141
|
+
console.warn(`⚠️ Visual diff: ${comp.name} (${diffPercent}%) → ${dashboardUrl}`);
|
|
142
|
+
|
|
143
|
+
// Return success so test continues and captures remaining screenshots
|
|
144
|
+
return {
|
|
145
|
+
success: true,
|
|
146
|
+
status: 'failed',
|
|
147
|
+
name: comp.name,
|
|
148
|
+
diffPercentage: comp.diffPercentage
|
|
149
|
+
};
|
|
87
150
|
}
|
|
88
151
|
throw new Error(`Screenshot failed: ${response.status} ${response.statusText} - ${errorData.error || 'Unknown error'}`);
|
|
89
152
|
}
|
|
90
153
|
return await response.json();
|
|
91
154
|
} catch (error) {
|
|
92
155
|
// In TDD mode with visual differences, throw the error to fail the test
|
|
93
|
-
if (error.message.includes('
|
|
156
|
+
if (error.message.toLowerCase().includes('visual diff')) {
|
|
94
157
|
// Clean output for TDD mode - don't spam with additional logs
|
|
95
158
|
throw error;
|
|
96
159
|
}
|
|
97
|
-
console.error(`
|
|
98
|
-
console.error(`Vizzly screenshot failed for ${name}: ${error.message}`);
|
|
160
|
+
console.error(`Vizzly screenshot failed for ${name}:`, error.message);
|
|
99
161
|
if (error.message.includes('fetch') || error.code === 'ECONNREFUSED') {
|
|
100
162
|
console.error(`Server URL: ${serverUrl}/screenshot`);
|
|
101
163
|
console.error('This usually means the Vizzly server is not running or not accessible');
|
|
@@ -157,7 +219,11 @@ export async function vizzlyScreenshot(name, imageBuffer, options = {}) {
|
|
|
157
219
|
}
|
|
158
220
|
const client = getClient();
|
|
159
221
|
if (!client) {
|
|
160
|
-
|
|
222
|
+
if (!hasLoggedWarning) {
|
|
223
|
+
console.warn('Vizzly client not initialized. Screenshots will be skipped.');
|
|
224
|
+
hasLoggedWarning = true;
|
|
225
|
+
disableVizzly();
|
|
226
|
+
}
|
|
161
227
|
return;
|
|
162
228
|
}
|
|
163
229
|
return client.screenshot(name, imageBuffer, options);
|
package/dist/commands/init.js
CHANGED
|
@@ -40,30 +40,36 @@ export class InitCommand {
|
|
|
40
40
|
const configContent = `export default {
|
|
41
41
|
// API configuration
|
|
42
42
|
// Set VIZZLY_TOKEN environment variable or uncomment and set here:
|
|
43
|
-
//
|
|
44
|
-
|
|
45
|
-
//
|
|
46
|
-
screenshots: {
|
|
47
|
-
directory: './screenshots',
|
|
48
|
-
formats: ['png']
|
|
49
|
-
},
|
|
50
|
-
|
|
51
|
-
// Server configuration
|
|
43
|
+
// apiKey: 'your-token-here',
|
|
44
|
+
|
|
45
|
+
// Server configuration (for run command)
|
|
52
46
|
server: {
|
|
53
47
|
port: 47392,
|
|
48
|
+
timeout: 30000,
|
|
54
49
|
screenshotPath: '/screenshot'
|
|
55
50
|
},
|
|
56
|
-
|
|
57
|
-
//
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
51
|
+
|
|
52
|
+
// Build configuration
|
|
53
|
+
build: {
|
|
54
|
+
name: 'Build {timestamp}',
|
|
55
|
+
environment: 'test'
|
|
61
56
|
},
|
|
62
|
-
|
|
63
|
-
// Upload configuration
|
|
57
|
+
|
|
58
|
+
// Upload configuration (for upload command)
|
|
64
59
|
upload: {
|
|
65
|
-
|
|
60
|
+
screenshotsDir: './screenshots',
|
|
61
|
+
batchSize: 10,
|
|
66
62
|
timeout: 30000
|
|
63
|
+
},
|
|
64
|
+
|
|
65
|
+
// Comparison configuration
|
|
66
|
+
comparison: {
|
|
67
|
+
threshold: 0.1
|
|
68
|
+
},
|
|
69
|
+
|
|
70
|
+
// TDD configuration
|
|
71
|
+
tdd: {
|
|
72
|
+
openReport: false // Whether to auto-open HTML report in browser
|
|
67
73
|
}
|
|
68
74
|
};
|
|
69
75
|
`;
|
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
import { writeFileSync, readFileSync, existsSync, unlinkSync, mkdirSync } from 'fs';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import { spawn } from 'child_process';
|
|
4
|
+
import { ConsoleUI } from '../utils/console-ui.js';
|
|
5
|
+
import { tddCommand } from './tdd.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Start TDD server in daemon mode
|
|
9
|
+
* @param {Object} options - Command options
|
|
10
|
+
* @param {Object} globalOptions - Global CLI options
|
|
11
|
+
*/
|
|
12
|
+
export async function tddStartCommand(options = {}, globalOptions = {}) {
|
|
13
|
+
const ui = new ConsoleUI({
|
|
14
|
+
json: globalOptions.json,
|
|
15
|
+
verbose: globalOptions.verbose,
|
|
16
|
+
color: !globalOptions.noColor
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
// Check if server already running
|
|
20
|
+
if (await isServerRunning(options.port || 47392)) {
|
|
21
|
+
const port = options.port || 47392;
|
|
22
|
+
ui.info(`TDD server already running at http://localhost:${port}`);
|
|
23
|
+
ui.info(`Dashboard: http://localhost:${port}/dashboard`);
|
|
24
|
+
if (options.open) {
|
|
25
|
+
openDashboard(port);
|
|
26
|
+
}
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
try {
|
|
30
|
+
// Ensure .vizzly directory exists
|
|
31
|
+
const vizzlyDir = join(process.cwd(), '.vizzly');
|
|
32
|
+
if (!existsSync(vizzlyDir)) {
|
|
33
|
+
mkdirSync(vizzlyDir, {
|
|
34
|
+
recursive: true
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
const port = options.port || 47392;
|
|
38
|
+
|
|
39
|
+
// Use existing tddCommand but with daemon mode - this will start and keep running
|
|
40
|
+
const {
|
|
41
|
+
cleanup
|
|
42
|
+
} = await tddCommand(null,
|
|
43
|
+
// No test command - server only
|
|
44
|
+
{
|
|
45
|
+
...options,
|
|
46
|
+
daemon: true // Flag to indicate daemon mode
|
|
47
|
+
}, globalOptions);
|
|
48
|
+
|
|
49
|
+
// The server is now running in this process
|
|
50
|
+
// Store our PID for the stop command
|
|
51
|
+
const pidFile = join(vizzlyDir, 'server.pid');
|
|
52
|
+
writeFileSync(pidFile, process.pid.toString());
|
|
53
|
+
const serverInfo = {
|
|
54
|
+
pid: process.pid,
|
|
55
|
+
port: port,
|
|
56
|
+
startTime: Date.now()
|
|
57
|
+
};
|
|
58
|
+
writeFileSync(join(vizzlyDir, 'server.json'), JSON.stringify(serverInfo, null, 2));
|
|
59
|
+
ui.success(`TDD server started at http://localhost:${port}`);
|
|
60
|
+
ui.info('');
|
|
61
|
+
ui.info('Dashboard URLs:');
|
|
62
|
+
ui.info(` Comparisons: http://localhost:${port}/`);
|
|
63
|
+
ui.info(` Stats: http://localhost:${port}/stats`);
|
|
64
|
+
ui.info('');
|
|
65
|
+
ui.info('Next steps:');
|
|
66
|
+
ui.info(' 1. Run your tests (any test runner)');
|
|
67
|
+
ui.info(' 2. Open the dashboard in your browser');
|
|
68
|
+
ui.info(' 3. Manage baselines in the Stats view');
|
|
69
|
+
ui.info('');
|
|
70
|
+
ui.info('Stop server: npx vizzly tdd stop');
|
|
71
|
+
if (options.open) {
|
|
72
|
+
openDashboard(port);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Set up graceful shutdown
|
|
76
|
+
const handleShutdown = async signal => {
|
|
77
|
+
ui.info(`\nReceived ${signal}, shutting down gracefully...`);
|
|
78
|
+
try {
|
|
79
|
+
// Clean up PID files
|
|
80
|
+
if (existsSync(pidFile)) unlinkSync(pidFile);
|
|
81
|
+
const serverFile = join(vizzlyDir, 'server.json');
|
|
82
|
+
if (existsSync(serverFile)) unlinkSync(serverFile);
|
|
83
|
+
|
|
84
|
+
// Use the cleanup function from tddCommand
|
|
85
|
+
await cleanup();
|
|
86
|
+
ui.success('TDD server stopped');
|
|
87
|
+
} catch (error) {
|
|
88
|
+
ui.error('Error during shutdown:', error);
|
|
89
|
+
}
|
|
90
|
+
process.exit(0);
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
// Register signal handlers
|
|
94
|
+
process.on('SIGINT', () => handleShutdown('SIGINT'));
|
|
95
|
+
process.on('SIGTERM', () => handleShutdown('SIGTERM'));
|
|
96
|
+
|
|
97
|
+
// Keep process alive
|
|
98
|
+
process.stdin.resume();
|
|
99
|
+
} catch (error) {
|
|
100
|
+
ui.error('Failed to start TDD daemon', error);
|
|
101
|
+
process.exit(1);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Stop TDD daemon server
|
|
107
|
+
* @param {Object} options - Command options
|
|
108
|
+
* @param {Object} globalOptions - Global CLI options
|
|
109
|
+
*/
|
|
110
|
+
export async function tddStopCommand(options = {}, globalOptions = {}) {
|
|
111
|
+
const ui = new ConsoleUI({
|
|
112
|
+
json: globalOptions.json,
|
|
113
|
+
verbose: globalOptions.verbose,
|
|
114
|
+
color: !globalOptions.noColor
|
|
115
|
+
});
|
|
116
|
+
const vizzlyDir = join(process.cwd(), '.vizzly');
|
|
117
|
+
const pidFile = join(vizzlyDir, 'server.pid');
|
|
118
|
+
const serverFile = join(vizzlyDir, 'server.json');
|
|
119
|
+
|
|
120
|
+
// First try to find process by PID file
|
|
121
|
+
let pid = null;
|
|
122
|
+
if (existsSync(pidFile)) {
|
|
123
|
+
try {
|
|
124
|
+
pid = parseInt(readFileSync(pidFile, 'utf8').trim(), 10);
|
|
125
|
+
} catch {
|
|
126
|
+
// Invalid PID file
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// If no PID file or invalid, try to find by port using lsof
|
|
131
|
+
const port = options.port || 47392;
|
|
132
|
+
if (!pid) {
|
|
133
|
+
try {
|
|
134
|
+
const lsofProcess = spawn('lsof', ['-ti', `:${port}`], {
|
|
135
|
+
stdio: 'pipe'
|
|
136
|
+
});
|
|
137
|
+
let lsofOutput = '';
|
|
138
|
+
lsofProcess.stdout.on('data', data => {
|
|
139
|
+
lsofOutput += data.toString();
|
|
140
|
+
});
|
|
141
|
+
await new Promise(resolve => {
|
|
142
|
+
lsofProcess.on('close', code => {
|
|
143
|
+
if (code === 0 && lsofOutput.trim()) {
|
|
144
|
+
const foundPid = parseInt(lsofOutput.trim().split('\n')[0], 10);
|
|
145
|
+
if (foundPid && !isNaN(foundPid)) {
|
|
146
|
+
pid = foundPid;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
resolve();
|
|
150
|
+
});
|
|
151
|
+
lsofProcess.on('error', () => {
|
|
152
|
+
// lsof not available, that's ok
|
|
153
|
+
resolve();
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
} catch {
|
|
157
|
+
// lsof failed, that's ok too
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
if (!pid) {
|
|
161
|
+
ui.warning('No TDD server running');
|
|
162
|
+
|
|
163
|
+
// Clean up any stale files
|
|
164
|
+
if (existsSync(pidFile)) unlinkSync(pidFile);
|
|
165
|
+
if (existsSync(serverFile)) unlinkSync(serverFile);
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
try {
|
|
169
|
+
// Try to kill the process gracefully
|
|
170
|
+
process.kill(pid, 'SIGTERM');
|
|
171
|
+
ui.info(`Stopping TDD server (PID: ${pid})...`);
|
|
172
|
+
|
|
173
|
+
// Give it a moment to shut down gracefully
|
|
174
|
+
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
175
|
+
|
|
176
|
+
// Check if it's still running
|
|
177
|
+
try {
|
|
178
|
+
process.kill(pid, 0); // Just check if process exists
|
|
179
|
+
// If we get here, process is still running, force kill it
|
|
180
|
+
process.kill(pid, 'SIGKILL');
|
|
181
|
+
ui.info('Force killed TDD server');
|
|
182
|
+
} catch {
|
|
183
|
+
// Process is gone, which is what we want
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Clean up files
|
|
187
|
+
if (existsSync(pidFile)) unlinkSync(pidFile);
|
|
188
|
+
if (existsSync(serverFile)) unlinkSync(serverFile);
|
|
189
|
+
ui.success('TDD server stopped');
|
|
190
|
+
} catch (error) {
|
|
191
|
+
if (error.code === 'ESRCH') {
|
|
192
|
+
// Process not found - clean up stale files
|
|
193
|
+
ui.warning('TDD server was not running (cleaning up stale files)');
|
|
194
|
+
if (existsSync(pidFile)) unlinkSync(pidFile);
|
|
195
|
+
if (existsSync(serverFile)) unlinkSync(serverFile);
|
|
196
|
+
} else {
|
|
197
|
+
ui.error('Error stopping TDD server', error);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Check TDD daemon server status
|
|
204
|
+
* @param {Object} options - Command options
|
|
205
|
+
* @param {Object} globalOptions - Global CLI options
|
|
206
|
+
*/
|
|
207
|
+
export async function tddStatusCommand(options, globalOptions = {}) {
|
|
208
|
+
const ui = new ConsoleUI({
|
|
209
|
+
json: globalOptions.json,
|
|
210
|
+
verbose: globalOptions.verbose,
|
|
211
|
+
color: !globalOptions.noColor
|
|
212
|
+
});
|
|
213
|
+
const vizzlyDir = join(process.cwd(), '.vizzly');
|
|
214
|
+
const pidFile = join(vizzlyDir, 'server.pid');
|
|
215
|
+
const serverFile = join(vizzlyDir, 'server.json');
|
|
216
|
+
if (!existsSync(pidFile)) {
|
|
217
|
+
ui.info('TDD server not running');
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
try {
|
|
221
|
+
const pid = parseInt(readFileSync(pidFile, 'utf8').trim(), 10);
|
|
222
|
+
|
|
223
|
+
// Check if process is actually running
|
|
224
|
+
process.kill(pid, 0); // Signal 0 just checks if process exists
|
|
225
|
+
|
|
226
|
+
let serverInfo = {
|
|
227
|
+
port: 47392
|
|
228
|
+
};
|
|
229
|
+
if (existsSync(serverFile)) {
|
|
230
|
+
serverInfo = JSON.parse(readFileSync(serverFile, 'utf8'));
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Try to check health endpoint
|
|
234
|
+
const health = await checkServerHealth(serverInfo.port);
|
|
235
|
+
if (health.running) {
|
|
236
|
+
ui.success(`TDD server running (PID: ${pid})`);
|
|
237
|
+
ui.info(`Server: http://localhost:${serverInfo.port}`);
|
|
238
|
+
ui.info(`Dashboard: http://localhost:${serverInfo.port}/dashboard`);
|
|
239
|
+
if (serverInfo.startTime) {
|
|
240
|
+
const uptime = Math.floor((Date.now() - serverInfo.startTime) / 1000);
|
|
241
|
+
ui.info(`Uptime: ${uptime} seconds`);
|
|
242
|
+
}
|
|
243
|
+
} else {
|
|
244
|
+
ui.warning('TDD server process exists but not responding to health checks');
|
|
245
|
+
}
|
|
246
|
+
} catch (error) {
|
|
247
|
+
if (error.code === 'ESRCH') {
|
|
248
|
+
ui.warning('TDD server process not found (cleaning up stale files)');
|
|
249
|
+
unlinkSync(pidFile);
|
|
250
|
+
if (existsSync(serverFile)) {
|
|
251
|
+
unlinkSync(serverFile);
|
|
252
|
+
}
|
|
253
|
+
} else {
|
|
254
|
+
ui.error('Error checking TDD server status', error);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Check if server is running on given port
|
|
261
|
+
* @private
|
|
262
|
+
*/
|
|
263
|
+
async function isServerRunning(port = 47392) {
|
|
264
|
+
try {
|
|
265
|
+
const health = await checkServerHealth(port);
|
|
266
|
+
return health.running;
|
|
267
|
+
} catch {
|
|
268
|
+
return false;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Check server health endpoint
|
|
274
|
+
* @private
|
|
275
|
+
*/
|
|
276
|
+
async function checkServerHealth(port = 47392) {
|
|
277
|
+
try {
|
|
278
|
+
const response = await fetch(`http://localhost:${port}/health`);
|
|
279
|
+
const data = await response.json();
|
|
280
|
+
return {
|
|
281
|
+
running: response.ok,
|
|
282
|
+
port: data.port,
|
|
283
|
+
uptime: data.uptime
|
|
284
|
+
};
|
|
285
|
+
} catch {
|
|
286
|
+
return {
|
|
287
|
+
running: false
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Open dashboard in default browser
|
|
294
|
+
* @private
|
|
295
|
+
*/
|
|
296
|
+
function openDashboard(port = 47392) {
|
|
297
|
+
const url = `http://localhost:${port}/dashboard`;
|
|
298
|
+
|
|
299
|
+
// Cross-platform open command
|
|
300
|
+
let openCmd;
|
|
301
|
+
if (process.platform === 'darwin') {
|
|
302
|
+
openCmd = 'open';
|
|
303
|
+
} else if (process.platform === 'win32') {
|
|
304
|
+
openCmd = 'start';
|
|
305
|
+
} else {
|
|
306
|
+
openCmd = 'xdg-open';
|
|
307
|
+
}
|
|
308
|
+
spawn(openCmd, [url], {
|
|
309
|
+
detached: true,
|
|
310
|
+
stdio: 'ignore'
|
|
311
|
+
}).unref();
|
|
312
|
+
}
|