@vizzly-testing/cli 0.20.0 → 0.20.1-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api/client.js +134 -0
- package/dist/api/core.js +341 -0
- package/dist/api/endpoints.js +314 -0
- package/dist/api/index.js +19 -0
- package/dist/auth/client.js +91 -0
- package/dist/auth/core.js +176 -0
- package/dist/auth/index.js +30 -0
- package/dist/auth/operations.js +148 -0
- package/dist/cli.js +178 -3
- package/dist/client/index.js +144 -77
- package/dist/commands/doctor.js +121 -36
- package/dist/commands/finalize.js +49 -18
- package/dist/commands/init.js +13 -18
- package/dist/commands/login.js +49 -55
- package/dist/commands/logout.js +17 -9
- package/dist/commands/project.js +100 -71
- package/dist/commands/run.js +189 -95
- package/dist/commands/status.js +101 -66
- package/dist/commands/tdd-daemon.js +61 -32
- package/dist/commands/tdd.js +104 -98
- package/dist/commands/upload.js +78 -34
- package/dist/commands/whoami.js +44 -42
- package/dist/config/core.js +438 -0
- package/dist/config/index.js +13 -0
- package/dist/config/operations.js +327 -0
- package/dist/index.js +1 -1
- package/dist/project/core.js +295 -0
- package/dist/project/index.js +13 -0
- package/dist/project/operations.js +393 -0
- package/dist/reporter/reporter-bundle.css +1 -1
- package/dist/reporter/reporter-bundle.iife.js +16 -16
- package/dist/screenshot-server/core.js +157 -0
- package/dist/screenshot-server/index.js +11 -0
- package/dist/screenshot-server/operations.js +183 -0
- package/dist/sdk/index.js +3 -2
- package/dist/server/handlers/api-handler.js +14 -5
- package/dist/server/handlers/tdd-handler.js +191 -53
- package/dist/server/http-server.js +9 -3
- package/dist/server/routers/baseline.js +58 -0
- package/dist/server/routers/dashboard.js +10 -6
- package/dist/server/routers/screenshot.js +32 -0
- package/dist/server-manager/core.js +186 -0
- package/dist/server-manager/index.js +81 -0
- package/dist/server-manager/operations.js +209 -0
- package/dist/services/build-manager.js +2 -69
- package/dist/services/index.js +21 -48
- package/dist/services/screenshot-server.js +40 -74
- package/dist/services/server-manager.js +45 -80
- package/dist/services/test-runner.js +90 -250
- package/dist/services/uploader.js +56 -358
- package/dist/tdd/core/hotspot-coverage.js +112 -0
- package/dist/tdd/core/signature.js +101 -0
- package/dist/tdd/index.js +19 -0
- package/dist/tdd/metadata/baseline-metadata.js +103 -0
- package/dist/tdd/metadata/hotspot-metadata.js +93 -0
- package/dist/tdd/services/baseline-downloader.js +151 -0
- package/dist/tdd/services/baseline-manager.js +166 -0
- package/dist/tdd/services/comparison-service.js +230 -0
- package/dist/tdd/services/hotspot-service.js +71 -0
- package/dist/tdd/services/result-service.js +123 -0
- package/dist/tdd/tdd-service.js +1145 -0
- package/dist/test-runner/core.js +255 -0
- package/dist/test-runner/index.js +13 -0
- package/dist/test-runner/operations.js +483 -0
- package/dist/types/client.d.ts +25 -2
- package/dist/uploader/core.js +396 -0
- package/dist/uploader/index.js +11 -0
- package/dist/uploader/operations.js +412 -0
- package/dist/utils/colors.js +187 -39
- package/dist/utils/config-loader.js +3 -6
- package/dist/utils/context.js +228 -0
- package/dist/utils/output.js +449 -14
- package/docs/api-reference.md +173 -8
- package/docs/tui-elements.md +560 -0
- package/package.json +13 -13
- package/dist/services/api-service.js +0 -412
- package/dist/services/auth-service.js +0 -226
- package/dist/services/config-service.js +0 -369
- package/dist/services/html-report-generator.js +0 -455
- package/dist/services/project-service.js +0 -326
- package/dist/services/report-generator/report.css +0 -411
- package/dist/services/report-generator/viewer.js +0 -102
- package/dist/services/static-report-generator.js +0 -207
- package/dist/services/tdd-service.js +0 -1437
|
@@ -1,17 +1,66 @@
|
|
|
1
|
-
import { Buffer } from 'node:buffer';
|
|
2
|
-
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
3
|
-
import { join, resolve } from 'node:path';
|
|
4
|
-
import { getDimensionsSync } from '@vizzly-testing/honeydiff';
|
|
5
|
-
import { TddService } from '../../
|
|
6
|
-
import { detectImageInputType } from '../../utils/image-input-detector.js';
|
|
7
|
-
import * as
|
|
8
|
-
import { sanitizeScreenshotName, validateScreenshotProperties } from '../../utils/security.js';
|
|
1
|
+
import { Buffer as defaultBuffer } from 'node:buffer';
|
|
2
|
+
import { existsSync as defaultExistsSync, readFileSync as defaultReadFileSync, unlinkSync as defaultUnlinkSync, writeFileSync as defaultWriteFileSync } from 'node:fs';
|
|
3
|
+
import { join as defaultJoin, resolve as defaultResolve } from 'node:path';
|
|
4
|
+
import { getDimensionsSync as defaultGetDimensionsSync } from '@vizzly-testing/honeydiff';
|
|
5
|
+
import { TddService as DefaultTddService } from '../../tdd/tdd-service.js';
|
|
6
|
+
import { detectImageInputType as defaultDetectImageInputType } from '../../utils/image-input-detector.js';
|
|
7
|
+
import * as defaultOutput from '../../utils/output.js';
|
|
8
|
+
import { safePath as defaultSafePath, sanitizeScreenshotName as defaultSanitizeScreenshotName, validateScreenshotProperties as defaultValidateScreenshotProperties } from '../../utils/security.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Unwrap double-nested properties if needed
|
|
12
|
+
* Client SDK wraps options in properties field, so we may get { properties: { properties: {...} } }
|
|
13
|
+
*/
|
|
14
|
+
export const unwrapProperties = properties => {
|
|
15
|
+
if (!properties) return {};
|
|
16
|
+
if (properties.properties && typeof properties.properties === 'object') {
|
|
17
|
+
// Merge top-level properties with nested properties
|
|
18
|
+
let unwrapped = {
|
|
19
|
+
...properties,
|
|
20
|
+
...properties.properties
|
|
21
|
+
};
|
|
22
|
+
// Remove the nested properties field to avoid confusion
|
|
23
|
+
delete unwrapped.properties;
|
|
24
|
+
return unwrapped;
|
|
25
|
+
}
|
|
26
|
+
return properties;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Extract properties to top-level format matching cloud API
|
|
31
|
+
* Normalizes viewport to viewport_width/height, ensures browser is set
|
|
32
|
+
*/
|
|
33
|
+
export const extractProperties = validatedProperties => {
|
|
34
|
+
if (!validatedProperties) return {};
|
|
35
|
+
return {
|
|
36
|
+
...validatedProperties,
|
|
37
|
+
// Normalize viewport to top-level viewport_width/height (cloud format)
|
|
38
|
+
viewport_width: validatedProperties.viewport?.width ?? validatedProperties.viewport_width ?? null,
|
|
39
|
+
viewport_height: validatedProperties.viewport?.height ?? validatedProperties.viewport_height ?? null,
|
|
40
|
+
browser: validatedProperties.browser ?? null,
|
|
41
|
+
// Preserve nested structure in metadata for backward compatibility
|
|
42
|
+
metadata: validatedProperties
|
|
43
|
+
};
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Convert absolute file paths to web-accessible URLs
|
|
48
|
+
*/
|
|
49
|
+
export const convertPathToUrl = (filePath, vizzlyDir) => {
|
|
50
|
+
if (!filePath) return null;
|
|
51
|
+
// Convert absolute path to relative path from .vizzly directory
|
|
52
|
+
if (filePath.startsWith(vizzlyDir)) {
|
|
53
|
+
let relativePath = filePath.substring(vizzlyDir.length + 1);
|
|
54
|
+
return `/images/${relativePath}`;
|
|
55
|
+
}
|
|
56
|
+
return filePath;
|
|
57
|
+
};
|
|
9
58
|
|
|
10
59
|
/**
|
|
11
60
|
* Group comparisons by screenshot name with variant structure
|
|
12
61
|
* Matches cloud product's grouping logic from comparison.js
|
|
13
62
|
*/
|
|
14
|
-
const groupComparisons = comparisons => {
|
|
63
|
+
export const groupComparisons = comparisons => {
|
|
15
64
|
const groups = new Map();
|
|
16
65
|
|
|
17
66
|
// Group by screenshot name
|
|
@@ -87,7 +136,24 @@ const groupComparisons = comparisons => {
|
|
|
87
136
|
return a.name.localeCompare(b.name);
|
|
88
137
|
});
|
|
89
138
|
};
|
|
90
|
-
export const createTddHandler = (config, workingDir, baselineBuild, baselineComparison, setBaseline = false) => {
|
|
139
|
+
export const createTddHandler = (config, workingDir, baselineBuild, baselineComparison, setBaseline = false, deps = {}) => {
|
|
140
|
+
// Inject dependencies with defaults
|
|
141
|
+
let {
|
|
142
|
+
TddService = DefaultTddService,
|
|
143
|
+
existsSync = defaultExistsSync,
|
|
144
|
+
readFileSync = defaultReadFileSync,
|
|
145
|
+
unlinkSync = defaultUnlinkSync,
|
|
146
|
+
writeFileSync = defaultWriteFileSync,
|
|
147
|
+
join = defaultJoin,
|
|
148
|
+
resolve = defaultResolve,
|
|
149
|
+
Buffer = defaultBuffer,
|
|
150
|
+
getDimensionsSync = defaultGetDimensionsSync,
|
|
151
|
+
detectImageInputType = defaultDetectImageInputType,
|
|
152
|
+
safePath = defaultSafePath,
|
|
153
|
+
sanitizeScreenshotName = defaultSanitizeScreenshotName,
|
|
154
|
+
validateScreenshotProperties = defaultValidateScreenshotProperties,
|
|
155
|
+
output = defaultOutput
|
|
156
|
+
} = deps;
|
|
91
157
|
const tddService = new TddService(config, workingDir, setBaseline);
|
|
92
158
|
const reportPath = join(workingDir, '.vizzly', 'report-data.json');
|
|
93
159
|
const readReportData = () => {
|
|
@@ -164,6 +230,7 @@ export const createTddHandler = (config, workingDir, baselineBuild, baselineComp
|
|
|
164
230
|
groups: reportData.groups.length,
|
|
165
231
|
passed: reportData.comparisons.filter(c => c.status === 'passed' || c.status === 'baseline-created' || c.status === 'new').length,
|
|
166
232
|
failed: reportData.comparisons.filter(c => c.status === 'failed').length,
|
|
233
|
+
rejected: reportData.comparisons.filter(c => c.status === 'rejected').length,
|
|
167
234
|
errors: reportData.comparisons.filter(c => c.status === 'error').length
|
|
168
235
|
};
|
|
169
236
|
writeFileSync(reportPath, JSON.stringify(reportData, null, 2));
|
|
@@ -172,7 +239,7 @@ export const createTddHandler = (config, workingDir, baselineBuild, baselineComp
|
|
|
172
239
|
}
|
|
173
240
|
};
|
|
174
241
|
const initialize = async () => {
|
|
175
|
-
output.debug('tdd', '
|
|
242
|
+
output.debug('tdd', 'initializing local mode');
|
|
176
243
|
|
|
177
244
|
// In baseline update mode, skip all baseline loading/downloading
|
|
178
245
|
if (setBaseline) {
|
|
@@ -183,7 +250,7 @@ export const createTddHandler = (config, workingDir, baselineBuild, baselineComp
|
|
|
183
250
|
// Check if we have baseline override flags that should force a fresh download
|
|
184
251
|
const shouldForceDownload = (baselineBuild || baselineComparison) && config.apiKey;
|
|
185
252
|
if (shouldForceDownload) {
|
|
186
|
-
output.debug('tdd', 'downloading baselines
|
|
253
|
+
output.debug('tdd', 'downloading baselines');
|
|
187
254
|
await tddService.downloadBaselines(config.build?.environment || 'test', config.build?.branch || null, baselineBuild, baselineComparison);
|
|
188
255
|
return;
|
|
189
256
|
}
|
|
@@ -191,13 +258,13 @@ export const createTddHandler = (config, workingDir, baselineBuild, baselineComp
|
|
|
191
258
|
if (!baseline) {
|
|
192
259
|
// Only download baselines if explicitly requested via baseline flags
|
|
193
260
|
if ((baselineBuild || baselineComparison) && config.apiKey) {
|
|
194
|
-
output.debug('tdd', 'downloading baselines
|
|
261
|
+
output.debug('tdd', 'downloading baselines');
|
|
195
262
|
await tddService.downloadBaselines(config.build?.environment || 'test', config.build?.branch || null, baselineBuild, baselineComparison);
|
|
196
263
|
} else {
|
|
197
|
-
output.debug('tdd', 'no baselines
|
|
264
|
+
output.debug('tdd', 'no baselines yet');
|
|
198
265
|
}
|
|
199
266
|
} else {
|
|
200
|
-
output.debug('tdd', `
|
|
267
|
+
output.debug('tdd', `baseline: ${baseline.buildName}`);
|
|
201
268
|
}
|
|
202
269
|
};
|
|
203
270
|
const handleScreenshot = async (_buildId, name, image, properties = {}) => {
|
|
@@ -217,18 +284,7 @@ export const createTddHandler = (config, workingDir, baselineBuild, baselineComp
|
|
|
217
284
|
}
|
|
218
285
|
|
|
219
286
|
// Unwrap double-nested properties if needed (client SDK wraps options in properties field)
|
|
220
|
-
|
|
221
|
-
// and client SDK wraps it as { properties: options }
|
|
222
|
-
let unwrappedProperties = properties;
|
|
223
|
-
if (properties.properties && typeof properties.properties === 'object') {
|
|
224
|
-
// Merge top-level properties with nested properties
|
|
225
|
-
unwrappedProperties = {
|
|
226
|
-
...properties,
|
|
227
|
-
...properties.properties
|
|
228
|
-
};
|
|
229
|
-
// Remove the nested properties field to avoid confusion
|
|
230
|
-
delete unwrappedProperties.properties;
|
|
231
|
-
}
|
|
287
|
+
let unwrappedProperties = unwrapProperties(properties);
|
|
232
288
|
|
|
233
289
|
// Validate and sanitize properties
|
|
234
290
|
let validatedProperties;
|
|
@@ -246,19 +302,7 @@ export const createTddHandler = (config, workingDir, baselineBuild, baselineComp
|
|
|
246
302
|
}
|
|
247
303
|
|
|
248
304
|
// Extract ALL properties to top-level (matching cloud API behavior)
|
|
249
|
-
|
|
250
|
-
// Spread all validated properties first, then normalize viewport/browser for cloud format
|
|
251
|
-
const extractedProperties = {
|
|
252
|
-
...validatedProperties,
|
|
253
|
-
// Normalize viewport to top-level viewport_width/height (cloud format)
|
|
254
|
-
// Use nullish coalescing to preserve any existing top-level values
|
|
255
|
-
viewport_width: validatedProperties.viewport?.width ?? validatedProperties.viewport_width ?? null,
|
|
256
|
-
viewport_height: validatedProperties.viewport?.height ?? validatedProperties.viewport_height ?? null,
|
|
257
|
-
browser: validatedProperties.browser ?? null,
|
|
258
|
-
// Preserve nested structure in metadata for backward compatibility
|
|
259
|
-
// Signature generation checks multiple locations: top-level, metadata.*, metadata.properties.*
|
|
260
|
-
metadata: validatedProperties
|
|
261
|
-
};
|
|
305
|
+
const extractedProperties = extractProperties(validatedProperties);
|
|
262
306
|
|
|
263
307
|
// Support both base64 encoded images and file paths
|
|
264
308
|
// Vitest browser mode returns file paths, so we need to handle both
|
|
@@ -336,16 +380,7 @@ export const createTddHandler = (config, workingDir, baselineBuild, baselineComp
|
|
|
336
380
|
// Comparison tracked by tdd.js event handler
|
|
337
381
|
|
|
338
382
|
// Convert absolute file paths to web-accessible URLs
|
|
339
|
-
const
|
|
340
|
-
if (!filePath) return null;
|
|
341
|
-
// Convert absolute path to relative path from .vizzly directory
|
|
342
|
-
const vizzlyDir = join(workingDir, '.vizzly');
|
|
343
|
-
if (filePath.startsWith(vizzlyDir)) {
|
|
344
|
-
const relativePath = filePath.substring(vizzlyDir.length + 1);
|
|
345
|
-
return `/images/${relativePath}`;
|
|
346
|
-
}
|
|
347
|
-
return filePath;
|
|
348
|
-
};
|
|
383
|
+
const vizzlyDir = join(workingDir, '.vizzly');
|
|
349
384
|
|
|
350
385
|
// Record the comparison for the dashboard
|
|
351
386
|
const newComparison = {
|
|
@@ -354,9 +389,9 @@ export const createTddHandler = (config, workingDir, baselineBuild, baselineComp
|
|
|
354
389
|
name: comparison.name,
|
|
355
390
|
originalName: name,
|
|
356
391
|
status: comparison.status,
|
|
357
|
-
baseline: convertPathToUrl(comparison.baseline),
|
|
358
|
-
current: convertPathToUrl(comparison.current),
|
|
359
|
-
diff: convertPathToUrl(comparison.diff),
|
|
392
|
+
baseline: convertPathToUrl(comparison.baseline, vizzlyDir),
|
|
393
|
+
current: convertPathToUrl(comparison.current, vizzlyDir),
|
|
394
|
+
diff: convertPathToUrl(comparison.diff, vizzlyDir),
|
|
360
395
|
diffPercentage: comparison.diffPercentage,
|
|
361
396
|
threshold: comparison.threshold,
|
|
362
397
|
properties: extractedProperties,
|
|
@@ -456,6 +491,33 @@ export const createTddHandler = (config, workingDir, baselineBuild, baselineComp
|
|
|
456
491
|
throw error;
|
|
457
492
|
}
|
|
458
493
|
};
|
|
494
|
+
const rejectBaseline = async comparisonId => {
|
|
495
|
+
try {
|
|
496
|
+
// Read current report data to get the comparison
|
|
497
|
+
const reportData = readReportData();
|
|
498
|
+
const comparison = reportData.comparisons.find(c => c.id === comparisonId);
|
|
499
|
+
if (!comparison) {
|
|
500
|
+
throw new Error(`Comparison not found with ID: ${comparisonId}`);
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
// Rejecting means: keep the current baseline, mark comparison as rejected
|
|
504
|
+
// The user is saying "I don't want this change, the baseline is correct"
|
|
505
|
+
// We update the status to 'rejected' so the UI shows the decision was made
|
|
506
|
+
const updatedComparison = {
|
|
507
|
+
...comparison,
|
|
508
|
+
status: 'rejected'
|
|
509
|
+
};
|
|
510
|
+
updateComparison(updatedComparison);
|
|
511
|
+
output.info(`Changes rejected for comparison ${comparisonId}`);
|
|
512
|
+
return {
|
|
513
|
+
success: true,
|
|
514
|
+
id: comparisonId
|
|
515
|
+
};
|
|
516
|
+
} catch (error) {
|
|
517
|
+
output.error(`Failed to reject baseline for ${comparisonId}:`, error);
|
|
518
|
+
throw error;
|
|
519
|
+
}
|
|
520
|
+
};
|
|
459
521
|
const acceptAllBaselines = async () => {
|
|
460
522
|
try {
|
|
461
523
|
output.debug('tdd', 'accepting all baselines');
|
|
@@ -590,6 +652,80 @@ export const createTddHandler = (config, workingDir, baselineBuild, baselineComp
|
|
|
590
652
|
throw error;
|
|
591
653
|
}
|
|
592
654
|
};
|
|
655
|
+
|
|
656
|
+
/**
|
|
657
|
+
* Safely delete a file within the .vizzly directory
|
|
658
|
+
* @param {string} imagePath - Path like "/images/baselines/foo.png"
|
|
659
|
+
* @param {string} label - Label for logging (e.g., "baseline", "current", "diff")
|
|
660
|
+
* @param {string} name - Screenshot name for logging
|
|
661
|
+
*/
|
|
662
|
+
const safeDeleteFile = (imagePath, label, name) => {
|
|
663
|
+
if (!imagePath) return;
|
|
664
|
+
try {
|
|
665
|
+
// Use safePath to validate the path stays within workingDir
|
|
666
|
+
const filePath = safePath(workingDir, '.vizzly', imagePath.replace('/images/', ''));
|
|
667
|
+
if (existsSync(filePath)) {
|
|
668
|
+
unlinkSync(filePath);
|
|
669
|
+
output.debug(`Deleted ${label} for ${name}`);
|
|
670
|
+
}
|
|
671
|
+
} catch (error) {
|
|
672
|
+
// safePath throws if path traversal is attempted
|
|
673
|
+
output.warn(`Failed to delete ${label} for ${name}: ${error.message}`);
|
|
674
|
+
}
|
|
675
|
+
};
|
|
676
|
+
const deleteComparison = async comparisonId => {
|
|
677
|
+
const reportData = readReportData();
|
|
678
|
+
const comparison = reportData.comparisons.find(c => c.id === comparisonId);
|
|
679
|
+
if (!comparison) {
|
|
680
|
+
const error = new Error(`Comparison not found with ID: ${comparisonId}`);
|
|
681
|
+
error.code = 'NOT_FOUND';
|
|
682
|
+
throw error;
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
// Delete image files (safePath validates paths stay within workingDir)
|
|
686
|
+
safeDeleteFile(comparison.baseline, 'baseline', comparison.name);
|
|
687
|
+
safeDeleteFile(comparison.current, 'current', comparison.name);
|
|
688
|
+
safeDeleteFile(comparison.diff, 'diff', comparison.name);
|
|
689
|
+
|
|
690
|
+
// Remove from baseline metadata if it exists
|
|
691
|
+
try {
|
|
692
|
+
const metadataPath = safePath(workingDir, '.vizzly', 'baselines', 'metadata.json');
|
|
693
|
+
if (existsSync(metadataPath) && comparison.signature) {
|
|
694
|
+
const metadata = JSON.parse(readFileSync(metadataPath, 'utf8'));
|
|
695
|
+
if (metadata.screenshots) {
|
|
696
|
+
const originalLength = metadata.screenshots.length;
|
|
697
|
+
metadata.screenshots = metadata.screenshots.filter(s => s.signature !== comparison.signature);
|
|
698
|
+
if (metadata.screenshots.length < originalLength) {
|
|
699
|
+
writeFileSync(metadataPath, JSON.stringify(metadata, null, 2));
|
|
700
|
+
output.debug(`Removed ${comparison.signature} from baseline metadata`);
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
} catch (error) {
|
|
705
|
+
output.warn(`Failed to update baseline metadata: ${error.message}`);
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
// Remove comparison from report data
|
|
709
|
+
reportData.comparisons = reportData.comparisons.filter(c => c.id !== comparisonId);
|
|
710
|
+
|
|
711
|
+
// Regenerate groups and summary
|
|
712
|
+
reportData.groups = groupComparisons(reportData.comparisons);
|
|
713
|
+
reportData.timestamp = Date.now();
|
|
714
|
+
reportData.summary = {
|
|
715
|
+
total: reportData.comparisons.length,
|
|
716
|
+
groups: reportData.groups.length,
|
|
717
|
+
passed: reportData.comparisons.filter(c => c.status === 'passed' || c.status === 'baseline-created' || c.status === 'new').length,
|
|
718
|
+
failed: reportData.comparisons.filter(c => c.status === 'failed').length,
|
|
719
|
+
rejected: reportData.comparisons.filter(c => c.status === 'rejected').length,
|
|
720
|
+
errors: reportData.comparisons.filter(c => c.status === 'error').length
|
|
721
|
+
};
|
|
722
|
+
writeFileSync(reportPath, JSON.stringify(reportData, null, 2));
|
|
723
|
+
output.info(`Deleted comparison ${comparisonId} (${comparison.name})`);
|
|
724
|
+
return {
|
|
725
|
+
success: true,
|
|
726
|
+
id: comparisonId
|
|
727
|
+
};
|
|
728
|
+
};
|
|
593
729
|
const cleanup = () => {
|
|
594
730
|
// Report data is persisted to file, no in-memory cleanup needed
|
|
595
731
|
};
|
|
@@ -598,8 +734,10 @@ export const createTddHandler = (config, workingDir, baselineBuild, baselineComp
|
|
|
598
734
|
handleScreenshot,
|
|
599
735
|
getResults,
|
|
600
736
|
acceptBaseline,
|
|
737
|
+
rejectBaseline,
|
|
601
738
|
acceptAllBaselines,
|
|
602
739
|
resetBaselines,
|
|
740
|
+
deleteComparison,
|
|
603
741
|
cleanup,
|
|
604
742
|
// Expose tddService for baseline download operations
|
|
605
743
|
tddService
|
|
@@ -28,7 +28,8 @@ export const createHttpServer = (port, screenshotHandler, services = {}) => {
|
|
|
28
28
|
configService,
|
|
29
29
|
authService,
|
|
30
30
|
projectService,
|
|
31
|
-
tddService
|
|
31
|
+
tddService,
|
|
32
|
+
workingDir
|
|
32
33
|
} = services;
|
|
33
34
|
|
|
34
35
|
// Create router context
|
|
@@ -40,6 +41,7 @@ export const createHttpServer = (port, screenshotHandler, services = {}) => {
|
|
|
40
41
|
authService,
|
|
41
42
|
projectService,
|
|
42
43
|
tddService,
|
|
44
|
+
workingDir: workingDir || process.cwd(),
|
|
43
45
|
apiUrl: 'https://app.vizzly.dev'
|
|
44
46
|
};
|
|
45
47
|
|
|
@@ -98,7 +100,8 @@ export const createHttpServer = (port, screenshotHandler, services = {}) => {
|
|
|
98
100
|
if (error) {
|
|
99
101
|
reject(error);
|
|
100
102
|
} else {
|
|
101
|
-
|
|
103
|
+
// Don't log here - let the caller handle success logging via onServerReady callback
|
|
104
|
+
// This prevents duplicate "listening on" messages
|
|
102
105
|
resolve();
|
|
103
106
|
}
|
|
104
107
|
});
|
|
@@ -114,9 +117,12 @@ export const createHttpServer = (port, screenshotHandler, services = {}) => {
|
|
|
114
117
|
const stop = () => {
|
|
115
118
|
if (server) {
|
|
116
119
|
return new Promise(resolve => {
|
|
120
|
+
// Close all keep-alive connections immediately (Node 18.2+)
|
|
121
|
+
if (server.closeAllConnections) {
|
|
122
|
+
server.closeAllConnections();
|
|
123
|
+
}
|
|
117
124
|
server.close(() => {
|
|
118
125
|
server = null;
|
|
119
|
-
output.debug('server', 'stopped');
|
|
120
126
|
resolve();
|
|
121
127
|
});
|
|
122
128
|
});
|
|
@@ -67,6 +67,64 @@ export function createBaselineRouter({
|
|
|
67
67
|
}
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
+
// Reject a single comparison (keep current baseline, discard changes)
|
|
71
|
+
if (req.method === 'POST' && pathname === '/api/baseline/reject') {
|
|
72
|
+
if (!screenshotHandler?.rejectBaseline) {
|
|
73
|
+
sendError(res, 400, 'Baseline management not available');
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
76
|
+
try {
|
|
77
|
+
const {
|
|
78
|
+
id
|
|
79
|
+
} = await parseJsonBody(req);
|
|
80
|
+
if (!id) {
|
|
81
|
+
sendError(res, 400, 'Comparison ID required');
|
|
82
|
+
return true;
|
|
83
|
+
}
|
|
84
|
+
await screenshotHandler.rejectBaseline(id);
|
|
85
|
+
sendSuccess(res, {
|
|
86
|
+
success: true,
|
|
87
|
+
message: `Changes rejected for comparison ${id}`
|
|
88
|
+
});
|
|
89
|
+
return true;
|
|
90
|
+
} catch (error) {
|
|
91
|
+
output.error('Error rejecting baseline:', error);
|
|
92
|
+
sendError(res, 500, error.message);
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Delete a comparison entirely (removes from report and deletes files)
|
|
98
|
+
if (req.method === 'POST' && pathname === '/api/baseline/delete') {
|
|
99
|
+
if (!screenshotHandler?.deleteComparison) {
|
|
100
|
+
sendError(res, 400, 'Baseline management not available');
|
|
101
|
+
return true;
|
|
102
|
+
}
|
|
103
|
+
try {
|
|
104
|
+
const {
|
|
105
|
+
id
|
|
106
|
+
} = await parseJsonBody(req);
|
|
107
|
+
if (!id) {
|
|
108
|
+
sendError(res, 400, 'Comparison ID required');
|
|
109
|
+
return true;
|
|
110
|
+
}
|
|
111
|
+
await screenshotHandler.deleteComparison(id);
|
|
112
|
+
sendSuccess(res, {
|
|
113
|
+
success: true,
|
|
114
|
+
message: `Comparison ${id} deleted`
|
|
115
|
+
});
|
|
116
|
+
return true;
|
|
117
|
+
} catch (error) {
|
|
118
|
+
if (error.code === 'NOT_FOUND') {
|
|
119
|
+
sendError(res, 404, error.message);
|
|
120
|
+
} else {
|
|
121
|
+
output.error('Error deleting comparison:', error);
|
|
122
|
+
sendError(res, 500, error.message);
|
|
123
|
+
}
|
|
124
|
+
return true;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
70
128
|
// Reset baselines to previous state
|
|
71
129
|
if (req.method === 'POST' && pathname === '/api/baseline/reset') {
|
|
72
130
|
if (!screenshotHandler?.resetBaselines) {
|
|
@@ -9,14 +9,18 @@ import * as output from '../../utils/output.js';
|
|
|
9
9
|
import { sendHtml, sendSuccess } from '../middleware/response.js';
|
|
10
10
|
|
|
11
11
|
// SPA routes that should serve the dashboard HTML
|
|
12
|
-
const SPA_ROUTES = ['/', '/
|
|
12
|
+
const SPA_ROUTES = ['/', '/stats', '/settings', '/projects', '/builds'];
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
15
|
* Create dashboard router
|
|
16
16
|
* @param {Object} context - Router context
|
|
17
|
+
* @param {string} context.workingDir - Working directory for report data
|
|
17
18
|
* @returns {Function} Route handler
|
|
18
19
|
*/
|
|
19
|
-
export function createDashboardRouter() {
|
|
20
|
+
export function createDashboardRouter(context) {
|
|
21
|
+
const {
|
|
22
|
+
workingDir = process.cwd()
|
|
23
|
+
} = context || {};
|
|
20
24
|
return async function handleDashboardRoute(req, res, pathname) {
|
|
21
25
|
if (req.method !== 'GET') {
|
|
22
26
|
return false;
|
|
@@ -24,7 +28,7 @@ export function createDashboardRouter() {
|
|
|
24
28
|
|
|
25
29
|
// API endpoint for fetching report data
|
|
26
30
|
if (pathname === '/api/report-data') {
|
|
27
|
-
const reportDataPath = join(
|
|
31
|
+
const reportDataPath = join(workingDir, '.vizzly', 'report-data.json');
|
|
28
32
|
if (existsSync(reportDataPath)) {
|
|
29
33
|
try {
|
|
30
34
|
const data = readFileSync(reportDataPath, 'utf8');
|
|
@@ -50,8 +54,8 @@ export function createDashboardRouter() {
|
|
|
50
54
|
|
|
51
55
|
// API endpoint for real-time status
|
|
52
56
|
if (pathname === '/api/status') {
|
|
53
|
-
const reportDataPath = join(
|
|
54
|
-
const baselineMetadataPath = join(
|
|
57
|
+
const reportDataPath = join(workingDir, '.vizzly', 'report-data.json');
|
|
58
|
+
const baselineMetadataPath = join(workingDir, '.vizzly', 'baselines', 'metadata.json');
|
|
55
59
|
let reportData = null;
|
|
56
60
|
let baselineInfo = null;
|
|
57
61
|
if (existsSync(reportDataPath)) {
|
|
@@ -84,7 +88,7 @@ export function createDashboardRouter() {
|
|
|
84
88
|
|
|
85
89
|
// Serve React SPA for dashboard routes
|
|
86
90
|
if (SPA_ROUTES.includes(pathname) || pathname.startsWith('/comparison/')) {
|
|
87
|
-
const reportDataPath = join(
|
|
91
|
+
const reportDataPath = join(workingDir, '.vizzly', 'report-data.json');
|
|
88
92
|
let reportData = null;
|
|
89
93
|
if (existsSync(reportDataPath)) {
|
|
90
94
|
try {
|
|
@@ -52,6 +52,38 @@ export function createScreenshotRouter({
|
|
|
52
52
|
}
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
+
// Flush endpoint - signals test completion and prints summary
|
|
56
|
+
if (pathname === '/flush') {
|
|
57
|
+
try {
|
|
58
|
+
if (screenshotHandler.getResults) {
|
|
59
|
+
// This triggers printResults() which outputs the summary
|
|
60
|
+
const results = await screenshotHandler.getResults();
|
|
61
|
+
sendJson(res, 200, {
|
|
62
|
+
success: true,
|
|
63
|
+
summary: {
|
|
64
|
+
total: results.total || 0,
|
|
65
|
+
passed: results.passed || 0,
|
|
66
|
+
failed: results.failed || 0,
|
|
67
|
+
new: results.new || 0,
|
|
68
|
+
errors: results.errors || 0
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
} else {
|
|
72
|
+
sendJson(res, 200, {
|
|
73
|
+
success: true,
|
|
74
|
+
message: 'No TDD results'
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
return true;
|
|
78
|
+
} catch (error) {
|
|
79
|
+
output.debug('Flush error:', {
|
|
80
|
+
error: error.message
|
|
81
|
+
});
|
|
82
|
+
sendError(res, 500, 'Failed to flush');
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
55
87
|
// Legacy accept-baseline endpoint
|
|
56
88
|
if (pathname === '/accept-baseline') {
|
|
57
89
|
try {
|