@vizzly-testing/cli 0.35.3-beta.2 → 0.35.3-beta.4
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 +3 -4
- package/dist/api/core.js +12 -5
- package/dist/api/endpoints.js +5 -50
- package/dist/api/index.js +1 -1
- package/dist/cli.js +4 -4
- package/dist/client/index.js +2 -1
- package/dist/commands/api.js +3 -3
- package/dist/commands/builds.js +12 -15
- package/dist/commands/comparisons.js +7 -9
- package/dist/commands/context.js +3 -24
- package/dist/commands/review.js +122 -35
- package/dist/commands/run.js +1 -3
- package/dist/commands/status.js +35 -79
- package/dist/commands/upload.js +1 -1
- package/dist/context/local-workspace-provider.js +36 -126
- package/dist/reporter/reporter-bundle.iife.js +49 -81
- package/dist/screenshot-server/core.js +5 -1
- package/dist/sdk/index.js +2 -1
- package/dist/server/handlers/api-handler.js +18 -2
- package/dist/server/handlers/tdd-handler.js +12 -11
- package/dist/server/routers/dashboard.js +5 -1
- package/dist/server/routers/screenshot.js +25 -2
- package/dist/tdd/runtime-context.js +0 -2
- package/dist/tdd/services/comparison-service.js +4 -68
- package/dist/tdd/tdd-service.js +10 -208
- package/dist/types/index.d.ts +6 -2
- package/dist/uploader/core.js +7 -8
- package/dist/utils/screenshot-options.js +61 -22
- package/dist/utils/visual-context-normalizers.js +86 -139
- package/package.json +1 -1
- package/skills/vizzly/SKILL.md +4 -4
- package/skills/vizzly/references/dynamic-content.md +2 -2
- package/dist/tdd/core/hotspot-coverage.js +0 -112
- package/dist/tdd/core/region-coverage.js +0 -203
- package/dist/tdd/metadata/hotspot-metadata.js +0 -93
- package/dist/tdd/metadata/region-metadata.js +0 -56
|
@@ -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
|
|
@@ -314,6 +314,7 @@ export class VizzlySDK extends EventEmitter {
|
|
|
314
314
|
type: 'base64',
|
|
315
315
|
properties: normalizedOptions.properties
|
|
316
316
|
};
|
|
317
|
+
Object.assign(screenshotData, getScreenshotOptionsPayload(normalizedOptions));
|
|
317
318
|
if (normalizedOptions.warnings.length > 0) {
|
|
318
319
|
screenshotData.warnings = normalizedOptions.warnings;
|
|
319
320
|
}
|
|
@@ -4,6 +4,7 @@ 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
|
* API Handler - Non-blocking screenshot upload
|
|
@@ -47,8 +48,23 @@ export const createApiHandler = (client, {
|
|
|
47
48
|
let vizzlyDisabled = false;
|
|
48
49
|
let screenshotCount = 0;
|
|
49
50
|
let uploadPromises = [];
|
|
50
|
-
const handleScreenshot = async (buildId, name, image, properties = {}, type, warnings = []) => {
|
|
51
|
+
const handleScreenshot = async (buildId, name, image, properties = {}, type, warnings = [], screenshotOptions = {}) => {
|
|
51
52
|
let handlerStart = Date.now();
|
|
53
|
+
let normalizedOptions = normalizeScreenshotOptions({
|
|
54
|
+
...screenshotOptions,
|
|
55
|
+
properties
|
|
56
|
+
});
|
|
57
|
+
properties = normalizedOptions.properties;
|
|
58
|
+
screenshotOptions = {
|
|
59
|
+
...screenshotOptions,
|
|
60
|
+
threshold: normalizedOptions.threshold,
|
|
61
|
+
minClusterSize: normalizedOptions.minClusterSize,
|
|
62
|
+
fullPage: normalizedOptions.fullPage,
|
|
63
|
+
captureMode: normalizedOptions.captureMode,
|
|
64
|
+
deviceScaleFactor: normalizedOptions.deviceScaleFactor,
|
|
65
|
+
selector: normalizedOptions.selector
|
|
66
|
+
};
|
|
67
|
+
warnings = [...(warnings || []), ...normalizedOptions.warnings];
|
|
52
68
|
output.debug('upload', `${name} received`, {
|
|
53
69
|
buildId: buildId?.slice(0, 8)
|
|
54
70
|
});
|
|
@@ -131,7 +147,7 @@ export const createApiHandler = (client, {
|
|
|
131
147
|
let uploadStart = Date.now();
|
|
132
148
|
|
|
133
149
|
// Fire upload in background - DON'T AWAIT!
|
|
134
|
-
let uploadPromise = uploadScreenshot(client, buildId, name, imageBuffer, properties ?? {}).then(result => {
|
|
150
|
+
let uploadPromise = uploadScreenshot(client, buildId, name, imageBuffer, properties ?? {}, false, screenshotOptions).then(result => {
|
|
135
151
|
let duration = Date.now() - uploadStart;
|
|
136
152
|
if (!result.skipped) {
|
|
137
153
|
output.debug('upload', `${name} completed`, {
|
|
@@ -5,6 +5,7 @@ import { getDimensionsSync as defaultGetDimensionsSync } from '@vizzly-testing/h
|
|
|
5
5
|
import { TddService as DefaultTddService } from '../../tdd/tdd-service.js';
|
|
6
6
|
import { detectImageInputType as defaultDetectImageInputType } from '../../utils/image-input-detector.js';
|
|
7
7
|
import * as defaultOutput from '../../utils/output.js';
|
|
8
|
+
import { normalizeScreenshotOptions } from '../../utils/screenshot-options.js';
|
|
8
9
|
import { safePath as defaultSafePath, sanitizeScreenshotName as defaultSanitizeScreenshotName, validateScreenshotProperties as defaultValidateScreenshotProperties } from '../../utils/security.js';
|
|
9
10
|
|
|
10
11
|
/**
|
|
@@ -305,7 +306,7 @@ export const createTddHandler = (config, workingDir, baselineBuild, baselineComp
|
|
|
305
306
|
output.debug('tdd', `baseline: ${baseline.buildName}`);
|
|
306
307
|
}
|
|
307
308
|
};
|
|
308
|
-
const handleScreenshot = async (_buildId, name, image, properties = {}, type, warnings = []) => {
|
|
309
|
+
const handleScreenshot = async (_buildId, name, image, properties = {}, type, warnings = [], screenshotOptions = {}) => {
|
|
309
310
|
let handlerStart = Date.now();
|
|
310
311
|
output.debug('tdd', `${name} received`);
|
|
311
312
|
|
|
@@ -324,13 +325,18 @@ export const createTddHandler = (config, workingDir, baselineBuild, baselineComp
|
|
|
324
325
|
};
|
|
325
326
|
}
|
|
326
327
|
|
|
327
|
-
//
|
|
328
|
+
// Preserve the old nested wrapper for older clients, then filter user
|
|
329
|
+
// properties before local comparison/report serialization.
|
|
328
330
|
let unwrappedProperties = unwrapProperties(properties);
|
|
331
|
+
let normalizedProperties = normalizeScreenshotOptions({
|
|
332
|
+
properties: unwrappedProperties
|
|
333
|
+
});
|
|
334
|
+
warnings = [...(warnings || []), ...normalizedProperties.warnings];
|
|
329
335
|
|
|
330
336
|
// Validate and sanitize properties
|
|
331
337
|
let validatedProperties;
|
|
332
338
|
try {
|
|
333
|
-
validatedProperties = validateScreenshotProperties(
|
|
339
|
+
validatedProperties = validateScreenshotProperties(normalizedProperties.properties);
|
|
334
340
|
} catch (error) {
|
|
335
341
|
return {
|
|
336
342
|
statusCode: 400,
|
|
@@ -419,7 +425,7 @@ export const createTddHandler = (config, workingDir, baselineBuild, baselineComp
|
|
|
419
425
|
|
|
420
426
|
// Use the sanitized name as-is (no modification with browser/viewport)
|
|
421
427
|
// Baseline matching uses signature logic (name + viewport_width + browser)
|
|
422
|
-
const comparison = await tddService.compareScreenshot(sanitizedName, imageBuffer, extractedProperties);
|
|
428
|
+
const comparison = await tddService.compareScreenshot(sanitizedName, imageBuffer, extractedProperties, screenshotOptions);
|
|
423
429
|
|
|
424
430
|
// Comparison tracked by tdd.js event handler
|
|
425
431
|
|
|
@@ -450,9 +456,7 @@ export const createTddHandler = (config, workingDir, baselineBuild, baselineComp
|
|
|
450
456
|
warnings,
|
|
451
457
|
originalName: name,
|
|
452
458
|
timestamp: Date.now(),
|
|
453
|
-
|
|
454
|
-
hasDiffClusters: comparison.diffClusters?.length > 0,
|
|
455
|
-
hasConfirmedRegions: comparison.confirmedRegions?.length > 0
|
|
459
|
+
hasDiffClusters: comparison.diffClusters?.length > 0
|
|
456
460
|
};
|
|
457
461
|
|
|
458
462
|
// Update lightweight comparison in report-data.json (triggers SSE broadcast)
|
|
@@ -462,10 +466,7 @@ export const createTddHandler = (config, workingDir, baselineBuild, baselineComp
|
|
|
462
466
|
updateComparisonDetails(comparison.id, {
|
|
463
467
|
diffClusters: comparison.diffClusters,
|
|
464
468
|
intensityStats: comparison.intensityStats,
|
|
465
|
-
boundingBox: comparison.boundingBox
|
|
466
|
-
regionAnalysis: comparison.regionAnalysis,
|
|
467
|
-
hotspotAnalysis: comparison.hotspotAnalysis,
|
|
468
|
-
confirmedRegions: comparison.confirmedRegions
|
|
469
|
+
boundingBox: comparison.boundingBox
|
|
469
470
|
});
|
|
470
471
|
|
|
471
472
|
// Log screenshot event for menubar
|
|
@@ -10,6 +10,10 @@ import { sendError, sendHtml, sendSuccess } from '../middleware/response.js';
|
|
|
10
10
|
|
|
11
11
|
// SPA routes that should serve the dashboard HTML
|
|
12
12
|
const SPA_ROUTES = ['/', '/stats', '/settings', '/projects', '/builds'];
|
|
13
|
+
let COMPARISON_DETAIL_FIELDS = new Set(['boundingBox', 'diffClusters', 'intensityStats']);
|
|
14
|
+
function presentComparisonDetails(details = {}) {
|
|
15
|
+
return Object.fromEntries(Object.entries(details).filter(([field]) => COMPARISON_DETAIL_FIELDS.has(field)));
|
|
16
|
+
}
|
|
13
17
|
|
|
14
18
|
/**
|
|
15
19
|
* Create dashboard router
|
|
@@ -99,7 +103,7 @@ export function createDashboardRouter(context) {
|
|
|
99
103
|
if (heavy) {
|
|
100
104
|
comparison = {
|
|
101
105
|
...comparison,
|
|
102
|
-
...heavy
|
|
106
|
+
...presentComparisonDetails(heavy)
|
|
103
107
|
};
|
|
104
108
|
}
|
|
105
109
|
} catch (error) {
|
|
@@ -4,6 +4,7 @@
|
|
|
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
|
|
|
@@ -33,7 +34,13 @@ export function createScreenshotRouter({
|
|
|
33
34
|
properties,
|
|
34
35
|
image,
|
|
35
36
|
type,
|
|
36
|
-
warnings
|
|
37
|
+
warnings,
|
|
38
|
+
threshold,
|
|
39
|
+
minClusterSize,
|
|
40
|
+
fullPage,
|
|
41
|
+
captureMode,
|
|
42
|
+
deviceScaleFactor,
|
|
43
|
+
selector
|
|
37
44
|
} = body;
|
|
38
45
|
if (!name || !image) {
|
|
39
46
|
sendError(res, 400, 'name and image are required');
|
|
@@ -42,7 +49,23 @@ export function createScreenshotRouter({
|
|
|
42
49
|
|
|
43
50
|
// Use buildId from request body, or fall back to server's buildId
|
|
44
51
|
const effectiveBuildId = buildId || defaultBuildId;
|
|
45
|
-
const
|
|
52
|
+
const normalizedOptions = normalizeScreenshotOptions({
|
|
53
|
+
properties,
|
|
54
|
+
threshold,
|
|
55
|
+
minClusterSize,
|
|
56
|
+
fullPage,
|
|
57
|
+
captureMode,
|
|
58
|
+
deviceScaleFactor,
|
|
59
|
+
selector
|
|
60
|
+
});
|
|
61
|
+
const result = await screenshotHandler.handleScreenshot(effectiveBuildId, name, image, normalizedOptions.properties, type, [...(warnings || []), ...normalizedOptions.warnings], {
|
|
62
|
+
threshold: normalizedOptions.threshold,
|
|
63
|
+
minClusterSize: normalizedOptions.minClusterSize,
|
|
64
|
+
fullPage: normalizedOptions.fullPage,
|
|
65
|
+
captureMode: normalizedOptions.captureMode,
|
|
66
|
+
deviceScaleFactor: normalizedOptions.deviceScaleFactor,
|
|
67
|
+
selector: normalizedOptions.selector
|
|
68
|
+
});
|
|
46
69
|
sendJson(res, result.statusCode, result.body);
|
|
47
70
|
return true;
|
|
48
71
|
} catch (error) {
|
|
@@ -9,7 +9,6 @@ export function buildTddDependencyOps(deps = {}, defaults = {}) {
|
|
|
9
9
|
colors = defaults.colors,
|
|
10
10
|
validatePathSecurity = defaults.validatePathSecurity,
|
|
11
11
|
initializeDirectories = defaults.initializeDirectories,
|
|
12
|
-
calculateHotspotCoverage = defaults.calculateHotspotCoverage,
|
|
13
12
|
fs = {},
|
|
14
13
|
api = {},
|
|
15
14
|
metadata = {},
|
|
@@ -51,7 +50,6 @@ export function buildTddDependencyOps(deps = {}, defaults = {}) {
|
|
|
51
50
|
colors,
|
|
52
51
|
validatePathSecurity,
|
|
53
52
|
initializeDirectories,
|
|
54
|
-
calculateHotspotCoverage,
|
|
55
53
|
...fsOps,
|
|
56
54
|
...apiOps,
|
|
57
55
|
...metadataOps,
|
|
@@ -5,8 +5,6 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { analyze } from '@vizzly-testing/honeydiff';
|
|
8
|
-
import { calculateHotspotCoverage } from '../core/hotspot-coverage.js';
|
|
9
|
-
import { calculateRegionCoverage, shouldAutoApproveFromRegions } from '../core/region-coverage.js';
|
|
10
8
|
import { generateComparisonId } from '../core/signature.js';
|
|
11
9
|
|
|
12
10
|
/**
|
|
@@ -149,8 +147,6 @@ export function buildNewComparison(params) {
|
|
|
149
147
|
* @param {number} params.threshold - Effective threshold used
|
|
150
148
|
* @param {number} params.minClusterSize - Effective minClusterSize used
|
|
151
149
|
* @param {Object} params.honeydiffResult - Result from honeydiff
|
|
152
|
-
* @param {Object} params.hotspotAnalysis - Hotspot data for this screenshot (optional)
|
|
153
|
-
* @param {Object} params.regionData - User-defined region data { confirmed: [], candidates: [] } (optional)
|
|
154
150
|
* @returns {Object} Comparison result
|
|
155
151
|
*/
|
|
156
152
|
export function buildFailedComparison(params) {
|
|
@@ -163,49 +159,13 @@ export function buildFailedComparison(params) {
|
|
|
163
159
|
properties,
|
|
164
160
|
threshold,
|
|
165
161
|
minClusterSize,
|
|
166
|
-
honeydiffResult
|
|
167
|
-
hotspotAnalysis,
|
|
168
|
-
regionData
|
|
162
|
+
honeydiffResult
|
|
169
163
|
} = params;
|
|
170
164
|
let diffClusters = honeydiffResult.diffClusters || [];
|
|
171
|
-
let ssimScore = honeydiffResult.perceptualScore ?? honeydiffResult.ssimScore ?? honeydiffResult.ssim_score ?? null;
|
|
172
|
-
let isFiltered = false;
|
|
173
|
-
let filterReason = 'pixel-diff';
|
|
174
|
-
|
|
175
|
-
// Region analysis (user-confirmed 2D boxes) - check FIRST (takes priority)
|
|
176
|
-
let regionCoverage = null;
|
|
177
|
-
let isRegionFiltered = false;
|
|
178
|
-
let confirmedRegions = regionData?.confirmed || [];
|
|
179
|
-
if (confirmedRegions.length > 0 && diffClusters.length > 0) {
|
|
180
|
-
regionCoverage = calculateRegionCoverage(diffClusters, confirmedRegions);
|
|
181
|
-
if (shouldAutoApproveFromRegions(confirmedRegions, regionCoverage, {
|
|
182
|
-
ssimScore
|
|
183
|
-
})) {
|
|
184
|
-
isRegionFiltered = true;
|
|
185
|
-
isFiltered = true;
|
|
186
|
-
filterReason = 'region-filtered';
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
// Hotspot analysis (1D Y-bands from historical data) - check SECOND
|
|
191
|
-
let hotspotCoverage = null;
|
|
192
|
-
let isHotspotFiltered = false;
|
|
193
|
-
if (!isFiltered && hotspotAnalysis && diffClusters.length > 0) {
|
|
194
|
-
hotspotCoverage = calculateHotspotCoverage(diffClusters, hotspotAnalysis);
|
|
195
|
-
|
|
196
|
-
// Check if diff should be filtered as hotspot noise
|
|
197
|
-
// (cloud uses confidence_score >= 70 which is >0.7 when normalized)
|
|
198
|
-
let isHighConfidence = hotspotAnalysis.confidence === 'high' || hotspotAnalysis.confidence_score !== undefined && hotspotAnalysis.confidence_score >= 70;
|
|
199
|
-
if (isHighConfidence && hotspotCoverage.coverage >= 0.8) {
|
|
200
|
-
isHotspotFiltered = true;
|
|
201
|
-
isFiltered = true;
|
|
202
|
-
filterReason = 'hotspot-filtered';
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
165
|
return {
|
|
206
166
|
id: generateComparisonId(signature),
|
|
207
167
|
name,
|
|
208
|
-
status:
|
|
168
|
+
status: 'failed',
|
|
209
169
|
baseline: baselinePath,
|
|
210
170
|
current: currentPath,
|
|
211
171
|
diff: diffPath,
|
|
@@ -215,38 +175,14 @@ export function buildFailedComparison(params) {
|
|
|
215
175
|
minClusterSize,
|
|
216
176
|
diffPercentage: honeydiffResult.diffPercentage,
|
|
217
177
|
diffCount: honeydiffResult.diffPixels,
|
|
218
|
-
reason:
|
|
178
|
+
reason: 'pixel-diff',
|
|
219
179
|
totalPixels: honeydiffResult.totalPixels,
|
|
220
180
|
aaPixelsIgnored: honeydiffResult.aaPixelsIgnored,
|
|
221
181
|
aaPercentage: honeydiffResult.aaPercentage,
|
|
222
182
|
boundingBox: honeydiffResult.boundingBox,
|
|
223
183
|
heightDiff: honeydiffResult.heightDiff,
|
|
224
184
|
intensityStats: honeydiffResult.intensityStats,
|
|
225
|
-
diffClusters
|
|
226
|
-
// User-defined region analysis (2D boxes)
|
|
227
|
-
regionAnalysis: regionCoverage ? {
|
|
228
|
-
coverage: regionCoverage.coverage,
|
|
229
|
-
clustersInRegions: regionCoverage.clustersInRegions,
|
|
230
|
-
totalClusters: regionCoverage.totalClusters,
|
|
231
|
-
pixelsInRegions: regionCoverage.pixelsInRegions,
|
|
232
|
-
totalPixels: regionCoverage.totalPixels,
|
|
233
|
-
matchedRegions: regionCoverage.matchedRegions,
|
|
234
|
-
confirmedCount: confirmedRegions.length,
|
|
235
|
-
ssimScore,
|
|
236
|
-
isFiltered: isRegionFiltered
|
|
237
|
-
} : null,
|
|
238
|
-
// Include confirmed regions for visualization in UI
|
|
239
|
-
confirmedRegions: confirmedRegions.length > 0 ? confirmedRegions : null,
|
|
240
|
-
// Historical hotspot analysis (1D Y-bands)
|
|
241
|
-
hotspotAnalysis: hotspotCoverage ? {
|
|
242
|
-
coverage: hotspotCoverage.coverage,
|
|
243
|
-
linesInHotspots: hotspotCoverage.linesInHotspots,
|
|
244
|
-
totalLines: hotspotCoverage.totalLines,
|
|
245
|
-
confidence: hotspotAnalysis?.confidence,
|
|
246
|
-
confidenceScore: hotspotAnalysis?.confidence_score,
|
|
247
|
-
regionCount: hotspotAnalysis?.regions?.length || 0,
|
|
248
|
-
isFiltered: isHotspotFiltered
|
|
249
|
-
} : null
|
|
185
|
+
diffClusters
|
|
250
186
|
};
|
|
251
187
|
}
|
|
252
188
|
|