@vizzly-testing/cli 0.11.2 → 0.13.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 +87 -544
- package/dist/client/index.js +7 -5
- package/dist/reporter/reporter-bundle.css +1 -1
- package/dist/reporter/reporter-bundle.iife.js +20 -13
- package/dist/server/handlers/api-handler.js +53 -1
- package/dist/server/handlers/tdd-handler.js +262 -24
- package/dist/server/http-server.js +9 -9
- package/dist/services/tdd-service.js +173 -57
- package/dist/types/reporter/src/components/comparison/comparison-card.d.ts +2 -1
- package/dist/types/reporter/src/components/comparison/comparison-group.d.ts +10 -0
- package/dist/types/reporter/src/components/comparison/variant-selector.d.ts +9 -0
- package/dist/types/reporter/src/services/api-client.d.ts +1 -1
- package/dist/types/server/handlers/api-handler.d.ts +48 -0
- package/dist/types/server/handlers/tdd-handler.d.ts +69 -12
- package/dist/types/services/server-manager.d.ts +117 -12
- package/dist/types/services/tdd-service.d.ts +4 -2
- package/dist/types/utils/image-input-detector.d.ts +71 -0
- package/dist/utils/image-input-detector.js +150 -0
- package/docs/tdd-mode.md +6 -6
- package/package.json +3 -3
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { Buffer } from 'buffer';
|
|
2
|
+
import { existsSync, readFileSync } from 'fs';
|
|
3
|
+
import { resolve } from 'path';
|
|
2
4
|
import { createServiceLogger } from '../../utils/logger-factory.js';
|
|
5
|
+
import { detectImageInputType } from '../../utils/image-input-detector.js';
|
|
3
6
|
const logger = createServiceLogger('API-HANDLER');
|
|
4
7
|
|
|
5
8
|
/**
|
|
@@ -60,7 +63,56 @@ export const createApiHandler = apiService => {
|
|
|
60
63
|
}
|
|
61
64
|
};
|
|
62
65
|
}
|
|
63
|
-
|
|
66
|
+
|
|
67
|
+
// Support both base64 encoded images and file paths
|
|
68
|
+
let imageBuffer;
|
|
69
|
+
const inputType = detectImageInputType(image);
|
|
70
|
+
if (inputType === 'file-path') {
|
|
71
|
+
// It's a file path - resolve and read the file
|
|
72
|
+
const filePath = resolve(image.replace('file://', ''));
|
|
73
|
+
if (!existsSync(filePath)) {
|
|
74
|
+
return {
|
|
75
|
+
statusCode: 400,
|
|
76
|
+
body: {
|
|
77
|
+
error: `Screenshot file not found: ${filePath}`,
|
|
78
|
+
originalPath: image
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
try {
|
|
83
|
+
imageBuffer = readFileSync(filePath);
|
|
84
|
+
logger.debug(`Loaded screenshot from file: ${filePath}`);
|
|
85
|
+
} catch (error) {
|
|
86
|
+
return {
|
|
87
|
+
statusCode: 500,
|
|
88
|
+
body: {
|
|
89
|
+
error: `Failed to read screenshot file: ${error.message}`,
|
|
90
|
+
filePath
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
} else if (inputType === 'base64') {
|
|
95
|
+
// It's base64 encoded
|
|
96
|
+
try {
|
|
97
|
+
imageBuffer = Buffer.from(image, 'base64');
|
|
98
|
+
} catch (error) {
|
|
99
|
+
return {
|
|
100
|
+
statusCode: 400,
|
|
101
|
+
body: {
|
|
102
|
+
error: `Invalid base64 image data: ${error.message}`
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
} else {
|
|
107
|
+
// Unknown input type
|
|
108
|
+
return {
|
|
109
|
+
statusCode: 400,
|
|
110
|
+
body: {
|
|
111
|
+
error: 'Invalid image input: must be a file path or base64 encoded image data',
|
|
112
|
+
receivedType: typeof image
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
}
|
|
64
116
|
screenshotCount++;
|
|
65
117
|
|
|
66
118
|
// Fire upload in background - DON'T AWAIT!
|
|
@@ -1,10 +1,134 @@
|
|
|
1
1
|
import { Buffer } from 'buffer';
|
|
2
2
|
import { writeFileSync, readFileSync, existsSync } from 'fs';
|
|
3
|
-
import { join } from 'path';
|
|
3
|
+
import { join, resolve } from 'path';
|
|
4
4
|
import { createServiceLogger } from '../../utils/logger-factory.js';
|
|
5
5
|
import { TddService } from '../../services/tdd-service.js';
|
|
6
6
|
import { sanitizeScreenshotName, validateScreenshotProperties } from '../../utils/security.js';
|
|
7
|
+
import { detectImageInputType } from '../../utils/image-input-detector.js';
|
|
7
8
|
const logger = createServiceLogger('TDD-HANDLER');
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Detect PNG dimensions by reading the IHDR chunk header
|
|
12
|
+
* PNG spec (ISO/IEC 15948:2004) guarantees width/height at bytes 16-23
|
|
13
|
+
* @param {Buffer} buffer - PNG image buffer
|
|
14
|
+
* @returns {{ width: number, height: number } | null} Dimensions or null if not a valid PNG
|
|
15
|
+
*/
|
|
16
|
+
const detectPNGDimensions = buffer => {
|
|
17
|
+
// Full PNG signature (8 bytes): 89 50 4E 47 0D 0A 1A 0A
|
|
18
|
+
const PNG_SIGNATURE = [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a];
|
|
19
|
+
|
|
20
|
+
// Need at least 24 bytes (8 signature + 4 length + 4 type + 8 width/height)
|
|
21
|
+
if (!buffer || buffer.length < 24) {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Validate full 8-byte PNG signature
|
|
26
|
+
for (let i = 0; i < PNG_SIGNATURE.length; i++) {
|
|
27
|
+
if (buffer[i] !== PNG_SIGNATURE[i]) {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Validate IHDR chunk type at bytes 12-15 (should be 'IHDR')
|
|
33
|
+
// 0x49484452 = 'IHDR' in ASCII
|
|
34
|
+
if (buffer[12] !== 0x49 || buffer[13] !== 0x48 || buffer[14] !== 0x44 || buffer[15] !== 0x52) {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Read width and height from IHDR chunk (guaranteed positions per PNG spec)
|
|
39
|
+
const width = buffer.readUInt32BE(16); // Bytes 16-19
|
|
40
|
+
const height = buffer.readUInt32BE(20); // Bytes 20-23
|
|
41
|
+
|
|
42
|
+
// Sanity check: dimensions should be positive and reasonable
|
|
43
|
+
if (width <= 0 || height <= 0 || width > 65535 || height > 65535) {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
return {
|
|
47
|
+
width,
|
|
48
|
+
height
|
|
49
|
+
};
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Group comparisons by screenshot name with variant structure
|
|
54
|
+
* Matches cloud product's grouping logic from comparison.js
|
|
55
|
+
*/
|
|
56
|
+
const groupComparisons = comparisons => {
|
|
57
|
+
const groups = new Map();
|
|
58
|
+
|
|
59
|
+
// Group by screenshot name
|
|
60
|
+
for (const comp of comparisons) {
|
|
61
|
+
if (!groups.has(comp.name)) {
|
|
62
|
+
groups.set(comp.name, {
|
|
63
|
+
name: comp.name,
|
|
64
|
+
comparisons: [],
|
|
65
|
+
browsers: new Set(),
|
|
66
|
+
viewports: new Set(),
|
|
67
|
+
devices: new Set(),
|
|
68
|
+
totalVariants: 0
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
const group = groups.get(comp.name);
|
|
72
|
+
group.comparisons.push(comp);
|
|
73
|
+
group.totalVariants++;
|
|
74
|
+
|
|
75
|
+
// Track unique browsers, viewports, devices
|
|
76
|
+
if (comp.properties?.browser) {
|
|
77
|
+
group.browsers.add(comp.properties.browser);
|
|
78
|
+
}
|
|
79
|
+
if (comp.properties?.viewport_width && comp.properties?.viewport_height) {
|
|
80
|
+
group.viewports.add(`${comp.properties.viewport_width}x${comp.properties.viewport_height}`);
|
|
81
|
+
}
|
|
82
|
+
if (comp.properties?.device) {
|
|
83
|
+
group.devices.add(comp.properties.device);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Convert to final structure
|
|
88
|
+
return Array.from(groups.values()).map(group => {
|
|
89
|
+
const browsers = Array.from(group.browsers);
|
|
90
|
+
const viewports = Array.from(group.viewports);
|
|
91
|
+
const devices = Array.from(group.devices);
|
|
92
|
+
|
|
93
|
+
// Build variants structure (browser -> viewport -> comparisons)
|
|
94
|
+
const variants = {};
|
|
95
|
+
group.comparisons.forEach(comp => {
|
|
96
|
+
const browser = comp.properties?.browser || null;
|
|
97
|
+
const viewport = comp.properties?.viewport_width && comp.properties?.viewport_height ? `${comp.properties.viewport_width}x${comp.properties.viewport_height}` : null;
|
|
98
|
+
if (!variants[browser]) variants[browser] = {};
|
|
99
|
+
if (!variants[browser][viewport]) variants[browser][viewport] = [];
|
|
100
|
+
variants[browser][viewport].push(comp);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
// Determine grouping strategy
|
|
104
|
+
let groupingStrategy = 'flat';
|
|
105
|
+
if (browsers.length > 1) groupingStrategy = 'browser';else if (viewports.length > 1) groupingStrategy = 'viewport';
|
|
106
|
+
|
|
107
|
+
// Sort comparisons by viewport area (largest first)
|
|
108
|
+
group.comparisons.sort((a, b) => {
|
|
109
|
+
const aArea = (a.properties?.viewport_width || 0) * (a.properties?.viewport_height || 0);
|
|
110
|
+
const bArea = (b.properties?.viewport_width || 0) * (b.properties?.viewport_height || 0);
|
|
111
|
+
if (bArea !== aArea) return bArea - aArea;
|
|
112
|
+
return (b.properties?.viewport_width || 0) - (a.properties?.viewport_width || 0);
|
|
113
|
+
});
|
|
114
|
+
return {
|
|
115
|
+
...group,
|
|
116
|
+
browsers,
|
|
117
|
+
viewports,
|
|
118
|
+
devices: Array.from(devices),
|
|
119
|
+
variants,
|
|
120
|
+
groupingStrategy
|
|
121
|
+
};
|
|
122
|
+
}).sort((a, b) => {
|
|
123
|
+
// Sort groups: multi-variant first (by variant count), then singles alphabetically
|
|
124
|
+
if (a.totalVariants > 1 && b.totalVariants === 1) return -1;
|
|
125
|
+
if (a.totalVariants === 1 && b.totalVariants > 1) return 1;
|
|
126
|
+
if (a.totalVariants > 1 && b.totalVariants > 1) {
|
|
127
|
+
return b.totalVariants - a.totalVariants;
|
|
128
|
+
}
|
|
129
|
+
return a.name.localeCompare(b.name);
|
|
130
|
+
});
|
|
131
|
+
};
|
|
8
132
|
export const createTddHandler = (config, workingDir, baselineBuild, baselineComparison, setBaseline = false) => {
|
|
9
133
|
const tddService = new TddService(config, workingDir, setBaseline);
|
|
10
134
|
const reportPath = join(workingDir, '.vizzly', 'report-data.json');
|
|
@@ -14,8 +138,12 @@ export const createTddHandler = (config, workingDir, baselineBuild, baselineComp
|
|
|
14
138
|
return {
|
|
15
139
|
timestamp: Date.now(),
|
|
16
140
|
comparisons: [],
|
|
141
|
+
// Internal flat list for easy updates
|
|
142
|
+
groups: [],
|
|
143
|
+
// Grouped structure for UI
|
|
17
144
|
summary: {
|
|
18
145
|
total: 0,
|
|
146
|
+
groups: 0,
|
|
19
147
|
passed: 0,
|
|
20
148
|
failed: 0,
|
|
21
149
|
errors: 0
|
|
@@ -29,8 +157,10 @@ export const createTddHandler = (config, workingDir, baselineBuild, baselineComp
|
|
|
29
157
|
return {
|
|
30
158
|
timestamp: Date.now(),
|
|
31
159
|
comparisons: [],
|
|
160
|
+
groups: [],
|
|
32
161
|
summary: {
|
|
33
162
|
total: 0,
|
|
163
|
+
groups: 0,
|
|
34
164
|
passed: 0,
|
|
35
165
|
failed: 0,
|
|
36
166
|
errors: 0
|
|
@@ -42,26 +172,36 @@ export const createTddHandler = (config, workingDir, baselineBuild, baselineComp
|
|
|
42
172
|
try {
|
|
43
173
|
const reportData = readReportData();
|
|
44
174
|
|
|
45
|
-
//
|
|
46
|
-
|
|
175
|
+
// Ensure comparisons array exists (backward compatibility)
|
|
176
|
+
if (!reportData.comparisons) {
|
|
177
|
+
reportData.comparisons = [];
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Find existing comparison by unique ID
|
|
181
|
+
// This ensures we update the correct variant even with same name
|
|
182
|
+
const existingIndex = reportData.comparisons.findIndex(c => c.id === newComparison.id);
|
|
47
183
|
if (existingIndex >= 0) {
|
|
48
184
|
reportData.comparisons[existingIndex] = newComparison;
|
|
49
|
-
logger.debug(`Updated comparison for ${newComparison.name}`);
|
|
185
|
+
logger.debug(`Updated comparison for ${newComparison.name} (${newComparison.properties?.viewport_width}x${newComparison.properties?.viewport_height})`);
|
|
50
186
|
} else {
|
|
51
187
|
reportData.comparisons.push(newComparison);
|
|
52
|
-
logger.debug(`Added new comparison for ${newComparison.name}`);
|
|
188
|
+
logger.debug(`Added new comparison for ${newComparison.name} (${newComparison.properties?.viewport_width}x${newComparison.properties?.viewport_height})`);
|
|
53
189
|
}
|
|
54
190
|
|
|
191
|
+
// Generate grouped structure from flat comparisons
|
|
192
|
+
reportData.groups = groupComparisons(reportData.comparisons);
|
|
193
|
+
|
|
55
194
|
// Update summary
|
|
56
195
|
reportData.timestamp = Date.now();
|
|
57
196
|
reportData.summary = {
|
|
58
197
|
total: reportData.comparisons.length,
|
|
59
|
-
|
|
198
|
+
groups: reportData.groups.length,
|
|
199
|
+
passed: reportData.comparisons.filter(c => c.status === 'passed' || c.status === 'baseline-created' || c.status === 'new').length,
|
|
60
200
|
failed: reportData.comparisons.filter(c => c.status === 'failed').length,
|
|
61
201
|
errors: reportData.comparisons.filter(c => c.status === 'error').length
|
|
62
202
|
};
|
|
63
203
|
writeFileSync(reportPath, JSON.stringify(reportData, null, 2));
|
|
64
|
-
logger.debug('Report data saved
|
|
204
|
+
logger.debug('Report data saved with grouped structure');
|
|
65
205
|
} catch (error) {
|
|
66
206
|
logger.error('Failed to update comparison:', error);
|
|
67
207
|
}
|
|
@@ -111,10 +251,24 @@ export const createTddHandler = (config, workingDir, baselineBuild, baselineComp
|
|
|
111
251
|
};
|
|
112
252
|
}
|
|
113
253
|
|
|
254
|
+
// Unwrap double-nested properties if needed (client SDK wraps options in properties field)
|
|
255
|
+
// This happens when test helper passes { properties: {...}, threshold: 0.1 }
|
|
256
|
+
// and client SDK wraps it as { properties: options }
|
|
257
|
+
let unwrappedProperties = properties;
|
|
258
|
+
if (properties.properties && typeof properties.properties === 'object') {
|
|
259
|
+
// Merge top-level properties with nested properties
|
|
260
|
+
unwrappedProperties = {
|
|
261
|
+
...properties,
|
|
262
|
+
...properties.properties
|
|
263
|
+
};
|
|
264
|
+
// Remove the nested properties field to avoid confusion
|
|
265
|
+
delete unwrappedProperties.properties;
|
|
266
|
+
}
|
|
267
|
+
|
|
114
268
|
// Validate and sanitize properties
|
|
115
269
|
let validatedProperties;
|
|
116
270
|
try {
|
|
117
|
-
validatedProperties = validateScreenshotProperties(
|
|
271
|
+
validatedProperties = validateScreenshotProperties(unwrappedProperties);
|
|
118
272
|
} catch (error) {
|
|
119
273
|
return {
|
|
120
274
|
statusCode: 400,
|
|
@@ -125,14 +279,94 @@ export const createTddHandler = (config, workingDir, baselineBuild, baselineComp
|
|
|
125
279
|
}
|
|
126
280
|
};
|
|
127
281
|
}
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
282
|
+
|
|
283
|
+
// Extract viewport/browser to top-level properties (matching cloud API behavior)
|
|
284
|
+
// This ensures signature generation works correctly with: name|viewport_width|browser
|
|
285
|
+
const extractedProperties = {
|
|
286
|
+
viewport_width: validatedProperties.viewport?.width || null,
|
|
287
|
+
viewport_height: validatedProperties.viewport?.height || null,
|
|
288
|
+
browser: validatedProperties.browser || null,
|
|
289
|
+
device: validatedProperties.device || null,
|
|
290
|
+
url: validatedProperties.url || null,
|
|
291
|
+
selector: validatedProperties.selector || null,
|
|
292
|
+
threshold: validatedProperties.threshold,
|
|
293
|
+
// Preserve full nested structure in metadata for compatibility
|
|
294
|
+
metadata: validatedProperties
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
// Support both base64 encoded images and file paths
|
|
298
|
+
// Vitest browser mode returns file paths, so we need to handle both
|
|
299
|
+
let imageBuffer;
|
|
300
|
+
const inputType = detectImageInputType(image);
|
|
301
|
+
if (inputType === 'file-path') {
|
|
302
|
+
// It's a file path - resolve and read the file
|
|
303
|
+
const filePath = resolve(image.replace('file://', ''));
|
|
304
|
+
if (!existsSync(filePath)) {
|
|
305
|
+
return {
|
|
306
|
+
statusCode: 400,
|
|
307
|
+
body: {
|
|
308
|
+
error: `Screenshot file not found: ${filePath}`,
|
|
309
|
+
originalPath: image,
|
|
310
|
+
tddMode: true
|
|
311
|
+
}
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
try {
|
|
315
|
+
imageBuffer = readFileSync(filePath);
|
|
316
|
+
logger.debug(`Loaded screenshot from file: ${filePath}`);
|
|
317
|
+
} catch (error) {
|
|
318
|
+
return {
|
|
319
|
+
statusCode: 500,
|
|
320
|
+
body: {
|
|
321
|
+
error: `Failed to read screenshot file: ${error.message}`,
|
|
322
|
+
filePath,
|
|
323
|
+
tddMode: true
|
|
324
|
+
}
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
} else if (inputType === 'base64') {
|
|
328
|
+
// It's base64 encoded
|
|
329
|
+
try {
|
|
330
|
+
imageBuffer = Buffer.from(image, 'base64');
|
|
331
|
+
} catch (error) {
|
|
332
|
+
return {
|
|
333
|
+
statusCode: 400,
|
|
334
|
+
body: {
|
|
335
|
+
error: `Invalid base64 image data: ${error.message}`,
|
|
336
|
+
tddMode: true
|
|
337
|
+
}
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
} else {
|
|
341
|
+
// Unknown input type
|
|
342
|
+
return {
|
|
343
|
+
statusCode: 400,
|
|
344
|
+
body: {
|
|
345
|
+
error: 'Invalid image input: must be a file path or base64 encoded image data',
|
|
346
|
+
receivedType: typeof image,
|
|
347
|
+
tddMode: true
|
|
348
|
+
}
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// Auto-detect image dimensions from PNG header if viewport not provided
|
|
353
|
+
// This matches cloud API behavior but without requiring Sharp
|
|
354
|
+
if (!extractedProperties.viewport_width || !extractedProperties.viewport_height) {
|
|
355
|
+
const dimensions = detectPNGDimensions(imageBuffer);
|
|
356
|
+
if (dimensions) {
|
|
357
|
+
if (!extractedProperties.viewport_width) {
|
|
358
|
+
extractedProperties.viewport_width = dimensions.width;
|
|
359
|
+
}
|
|
360
|
+
if (!extractedProperties.viewport_height) {
|
|
361
|
+
extractedProperties.viewport_height = dimensions.height;
|
|
362
|
+
}
|
|
363
|
+
logger.debug(`Auto-detected dimensions from PNG: ${dimensions.width}x${dimensions.height}`);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
132
366
|
|
|
133
367
|
// Use the sanitized name as-is (no modification with browser/viewport)
|
|
134
368
|
// Baseline matching uses signature logic (name + viewport_width + browser)
|
|
135
|
-
const comparison = await tddService.compareScreenshot(sanitizedName, imageBuffer,
|
|
369
|
+
const comparison = await tddService.compareScreenshot(sanitizedName, imageBuffer, extractedProperties);
|
|
136
370
|
logger.debug(`Comparison result: ${comparison.status}`);
|
|
137
371
|
|
|
138
372
|
// Convert absolute file paths to web-accessible URLs
|
|
@@ -149,6 +383,8 @@ export const createTddHandler = (config, workingDir, baselineBuild, baselineComp
|
|
|
149
383
|
|
|
150
384
|
// Record the comparison for the dashboard
|
|
151
385
|
const newComparison = {
|
|
386
|
+
id: comparison.id,
|
|
387
|
+
// Include unique ID for variant identification
|
|
152
388
|
name: comparison.name,
|
|
153
389
|
originalName: name,
|
|
154
390
|
status: comparison.status,
|
|
@@ -157,7 +393,10 @@ export const createTddHandler = (config, workingDir, baselineBuild, baselineComp
|
|
|
157
393
|
diff: convertPathToUrl(comparison.diff),
|
|
158
394
|
diffPercentage: comparison.diffPercentage,
|
|
159
395
|
threshold: comparison.threshold,
|
|
160
|
-
properties:
|
|
396
|
+
properties: extractedProperties,
|
|
397
|
+
// Use extracted properties with top-level viewport_width/browser
|
|
398
|
+
signature: comparison.signature,
|
|
399
|
+
// Include signature for debugging
|
|
161
400
|
timestamp: Date.now()
|
|
162
401
|
};
|
|
163
402
|
|
|
@@ -223,16 +462,14 @@ export const createTddHandler = (config, workingDir, baselineBuild, baselineComp
|
|
|
223
462
|
const getResults = async () => {
|
|
224
463
|
return await tddService.printResults();
|
|
225
464
|
};
|
|
226
|
-
const acceptBaseline = async
|
|
465
|
+
const acceptBaseline = async comparisonId => {
|
|
227
466
|
try {
|
|
228
|
-
logger.debug(`Accepting baseline for screenshot: ${screenshotName}`);
|
|
229
|
-
|
|
230
467
|
// Use TDD service to accept the baseline
|
|
231
|
-
const result = await tddService.acceptBaseline(
|
|
468
|
+
const result = await tddService.acceptBaseline(comparisonId);
|
|
232
469
|
|
|
233
470
|
// Read current report data and update the comparison status
|
|
234
471
|
const reportData = readReportData();
|
|
235
|
-
const comparison = reportData.comparisons.find(c => c.
|
|
472
|
+
const comparison = reportData.comparisons.find(c => c.id === comparisonId);
|
|
236
473
|
if (comparison) {
|
|
237
474
|
// Update the comparison to passed status
|
|
238
475
|
const updatedComparison = {
|
|
@@ -242,14 +479,13 @@ export const createTddHandler = (config, workingDir, baselineBuild, baselineComp
|
|
|
242
479
|
diff: null
|
|
243
480
|
};
|
|
244
481
|
updateComparison(updatedComparison);
|
|
245
|
-
logger.debug('Comparison updated in report-data.json');
|
|
246
482
|
} else {
|
|
247
|
-
logger.error(`Comparison not found in report data for: ${
|
|
483
|
+
logger.error(`Comparison not found in report data for ID: ${comparisonId}`);
|
|
248
484
|
}
|
|
249
|
-
logger.info(`Baseline accepted for ${
|
|
485
|
+
logger.info(`Baseline accepted for comparison ${comparisonId}`);
|
|
250
486
|
return result;
|
|
251
487
|
} catch (error) {
|
|
252
|
-
logger.error(`Failed to accept baseline for ${
|
|
488
|
+
logger.error(`Failed to accept baseline for ${comparisonId}:`, error);
|
|
253
489
|
throw error;
|
|
254
490
|
}
|
|
255
491
|
};
|
|
@@ -262,7 +498,7 @@ export const createTddHandler = (config, workingDir, baselineBuild, baselineComp
|
|
|
262
498
|
// Accept all failed or new comparisons
|
|
263
499
|
for (const comparison of reportData.comparisons) {
|
|
264
500
|
if (comparison.status === 'failed' || comparison.status === 'new') {
|
|
265
|
-
await tddService.acceptBaseline(comparison.
|
|
501
|
+
await tddService.acceptBaseline(comparison.id);
|
|
266
502
|
|
|
267
503
|
// Update the comparison to passed status
|
|
268
504
|
updateComparison({
|
|
@@ -363,8 +599,10 @@ export const createTddHandler = (config, workingDir, baselineBuild, baselineComp
|
|
|
363
599
|
const freshReportData = {
|
|
364
600
|
timestamp: Date.now(),
|
|
365
601
|
comparisons: [],
|
|
602
|
+
groups: [],
|
|
366
603
|
summary: {
|
|
367
604
|
total: 0,
|
|
605
|
+
groups: 0,
|
|
368
606
|
passed: 0,
|
|
369
607
|
failed: 0,
|
|
370
608
|
errors: 0
|
|
@@ -233,21 +233,21 @@ export const createHttpServer = (port, screenshotHandler) => {
|
|
|
233
233
|
}
|
|
234
234
|
try {
|
|
235
235
|
const {
|
|
236
|
-
|
|
236
|
+
id
|
|
237
237
|
} = await parseRequestBody(req);
|
|
238
|
-
if (!
|
|
238
|
+
if (!id) {
|
|
239
239
|
res.statusCode = 400;
|
|
240
240
|
res.end(JSON.stringify({
|
|
241
|
-
error: '
|
|
241
|
+
error: 'Comparison ID required'
|
|
242
242
|
}));
|
|
243
243
|
return;
|
|
244
244
|
}
|
|
245
|
-
await screenshotHandler.acceptBaseline(
|
|
245
|
+
await screenshotHandler.acceptBaseline(id);
|
|
246
246
|
res.setHeader('Content-Type', 'application/json');
|
|
247
247
|
res.statusCode = 200;
|
|
248
248
|
res.end(JSON.stringify({
|
|
249
249
|
success: true,
|
|
250
|
-
message: `Baseline accepted for ${
|
|
250
|
+
message: `Baseline accepted for comparison ${id}`
|
|
251
251
|
}));
|
|
252
252
|
} catch (error) {
|
|
253
253
|
logger.error('Error accepting baseline:', error);
|
|
@@ -368,19 +368,19 @@ export const createHttpServer = (port, screenshotHandler) => {
|
|
|
368
368
|
try {
|
|
369
369
|
const body = await parseRequestBody(req);
|
|
370
370
|
const {
|
|
371
|
-
|
|
371
|
+
id
|
|
372
372
|
} = body;
|
|
373
|
-
if (!
|
|
373
|
+
if (!id) {
|
|
374
374
|
res.statusCode = 400;
|
|
375
375
|
res.end(JSON.stringify({
|
|
376
|
-
error: '
|
|
376
|
+
error: 'comparison ID is required'
|
|
377
377
|
}));
|
|
378
378
|
return;
|
|
379
379
|
}
|
|
380
380
|
|
|
381
381
|
// Call the screenshot handler's accept baseline method if it exists
|
|
382
382
|
if (screenshotHandler.acceptBaseline) {
|
|
383
|
-
const result = await screenshotHandler.acceptBaseline(
|
|
383
|
+
const result = await screenshotHandler.acceptBaseline(id);
|
|
384
384
|
res.statusCode = 200;
|
|
385
385
|
res.end(JSON.stringify({
|
|
386
386
|
success: true,
|