@vizzly-testing/cli 0.13.4 → 0.15.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/dist/cli.js +68 -68
- package/dist/commands/doctor.js +30 -34
- package/dist/commands/finalize.js +24 -23
- package/dist/commands/init.js +30 -28
- package/dist/commands/login.js +49 -55
- package/dist/commands/logout.js +14 -19
- package/dist/commands/project.js +83 -103
- package/dist/commands/run.js +77 -89
- package/dist/commands/status.js +48 -49
- package/dist/commands/tdd-daemon.js +90 -86
- package/dist/commands/tdd.js +59 -88
- package/dist/commands/upload.js +57 -57
- package/dist/commands/whoami.js +40 -45
- package/dist/index.js +2 -5
- package/dist/plugin-loader.js +15 -17
- package/dist/reporter/reporter-bundle.css +1 -1
- package/dist/reporter/reporter-bundle.iife.js +78 -32
- package/dist/sdk/index.js +36 -45
- package/dist/server/handlers/api-handler.js +14 -15
- package/dist/server/handlers/tdd-handler.js +34 -37
- package/dist/server/http-server.js +75 -869
- package/dist/server/middleware/cors.js +22 -0
- package/dist/server/middleware/json-parser.js +35 -0
- package/dist/server/middleware/response.js +79 -0
- package/dist/server/routers/assets.js +91 -0
- package/dist/server/routers/auth.js +144 -0
- package/dist/server/routers/baseline.js +163 -0
- package/dist/server/routers/cloud-proxy.js +146 -0
- package/dist/server/routers/config.js +126 -0
- package/dist/server/routers/dashboard.js +130 -0
- package/dist/server/routers/health.js +61 -0
- package/dist/server/routers/projects.js +168 -0
- package/dist/server/routers/screenshot.js +86 -0
- package/dist/services/auth-service.js +1 -1
- package/dist/services/build-manager.js +13 -40
- package/dist/services/config-service.js +2 -4
- package/dist/services/html-report-generator.js +6 -5
- package/dist/services/index.js +64 -0
- package/dist/services/project-service.js +121 -40
- package/dist/services/screenshot-server.js +9 -9
- package/dist/services/server-manager.js +11 -18
- package/dist/services/static-report-generator.js +3 -4
- package/dist/services/tdd-service.js +246 -103
- package/dist/services/test-runner.js +24 -25
- package/dist/services/uploader.js +5 -4
- package/dist/types/commands/init.d.ts +1 -2
- package/dist/types/index.d.ts +2 -3
- package/dist/types/plugin-loader.d.ts +1 -2
- package/dist/types/reporter/src/api/client.d.ts +178 -0
- package/dist/types/reporter/src/components/app-router.d.ts +1 -3
- package/dist/types/reporter/src/components/code-block.d.ts +4 -0
- package/dist/types/reporter/src/components/comparison/comparison-modes/onion-skin-mode.d.ts +10 -0
- package/dist/types/reporter/src/components/comparison/comparison-modes/overlay-mode.d.ts +11 -0
- package/dist/types/reporter/src/components/comparison/comparison-modes/shared/base-comparison-mode.d.ts +14 -0
- package/dist/types/reporter/src/components/comparison/comparison-modes/shared/image-renderer.d.ts +30 -0
- package/dist/types/reporter/src/components/comparison/comparison-modes/toggle-view.d.ts +8 -0
- package/dist/types/reporter/src/components/comparison/comparison-viewer.d.ts +4 -0
- package/dist/types/reporter/src/components/comparison/fullscreen-viewer.d.ts +13 -0
- package/dist/types/reporter/src/components/comparison/screenshot-display.d.ts +16 -0
- package/dist/types/reporter/src/components/comparison/screenshot-list.d.ts +9 -0
- package/dist/types/reporter/src/components/comparison/variant-selector.d.ts +1 -1
- package/dist/types/reporter/src/components/design-system/alert.d.ts +9 -0
- package/dist/types/reporter/src/components/design-system/badge.d.ts +17 -0
- package/dist/types/reporter/src/components/design-system/button.d.ts +19 -0
- package/dist/types/reporter/src/components/design-system/card.d.ts +31 -0
- package/dist/types/reporter/src/components/design-system/empty-state.d.ts +13 -0
- package/dist/types/reporter/src/components/design-system/form-controls.d.ts +44 -0
- package/dist/types/reporter/src/components/design-system/health-ring.d.ts +7 -0
- package/dist/types/reporter/src/components/design-system/index.d.ts +11 -0
- package/dist/types/reporter/src/components/design-system/modal.d.ts +10 -0
- package/dist/types/reporter/src/components/design-system/skeleton.d.ts +19 -0
- package/dist/types/reporter/src/components/design-system/spinner.d.ts +10 -0
- package/dist/types/reporter/src/components/design-system/tabs.d.ts +13 -0
- package/dist/types/reporter/src/components/layout/header.d.ts +5 -0
- package/dist/types/reporter/src/components/layout/index.d.ts +2 -0
- package/dist/types/reporter/src/components/layout/layout.d.ts +6 -0
- package/dist/types/reporter/src/components/views/builds-view.d.ts +1 -0
- package/dist/types/reporter/src/components/views/comparison-detail-view.d.ts +5 -0
- package/dist/types/reporter/src/components/views/comparisons-view.d.ts +5 -6
- package/dist/types/reporter/src/components/views/stats-view.d.ts +1 -6
- package/dist/types/reporter/src/components/waiting-for-screenshots.d.ts +1 -0
- package/dist/types/reporter/src/hooks/queries/use-auth-queries.d.ts +15 -0
- package/dist/types/reporter/src/hooks/queries/use-cloud-queries.d.ts +6 -0
- package/dist/types/reporter/src/hooks/queries/use-config-queries.d.ts +6 -0
- package/dist/types/reporter/src/hooks/queries/use-tdd-queries.d.ts +9 -0
- package/dist/types/reporter/src/lib/query-client.d.ts +2 -0
- package/dist/types/reporter/src/lib/query-keys.d.ts +13 -0
- package/dist/types/sdk/index.d.ts +2 -4
- package/dist/types/server/handlers/tdd-handler.d.ts +2 -0
- package/dist/types/server/http-server.d.ts +1 -1
- package/dist/types/server/middleware/cors.d.ts +11 -0
- package/dist/types/server/middleware/json-parser.d.ts +10 -0
- package/dist/types/server/middleware/response.d.ts +50 -0
- package/dist/types/server/routers/assets.d.ts +6 -0
- package/dist/types/server/routers/auth.d.ts +9 -0
- package/dist/types/server/routers/baseline.d.ts +13 -0
- package/dist/types/server/routers/cloud-proxy.d.ts +11 -0
- package/dist/types/server/routers/config.d.ts +9 -0
- package/dist/types/server/routers/dashboard.d.ts +6 -0
- package/dist/types/server/routers/health.d.ts +11 -0
- package/dist/types/server/routers/projects.d.ts +9 -0
- package/dist/types/server/routers/screenshot.d.ts +11 -0
- package/dist/types/services/build-manager.d.ts +4 -3
- package/dist/types/services/config-service.d.ts +2 -3
- package/dist/types/services/index.d.ts +7 -0
- package/dist/types/services/project-service.d.ts +6 -4
- package/dist/types/services/screenshot-server.d.ts +5 -5
- package/dist/types/services/server-manager.d.ts +5 -3
- package/dist/types/services/tdd-service.d.ts +12 -1
- package/dist/types/services/test-runner.d.ts +3 -3
- package/dist/types/utils/output.d.ts +84 -0
- package/dist/utils/config-loader.js +24 -48
- package/dist/utils/global-config.js +2 -17
- package/dist/utils/output.js +445 -0
- package/dist/utils/security.js +3 -4
- package/docs/api-reference.md +0 -1
- package/docs/plugins.md +22 -22
- package/package.json +3 -2
- package/dist/container/index.js +0 -215
- package/dist/services/base-service.js +0 -154
- package/dist/types/container/index.d.ts +0 -59
- package/dist/types/reporter/src/components/comparison/viewer-modes/onion-viewer.d.ts +0 -3
- package/dist/types/reporter/src/components/comparison/viewer-modes/overlay-viewer.d.ts +0 -3
- package/dist/types/reporter/src/components/comparison/viewer-modes/side-by-side-viewer.d.ts +0 -3
- package/dist/types/reporter/src/components/comparison/viewer-modes/toggle-viewer.d.ts +0 -3
- package/dist/types/reporter/src/components/dashboard/dashboard-header.d.ts +0 -5
- package/dist/types/reporter/src/components/dashboard/dashboard-stats.d.ts +0 -4
- package/dist/types/reporter/src/components/dashboard/empty-state.d.ts +0 -8
- package/dist/types/reporter/src/components/ui/form-field.d.ts +0 -16
- package/dist/types/reporter/src/components/ui/status-badge.d.ts +0 -5
- package/dist/types/reporter/src/hooks/use-auth.d.ts +0 -10
- package/dist/types/reporter/src/hooks/use-baseline-actions.d.ts +0 -5
- package/dist/types/reporter/src/hooks/use-config.d.ts +0 -9
- package/dist/types/reporter/src/hooks/use-projects.d.ts +0 -10
- package/dist/types/reporter/src/hooks/use-report-data.d.ts +0 -7
- package/dist/types/reporter/src/hooks/use-vizzly-api.d.ts +0 -9
- package/dist/types/services/base-service.d.ts +0 -71
- package/dist/types/utils/console-ui.d.ts +0 -61
- package/dist/types/utils/logger-factory.d.ts +0 -26
- package/dist/types/utils/logger.d.ts +0 -79
- package/dist/utils/console-ui.js +0 -241
- package/dist/utils/logger-factory.js +0 -76
- package/dist/utils/logger.js +0 -231
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Config Router
|
|
3
|
+
* Handles configuration management endpoints
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { parseJsonBody } from '../middleware/json-parser.js';
|
|
7
|
+
import { sendSuccess, sendError, sendServiceUnavailable } from '../middleware/response.js';
|
|
8
|
+
import * as output from '../../utils/output.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Create config router
|
|
12
|
+
* @param {Object} context - Router context
|
|
13
|
+
* @param {Object} context.configService - Config service
|
|
14
|
+
* @returns {Function} Route handler
|
|
15
|
+
*/
|
|
16
|
+
export function createConfigRouter({
|
|
17
|
+
configService
|
|
18
|
+
}) {
|
|
19
|
+
return async function handleConfigRoute(req, res, pathname) {
|
|
20
|
+
// Check if config service is available for all config routes
|
|
21
|
+
if (pathname.startsWith('/api/config') && !configService) {
|
|
22
|
+
sendServiceUnavailable(res, 'Config service');
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Get merged config with sources
|
|
27
|
+
if (req.method === 'GET' && pathname === '/api/config') {
|
|
28
|
+
try {
|
|
29
|
+
let configData = await configService.getConfig('merged');
|
|
30
|
+
sendSuccess(res, configData);
|
|
31
|
+
return true;
|
|
32
|
+
} catch (error) {
|
|
33
|
+
output.debug('Error fetching config:', {
|
|
34
|
+
error: error.message
|
|
35
|
+
});
|
|
36
|
+
sendError(res, 500, error.message);
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Get project-level config
|
|
42
|
+
if (req.method === 'GET' && pathname === '/api/config/project') {
|
|
43
|
+
try {
|
|
44
|
+
let configData = await configService.getConfig('project');
|
|
45
|
+
sendSuccess(res, configData);
|
|
46
|
+
return true;
|
|
47
|
+
} catch (error) {
|
|
48
|
+
output.debug('Error fetching project config:', {
|
|
49
|
+
error: error.message
|
|
50
|
+
});
|
|
51
|
+
sendError(res, 500, error.message);
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Get global config
|
|
57
|
+
if (req.method === 'GET' && pathname === '/api/config/global') {
|
|
58
|
+
try {
|
|
59
|
+
let configData = await configService.getConfig('global');
|
|
60
|
+
sendSuccess(res, configData);
|
|
61
|
+
return true;
|
|
62
|
+
} catch (error) {
|
|
63
|
+
output.debug('Error fetching global config:', {
|
|
64
|
+
error: error.message
|
|
65
|
+
});
|
|
66
|
+
sendError(res, 500, error.message);
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Update project config
|
|
72
|
+
if (req.method === 'POST' && pathname === '/api/config/project') {
|
|
73
|
+
try {
|
|
74
|
+
let body = await parseJsonBody(req);
|
|
75
|
+
let result = await configService.updateConfig('project', body);
|
|
76
|
+
sendSuccess(res, {
|
|
77
|
+
success: true,
|
|
78
|
+
...result
|
|
79
|
+
});
|
|
80
|
+
return true;
|
|
81
|
+
} catch (error) {
|
|
82
|
+
output.debug('Error updating project config:', {
|
|
83
|
+
error: error.message
|
|
84
|
+
});
|
|
85
|
+
sendError(res, 500, error.message);
|
|
86
|
+
return true;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Update global config
|
|
91
|
+
if (req.method === 'POST' && pathname === '/api/config/global') {
|
|
92
|
+
try {
|
|
93
|
+
let body = await parseJsonBody(req);
|
|
94
|
+
let result = await configService.updateConfig('global', body);
|
|
95
|
+
sendSuccess(res, {
|
|
96
|
+
success: true,
|
|
97
|
+
...result
|
|
98
|
+
});
|
|
99
|
+
return true;
|
|
100
|
+
} catch (error) {
|
|
101
|
+
output.debug('Error updating global config:', {
|
|
102
|
+
error: error.message
|
|
103
|
+
});
|
|
104
|
+
sendError(res, 500, error.message);
|
|
105
|
+
return true;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Validate config
|
|
110
|
+
if (req.method === 'POST' && pathname === '/api/config/validate') {
|
|
111
|
+
try {
|
|
112
|
+
let body = await parseJsonBody(req);
|
|
113
|
+
let result = await configService.validateConfig(body);
|
|
114
|
+
sendSuccess(res, result);
|
|
115
|
+
return true;
|
|
116
|
+
} catch (error) {
|
|
117
|
+
output.debug('Error validating config:', {
|
|
118
|
+
error: error.message
|
|
119
|
+
});
|
|
120
|
+
sendError(res, 500, error.message);
|
|
121
|
+
return true;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return false;
|
|
125
|
+
};
|
|
126
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dashboard Router
|
|
3
|
+
* Serves the React SPA for all dashboard routes
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { readFileSync, existsSync } from 'fs';
|
|
7
|
+
import { join } from 'path';
|
|
8
|
+
import { sendHtml, sendSuccess } from '../middleware/response.js';
|
|
9
|
+
import * as output from '../../utils/output.js';
|
|
10
|
+
|
|
11
|
+
// SPA routes that should serve the dashboard HTML
|
|
12
|
+
let SPA_ROUTES = ['/', '/dashboard', '/stats', '/settings', '/projects', '/builds'];
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Create dashboard router
|
|
16
|
+
* @param {Object} context - Router context
|
|
17
|
+
* @returns {Function} Route handler
|
|
18
|
+
*/
|
|
19
|
+
export function createDashboardRouter() {
|
|
20
|
+
return async function handleDashboardRoute(req, res, pathname) {
|
|
21
|
+
if (req.method !== 'GET') {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// API endpoint for fetching report data
|
|
26
|
+
if (pathname === '/api/report-data') {
|
|
27
|
+
let reportDataPath = join(process.cwd(), '.vizzly', 'report-data.json');
|
|
28
|
+
if (existsSync(reportDataPath)) {
|
|
29
|
+
try {
|
|
30
|
+
let data = readFileSync(reportDataPath, 'utf8');
|
|
31
|
+
res.setHeader('Content-Type', 'application/json');
|
|
32
|
+
res.statusCode = 200;
|
|
33
|
+
res.end(data);
|
|
34
|
+
return true;
|
|
35
|
+
} catch (error) {
|
|
36
|
+
output.debug('Error reading report data:', {
|
|
37
|
+
error: error.message
|
|
38
|
+
});
|
|
39
|
+
res.statusCode = 500;
|
|
40
|
+
res.end(JSON.stringify({
|
|
41
|
+
error: 'Failed to read report data'
|
|
42
|
+
}));
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
} else {
|
|
46
|
+
sendSuccess(res, null);
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// API endpoint for real-time status
|
|
52
|
+
if (pathname === '/api/status') {
|
|
53
|
+
let reportDataPath = join(process.cwd(), '.vizzly', 'report-data.json');
|
|
54
|
+
let baselineMetadataPath = join(process.cwd(), '.vizzly', 'baselines', 'metadata.json');
|
|
55
|
+
let reportData = null;
|
|
56
|
+
let baselineInfo = null;
|
|
57
|
+
if (existsSync(reportDataPath)) {
|
|
58
|
+
try {
|
|
59
|
+
reportData = JSON.parse(readFileSync(reportDataPath, 'utf8'));
|
|
60
|
+
} catch {
|
|
61
|
+
// Ignore
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
if (existsSync(baselineMetadataPath)) {
|
|
65
|
+
try {
|
|
66
|
+
baselineInfo = JSON.parse(readFileSync(baselineMetadataPath, 'utf8'));
|
|
67
|
+
} catch {
|
|
68
|
+
// Ignore
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
sendSuccess(res, {
|
|
72
|
+
timestamp: Date.now(),
|
|
73
|
+
baseline: baselineInfo,
|
|
74
|
+
comparisons: reportData?.comparisons || [],
|
|
75
|
+
summary: reportData?.summary || {
|
|
76
|
+
total: 0,
|
|
77
|
+
passed: 0,
|
|
78
|
+
failed: 0,
|
|
79
|
+
errors: 0
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
return true;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Serve React SPA for dashboard routes
|
|
86
|
+
if (SPA_ROUTES.includes(pathname) || pathname.startsWith('/comparison/')) {
|
|
87
|
+
let reportDataPath = join(process.cwd(), '.vizzly', 'report-data.json');
|
|
88
|
+
let reportData = null;
|
|
89
|
+
if (existsSync(reportDataPath)) {
|
|
90
|
+
try {
|
|
91
|
+
let data = readFileSync(reportDataPath, 'utf8');
|
|
92
|
+
reportData = JSON.parse(data);
|
|
93
|
+
} catch (error) {
|
|
94
|
+
output.debug('Could not read report data:', {
|
|
95
|
+
error: error.message
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
let dashboardHtml = `
|
|
100
|
+
<!DOCTYPE html>
|
|
101
|
+
<html>
|
|
102
|
+
<head>
|
|
103
|
+
<title>Vizzly Dev Dashboard</title>
|
|
104
|
+
<meta charset="utf-8">
|
|
105
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
106
|
+
<link rel="stylesheet" href="/reporter-bundle.css">
|
|
107
|
+
</head>
|
|
108
|
+
<body>
|
|
109
|
+
<div id="vizzly-reporter-root">
|
|
110
|
+
<div class="reporter-loading">
|
|
111
|
+
<div>
|
|
112
|
+
<div class="spinner"></div>
|
|
113
|
+
<p>Loading Vizzly Dev Dashboard...</p>
|
|
114
|
+
</div>
|
|
115
|
+
</div>
|
|
116
|
+
</div>
|
|
117
|
+
|
|
118
|
+
<script>
|
|
119
|
+
// Inject report data if available
|
|
120
|
+
${reportData ? `window.VIZZLY_REPORTER_DATA = ${JSON.stringify(reportData)};` : ''}
|
|
121
|
+
</script>
|
|
122
|
+
<script src="/reporter-bundle.js"></script>
|
|
123
|
+
</body>
|
|
124
|
+
</html>`;
|
|
125
|
+
sendHtml(res, 200, dashboardHtml);
|
|
126
|
+
return true;
|
|
127
|
+
}
|
|
128
|
+
return false;
|
|
129
|
+
};
|
|
130
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Health Router
|
|
3
|
+
* Health check endpoint with diagnostics
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { readFileSync, existsSync } from 'fs';
|
|
7
|
+
import { join } from 'path';
|
|
8
|
+
import { sendSuccess } from '../middleware/response.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Create health router
|
|
12
|
+
* @param {Object} context - Router context
|
|
13
|
+
* @param {number} context.port - Server port
|
|
14
|
+
* @param {Object} context.screenshotHandler - Screenshot handler
|
|
15
|
+
* @returns {Function} Route handler
|
|
16
|
+
*/
|
|
17
|
+
export function createHealthRouter({
|
|
18
|
+
port,
|
|
19
|
+
screenshotHandler
|
|
20
|
+
}) {
|
|
21
|
+
return async function handleHealthRoute(req, res, pathname) {
|
|
22
|
+
if (req.method !== 'GET' || pathname !== '/health') {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
let reportDataPath = join(process.cwd(), '.vizzly', 'report-data.json');
|
|
26
|
+
let baselineMetadataPath = join(process.cwd(), '.vizzly', 'baselines', 'metadata.json');
|
|
27
|
+
let reportData = null;
|
|
28
|
+
let baselineInfo = null;
|
|
29
|
+
if (existsSync(reportDataPath)) {
|
|
30
|
+
try {
|
|
31
|
+
reportData = JSON.parse(readFileSync(reportDataPath, 'utf8'));
|
|
32
|
+
} catch {
|
|
33
|
+
// Ignore read errors
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
if (existsSync(baselineMetadataPath)) {
|
|
37
|
+
try {
|
|
38
|
+
baselineInfo = JSON.parse(readFileSync(baselineMetadataPath, 'utf8'));
|
|
39
|
+
} catch {
|
|
40
|
+
// Ignore read errors
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
sendSuccess(res, {
|
|
44
|
+
status: 'ok',
|
|
45
|
+
port,
|
|
46
|
+
uptime: process.uptime(),
|
|
47
|
+
mode: screenshotHandler ? 'tdd' : 'upload',
|
|
48
|
+
baseline: baselineInfo ? {
|
|
49
|
+
buildName: baselineInfo.buildName,
|
|
50
|
+
createdAt: baselineInfo.createdAt
|
|
51
|
+
} : null,
|
|
52
|
+
stats: reportData ? {
|
|
53
|
+
total: reportData.summary?.total || 0,
|
|
54
|
+
passed: reportData.summary?.passed || 0,
|
|
55
|
+
failed: reportData.summary?.failed || 0,
|
|
56
|
+
errors: reportData.summary?.errors || 0
|
|
57
|
+
} : null
|
|
58
|
+
});
|
|
59
|
+
return true;
|
|
60
|
+
};
|
|
61
|
+
}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Projects Router
|
|
3
|
+
* Handles project management and builds endpoints
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { parseJsonBody } from '../middleware/json-parser.js';
|
|
7
|
+
import { sendSuccess, sendError, sendServiceUnavailable } from '../middleware/response.js';
|
|
8
|
+
import * as output from '../../utils/output.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Create projects router
|
|
12
|
+
* @param {Object} context - Router context
|
|
13
|
+
* @param {Object} context.projectService - Project service
|
|
14
|
+
* @returns {Function} Route handler
|
|
15
|
+
*/
|
|
16
|
+
export function createProjectsRouter({
|
|
17
|
+
projectService
|
|
18
|
+
}) {
|
|
19
|
+
return async function handleProjectsRoute(req, res, pathname, parsedUrl) {
|
|
20
|
+
// Check if project service is available for all project routes
|
|
21
|
+
if (pathname.startsWith('/api/projects') && !projectService) {
|
|
22
|
+
sendServiceUnavailable(res, 'Project service');
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// List all projects from API
|
|
27
|
+
if (req.method === 'GET' && pathname === '/api/projects') {
|
|
28
|
+
try {
|
|
29
|
+
let projects = await projectService.listProjects();
|
|
30
|
+
sendSuccess(res, {
|
|
31
|
+
projects
|
|
32
|
+
});
|
|
33
|
+
return true;
|
|
34
|
+
} catch (error) {
|
|
35
|
+
output.debug('Error listing projects:', {
|
|
36
|
+
error: error.message
|
|
37
|
+
});
|
|
38
|
+
sendError(res, 500, error.message);
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// List project directory mappings
|
|
44
|
+
if (req.method === 'GET' && pathname === '/api/projects/mappings') {
|
|
45
|
+
try {
|
|
46
|
+
let mappings = await projectService.listMappings();
|
|
47
|
+
sendSuccess(res, {
|
|
48
|
+
mappings
|
|
49
|
+
});
|
|
50
|
+
return true;
|
|
51
|
+
} catch (error) {
|
|
52
|
+
output.debug('Error listing project mappings:', {
|
|
53
|
+
error: error.message
|
|
54
|
+
});
|
|
55
|
+
sendError(res, 500, error.message);
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Create or update project mapping
|
|
61
|
+
if (req.method === 'POST' && pathname === '/api/projects/mappings') {
|
|
62
|
+
try {
|
|
63
|
+
let body = await parseJsonBody(req);
|
|
64
|
+
let {
|
|
65
|
+
directory,
|
|
66
|
+
projectSlug,
|
|
67
|
+
organizationSlug,
|
|
68
|
+
token,
|
|
69
|
+
projectName
|
|
70
|
+
} = body;
|
|
71
|
+
let mapping = await projectService.createMapping(directory, {
|
|
72
|
+
projectSlug,
|
|
73
|
+
organizationSlug,
|
|
74
|
+
token,
|
|
75
|
+
projectName
|
|
76
|
+
});
|
|
77
|
+
sendSuccess(res, {
|
|
78
|
+
success: true,
|
|
79
|
+
mapping
|
|
80
|
+
});
|
|
81
|
+
return true;
|
|
82
|
+
} catch (error) {
|
|
83
|
+
output.debug('Error creating project mapping:', {
|
|
84
|
+
error: error.message
|
|
85
|
+
});
|
|
86
|
+
sendError(res, 500, error.message);
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Delete project mapping
|
|
92
|
+
if (req.method === 'DELETE' && pathname.startsWith('/api/projects/mappings/')) {
|
|
93
|
+
try {
|
|
94
|
+
let directory = decodeURIComponent(pathname.replace('/api/projects/mappings/', ''));
|
|
95
|
+
await projectService.removeMapping(directory);
|
|
96
|
+
sendSuccess(res, {
|
|
97
|
+
success: true,
|
|
98
|
+
message: 'Mapping deleted'
|
|
99
|
+
});
|
|
100
|
+
return true;
|
|
101
|
+
} catch (error) {
|
|
102
|
+
output.debug('Error deleting project mapping:', {
|
|
103
|
+
error: error.message
|
|
104
|
+
});
|
|
105
|
+
sendError(res, 500, error.message);
|
|
106
|
+
return true;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Get recent builds for current project
|
|
111
|
+
if (req.method === 'GET' && pathname === '/api/builds/recent') {
|
|
112
|
+
if (!projectService) {
|
|
113
|
+
sendServiceUnavailable(res, 'Project service');
|
|
114
|
+
return true;
|
|
115
|
+
}
|
|
116
|
+
try {
|
|
117
|
+
let currentDir = process.cwd();
|
|
118
|
+
let mapping = await projectService.getMapping(currentDir);
|
|
119
|
+
if (!mapping || !mapping.projectSlug || !mapping.organizationSlug) {
|
|
120
|
+
sendError(res, 400, 'No project configured for this directory');
|
|
121
|
+
return true;
|
|
122
|
+
}
|
|
123
|
+
let limit = parseInt(parsedUrl.searchParams.get('limit') || '10', 10);
|
|
124
|
+
let branch = parsedUrl.searchParams.get('branch') || undefined;
|
|
125
|
+
let builds = await projectService.getRecentBuilds(mapping.projectSlug, mapping.organizationSlug, {
|
|
126
|
+
limit,
|
|
127
|
+
branch
|
|
128
|
+
});
|
|
129
|
+
sendSuccess(res, {
|
|
130
|
+
builds
|
|
131
|
+
});
|
|
132
|
+
return true;
|
|
133
|
+
} catch (error) {
|
|
134
|
+
output.debug('Error fetching recent builds:', {
|
|
135
|
+
error: error.message
|
|
136
|
+
});
|
|
137
|
+
sendError(res, 500, error.message);
|
|
138
|
+
return true;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Get builds for a specific project (used by /builds page)
|
|
143
|
+
let projectBuildsMatch = pathname.match(/^\/api\/projects\/([^/]+)\/([^/]+)\/builds$/);
|
|
144
|
+
if (req.method === 'GET' && projectBuildsMatch) {
|
|
145
|
+
try {
|
|
146
|
+
let organizationSlug = decodeURIComponent(projectBuildsMatch[1]);
|
|
147
|
+
let projectSlug = decodeURIComponent(projectBuildsMatch[2]);
|
|
148
|
+
let limit = parseInt(parsedUrl.searchParams.get('limit') || '20', 10);
|
|
149
|
+
let branch = parsedUrl.searchParams.get('branch') || undefined;
|
|
150
|
+
let builds = await projectService.getRecentBuilds(projectSlug, organizationSlug, {
|
|
151
|
+
limit,
|
|
152
|
+
branch
|
|
153
|
+
});
|
|
154
|
+
sendSuccess(res, {
|
|
155
|
+
builds
|
|
156
|
+
});
|
|
157
|
+
return true;
|
|
158
|
+
} catch (error) {
|
|
159
|
+
output.debug('Error fetching project builds:', {
|
|
160
|
+
error: error.message
|
|
161
|
+
});
|
|
162
|
+
sendError(res, 500, error.message);
|
|
163
|
+
return true;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return false;
|
|
167
|
+
};
|
|
168
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Screenshot Router
|
|
3
|
+
* Handles screenshot uploads and legacy baseline accept
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { parseJsonBody } from '../middleware/json-parser.js';
|
|
7
|
+
import { sendJson, sendError } from '../middleware/response.js';
|
|
8
|
+
import * as output from '../../utils/output.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Create screenshot router
|
|
12
|
+
* @param {Object} context - Router context
|
|
13
|
+
* @param {Object} context.screenshotHandler - Screenshot handler
|
|
14
|
+
* @param {string|null} context.defaultBuildId - Default build ID
|
|
15
|
+
* @returns {Function} Route handler
|
|
16
|
+
*/
|
|
17
|
+
export function createScreenshotRouter({
|
|
18
|
+
screenshotHandler,
|
|
19
|
+
defaultBuildId
|
|
20
|
+
}) {
|
|
21
|
+
return async function handleScreenshotRoute(req, res, pathname) {
|
|
22
|
+
if (req.method !== 'POST') {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Main screenshot upload endpoint
|
|
27
|
+
if (pathname === '/screenshot') {
|
|
28
|
+
try {
|
|
29
|
+
let body = await parseJsonBody(req);
|
|
30
|
+
let {
|
|
31
|
+
buildId,
|
|
32
|
+
name,
|
|
33
|
+
properties,
|
|
34
|
+
image
|
|
35
|
+
} = body;
|
|
36
|
+
if (!name || !image) {
|
|
37
|
+
sendError(res, 400, 'name and image are required');
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Use buildId from request body, or fall back to server's buildId
|
|
42
|
+
let effectiveBuildId = buildId || defaultBuildId;
|
|
43
|
+
let result = await screenshotHandler.handleScreenshot(effectiveBuildId, name, image, properties);
|
|
44
|
+
sendJson(res, result.statusCode, result.body);
|
|
45
|
+
return true;
|
|
46
|
+
} catch (error) {
|
|
47
|
+
output.debug('Screenshot processing error:', {
|
|
48
|
+
error: error.message
|
|
49
|
+
});
|
|
50
|
+
sendError(res, 500, 'Failed to process screenshot');
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Legacy accept-baseline endpoint
|
|
56
|
+
if (pathname === '/accept-baseline') {
|
|
57
|
+
try {
|
|
58
|
+
let body = await parseJsonBody(req);
|
|
59
|
+
let {
|
|
60
|
+
id
|
|
61
|
+
} = body;
|
|
62
|
+
if (!id) {
|
|
63
|
+
sendError(res, 400, 'comparison ID is required');
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
if (screenshotHandler.acceptBaseline) {
|
|
67
|
+
let result = await screenshotHandler.acceptBaseline(id);
|
|
68
|
+
sendJson(res, 200, {
|
|
69
|
+
success: true,
|
|
70
|
+
...result
|
|
71
|
+
});
|
|
72
|
+
} else {
|
|
73
|
+
sendError(res, 501, 'Accept baseline not implemented');
|
|
74
|
+
}
|
|
75
|
+
return true;
|
|
76
|
+
} catch (error) {
|
|
77
|
+
output.debug('Accept baseline error:', {
|
|
78
|
+
error: error.message
|
|
79
|
+
});
|
|
80
|
+
sendError(res, 500, 'Failed to accept baseline');
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return false;
|
|
85
|
+
};
|
|
86
|
+
}
|
|
@@ -95,7 +95,7 @@ export class AuthService {
|
|
|
95
95
|
if (response.status === 401) {
|
|
96
96
|
throw new AuthError('Authentication token is invalid or expired. Please run "vizzly login" again.');
|
|
97
97
|
}
|
|
98
|
-
throw new VizzlyError(`API request failed: ${response.status}${errorText ? ` - ${errorText}` : ''}`, 'API_REQUEST_ERROR');
|
|
98
|
+
throw new VizzlyError(`API request failed: ${response.status}${errorText ? ` - ${errorText}` : ''} (${endpoint})`, 'API_REQUEST_ERROR');
|
|
99
99
|
}
|
|
100
100
|
return response.json();
|
|
101
101
|
}
|
|
@@ -5,7 +5,6 @@
|
|
|
5
5
|
|
|
6
6
|
import crypto from 'crypto';
|
|
7
7
|
import { VizzlyError } from '../errors/vizzly-error.js';
|
|
8
|
-
import { BaseService } from './base-service.js';
|
|
9
8
|
|
|
10
9
|
/**
|
|
11
10
|
* Generate unique build ID for local build management only.
|
|
@@ -121,31 +120,15 @@ export function validateBuildOptions(buildOptions) {
|
|
|
121
120
|
errors
|
|
122
121
|
};
|
|
123
122
|
}
|
|
124
|
-
export class BuildManager
|
|
125
|
-
constructor(config
|
|
126
|
-
|
|
127
|
-
logger
|
|
128
|
-
});
|
|
123
|
+
export class BuildManager {
|
|
124
|
+
constructor(config) {
|
|
125
|
+
this.config = config;
|
|
129
126
|
this.currentBuild = null;
|
|
130
127
|
this.buildQueue = [];
|
|
131
128
|
}
|
|
132
|
-
async onStart() {
|
|
133
|
-
this.emitProgress('initializing', 0);
|
|
134
|
-
}
|
|
135
|
-
async onStop() {
|
|
136
|
-
if (this.currentBuild && this.currentBuild.status === 'pending') {
|
|
137
|
-
await this.updateBuildStatus(this.currentBuild.id, 'cancelled');
|
|
138
|
-
}
|
|
139
|
-
this.buildQueue.length = 0;
|
|
140
|
-
this.currentBuild = null;
|
|
141
|
-
}
|
|
142
129
|
async createBuild(buildOptions) {
|
|
143
|
-
|
|
144
|
-
const build = createBuildObject(buildOptions);
|
|
130
|
+
let build = createBuildObject(buildOptions);
|
|
145
131
|
this.currentBuild = build;
|
|
146
|
-
this.emitProgress('created', `Build created: ${build.name}`, {
|
|
147
|
-
build
|
|
148
|
-
});
|
|
149
132
|
return build;
|
|
150
133
|
}
|
|
151
134
|
async updateBuildStatus(buildId, status, updates = {}) {
|
|
@@ -153,11 +136,6 @@ export class BuildManager extends BaseService {
|
|
|
153
136
|
throw new VizzlyError(`Build ${buildId} not found`, 'BUILD_NOT_FOUND');
|
|
154
137
|
}
|
|
155
138
|
this.currentBuild = updateBuild(this.currentBuild, status, updates);
|
|
156
|
-
this.emitProgress('updated', `Build status: ${status}`, {
|
|
157
|
-
buildId,
|
|
158
|
-
status,
|
|
159
|
-
build: this.currentBuild
|
|
160
|
-
});
|
|
161
139
|
return this.currentBuild;
|
|
162
140
|
}
|
|
163
141
|
async addScreenshot(buildId, screenshot) {
|
|
@@ -165,11 +143,6 @@ export class BuildManager extends BaseService {
|
|
|
165
143
|
throw new VizzlyError(`Build ${buildId} not found`, 'BUILD_NOT_FOUND');
|
|
166
144
|
}
|
|
167
145
|
this.currentBuild = addScreenshotToBuild(this.currentBuild, screenshot);
|
|
168
|
-
this.emitProgress('screenshot-added', 'Screenshot added to build', {
|
|
169
|
-
buildId,
|
|
170
|
-
screenshotCount: this.currentBuild.screenshots.length,
|
|
171
|
-
screenshot
|
|
172
|
-
});
|
|
173
146
|
return this.currentBuild;
|
|
174
147
|
}
|
|
175
148
|
async finalizeBuild(buildId, result = {}) {
|
|
@@ -177,22 +150,22 @@ export class BuildManager extends BaseService {
|
|
|
177
150
|
throw new VizzlyError(`Build ${buildId} not found`, 'BUILD_NOT_FOUND');
|
|
178
151
|
}
|
|
179
152
|
this.currentBuild = finalizeBuildObject(this.currentBuild, result);
|
|
180
|
-
this.emitProgress('finalized', `Build ${this.currentBuild.status}`, {
|
|
181
|
-
buildId,
|
|
182
|
-
build: this.currentBuild,
|
|
183
|
-
result
|
|
184
|
-
});
|
|
185
153
|
return this.currentBuild;
|
|
186
154
|
}
|
|
187
155
|
getCurrentBuild() {
|
|
188
156
|
return this.currentBuild;
|
|
189
157
|
}
|
|
190
158
|
queueBuild(buildOptions) {
|
|
191
|
-
|
|
159
|
+
let queuedBuild = createQueuedBuild(buildOptions);
|
|
192
160
|
this.buildQueue.push(queuedBuild);
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
161
|
+
}
|
|
162
|
+
async clear() {
|
|
163
|
+
// Cancel pending build if exists
|
|
164
|
+
if (this.currentBuild && this.currentBuild.status === 'pending') {
|
|
165
|
+
await this.updateBuildStatus(this.currentBuild.id, 'cancelled');
|
|
166
|
+
}
|
|
167
|
+
this.buildQueue.length = 0;
|
|
168
|
+
this.currentBuild = null;
|
|
196
169
|
}
|
|
197
170
|
async processNextBuild() {
|
|
198
171
|
if (this.buildQueue.length === 0) {
|