@vizzly-testing/cli 0.35.3-beta.3 → 0.36.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 +8 -8
- package/dist/api/core.js +20 -13
- package/dist/api/endpoints.js +14 -11
- package/dist/client/index.js +2 -7
- package/dist/commands/run.js +55 -3
- package/dist/reporter/reporter-bundle.iife.js +51 -51
- package/dist/screenshot-server/core.js +5 -1
- package/dist/sdk/index.js +2 -11
- package/dist/server/handlers/api-handler.js +117 -191
- package/dist/server/handlers/tdd-handler.js +15 -50
- package/dist/server/routers/screenshot.js +30 -4
- package/dist/tdd/tdd-service.js +3 -3
- package/dist/test-runner/core.js +7 -1
- package/dist/test-runner/operations.js +22 -7
- package/dist/types/client.d.ts +12 -5
- package/dist/types/index.d.ts +5 -1
- package/dist/uploader/core.js +4 -3
- package/dist/utils/screenshot-options.js +18 -59
- package/dist/utils/security.js +27 -42
- package/package.json +1 -1
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
* No I/O, no side effects - just data transformations.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
import { getScreenshotOptionsPayload } from '../utils/screenshot-options.js';
|
|
8
|
+
|
|
7
9
|
// ============================================================================
|
|
8
10
|
// Request Validation
|
|
9
11
|
// ============================================================================
|
|
@@ -149,9 +151,11 @@ export function buildServerStoppedMessage() {
|
|
|
149
151
|
* @returns {{ name: string, image: string, properties?: Object }}
|
|
150
152
|
*/
|
|
151
153
|
export function extractScreenshotData(body) {
|
|
152
|
-
|
|
154
|
+
let screenshotData = {
|
|
153
155
|
name: body.name,
|
|
154
156
|
image: body.image,
|
|
155
157
|
properties: body.properties
|
|
156
158
|
};
|
|
159
|
+
Object.assign(screenshotData, getScreenshotOptionsPayload(body));
|
|
160
|
+
return screenshotData;
|
|
157
161
|
}
|
package/dist/sdk/index.js
CHANGED
|
@@ -19,7 +19,7 @@ import { createUploader } from '../uploader/index.js';
|
|
|
19
19
|
import { loadConfig } from '../utils/config-loader.js';
|
|
20
20
|
import { resolveImageBuffer } from '../utils/file-helpers.js';
|
|
21
21
|
import * as output from '../utils/output.js';
|
|
22
|
-
import { normalizeScreenshotOptions } from '../utils/screenshot-options.js';
|
|
22
|
+
import { getScreenshotOptionsPayload, normalizeScreenshotOptions } from '../utils/screenshot-options.js';
|
|
23
23
|
|
|
24
24
|
/**
|
|
25
25
|
* Create a new Vizzly instance with custom configuration
|
|
@@ -294,12 +294,6 @@ export class VizzlySDK extends EventEmitter {
|
|
|
294
294
|
// Resolve Buffer or file path using shared utility
|
|
295
295
|
let buffer = resolveImageBuffer(imageBuffer, 'screenshot');
|
|
296
296
|
let normalizedOptions = normalizeScreenshotOptions(options);
|
|
297
|
-
for (let warning of normalizedOptions.warnings) {
|
|
298
|
-
output.warn(warning.message, {
|
|
299
|
-
code: warning.code,
|
|
300
|
-
option: warning.option
|
|
301
|
-
});
|
|
302
|
-
}
|
|
303
297
|
|
|
304
298
|
// Generate or use provided build ID
|
|
305
299
|
let buildId = normalizedOptions.buildId || this.currentBuildId || 'default';
|
|
@@ -314,10 +308,7 @@ export class VizzlySDK extends EventEmitter {
|
|
|
314
308
|
type: 'base64',
|
|
315
309
|
properties: normalizedOptions.properties
|
|
316
310
|
};
|
|
317
|
-
|
|
318
|
-
screenshotData.warnings = normalizedOptions.warnings;
|
|
319
|
-
}
|
|
320
|
-
|
|
311
|
+
Object.assign(screenshotData, getScreenshotOptionsPayload(normalizedOptions));
|
|
321
312
|
// POST to the local screenshot server
|
|
322
313
|
let serverUrl = `http://localhost:${this.config.server?.port || 3000}`;
|
|
323
314
|
let fetchFn = this.services.fetch || fetch;
|
|
@@ -1,234 +1,160 @@
|
|
|
1
1
|
import { Buffer } from 'node:buffer';
|
|
2
|
-
import {
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
3
|
import { resolve } from 'node:path';
|
|
4
4
|
import { uploadScreenshot as defaultUploadScreenshot } from '../../api/index.js';
|
|
5
5
|
import { detectImageInputType } from '../../utils/image-input-detector.js';
|
|
6
6
|
import * as output from '../../utils/output.js';
|
|
7
|
+
import { normalizeScreenshotOptions } from '../../utils/screenshot-options.js';
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
|
-
*
|
|
10
|
+
* Create a cloud screenshot handler for one run.
|
|
10
11
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
* │ ├─→ Convert base64 to Buffer │
|
|
20
|
-
* │ ├─→ Fire upload promise (NO AWAIT) ─────┐ │
|
|
21
|
-
* │ └─→ Return 200 immediately │ │
|
|
22
|
-
* │ │ │
|
|
23
|
-
* │ Test continues (NO BLOCKING) ✓ │ │
|
|
24
|
-
* │ ↓ │
|
|
25
|
-
* │ Background Upload │
|
|
26
|
-
* │ (to Vizzly API) │
|
|
27
|
-
* │ ↓ │
|
|
28
|
-
* │ Promise resolves/rejects │
|
|
29
|
-
* │ │
|
|
30
|
-
* │ Build Finalization │
|
|
31
|
-
* │ ↓ flush() │
|
|
32
|
-
* │ └─→ await Promise.allSettled(uploadPromises) │
|
|
33
|
-
* │ ↓ │
|
|
34
|
-
* │ All uploads complete ✓ │
|
|
35
|
-
* └─────────────────────────────────────────────────────────────┘
|
|
36
|
-
*/
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Create an API handler for screenshot uploads.
|
|
40
|
-
* @param {Object} client - API client with request method
|
|
41
|
-
* @param {Object} options - Optional dependencies for testing
|
|
42
|
-
* @param {Function} options.uploadScreenshot - Upload function (defaults to API uploadScreenshot)
|
|
12
|
+
* Captures queue uploads without waiting for the API. Each capture keeps its
|
|
13
|
+
* outcome so a rejection cannot disable later uploads or disappear after flush.
|
|
14
|
+
* Flush pending uploads before cleanup, which clears the run's records.
|
|
15
|
+
*
|
|
16
|
+
* @param {Object} client - API client used to upload screenshots.
|
|
17
|
+
* @param {Object} [options] - Upload dependencies.
|
|
18
|
+
* @param {typeof defaultUploadScreenshot} [options.uploadScreenshot] - Uploader.
|
|
19
|
+
* @returns {Object} Capture, count, flush, and cleanup methods for this run.
|
|
43
20
|
*/
|
|
44
|
-
export
|
|
21
|
+
export let createApiHandler = (client, {
|
|
45
22
|
uploadScreenshot = defaultUploadScreenshot
|
|
46
23
|
} = {}) => {
|
|
47
|
-
let
|
|
48
|
-
let screenshotCount = 0;
|
|
24
|
+
let captures = [];
|
|
49
25
|
let uploadPromises = [];
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
if (vizzlyDisabled) {
|
|
56
|
-
output.debug('upload', `${name} (disabled)`);
|
|
57
|
-
return {
|
|
58
|
-
statusCode: 200,
|
|
59
|
-
body: {
|
|
60
|
-
success: true,
|
|
61
|
-
disabled: true,
|
|
62
|
-
count: ++screenshotCount,
|
|
63
|
-
message: `Vizzly disabled - ${screenshotCount} screenshots captured but not uploaded`
|
|
64
|
-
}
|
|
65
|
-
};
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
// buildId is optional - API will handle it appropriately
|
|
26
|
+
function recordFailure(capture, error) {
|
|
27
|
+
capture.status = 'failed';
|
|
28
|
+
capture.error = error.message;
|
|
29
|
+
output.warn(`Screenshot "${capture.name}" failed to upload: ${error.message}`);
|
|
30
|
+
}
|
|
69
31
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
32
|
+
/**
|
|
33
|
+
* Upload one capture and record its outcome, including SHA reuse.
|
|
34
|
+
* API failures are recorded and logged rather than rejecting the background
|
|
35
|
+
* promise, so flush can collect every outcome and later uploads can continue.
|
|
36
|
+
*/
|
|
37
|
+
async function upload(capture, buildId, imageBuffer, properties, screenshotOptions) {
|
|
38
|
+
try {
|
|
39
|
+
let result = await uploadScreenshot(client, buildId, capture.name, imageBuffer, properties ?? {}, false, screenshotOptions);
|
|
40
|
+
capture.status = result.skipped ? 'reused' : 'uploaded';
|
|
41
|
+
} catch (error) {
|
|
42
|
+
recordFailure(capture, error);
|
|
77
43
|
}
|
|
44
|
+
}
|
|
78
45
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
46
|
+
/**
|
|
47
|
+
* Read a capture and queue its cloud upload. Local input failures are included
|
|
48
|
+
* in the run's failed count; HTTP 200 means queued, not uploaded successfully.
|
|
49
|
+
*
|
|
50
|
+
* @param {string} buildId - Cloud build receiving the screenshot.
|
|
51
|
+
* @param {string} name - Stable screenshot name used for baseline matching.
|
|
52
|
+
* @param {string} image - Base64 image data or a file path.
|
|
53
|
+
* @param {Object} [properties={}] - Screenshot metadata.
|
|
54
|
+
* @param {string} [type] - Base64 or file-path hint; otherwise detected.
|
|
55
|
+
* @param {Object[]} [warnings=[]] - Warnings already collected by the router.
|
|
56
|
+
* @param {Object} [screenshotOptions={}] - Capture and comparison options.
|
|
57
|
+
* @returns {Promise<{statusCode: number, body: Object}>} Local HTTP response.
|
|
58
|
+
*/
|
|
59
|
+
async function handleScreenshot(buildId, name, image, properties = {}, type, warnings = [], screenshotOptions = {}) {
|
|
60
|
+
let normalizedOptions = normalizeScreenshotOptions({
|
|
61
|
+
...screenshotOptions,
|
|
62
|
+
properties
|
|
63
|
+
});
|
|
64
|
+
properties = normalizedOptions.properties;
|
|
65
|
+
screenshotOptions = {
|
|
66
|
+
...screenshotOptions,
|
|
67
|
+
threshold: normalizedOptions.threshold,
|
|
68
|
+
minClusterSize: normalizedOptions.minClusterSize,
|
|
69
|
+
fullPage: normalizedOptions.fullPage,
|
|
70
|
+
captureMode: normalizedOptions.captureMode,
|
|
71
|
+
deviceScaleFactor: normalizedOptions.deviceScaleFactor,
|
|
72
|
+
selector: normalizedOptions.selector
|
|
73
|
+
};
|
|
74
|
+
warnings = warnings || [];
|
|
75
|
+
let capture = {
|
|
76
|
+
name,
|
|
77
|
+
status: 'pending'
|
|
78
|
+
};
|
|
79
|
+
captures.push(capture);
|
|
80
|
+
let inputType = ['base64', 'file-path'].includes(type) ? type : detectImageInputType(image);
|
|
82
81
|
let imageBuffer;
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
originalPath: image
|
|
94
|
-
}
|
|
95
|
-
};
|
|
96
|
-
}
|
|
97
|
-
try {
|
|
98
|
-
imageBuffer = readFileSync(filePath);
|
|
99
|
-
} catch (error) {
|
|
100
|
-
return {
|
|
101
|
-
statusCode: 500,
|
|
102
|
-
body: {
|
|
103
|
-
error: `Failed to read screenshot file: ${error.message}`,
|
|
104
|
-
filePath
|
|
105
|
-
}
|
|
106
|
-
};
|
|
107
|
-
}
|
|
108
|
-
} else if (inputType === 'base64') {
|
|
109
|
-
// It's base64 encoded
|
|
110
|
-
try {
|
|
82
|
+
try {
|
|
83
|
+
if (!client) throw new Error('API client not available');
|
|
84
|
+
if (inputType === 'file-path') {
|
|
85
|
+
let filePath = resolve(image.replace('file://', ''));
|
|
86
|
+
try {
|
|
87
|
+
imageBuffer = readFileSync(filePath);
|
|
88
|
+
} catch (error) {
|
|
89
|
+
throw new Error(error.code === 'ENOENT' ? `Screenshot file not found: ${filePath}` : `Failed to read screenshot file: ${error.message}`);
|
|
90
|
+
}
|
|
91
|
+
} else if (inputType === 'base64') {
|
|
111
92
|
imageBuffer = Buffer.from(image, 'base64');
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
statusCode: 400,
|
|
115
|
-
body: {
|
|
116
|
-
error: `Invalid base64 image data: ${error.message}`
|
|
117
|
-
}
|
|
118
|
-
};
|
|
93
|
+
} else {
|
|
94
|
+
throw new Error('Invalid image input: must be a file path or base64 encoded image data');
|
|
119
95
|
}
|
|
120
|
-
}
|
|
121
|
-
|
|
96
|
+
} catch (error) {
|
|
97
|
+
recordFailure(capture, error);
|
|
122
98
|
return {
|
|
123
|
-
statusCode: 400,
|
|
99
|
+
statusCode: client ? 400 : 500,
|
|
124
100
|
body: {
|
|
125
|
-
|
|
126
|
-
|
|
101
|
+
success: false,
|
|
102
|
+
name,
|
|
103
|
+
error: error.message
|
|
127
104
|
}
|
|
128
105
|
};
|
|
129
106
|
}
|
|
130
|
-
|
|
131
|
-
let uploadStart = Date.now();
|
|
132
|
-
|
|
133
|
-
// Fire upload in background - DON'T AWAIT!
|
|
134
|
-
let uploadPromise = uploadScreenshot(client, buildId, name, imageBuffer, properties ?? {}).then(result => {
|
|
135
|
-
let duration = Date.now() - uploadStart;
|
|
136
|
-
if (!result.skipped) {
|
|
137
|
-
output.debug('upload', `${name} completed`, {
|
|
138
|
-
ms: duration
|
|
139
|
-
});
|
|
140
|
-
} else {
|
|
141
|
-
output.debug('upload', `${name} skipped (dedup)`, {
|
|
142
|
-
ms: duration
|
|
143
|
-
});
|
|
144
|
-
}
|
|
145
|
-
return {
|
|
146
|
-
success: true,
|
|
147
|
-
name,
|
|
148
|
-
result,
|
|
149
|
-
duration
|
|
150
|
-
};
|
|
151
|
-
}).catch(uploadError => {
|
|
152
|
-
let duration = Date.now() - uploadStart;
|
|
153
|
-
output.debug('upload', `${name} failed`, {
|
|
154
|
-
error: uploadError.message,
|
|
155
|
-
ms: duration
|
|
156
|
-
});
|
|
157
|
-
vizzlyDisabled = true;
|
|
158
|
-
output.warn('Vizzly disabled due to upload error - continuing tests without visual testing');
|
|
159
|
-
return {
|
|
160
|
-
success: false,
|
|
161
|
-
name,
|
|
162
|
-
error: uploadError,
|
|
163
|
-
duration
|
|
164
|
-
};
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
// Collect promise for later flushing
|
|
168
|
-
uploadPromises.push(uploadPromise);
|
|
169
|
-
|
|
170
|
-
// Return immediately - test continues without waiting!
|
|
171
|
-
let handlerMs = Date.now() - handlerStart;
|
|
172
|
-
output.debug('upload', `${name} handler returning`, {
|
|
173
|
-
ms: handlerMs
|
|
174
|
-
});
|
|
107
|
+
uploadPromises.push(upload(capture, buildId, imageBuffer, properties, screenshotOptions));
|
|
175
108
|
return {
|
|
176
109
|
statusCode: 200,
|
|
177
110
|
body: {
|
|
178
111
|
success: true,
|
|
112
|
+
queued: true,
|
|
179
113
|
name,
|
|
180
|
-
count:
|
|
114
|
+
count: captures.length,
|
|
181
115
|
warnings
|
|
182
116
|
}
|
|
183
117
|
};
|
|
184
|
-
}
|
|
185
|
-
const getScreenshotCount = () => screenshotCount;
|
|
118
|
+
}
|
|
186
119
|
|
|
187
120
|
/**
|
|
188
|
-
* Wait for
|
|
189
|
-
*
|
|
121
|
+
* Wait for pending uploads, including captures queued while waiting.
|
|
122
|
+
* Results cover the whole run and remain available across repeated flushes.
|
|
123
|
+
* Uploaded and reused counts are separate; failed includes local input errors.
|
|
124
|
+
*
|
|
125
|
+
* @returns {Promise<Object>} Uploaded, reused, failed, and total counts, plus
|
|
126
|
+
* failed captures as `failures: [{ name, error }]`.
|
|
190
127
|
*/
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
} else {
|
|
207
|
-
failed++;
|
|
208
|
-
}
|
|
209
|
-
});
|
|
210
|
-
output.debug('upload', 'flush complete', {
|
|
211
|
-
uploaded,
|
|
212
|
-
failed
|
|
213
|
-
});
|
|
214
|
-
|
|
215
|
-
// Clear promises array
|
|
216
|
-
uploadPromises = [];
|
|
128
|
+
async function flush() {
|
|
129
|
+
let count;
|
|
130
|
+
do {
|
|
131
|
+
count = uploadPromises.length;
|
|
132
|
+
await Promise.all(uploadPromises);
|
|
133
|
+
} while (count !== uploadPromises.length);
|
|
134
|
+
let uploaded = captures.filter(capture => capture.status === 'uploaded').length;
|
|
135
|
+
let reused = captures.filter(capture => capture.status === 'reused').length;
|
|
136
|
+
let failures = captures.filter(capture => capture.status === 'failed').map(({
|
|
137
|
+
name,
|
|
138
|
+
error
|
|
139
|
+
}) => ({
|
|
140
|
+
name,
|
|
141
|
+
error
|
|
142
|
+
}));
|
|
217
143
|
return {
|
|
218
144
|
uploaded,
|
|
219
|
-
|
|
220
|
-
|
|
145
|
+
reused,
|
|
146
|
+
failed: failures.length,
|
|
147
|
+
total: captures.length,
|
|
148
|
+
failures
|
|
221
149
|
};
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
screenshotCount = 0;
|
|
150
|
+
}
|
|
151
|
+
function cleanup() {
|
|
152
|
+
captures = [];
|
|
226
153
|
uploadPromises = [];
|
|
227
|
-
|
|
228
|
-
};
|
|
154
|
+
}
|
|
229
155
|
return {
|
|
230
156
|
handleScreenshot,
|
|
231
|
-
getScreenshotCount,
|
|
157
|
+
getScreenshotCount: () => captures.length,
|
|
232
158
|
flush,
|
|
233
159
|
cleanup
|
|
234
160
|
};
|
|
@@ -8,37 +8,16 @@ import * as defaultOutput from '../../utils/output.js';
|
|
|
8
8
|
import { safePath as defaultSafePath, sanitizeScreenshotName as defaultSanitizeScreenshotName, validateScreenshotProperties as defaultValidateScreenshotProperties } from '../../utils/security.js';
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
|
-
*
|
|
12
|
-
*
|
|
11
|
+
* Build the flat internal comparison shape. Width and height always come from
|
|
12
|
+
* the captured image. The untouched user bag remains available in metadata.
|
|
13
13
|
*/
|
|
14
|
-
export const
|
|
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 => {
|
|
14
|
+
export const extractProperties = (validatedProperties, imageDimensions = {}) => {
|
|
34
15
|
if (!validatedProperties) return {};
|
|
35
16
|
return {
|
|
36
17
|
...validatedProperties,
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
viewport_height: validatedProperties.viewport?.height ?? validatedProperties.viewport_height ?? null,
|
|
18
|
+
viewport_width: imageDimensions.width ?? null,
|
|
19
|
+
viewport_height: imageDimensions.height ?? null,
|
|
40
20
|
browser: validatedProperties.browser ?? null,
|
|
41
|
-
// Preserve nested structure in metadata for backward compatibility
|
|
42
21
|
metadata: validatedProperties
|
|
43
22
|
};
|
|
44
23
|
};
|
|
@@ -305,7 +284,7 @@ export const createTddHandler = (config, workingDir, baselineBuild, baselineComp
|
|
|
305
284
|
output.debug('tdd', `baseline: ${baseline.buildName}`);
|
|
306
285
|
}
|
|
307
286
|
};
|
|
308
|
-
const handleScreenshot = async (_buildId, name, image, properties = {}, type, warnings = []) => {
|
|
287
|
+
const handleScreenshot = async (_buildId, name, image, properties = {}, type, warnings = [], screenshotOptions = {}) => {
|
|
309
288
|
let handlerStart = Date.now();
|
|
310
289
|
output.debug('tdd', `${name} received`);
|
|
311
290
|
|
|
@@ -324,13 +303,10 @@ export const createTddHandler = (config, workingDir, baselineBuild, baselineComp
|
|
|
324
303
|
};
|
|
325
304
|
}
|
|
326
305
|
|
|
327
|
-
//
|
|
328
|
-
let unwrappedProperties = unwrapProperties(properties);
|
|
329
|
-
|
|
330
|
-
// Validate and sanitize properties
|
|
306
|
+
// Validate user metadata without unwrapping nested keys.
|
|
331
307
|
let validatedProperties;
|
|
332
308
|
try {
|
|
333
|
-
validatedProperties = validateScreenshotProperties(
|
|
309
|
+
validatedProperties = validateScreenshotProperties(properties);
|
|
334
310
|
} catch (error) {
|
|
335
311
|
return {
|
|
336
312
|
statusCode: 400,
|
|
@@ -342,9 +318,6 @@ export const createTddHandler = (config, workingDir, baselineBuild, baselineComp
|
|
|
342
318
|
};
|
|
343
319
|
}
|
|
344
320
|
|
|
345
|
-
// Extract ALL properties to top-level (matching cloud API behavior)
|
|
346
|
-
const extractedProperties = extractProperties(validatedProperties);
|
|
347
|
-
|
|
348
321
|
// Support both base64 encoded images and file paths
|
|
349
322
|
// Vitest browser mode returns file paths, so we need to handle both
|
|
350
323
|
// Use explicit type from client if provided (fast path), otherwise detect (slow path)
|
|
@@ -401,25 +374,17 @@ export const createTddHandler = (config, workingDir, baselineBuild, baselineComp
|
|
|
401
374
|
}
|
|
402
375
|
};
|
|
403
376
|
}
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
if (!extractedProperties.viewport_width) {
|
|
410
|
-
extractedProperties.viewport_width = dimensions.width;
|
|
411
|
-
}
|
|
412
|
-
if (!extractedProperties.viewport_height) {
|
|
413
|
-
extractedProperties.viewport_height = dimensions.height;
|
|
414
|
-
}
|
|
415
|
-
} catch {
|
|
416
|
-
// Dimensions will use defaults
|
|
417
|
-
}
|
|
377
|
+
let imageDimensions = {};
|
|
378
|
+
try {
|
|
379
|
+
imageDimensions = getDimensionsSync(imageBuffer);
|
|
380
|
+
} catch {
|
|
381
|
+
// Invalid images are handled by the comparison service.
|
|
418
382
|
}
|
|
383
|
+
let extractedProperties = extractProperties(validatedProperties, imageDimensions);
|
|
419
384
|
|
|
420
385
|
// Use the sanitized name as-is (no modification with browser/viewport)
|
|
421
386
|
// Baseline matching uses signature logic (name + viewport_width + browser)
|
|
422
|
-
const comparison = await tddService.compareScreenshot(sanitizedName, imageBuffer, extractedProperties);
|
|
387
|
+
const comparison = await tddService.compareScreenshot(sanitizedName, imageBuffer, extractedProperties, screenshotOptions);
|
|
423
388
|
|
|
424
389
|
// Comparison tracked by tdd.js event handler
|
|
425
390
|
|
|
@@ -4,11 +4,15 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import * as output from '../../utils/output.js';
|
|
7
|
+
import { normalizeScreenshotOptions } from '../../utils/screenshot-options.js';
|
|
7
8
|
import { parseJsonBody } from '../middleware/json-parser.js';
|
|
8
9
|
import { sendError, sendJson } from '../middleware/response.js';
|
|
9
10
|
|
|
10
11
|
/**
|
|
11
|
-
*
|
|
12
|
+
* Route local capture and flush requests to the active cloud or TDD handler.
|
|
13
|
+
* A cloud flush returns HTTP 200 with cumulative results; its success field is
|
|
14
|
+
* false when any capture failed, even if that failure appeared in an earlier flush.
|
|
15
|
+
*
|
|
12
16
|
* @param {Object} context - Router context
|
|
13
17
|
* @param {Object} context.screenshotHandler - Screenshot handler
|
|
14
18
|
* @param {string|null} context.defaultBuildId - Default build ID
|
|
@@ -33,7 +37,13 @@ export function createScreenshotRouter({
|
|
|
33
37
|
properties,
|
|
34
38
|
image,
|
|
35
39
|
type,
|
|
36
|
-
warnings
|
|
40
|
+
warnings,
|
|
41
|
+
threshold,
|
|
42
|
+
minClusterSize,
|
|
43
|
+
fullPage,
|
|
44
|
+
captureMode,
|
|
45
|
+
deviceScaleFactor,
|
|
46
|
+
selector
|
|
37
47
|
} = body;
|
|
38
48
|
if (!name || !image) {
|
|
39
49
|
sendError(res, 400, 'name and image are required');
|
|
@@ -42,7 +52,23 @@ export function createScreenshotRouter({
|
|
|
42
52
|
|
|
43
53
|
// Use buildId from request body, or fall back to server's buildId
|
|
44
54
|
const effectiveBuildId = buildId || defaultBuildId;
|
|
45
|
-
const
|
|
55
|
+
const normalizedOptions = normalizeScreenshotOptions({
|
|
56
|
+
properties,
|
|
57
|
+
threshold,
|
|
58
|
+
minClusterSize,
|
|
59
|
+
fullPage,
|
|
60
|
+
captureMode,
|
|
61
|
+
deviceScaleFactor,
|
|
62
|
+
selector
|
|
63
|
+
});
|
|
64
|
+
const result = await screenshotHandler.handleScreenshot(effectiveBuildId, name, image, normalizedOptions.properties, type, warnings || [], {
|
|
65
|
+
threshold: normalizedOptions.threshold,
|
|
66
|
+
minClusterSize: normalizedOptions.minClusterSize,
|
|
67
|
+
fullPage: normalizedOptions.fullPage,
|
|
68
|
+
captureMode: normalizedOptions.captureMode,
|
|
69
|
+
deviceScaleFactor: normalizedOptions.deviceScaleFactor,
|
|
70
|
+
selector: normalizedOptions.selector
|
|
71
|
+
});
|
|
46
72
|
sendJson(res, result.statusCode, result.body);
|
|
47
73
|
return true;
|
|
48
74
|
} catch (error) {
|
|
@@ -61,7 +87,7 @@ export function createScreenshotRouter({
|
|
|
61
87
|
if (screenshotHandler.flush) {
|
|
62
88
|
let stats = await screenshotHandler.flush();
|
|
63
89
|
sendJson(res, 200, {
|
|
64
|
-
success:
|
|
90
|
+
success: stats.failed === 0,
|
|
65
91
|
...stats
|
|
66
92
|
});
|
|
67
93
|
} else if (screenshotHandler.getResults) {
|
package/dist/tdd/tdd-service.js
CHANGED
|
@@ -752,7 +752,7 @@ export class TddService {
|
|
|
752
752
|
/**
|
|
753
753
|
* Compare a screenshot against baseline
|
|
754
754
|
*/
|
|
755
|
-
async compareScreenshot(name, imageBuffer, properties = {}) {
|
|
755
|
+
async compareScreenshot(name, imageBuffer, properties = {}, screenshotOptions = {}) {
|
|
756
756
|
// Destructure dependencies
|
|
757
757
|
let {
|
|
758
758
|
output,
|
|
@@ -850,8 +850,8 @@ export class TddService {
|
|
|
850
850
|
}
|
|
851
851
|
|
|
852
852
|
// Baseline exists - compare
|
|
853
|
-
let effectiveThreshold = typeof
|
|
854
|
-
let effectiveMinClusterSize = Number.isInteger(
|
|
853
|
+
let effectiveThreshold = typeof screenshotOptions.threshold === 'number' && screenshotOptions.threshold >= 0 ? screenshotOptions.threshold : this.threshold;
|
|
854
|
+
let effectiveMinClusterSize = Number.isInteger(screenshotOptions.minClusterSize) && screenshotOptions.minClusterSize >= 1 ? screenshotOptions.minClusterSize : this.minClusterSize;
|
|
855
855
|
try {
|
|
856
856
|
let honeydiffResult = await compareImages(baselineImagePath, currentImagePath, diffImagePath, {
|
|
857
857
|
threshold: effectiveThreshold,
|
package/dist/test-runner/core.js
CHANGED
|
@@ -133,6 +133,8 @@ export function buildDisabledRunResult() {
|
|
|
133
133
|
* @param {boolean} options.testSuccess - Whether tests passed
|
|
134
134
|
* @param {number} options.screenshotCount - Number of screenshots
|
|
135
135
|
* @param {Object|null} options.tddResults - TDD results (comparisons, etc.)
|
|
136
|
+
* @param {Object|null} [options.uploadStats] - Cumulative cloud upload outcomes,
|
|
137
|
+
* exposed as `uploads` independently of test and local comparison results.
|
|
136
138
|
* @returns {Object} Final result object
|
|
137
139
|
*/
|
|
138
140
|
export function buildRunResult({
|
|
@@ -140,7 +142,8 @@ export function buildRunResult({
|
|
|
140
142
|
buildUrl,
|
|
141
143
|
testSuccess,
|
|
142
144
|
screenshotCount,
|
|
143
|
-
tddResults
|
|
145
|
+
tddResults,
|
|
146
|
+
uploadStats
|
|
144
147
|
}) {
|
|
145
148
|
return {
|
|
146
149
|
buildId,
|
|
@@ -148,6 +151,9 @@ export function buildRunResult({
|
|
|
148
151
|
testsPassed: testSuccess ? 1 : 0,
|
|
149
152
|
testsFailed: testSuccess ? 0 : 1,
|
|
150
153
|
screenshotsCaptured: screenshotCount,
|
|
154
|
+
...(uploadStats ? {
|
|
155
|
+
uploads: uploadStats
|
|
156
|
+
} : {}),
|
|
151
157
|
comparisons: tddResults?.comparisons || null,
|
|
152
158
|
failed: (tddResults?.failed || 0) > 0
|
|
153
159
|
};
|