@vizzly-testing/cli 0.7.2 → 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 +27 -14
- 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/commands/upload.js +3 -1
- package/dist/reporter/reporter-bundle.css +1 -0
- package/dist/reporter/reporter-bundle.iife.js +57 -0
- package/dist/sdk/index.js +1 -1
- 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/api-service.js +6 -1
- 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 +2 -3
- 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/config-loader.js +1 -1
- package/dist/utils/console-ui.js +2 -1
- package/dist/utils/environment-config.js +1 -1
- package/dist/utils/security.js +14 -5
- package/docs/api-reference.md +2 -4
- package/docs/doctor-command.md +1 -1
- package/docs/getting-started.md +1 -1
- package/docs/tdd-mode.md +176 -112
- package/package.json +17 -4
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
import { createServer } from 'http';
|
|
2
|
+
import { readFileSync, existsSync } from 'fs';
|
|
3
|
+
import { join, dirname } from 'path';
|
|
4
|
+
import { fileURLToPath } from 'url';
|
|
2
5
|
import { createServiceLogger } from '../utils/logger-factory.js';
|
|
6
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
7
|
+
const __dirname = dirname(__filename);
|
|
8
|
+
const PROJECT_ROOT = join(__dirname, '..', '..');
|
|
3
9
|
const logger = createServiceLogger('HTTP-SERVER');
|
|
4
|
-
export const createHttpServer = (port, screenshotHandler
|
|
10
|
+
export const createHttpServer = (port, screenshotHandler) => {
|
|
5
11
|
let server = null;
|
|
6
12
|
const parseRequestBody = req => {
|
|
7
13
|
return new Promise((resolve, reject) => {
|
|
@@ -30,16 +36,304 @@ export const createHttpServer = (port, screenshotHandler, emitter = null) => {
|
|
|
30
36
|
res.end();
|
|
31
37
|
return;
|
|
32
38
|
}
|
|
33
|
-
|
|
39
|
+
|
|
40
|
+
// Parse URL to handle query params properly for all routes
|
|
41
|
+
const parsedUrl = new URL(req.url, `http://${req.headers.host}`);
|
|
42
|
+
if (req.method === 'GET' && parsedUrl.pathname === '/health') {
|
|
43
|
+
// Enhanced health endpoint with diagnostics
|
|
44
|
+
const reportDataPath = join(process.cwd(), '.vizzly', 'report-data.json');
|
|
45
|
+
const baselineMetadataPath = join(process.cwd(), '.vizzly', 'baselines', 'metadata.json');
|
|
46
|
+
let reportData = null;
|
|
47
|
+
let baselineInfo = null;
|
|
48
|
+
if (existsSync(reportDataPath)) {
|
|
49
|
+
try {
|
|
50
|
+
reportData = JSON.parse(readFileSync(reportDataPath, 'utf8'));
|
|
51
|
+
} catch {
|
|
52
|
+
// Ignore read errors
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
if (existsSync(baselineMetadataPath)) {
|
|
56
|
+
try {
|
|
57
|
+
baselineInfo = JSON.parse(readFileSync(baselineMetadataPath, 'utf8'));
|
|
58
|
+
} catch {
|
|
59
|
+
// Ignore read errors
|
|
60
|
+
}
|
|
61
|
+
}
|
|
34
62
|
res.statusCode = 200;
|
|
35
63
|
res.end(JSON.stringify({
|
|
36
64
|
status: 'ok',
|
|
37
65
|
port: port,
|
|
38
|
-
uptime: process.uptime()
|
|
66
|
+
uptime: process.uptime(),
|
|
67
|
+
mode: screenshotHandler ? 'tdd' : 'upload',
|
|
68
|
+
baseline: baselineInfo ? {
|
|
69
|
+
buildName: baselineInfo.buildName,
|
|
70
|
+
createdAt: baselineInfo.createdAt
|
|
71
|
+
} : null,
|
|
72
|
+
stats: reportData ? {
|
|
73
|
+
total: reportData.summary?.total || 0,
|
|
74
|
+
passed: reportData.summary?.passed || 0,
|
|
75
|
+
failed: reportData.summary?.failed || 0,
|
|
76
|
+
errors: reportData.summary?.errors || 0
|
|
77
|
+
} : null
|
|
39
78
|
}));
|
|
40
79
|
return;
|
|
41
80
|
}
|
|
42
|
-
|
|
81
|
+
|
|
82
|
+
// Serve the main React app for all non-API routes
|
|
83
|
+
if (req.method === 'GET' && (parsedUrl.pathname === '/' || parsedUrl.pathname === '/dashboard' || parsedUrl.pathname === '/stats')) {
|
|
84
|
+
// Serve React-powered dashboard
|
|
85
|
+
const reportDataPath = join(process.cwd(), '.vizzly', 'report-data.json');
|
|
86
|
+
|
|
87
|
+
// Try to read existing report data
|
|
88
|
+
let reportData = null;
|
|
89
|
+
if (existsSync(reportDataPath)) {
|
|
90
|
+
try {
|
|
91
|
+
const data = readFileSync(reportDataPath, 'utf8');
|
|
92
|
+
reportData = JSON.parse(data);
|
|
93
|
+
} catch (error) {
|
|
94
|
+
logger.debug('Could not read report data:', error.message);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
const dashboardHtml = `
|
|
98
|
+
<!DOCTYPE html>
|
|
99
|
+
<html>
|
|
100
|
+
<head>
|
|
101
|
+
<title>Vizzly TDD Dashboard</title>
|
|
102
|
+
<meta charset="utf-8">
|
|
103
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
104
|
+
<link rel="stylesheet" href="/reporter-bundle.css">
|
|
105
|
+
</head>
|
|
106
|
+
<body>
|
|
107
|
+
<div id="vizzly-reporter-root">
|
|
108
|
+
<div class="reporter-loading">
|
|
109
|
+
<div>
|
|
110
|
+
<div class="spinner"></div>
|
|
111
|
+
<p>Loading Vizzly TDD Dashboard...</p>
|
|
112
|
+
</div>
|
|
113
|
+
</div>
|
|
114
|
+
</div>
|
|
115
|
+
|
|
116
|
+
<script>
|
|
117
|
+
// Inject report data if available
|
|
118
|
+
${reportData ? `window.VIZZLY_REPORTER_DATA = ${JSON.stringify(reportData)};` : ''}
|
|
119
|
+
</script>
|
|
120
|
+
<script src="/reporter-bundle.js"></script>
|
|
121
|
+
</body>
|
|
122
|
+
</html>`;
|
|
123
|
+
res.setHeader('Content-Type', 'text/html');
|
|
124
|
+
res.statusCode = 200;
|
|
125
|
+
res.end(dashboardHtml);
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
if (req.method === 'GET' && parsedUrl.pathname === '/reporter-bundle.js') {
|
|
129
|
+
// Serve the React bundle
|
|
130
|
+
const bundlePath = join(PROJECT_ROOT, 'dist', 'reporter', 'reporter-bundle.iife.js');
|
|
131
|
+
if (existsSync(bundlePath)) {
|
|
132
|
+
try {
|
|
133
|
+
const bundle = readFileSync(bundlePath, 'utf8');
|
|
134
|
+
res.setHeader('Content-Type', 'application/javascript');
|
|
135
|
+
res.statusCode = 200;
|
|
136
|
+
res.end(bundle);
|
|
137
|
+
} catch (error) {
|
|
138
|
+
logger.error('Error serving reporter bundle:', error);
|
|
139
|
+
res.statusCode = 500;
|
|
140
|
+
res.end('Error loading reporter bundle');
|
|
141
|
+
}
|
|
142
|
+
} else {
|
|
143
|
+
res.statusCode = 404;
|
|
144
|
+
res.end('Reporter bundle not found');
|
|
145
|
+
}
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
if (req.method === 'GET' && parsedUrl.pathname === '/reporter-bundle.css') {
|
|
149
|
+
// Serve the React CSS bundle
|
|
150
|
+
const cssPath = join(PROJECT_ROOT, 'dist', 'reporter', 'reporter-bundle.css');
|
|
151
|
+
if (existsSync(cssPath)) {
|
|
152
|
+
try {
|
|
153
|
+
const css = readFileSync(cssPath, 'utf8');
|
|
154
|
+
res.setHeader('Content-Type', 'text/css');
|
|
155
|
+
res.statusCode = 200;
|
|
156
|
+
res.end(css);
|
|
157
|
+
} catch (error) {
|
|
158
|
+
logger.error('Error serving reporter CSS:', error);
|
|
159
|
+
res.statusCode = 500;
|
|
160
|
+
res.end('Error loading reporter CSS');
|
|
161
|
+
}
|
|
162
|
+
} else {
|
|
163
|
+
res.statusCode = 404;
|
|
164
|
+
res.end('Reporter CSS not found');
|
|
165
|
+
}
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
if (req.method === 'GET' && parsedUrl.pathname === '/api/report-data') {
|
|
169
|
+
// API endpoint for fetching report data
|
|
170
|
+
const reportDataPath = join(process.cwd(), '.vizzly', 'report-data.json');
|
|
171
|
+
if (existsSync(reportDataPath)) {
|
|
172
|
+
try {
|
|
173
|
+
const data = readFileSync(reportDataPath, 'utf8');
|
|
174
|
+
res.setHeader('Content-Type', 'application/json');
|
|
175
|
+
res.statusCode = 200;
|
|
176
|
+
res.end(data);
|
|
177
|
+
} catch (error) {
|
|
178
|
+
logger.error('Error reading report data:', error);
|
|
179
|
+
res.statusCode = 500;
|
|
180
|
+
res.end(JSON.stringify({
|
|
181
|
+
error: 'Failed to read report data'
|
|
182
|
+
}));
|
|
183
|
+
}
|
|
184
|
+
} else {
|
|
185
|
+
res.statusCode = 200;
|
|
186
|
+
res.end(JSON.stringify(null)); // No data available yet
|
|
187
|
+
}
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
if (req.method === 'GET' && parsedUrl.pathname === '/api/status') {
|
|
191
|
+
// Real-time status endpoint
|
|
192
|
+
const reportDataPath = join(process.cwd(), '.vizzly', 'report-data.json');
|
|
193
|
+
const baselineMetadataPath = join(process.cwd(), '.vizzly', 'baselines', 'metadata.json');
|
|
194
|
+
let reportData = null;
|
|
195
|
+
let baselineInfo = null;
|
|
196
|
+
if (existsSync(reportDataPath)) {
|
|
197
|
+
try {
|
|
198
|
+
reportData = JSON.parse(readFileSync(reportDataPath, 'utf8'));
|
|
199
|
+
} catch {
|
|
200
|
+
// Ignore
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
if (existsSync(baselineMetadataPath)) {
|
|
204
|
+
try {
|
|
205
|
+
baselineInfo = JSON.parse(readFileSync(baselineMetadataPath, 'utf8'));
|
|
206
|
+
} catch {
|
|
207
|
+
// Ignore
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
res.setHeader('Content-Type', 'application/json');
|
|
211
|
+
res.statusCode = 200;
|
|
212
|
+
res.end(JSON.stringify({
|
|
213
|
+
timestamp: Date.now(),
|
|
214
|
+
baseline: baselineInfo,
|
|
215
|
+
comparisons: reportData?.comparisons || [],
|
|
216
|
+
summary: reportData?.summary || {
|
|
217
|
+
total: 0,
|
|
218
|
+
passed: 0,
|
|
219
|
+
failed: 0,
|
|
220
|
+
errors: 0
|
|
221
|
+
}
|
|
222
|
+
}));
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
if (req.method === 'POST' && parsedUrl.pathname === '/api/baseline/accept') {
|
|
226
|
+
// Accept a single screenshot as baseline
|
|
227
|
+
if (!screenshotHandler?.acceptBaseline) {
|
|
228
|
+
res.statusCode = 400;
|
|
229
|
+
res.end(JSON.stringify({
|
|
230
|
+
error: 'Baseline management not available'
|
|
231
|
+
}));
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
try {
|
|
235
|
+
const {
|
|
236
|
+
name
|
|
237
|
+
} = await parseRequestBody(req);
|
|
238
|
+
if (!name) {
|
|
239
|
+
res.statusCode = 400;
|
|
240
|
+
res.end(JSON.stringify({
|
|
241
|
+
error: 'Screenshot name required'
|
|
242
|
+
}));
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
await screenshotHandler.acceptBaseline(name);
|
|
246
|
+
res.setHeader('Content-Type', 'application/json');
|
|
247
|
+
res.statusCode = 200;
|
|
248
|
+
res.end(JSON.stringify({
|
|
249
|
+
success: true,
|
|
250
|
+
message: `Baseline accepted for ${name}`
|
|
251
|
+
}));
|
|
252
|
+
} catch (error) {
|
|
253
|
+
logger.error('Error accepting baseline:', error);
|
|
254
|
+
res.statusCode = 500;
|
|
255
|
+
res.end(JSON.stringify({
|
|
256
|
+
error: error.message
|
|
257
|
+
}));
|
|
258
|
+
}
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
if (req.method === 'POST' && parsedUrl.pathname === '/api/baseline/accept-all') {
|
|
262
|
+
// Accept all screenshots as baseline
|
|
263
|
+
if (!screenshotHandler?.acceptAllBaselines) {
|
|
264
|
+
res.statusCode = 400;
|
|
265
|
+
res.end(JSON.stringify({
|
|
266
|
+
error: 'Baseline management not available'
|
|
267
|
+
}));
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
try {
|
|
271
|
+
const result = await screenshotHandler.acceptAllBaselines();
|
|
272
|
+
res.setHeader('Content-Type', 'application/json');
|
|
273
|
+
res.statusCode = 200;
|
|
274
|
+
res.end(JSON.stringify({
|
|
275
|
+
success: true,
|
|
276
|
+
message: `Accepted ${result.count} baselines`,
|
|
277
|
+
count: result.count
|
|
278
|
+
}));
|
|
279
|
+
} catch (error) {
|
|
280
|
+
logger.error('Error accepting all baselines:', error);
|
|
281
|
+
res.statusCode = 500;
|
|
282
|
+
res.end(JSON.stringify({
|
|
283
|
+
error: error.message
|
|
284
|
+
}));
|
|
285
|
+
}
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
if (req.method === 'POST' && parsedUrl.pathname === '/api/baseline/reset') {
|
|
289
|
+
// Reset baselines to previous state
|
|
290
|
+
if (!screenshotHandler?.resetBaselines) {
|
|
291
|
+
res.statusCode = 400;
|
|
292
|
+
res.end(JSON.stringify({
|
|
293
|
+
error: 'Baseline management not available'
|
|
294
|
+
}));
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
try {
|
|
298
|
+
await screenshotHandler.resetBaselines();
|
|
299
|
+
res.setHeader('Content-Type', 'application/json');
|
|
300
|
+
res.statusCode = 200;
|
|
301
|
+
res.end(JSON.stringify({
|
|
302
|
+
success: true,
|
|
303
|
+
message: 'Baselines reset to previous state'
|
|
304
|
+
}));
|
|
305
|
+
} catch (error) {
|
|
306
|
+
logger.error('Error resetting baselines:', error);
|
|
307
|
+
res.statusCode = 500;
|
|
308
|
+
res.end(JSON.stringify({
|
|
309
|
+
error: error.message
|
|
310
|
+
}));
|
|
311
|
+
}
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// Serve images from .vizzly directory
|
|
316
|
+
if (req.method === 'GET' && parsedUrl.pathname.startsWith('/images/')) {
|
|
317
|
+
const imagePath = parsedUrl.pathname.replace('/images/', '');
|
|
318
|
+
const fullImagePath = join(process.cwd(), '.vizzly', imagePath);
|
|
319
|
+
if (existsSync(fullImagePath)) {
|
|
320
|
+
try {
|
|
321
|
+
const imageData = readFileSync(fullImagePath);
|
|
322
|
+
res.setHeader('Content-Type', 'image/png');
|
|
323
|
+
res.statusCode = 200;
|
|
324
|
+
res.end(imageData);
|
|
325
|
+
} catch (error) {
|
|
326
|
+
logger.error('Error serving image:', error);
|
|
327
|
+
res.statusCode = 500;
|
|
328
|
+
res.end('Error loading image');
|
|
329
|
+
}
|
|
330
|
+
} else {
|
|
331
|
+
res.statusCode = 404;
|
|
332
|
+
res.end('Image not found');
|
|
333
|
+
}
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
if (req.method === 'POST' && parsedUrl.pathname === '/screenshot') {
|
|
43
337
|
try {
|
|
44
338
|
const body = await parseRequestBody(req);
|
|
45
339
|
const {
|
|
@@ -48,23 +342,17 @@ export const createHttpServer = (port, screenshotHandler, emitter = null) => {
|
|
|
48
342
|
properties,
|
|
49
343
|
image
|
|
50
344
|
} = body;
|
|
51
|
-
if (!
|
|
345
|
+
if (!name || !image) {
|
|
52
346
|
res.statusCode = 400;
|
|
53
347
|
res.end(JSON.stringify({
|
|
54
|
-
error: '
|
|
348
|
+
error: 'name and image are required'
|
|
55
349
|
}));
|
|
56
350
|
return;
|
|
57
351
|
}
|
|
58
|
-
const result = await screenshotHandler.handleScreenshot(buildId, name, image, properties);
|
|
59
352
|
|
|
60
|
-
//
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
name,
|
|
64
|
-
count: screenshotHandler.getScreenshotCount?.(buildId) || 0,
|
|
65
|
-
skipped: result.body?.skipped
|
|
66
|
-
});
|
|
67
|
-
}
|
|
353
|
+
// Use default buildId if none provided
|
|
354
|
+
const effectiveBuildId = buildId || 'default';
|
|
355
|
+
const result = await screenshotHandler.handleScreenshot(effectiveBuildId, name, image, properties);
|
|
68
356
|
res.statusCode = result.statusCode;
|
|
69
357
|
res.end(JSON.stringify(result.body));
|
|
70
358
|
} catch (error) {
|
|
@@ -76,6 +364,43 @@ export const createHttpServer = (port, screenshotHandler, emitter = null) => {
|
|
|
76
364
|
}
|
|
77
365
|
return;
|
|
78
366
|
}
|
|
367
|
+
if (req.method === 'POST' && parsedUrl.pathname === '/accept-baseline') {
|
|
368
|
+
try {
|
|
369
|
+
const body = await parseRequestBody(req);
|
|
370
|
+
const {
|
|
371
|
+
name
|
|
372
|
+
} = body;
|
|
373
|
+
if (!name) {
|
|
374
|
+
res.statusCode = 400;
|
|
375
|
+
res.end(JSON.stringify({
|
|
376
|
+
error: 'screenshot name is required'
|
|
377
|
+
}));
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// Call the screenshot handler's accept baseline method if it exists
|
|
382
|
+
if (screenshotHandler.acceptBaseline) {
|
|
383
|
+
const result = await screenshotHandler.acceptBaseline(name);
|
|
384
|
+
res.statusCode = 200;
|
|
385
|
+
res.end(JSON.stringify({
|
|
386
|
+
success: true,
|
|
387
|
+
...result
|
|
388
|
+
}));
|
|
389
|
+
} else {
|
|
390
|
+
res.statusCode = 501;
|
|
391
|
+
res.end(JSON.stringify({
|
|
392
|
+
error: 'Accept baseline not implemented'
|
|
393
|
+
}));
|
|
394
|
+
}
|
|
395
|
+
} catch (error) {
|
|
396
|
+
logger.error('Accept baseline error:', error);
|
|
397
|
+
res.statusCode = 500;
|
|
398
|
+
res.end(JSON.stringify({
|
|
399
|
+
error: 'Failed to accept baseline'
|
|
400
|
+
}));
|
|
401
|
+
}
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
79
404
|
res.statusCode = 404;
|
|
80
405
|
res.end(JSON.stringify({
|
|
81
406
|
error: 'Not found'
|
|
@@ -124,9 +449,27 @@ export const createHttpServer = (port, screenshotHandler, emitter = null) => {
|
|
|
124
449
|
}
|
|
125
450
|
return Promise.resolve();
|
|
126
451
|
};
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* Finish build - flush any pending background operations
|
|
455
|
+
* Call this before finalizing a build to ensure all uploads complete
|
|
456
|
+
*/
|
|
457
|
+
const finishBuild = async buildId => {
|
|
458
|
+
logger.debug(`Finishing build ${buildId}...`);
|
|
459
|
+
|
|
460
|
+
// Flush screenshot handler if it has a flush method (API mode)
|
|
461
|
+
if (screenshotHandler?.flush) {
|
|
462
|
+
const stats = await screenshotHandler.flush();
|
|
463
|
+
logger.debug(`Build ${buildId} uploads complete: ${stats.uploaded} uploaded, ${stats.failed} failed`);
|
|
464
|
+
return stats;
|
|
465
|
+
}
|
|
466
|
+
logger.debug(`Build ${buildId} finished (no flush needed)`);
|
|
467
|
+
return null;
|
|
468
|
+
};
|
|
127
469
|
return {
|
|
128
470
|
start,
|
|
129
471
|
stop,
|
|
472
|
+
finishBuild,
|
|
130
473
|
getServer: () => server
|
|
131
474
|
};
|
|
132
475
|
};
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { URLSearchParams } from 'url';
|
|
7
|
-
import { VizzlyError } from '../errors/vizzly-error.js';
|
|
7
|
+
import { VizzlyError, AuthError } from '../errors/vizzly-error.js';
|
|
8
8
|
import crypto from 'crypto';
|
|
9
9
|
import { getPackageVersion } from '../utils/package-info.js';
|
|
10
10
|
import { getApiUrl, getApiToken, getUserAgent } from '../utils/environment-config.js';
|
|
@@ -58,6 +58,11 @@ export class ApiService {
|
|
|
58
58
|
} catch {
|
|
59
59
|
// ignore
|
|
60
60
|
}
|
|
61
|
+
|
|
62
|
+
// Handle authentication errors with user-friendly messages
|
|
63
|
+
if (response.status === 401) {
|
|
64
|
+
throw new AuthError('Invalid or expired API token. Please check your VIZZLY_TOKEN environment variable and ensure it is valid.');
|
|
65
|
+
}
|
|
61
66
|
throw new VizzlyError(`API request failed: ${response.status}${errorText ? ` - ${errorText}` : ''} (URL: ${url})`);
|
|
62
67
|
}
|
|
63
68
|
return response.json();
|
|
@@ -297,6 +297,74 @@ document.addEventListener('DOMContentLoaded', function () {
|
|
|
297
297
|
|
|
298
298
|
console.log('Vizzly TDD Report loaded successfully');
|
|
299
299
|
});
|
|
300
|
+
|
|
301
|
+
// Accept/Reject baseline functions
|
|
302
|
+
async function acceptBaseline(screenshotName) {
|
|
303
|
+
const button = document.querySelector(\`button[onclick*="\${screenshotName}"]\`);
|
|
304
|
+
if (button) {
|
|
305
|
+
button.disabled = true;
|
|
306
|
+
button.innerHTML = '⏳ Accepting...';
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
try {
|
|
310
|
+
const response = await fetch('/accept-baseline', {
|
|
311
|
+
method: 'POST',
|
|
312
|
+
headers: { 'Content-Type': 'application/json' },
|
|
313
|
+
body: JSON.stringify({ name: screenshotName })
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
if (response.ok) {
|
|
317
|
+
// Mark as accepted and hide the comparison
|
|
318
|
+
const comparison = document.querySelector(\`[data-comparison="\${screenshotName}"]\`);
|
|
319
|
+
if (comparison) {
|
|
320
|
+
comparison.style.background = '#e8f5e8';
|
|
321
|
+
comparison.style.border = '2px solid #4caf50';
|
|
322
|
+
|
|
323
|
+
const status = comparison.querySelector('.diff-status');
|
|
324
|
+
if (status) {
|
|
325
|
+
status.innerHTML = '✅ Accepted as new baseline';
|
|
326
|
+
status.style.color = '#4caf50';
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const actions = comparison.querySelector('.comparison-actions');
|
|
330
|
+
if (actions) {
|
|
331
|
+
actions.innerHTML = '<div style="color: #4caf50; padding: 0.5rem;">✅ Screenshot accepted as new baseline</div>';
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// Auto-refresh after short delay to show updated report
|
|
336
|
+
setTimeout(() => window.location.reload(), 2000);
|
|
337
|
+
} else {
|
|
338
|
+
throw new Error('Failed to accept baseline');
|
|
339
|
+
}
|
|
340
|
+
} catch (error) {
|
|
341
|
+
console.error('Error accepting baseline:', error);
|
|
342
|
+
if (button) {
|
|
343
|
+
button.disabled = false;
|
|
344
|
+
button.innerHTML = '✅ Accept as Baseline';
|
|
345
|
+
}
|
|
346
|
+
alert('Failed to accept baseline. Please try again.');
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function rejectChanges(screenshotName) {
|
|
351
|
+
const comparison = document.querySelector(\`[data-comparison="\${screenshotName}"]\`);
|
|
352
|
+
if (comparison) {
|
|
353
|
+
comparison.style.background = '#fff3cd';
|
|
354
|
+
comparison.style.border = '2px solid #ffc107';
|
|
355
|
+
|
|
356
|
+
const status = comparison.querySelector('.diff-status');
|
|
357
|
+
if (status) {
|
|
358
|
+
status.innerHTML = '⚠️ Changes rejected - baseline unchanged';
|
|
359
|
+
status.style.color = '#856404';
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
const actions = comparison.querySelector('.comparison-actions');
|
|
363
|
+
if (actions) {
|
|
364
|
+
actions.innerHTML = '<div style="color: #856404; padding: 0.5rem;">⚠️ Changes rejected - baseline kept as-is</div>';
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
}
|
|
300
368
|
</script>
|
|
301
369
|
</body>
|
|
302
370
|
</html>`;
|
|
@@ -331,6 +399,15 @@ document.addEventListener('DOMContentLoaded', function () {
|
|
|
331
399
|
<button class="view-mode-btn" data-mode="side-by-side">Side by Side</button>
|
|
332
400
|
</div>
|
|
333
401
|
|
|
402
|
+
<div class="comparison-actions">
|
|
403
|
+
<button class="accept-btn" onclick="acceptBaseline('${safeName}')">
|
|
404
|
+
✅ Accept as Baseline
|
|
405
|
+
</button>
|
|
406
|
+
<button class="reject-btn" onclick="rejectChanges('${safeName}')">
|
|
407
|
+
❌ Keep Current Baseline
|
|
408
|
+
</button>
|
|
409
|
+
</div>
|
|
410
|
+
|
|
334
411
|
<div class="comparison-viewer">
|
|
335
412
|
<!-- Overlay Mode -->
|
|
336
413
|
<div class="mode-container overlay-mode" data-mode="overlay">
|
|
@@ -337,6 +337,59 @@ body {
|
|
|
337
337
|
padding: 40px;
|
|
338
338
|
}
|
|
339
339
|
|
|
340
|
+
/* Action buttons for accept/reject */
|
|
341
|
+
.comparison-actions {
|
|
342
|
+
display: flex;
|
|
343
|
+
gap: 12px;
|
|
344
|
+
margin: 16px 0;
|
|
345
|
+
padding: 16px;
|
|
346
|
+
background: #1e293b;
|
|
347
|
+
border-radius: 8px;
|
|
348
|
+
border: 1px solid #334155;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
.accept-btn,
|
|
352
|
+
.reject-btn {
|
|
353
|
+
padding: 10px 16px;
|
|
354
|
+
border: none;
|
|
355
|
+
border-radius: 6px;
|
|
356
|
+
font-size: 14px;
|
|
357
|
+
font-weight: 500;
|
|
358
|
+
cursor: pointer;
|
|
359
|
+
transition: all 0.2s ease;
|
|
360
|
+
display: inline-flex;
|
|
361
|
+
align-items: center;
|
|
362
|
+
gap: 8px;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
.accept-btn {
|
|
366
|
+
background: #059669;
|
|
367
|
+
color: white;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
.accept-btn:hover {
|
|
371
|
+
background: #047857;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
.accept-btn:disabled {
|
|
375
|
+
background: #6b7280;
|
|
376
|
+
cursor: not-allowed;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
.reject-btn {
|
|
380
|
+
background: #dc2626;
|
|
381
|
+
color: white;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
.reject-btn:hover {
|
|
385
|
+
background: #b91c1c;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
.reject-btn:disabled {
|
|
389
|
+
background: #6b7280;
|
|
390
|
+
cursor: not-allowed;
|
|
391
|
+
}
|
|
392
|
+
|
|
340
393
|
@media (max-width: 768px) {
|
|
341
394
|
.container {
|
|
342
395
|
padding: 10px;
|
|
@@ -352,4 +405,7 @@ body {
|
|
|
352
405
|
grid-template-columns: 1fr;
|
|
353
406
|
gap: 15px;
|
|
354
407
|
}
|
|
408
|
+
.comparison-actions {
|
|
409
|
+
flex-direction: column;
|
|
410
|
+
}
|
|
355
411
|
}
|
|
@@ -45,14 +45,17 @@ export class ScreenshotServer extends BaseService {
|
|
|
45
45
|
image,
|
|
46
46
|
properties
|
|
47
47
|
} = body;
|
|
48
|
-
if (!
|
|
48
|
+
if (!name || !image) {
|
|
49
49
|
res.statusCode = 400;
|
|
50
50
|
res.end(JSON.stringify({
|
|
51
|
-
error: '
|
|
51
|
+
error: 'name and image are required'
|
|
52
52
|
}));
|
|
53
53
|
return;
|
|
54
54
|
}
|
|
55
|
-
|
|
55
|
+
|
|
56
|
+
// Use default buildId if none provided
|
|
57
|
+
const effectiveBuildId = buildId || 'default';
|
|
58
|
+
await this.buildManager.addScreenshot(effectiveBuildId, {
|
|
56
59
|
name,
|
|
57
60
|
image,
|
|
58
61
|
properties
|
|
@@ -7,7 +7,6 @@ import { BaseService } from './base-service.js';
|
|
|
7
7
|
import { createHttpServer } from '../server/http-server.js';
|
|
8
8
|
import { createTddHandler } from '../server/handlers/tdd-handler.js';
|
|
9
9
|
import { createApiHandler } from '../server/handlers/api-handler.js';
|
|
10
|
-
import { EventEmitter } from 'events';
|
|
11
10
|
export class ServerManager extends BaseService {
|
|
12
11
|
constructor(config, logger) {
|
|
13
12
|
super(config, {
|
|
@@ -15,7 +14,6 @@ export class ServerManager extends BaseService {
|
|
|
15
14
|
});
|
|
16
15
|
this.httpServer = null;
|
|
17
16
|
this.handler = null;
|
|
18
|
-
this.emitter = null;
|
|
19
17
|
}
|
|
20
18
|
async start(buildId = null, tddMode = false, setBaseline = false) {
|
|
21
19
|
this.buildId = buildId;
|
|
@@ -24,19 +22,15 @@ export class ServerManager extends BaseService {
|
|
|
24
22
|
return super.start();
|
|
25
23
|
}
|
|
26
24
|
async onStart() {
|
|
27
|
-
this.emitter = new EventEmitter();
|
|
28
25
|
const port = this.config?.server?.port || 47392;
|
|
29
26
|
if (this.tddMode) {
|
|
30
27
|
this.handler = createTddHandler(this.config, process.cwd(), this.config?.baselineBuildId, this.config?.baselineComparisonId, this.setBaseline);
|
|
31
28
|
await this.handler.initialize();
|
|
32
|
-
if (this.buildId) {
|
|
33
|
-
this.handler.registerBuild(this.buildId);
|
|
34
|
-
}
|
|
35
29
|
} else {
|
|
36
30
|
const apiService = await this.createApiService();
|
|
37
31
|
this.handler = createApiHandler(apiService);
|
|
38
32
|
}
|
|
39
|
-
this.httpServer = createHttpServer(port, this.handler
|
|
33
|
+
this.httpServer = createHttpServer(port, this.handler);
|
|
40
34
|
if (this.httpServer) {
|
|
41
35
|
await this.httpServer.start();
|
|
42
36
|
}
|
|
@@ -70,9 +64,8 @@ export class ServerManager extends BaseService {
|
|
|
70
64
|
// Expose server interface for compatibility
|
|
71
65
|
get server() {
|
|
72
66
|
return {
|
|
73
|
-
emitter: this.emitter,
|
|
74
67
|
getScreenshotCount: buildId => this.handler?.getScreenshotCount?.(buildId) || 0,
|
|
75
|
-
finishBuild: buildId => this.
|
|
68
|
+
finishBuild: buildId => this.httpServer?.finishBuild?.(buildId)
|
|
76
69
|
};
|
|
77
70
|
}
|
|
78
71
|
}
|