@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
|
@@ -1,44 +1,43 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Read the API's
|
|
2
|
+
* Read the API's visual-review state.
|
|
3
3
|
*
|
|
4
4
|
* @param {Object} record - Comparison or review record from the API.
|
|
5
5
|
* @returns {string|null} The review state when the API supplied one.
|
|
6
6
|
*/
|
|
7
7
|
export function getVisualReviewState(record = {}) {
|
|
8
|
-
return record.visual_review?.state ||
|
|
8
|
+
return record.visual_review?.state || null;
|
|
9
9
|
}
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
|
-
*
|
|
12
|
+
* Read the visual comparison result.
|
|
13
13
|
*
|
|
14
14
|
* @param {Object} comparison - Comparison record from the API.
|
|
15
|
-
* @returns {string|null} The visual result
|
|
15
|
+
* @returns {string|null} The visual result when the API supplied one.
|
|
16
16
|
*/
|
|
17
17
|
export function getComparisonResult(comparison = {}) {
|
|
18
|
-
return comparison.result ||
|
|
18
|
+
return comparison.result || null;
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
/**
|
|
22
|
-
*
|
|
22
|
+
* Read the current screenshot name.
|
|
23
23
|
*
|
|
24
24
|
* @param {Object} comparison - Comparison record from the API.
|
|
25
|
-
* @returns {string|null} The
|
|
25
|
+
* @returns {string|null} The screenshot name when the API supplied one.
|
|
26
26
|
*/
|
|
27
27
|
export function getComparisonName(comparison = {}) {
|
|
28
|
-
return comparison.
|
|
28
|
+
return comparison.screenshot?.name || null;
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
/**
|
|
32
|
-
*
|
|
32
|
+
* Read CSS viewport dimensions without confusing them with bitmap size.
|
|
33
33
|
*
|
|
34
34
|
* @param {Object} comparison - Comparison record from the API.
|
|
35
35
|
* @returns {{width: number|null, height: number|null}|null} Viewport dimensions.
|
|
36
36
|
*/
|
|
37
37
|
export function getComparisonViewport(comparison = {}) {
|
|
38
|
-
let
|
|
39
|
-
let
|
|
40
|
-
let
|
|
41
|
-
let height = viewport.height ?? comparison.viewport_height ?? current.viewport_height ?? comparison.current_viewport_height ?? null;
|
|
38
|
+
let viewport = comparison.screenshot?.viewport || {};
|
|
39
|
+
let width = viewport.width ?? null;
|
|
40
|
+
let height = viewport.height ?? null;
|
|
42
41
|
return width != null || height != null ? {
|
|
43
42
|
width,
|
|
44
43
|
height
|
|
@@ -46,26 +45,25 @@ export function getComparisonViewport(comparison = {}) {
|
|
|
46
45
|
}
|
|
47
46
|
|
|
48
47
|
/**
|
|
49
|
-
*
|
|
48
|
+
* Read the browser from the current screenshot.
|
|
50
49
|
*
|
|
51
50
|
* @param {Object} comparison - Comparison record from the API.
|
|
52
51
|
* @returns {string|null} The browser name when present.
|
|
53
52
|
*/
|
|
54
53
|
export function getComparisonBrowser(comparison = {}) {
|
|
55
|
-
return comparison.
|
|
54
|
+
return comparison.screenshot?.browser || null;
|
|
56
55
|
}
|
|
57
56
|
|
|
58
57
|
/**
|
|
59
|
-
*
|
|
58
|
+
* Read bitmap dimensions from a screenshot record.
|
|
60
59
|
*
|
|
61
|
-
* @param {Object} record -
|
|
62
|
-
* @param {Object} fallback - Legacy fields to use when the record is incomplete.
|
|
60
|
+
* @param {Object} record - Screenshot record from the API.
|
|
63
61
|
* @returns {{width: number|null, height: number|null}|null} Bitmap dimensions.
|
|
64
62
|
*/
|
|
65
|
-
function getBitmap(record = {}
|
|
66
|
-
let bitmap = record.bitmap ||
|
|
67
|
-
let width = bitmap.width ??
|
|
68
|
-
let height = bitmap.height ??
|
|
63
|
+
function getBitmap(record = {}) {
|
|
64
|
+
let bitmap = record.bitmap || {};
|
|
65
|
+
let width = bitmap.width ?? null;
|
|
66
|
+
let height = bitmap.height ?? null;
|
|
69
67
|
return width != null || height != null ? {
|
|
70
68
|
width,
|
|
71
69
|
height
|
|
@@ -73,16 +71,15 @@ function getBitmap(record = {}, fallback = {}) {
|
|
|
73
71
|
}
|
|
74
72
|
|
|
75
73
|
/**
|
|
76
|
-
*
|
|
74
|
+
* Read viewport dimensions from a screenshot record.
|
|
77
75
|
*
|
|
78
|
-
* @param {Object} record -
|
|
79
|
-
* @param {Object} fallback - Legacy fields to use when the record is incomplete.
|
|
76
|
+
* @param {Object} record - Screenshot record from the API.
|
|
80
77
|
* @returns {{width: number|null, height: number|null}|null} Viewport dimensions.
|
|
81
78
|
*/
|
|
82
|
-
function getRecordViewport(record = {}
|
|
83
|
-
let viewport = record.viewport ||
|
|
84
|
-
let width = viewport.width ??
|
|
85
|
-
let height = viewport.height ??
|
|
79
|
+
function getRecordViewport(record = {}) {
|
|
80
|
+
let viewport = record.viewport || {};
|
|
81
|
+
let width = viewport.width ?? null;
|
|
82
|
+
let height = viewport.height ?? null;
|
|
86
83
|
return width != null || height != null ? {
|
|
87
84
|
width,
|
|
88
85
|
height
|
|
@@ -93,30 +90,22 @@ function getRecordViewport(record = {}, fallback = {}) {
|
|
|
93
90
|
* Normalize current screenshot identity, render facts, metadata, and asset URL.
|
|
94
91
|
*
|
|
95
92
|
* @param {Object} comparison - Comparison record from the API.
|
|
96
|
-
* @returns {Object}
|
|
93
|
+
* @returns {Object} Current screenshot details.
|
|
97
94
|
*/
|
|
98
95
|
function getCurrentScreenshot(comparison = {}) {
|
|
99
|
-
let screenshot = comparison.
|
|
96
|
+
let screenshot = comparison.screenshot || {};
|
|
100
97
|
return {
|
|
101
|
-
id: screenshot.id ||
|
|
102
|
-
name: screenshot.name ||
|
|
103
|
-
status: screenshot.status ||
|
|
104
|
-
browser: screenshot.browser ||
|
|
105
|
-
device: screenshot.device ||
|
|
106
|
-
viewport: getRecordViewport(screenshot,
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
bitmap: comparison.bitmap,
|
|
113
|
-
bitmap_width: comparison.current_width ?? comparison.bitmap_width,
|
|
114
|
-
bitmap_height: comparison.current_height ?? comparison.bitmap_height
|
|
115
|
-
}),
|
|
116
|
-
metadata: screenshot.metadata || comparison.current_metadata || comparison.metadata || null,
|
|
117
|
-
signature: screenshot.signature || comparison.current_signature || comparison.signature || null,
|
|
118
|
-
url: screenshot.url || screenshot.original_url || comparison.current_url || comparison.current_original_url || comparison.current_screenshot_url || null,
|
|
119
|
-
error_message: screenshot.error_message || comparison.error_message || null
|
|
98
|
+
id: screenshot.id || null,
|
|
99
|
+
name: screenshot.name || null,
|
|
100
|
+
status: screenshot.status || null,
|
|
101
|
+
browser: screenshot.browser || null,
|
|
102
|
+
device: screenshot.device || null,
|
|
103
|
+
viewport: getRecordViewport(screenshot),
|
|
104
|
+
bitmap: getBitmap(screenshot),
|
|
105
|
+
metadata: screenshot.metadata || null,
|
|
106
|
+
signature: screenshot.signature || null,
|
|
107
|
+
url: screenshot.url || null,
|
|
108
|
+
error_message: screenshot.error_message || null
|
|
120
109
|
};
|
|
121
110
|
}
|
|
122
111
|
|
|
@@ -124,31 +113,28 @@ function getCurrentScreenshot(comparison = {}) {
|
|
|
124
113
|
* Normalize the baseline screenshot without inventing missing render facts.
|
|
125
114
|
*
|
|
126
115
|
* @param {Object} comparison - Comparison record from the API.
|
|
127
|
-
* @returns {Object}
|
|
116
|
+
* @returns {Object} Baseline screenshot details.
|
|
128
117
|
*/
|
|
129
118
|
function getBaselineScreenshot(comparison = {}) {
|
|
130
|
-
let
|
|
131
|
-
let baseline = comparison.baseline_screenshot || comparison.baseline || current.baseline || {};
|
|
119
|
+
let baseline = comparison.screenshot?.baseline || {};
|
|
132
120
|
return {
|
|
133
|
-
id: baseline.id ||
|
|
134
|
-
build_id: baseline.build_id ||
|
|
135
|
-
name: baseline.name ||
|
|
121
|
+
id: baseline.id || null,
|
|
122
|
+
build_id: baseline.build_id || null,
|
|
123
|
+
name: baseline.name || null,
|
|
136
124
|
browser: baseline.browser || null,
|
|
137
125
|
viewport: getRecordViewport(baseline),
|
|
138
126
|
bitmap: getBitmap(baseline),
|
|
139
|
-
metadata: baseline.metadata ||
|
|
140
|
-
signature: baseline.signature ||
|
|
141
|
-
url: baseline.url ||
|
|
127
|
+
metadata: baseline.metadata || null,
|
|
128
|
+
signature: baseline.signature || null,
|
|
129
|
+
url: baseline.url || null
|
|
142
130
|
};
|
|
143
131
|
}
|
|
144
132
|
|
|
145
133
|
/**
|
|
146
|
-
*
|
|
134
|
+
* Keep the useful Honeydiff facts without including raw geometry by default.
|
|
147
135
|
*
|
|
148
|
-
* Agents need
|
|
149
|
-
*
|
|
150
|
-
* that manifest intact prevents the CLI from becoming less authoritative than
|
|
151
|
-
* the API. Raw regions and scoring details stay behind an explicit include
|
|
136
|
+
* Agents need counts, fingerprints, URLs, analysis details, and the server's
|
|
137
|
+
* artifact list for first-pass diagnosis. Raw regions and scoring details stay behind an explicit include
|
|
152
138
|
* because they can dominate an otherwise bounded handoff.
|
|
153
139
|
*
|
|
154
140
|
* @param {Object} comparison - Comparison record from the API.
|
|
@@ -156,20 +142,19 @@ function getBaselineScreenshot(comparison = {}) {
|
|
|
156
142
|
* @returns {Object} Compact diff evidence suitable for context summaries.
|
|
157
143
|
*/
|
|
158
144
|
function getComparisonDiff(comparison = {}, includeDiffs = false) {
|
|
159
|
-
let diff = comparison.diff ||
|
|
160
|
-
let
|
|
161
|
-
let
|
|
162
|
-
let
|
|
163
|
-
let artifacts = diff.artifacts ?? analysis.artifacts ?? comparison.artifacts ?? null;
|
|
145
|
+
let diff = comparison.diff || {};
|
|
146
|
+
let regions = diff.regions;
|
|
147
|
+
let details = diff.details || null;
|
|
148
|
+
let artifacts = diff.artifacts ?? null;
|
|
164
149
|
let compact = {
|
|
165
|
-
percentage: diff.percentage ??
|
|
166
|
-
changed_pixels: diff.changed_pixels ??
|
|
167
|
-
total_pixels: diff.total_pixels ??
|
|
168
|
-
threshold: diff.threshold ??
|
|
169
|
-
fingerprint_hash: diff.fingerprint_hash ||
|
|
170
|
-
region_count: diff.region_count ??
|
|
171
|
-
|
|
172
|
-
image_url: diff.image_url ||
|
|
150
|
+
percentage: diff.percentage ?? null,
|
|
151
|
+
changed_pixels: diff.changed_pixels ?? null,
|
|
152
|
+
total_pixels: diff.total_pixels ?? null,
|
|
153
|
+
threshold: diff.threshold ?? null,
|
|
154
|
+
fingerprint_hash: diff.fingerprint_hash || null,
|
|
155
|
+
region_count: diff.region_count ?? details?.clusters?.count ?? (Array.isArray(regions) ? regions.length : null),
|
|
156
|
+
details,
|
|
157
|
+
image_url: diff.image_url || null
|
|
173
158
|
};
|
|
174
159
|
|
|
175
160
|
// Preserve server-owned evidence exactly so agents can verify and download
|
|
@@ -179,10 +164,10 @@ function getComparisonDiff(comparison = {}, includeDiffs = false) {
|
|
|
179
164
|
}
|
|
180
165
|
if (includeDiffs) {
|
|
181
166
|
compact.regions = regions || [];
|
|
182
|
-
compact.cluster_metadata = diff.cluster_metadata ||
|
|
183
|
-
compact.ssim_score = diff.ssim_score ??
|
|
184
|
-
compact.gmsd_score = diff.gmsd_score ??
|
|
185
|
-
compact.diff_lines = diff.diff_lines ||
|
|
167
|
+
compact.cluster_metadata = diff.cluster_metadata || null;
|
|
168
|
+
compact.ssim_score = diff.ssim_score ?? null;
|
|
169
|
+
compact.gmsd_score = diff.gmsd_score ?? null;
|
|
170
|
+
compact.diff_lines = diff.diff_lines || [];
|
|
186
171
|
}
|
|
187
172
|
return compact;
|
|
188
173
|
}
|
|
@@ -191,7 +176,7 @@ function getComparisonDiff(comparison = {}, includeDiffs = false) {
|
|
|
191
176
|
* Prefer explicit review need and return null when the API supplied no review fact.
|
|
192
177
|
*
|
|
193
178
|
* @param {Object} comparison - Comparison record from the API.
|
|
194
|
-
* @param {string|null} reviewState -
|
|
179
|
+
* @param {string|null} reviewState - Review state for the record.
|
|
195
180
|
* @returns {boolean|null} Whether the comparison needs review, when known.
|
|
196
181
|
*/
|
|
197
182
|
function comparisonNeedsReview(comparison = {}, reviewState = null) {
|
|
@@ -205,28 +190,24 @@ function comparisonNeedsReview(comparison = {}, reviewState = null) {
|
|
|
205
190
|
}
|
|
206
191
|
|
|
207
192
|
/**
|
|
208
|
-
* Normalize one comparison
|
|
193
|
+
* Normalize one comparison from the current build-context API.
|
|
209
194
|
*
|
|
210
195
|
* @param {Object} comparison - Comparison record from the API.
|
|
211
196
|
* @param {Object} options - Normalization options.
|
|
212
|
-
* @param {string|null} [options.fallbackName] - Group name for unnamed variants.
|
|
213
197
|
* @param {boolean} [options.includeDiffs] - Include raw Honeydiff diagnostics.
|
|
214
198
|
* @returns {Object} A stable comparison evidence record.
|
|
215
199
|
*/
|
|
216
200
|
export function normalizeComparisonRecord(comparison = {}, options = {}) {
|
|
217
201
|
let reviewState = getVisualReviewState(comparison);
|
|
218
|
-
let name = comparison.name || comparison.screenshot_name || comparison.current_screenshot?.name || comparison.screenshot?.name || comparison.current_name || options.fallbackName || comparison.id || null;
|
|
219
202
|
return {
|
|
220
203
|
id: comparison.id || null,
|
|
221
|
-
name,
|
|
204
|
+
name: getComparisonName(comparison),
|
|
222
205
|
result: getComparisonResult(comparison),
|
|
223
206
|
status: comparison.status || null,
|
|
224
207
|
review_state: reviewState,
|
|
225
208
|
visual_review: comparison.visual_review || null,
|
|
226
|
-
approval_status: comparison.approval_status || null,
|
|
227
209
|
build_branch: comparison.build_branch || null,
|
|
228
210
|
needs_review: comparisonNeedsReview(comparison, reviewState),
|
|
229
|
-
is_flaky: comparison.is_flaky == null ? null : comparison.is_flaky === true,
|
|
230
211
|
browser: getComparisonBrowser(comparison),
|
|
231
212
|
viewport: getComparisonViewport(comparison),
|
|
232
213
|
screenshot: getCurrentScreenshot(comparison),
|
|
@@ -236,13 +217,13 @@ export function normalizeComparisonRecord(comparison = {}, options = {}) {
|
|
|
236
217
|
}
|
|
237
218
|
|
|
238
219
|
/**
|
|
239
|
-
* Read the server's total variant count
|
|
220
|
+
* Read the server's total variant count.
|
|
240
221
|
*
|
|
241
222
|
* @param {Object} group - Screenshot group from the API.
|
|
242
223
|
* @returns {number|null} The explicit total when present.
|
|
243
224
|
*/
|
|
244
225
|
function getExplicitVariantCount(group = {}) {
|
|
245
|
-
return group.
|
|
226
|
+
return group.total_variants ?? null;
|
|
246
227
|
}
|
|
247
228
|
|
|
248
229
|
/**
|
|
@@ -250,13 +231,9 @@ function getExplicitVariantCount(group = {}) {
|
|
|
250
231
|
*
|
|
251
232
|
* @param {Object} group - Screenshot group from the API.
|
|
252
233
|
* @param {Object[]} variants - Returned normalized variants.
|
|
253
|
-
* @param {boolean} forcedComplete - Whether the caller owns the complete set.
|
|
254
234
|
* @returns {boolean} Whether aggregate derivation is safe.
|
|
255
235
|
*/
|
|
256
|
-
function hasCompleteVariants(group = {}, variants = []
|
|
257
|
-
if (forcedComplete || group.variants_complete === true) {
|
|
258
|
-
return true;
|
|
259
|
-
}
|
|
236
|
+
function hasCompleteVariants(group = {}, variants = []) {
|
|
260
237
|
let explicitCount = getExplicitVariantCount(group);
|
|
261
238
|
return explicitCount != null && explicitCount === variants.length;
|
|
262
239
|
}
|
|
@@ -264,7 +241,7 @@ function hasCompleteVariants(group = {}, variants = [], forcedComplete = false)
|
|
|
264
241
|
/**
|
|
265
242
|
* Derive only facts backed by a complete variant set and complete source fields.
|
|
266
243
|
*
|
|
267
|
-
* Unknown review,
|
|
244
|
+
* Unknown review, result, or diff values stay null instead of becoming
|
|
268
245
|
* client-authored false or zero values.
|
|
269
246
|
*
|
|
270
247
|
* @param {Object[]} variants - Complete normalized variants for one screenshot.
|
|
@@ -278,12 +255,10 @@ function deriveAggregateFacts(variants = [], complete = false) {
|
|
|
278
255
|
let results = variants.map(variant => variant.result);
|
|
279
256
|
let reviewStates = variants.map(variant => variant.review_state);
|
|
280
257
|
let needsReview = variants.map(variant => variant.needs_review);
|
|
281
|
-
let flaky = variants.map(variant => variant.is_flaky);
|
|
282
258
|
let percentages = variants.map(variant => variant.diff.percentage).filter(value => value != null);
|
|
283
259
|
let allResultsKnown = results.every(value => value != null);
|
|
284
260
|
let allReviewStatesKnown = reviewStates.every(value => value != null);
|
|
285
261
|
let allNeedsReviewKnown = needsReview.every(value => value != null);
|
|
286
|
-
let allFlakyKnown = flaky.every(value => value != null);
|
|
287
262
|
return {
|
|
288
263
|
has_changes: results.includes('changed') ? true : allResultsKnown ? false : null,
|
|
289
264
|
has_new: results.includes('new') ? true : allResultsKnown ? false : null,
|
|
@@ -292,27 +267,24 @@ function deriveAggregateFacts(variants = [], complete = false) {
|
|
|
292
267
|
needs_review_count: allNeedsReviewKnown ? needsReview.filter(value => value === true).length : null,
|
|
293
268
|
failed_count: allResultsKnown ? results.filter(result => ['failed', 'error'].includes(result)).length : null,
|
|
294
269
|
has_rejected: reviewStates.includes('rejected') ? true : allReviewStatesKnown ? false : null,
|
|
295
|
-
has_flaky: flaky.includes(true) ? true : allFlakyKnown ? false : null,
|
|
296
270
|
max_diff_percentage: percentages.length === variants.length && percentages.length > 0 ? Math.max(...percentages) : null
|
|
297
271
|
};
|
|
298
272
|
}
|
|
299
273
|
|
|
300
274
|
/**
|
|
301
|
-
* Normalize a screenshot group while keeping explicit server
|
|
275
|
+
* Normalize a screenshot group while keeping explicit server totals unchanged.
|
|
302
276
|
*
|
|
303
277
|
* @param {Object} group - Grouped visual-review record from the API.
|
|
304
278
|
* @param {Object} options - Normalization options.
|
|
305
|
-
* @param {boolean} [options.variantsComplete] - Marks a caller-owned complete set.
|
|
306
279
|
* @returns {Object} A normalized screenshot group and its variants.
|
|
307
280
|
*/
|
|
308
281
|
export function normalizeComparisonGroup(group = {}, options = {}) {
|
|
309
|
-
let rawVariants = group.
|
|
310
|
-
let groupName = group.name ||
|
|
282
|
+
let rawVariants = group.comparisons || [];
|
|
283
|
+
let groupName = group.name || null;
|
|
311
284
|
let variants = rawVariants.map(variant => normalizeComparisonRecord(variant, {
|
|
312
|
-
fallbackName: groupName,
|
|
313
285
|
includeDiffs: options.includeDiffs
|
|
314
286
|
}));
|
|
315
|
-
let complete = hasCompleteVariants(group, variants
|
|
287
|
+
let complete = hasCompleteVariants(group, variants);
|
|
316
288
|
let derived = deriveAggregateFacts(variants, complete);
|
|
317
289
|
let aggregate = group.aggregate_status || {};
|
|
318
290
|
let needsReviewCount = aggregate.needs_review_count ?? derived.needs_review_count ?? null;
|
|
@@ -329,36 +301,11 @@ export function normalizeComparisonGroup(group = {}, options = {}) {
|
|
|
329
301
|
needs_review_count: needsReviewCount,
|
|
330
302
|
failed_count: aggregate.failed_count ?? derived.failed_count ?? null,
|
|
331
303
|
has_rejected: aggregate.has_rejected ?? derived.has_rejected ?? null,
|
|
332
|
-
has_flaky: aggregate.has_flaky ?? derived.has_flaky ?? null,
|
|
333
304
|
max_diff_percentage: aggregate.max_diff_percentage ?? derived.max_diff_percentage ?? null
|
|
334
305
|
}
|
|
335
306
|
};
|
|
336
307
|
}
|
|
337
308
|
|
|
338
|
-
/**
|
|
339
|
-
* Group a complete legacy flat comparison list by screenshot name.
|
|
340
|
-
*
|
|
341
|
-
* @param {Object[]} comparisons - Complete flat comparisons from build context.
|
|
342
|
-
* @param {Object} options - Normalization options passed to each comparison.
|
|
343
|
-
* @returns {Object[]} Normalized screenshot groups in API order.
|
|
344
|
-
*/
|
|
345
|
-
function groupFlatComparisons(comparisons = [], options = {}) {
|
|
346
|
-
let grouped = new Map();
|
|
347
|
-
for (let comparison of comparisons) {
|
|
348
|
-
let name = getComparisonName(comparison) || 'unknown screenshot';
|
|
349
|
-
let group = grouped.get(name) || {
|
|
350
|
-
name,
|
|
351
|
-
variants: []
|
|
352
|
-
};
|
|
353
|
-
group.variants.push(comparison);
|
|
354
|
-
grouped.set(name, group);
|
|
355
|
-
}
|
|
356
|
-
return [...grouped.values()].map(group => normalizeComparisonGroup(group, {
|
|
357
|
-
...options,
|
|
358
|
-
variantsComplete: true
|
|
359
|
-
}));
|
|
360
|
-
}
|
|
361
|
-
|
|
362
309
|
/**
|
|
363
310
|
* Join compact group variants to the exact comparison records in the same API
|
|
364
311
|
* response.
|
|
@@ -375,11 +322,10 @@ function groupFlatComparisons(comparisons = [], options = {}) {
|
|
|
375
322
|
function joinGroupComparisons(groups = [], comparisons = []) {
|
|
376
323
|
let comparisonsById = new Map(comparisons.filter(comparison => comparison?.id).map(comparison => [comparison.id, comparison]));
|
|
377
324
|
return groups.map(group => {
|
|
378
|
-
let
|
|
379
|
-
let variants = Array.isArray(group[variantKey]) ? group[variantKey] : [];
|
|
325
|
+
let comparisons = Array.isArray(group.comparisons) ? group.comparisons : [];
|
|
380
326
|
return {
|
|
381
327
|
...group,
|
|
382
|
-
|
|
328
|
+
comparisons: comparisons.map(variant => ({
|
|
383
329
|
...variant,
|
|
384
330
|
...(comparisonsById.get(variant.id) || {})
|
|
385
331
|
}))
|
|
@@ -399,9 +345,10 @@ function joinGroupComparisons(groups = [], comparisons = []) {
|
|
|
399
345
|
*/
|
|
400
346
|
function normalizeFailedCapture(screenshot = {}, options = {}) {
|
|
401
347
|
let normalized = normalizeComparisonRecord({
|
|
402
|
-
...screenshot,
|
|
403
348
|
screenshot,
|
|
404
|
-
result:
|
|
349
|
+
result: 'failed',
|
|
350
|
+
status: screenshot.status || null,
|
|
351
|
+
visual_review: screenshot.comparison?.visual_review || null
|
|
405
352
|
}, options);
|
|
406
353
|
return {
|
|
407
354
|
...normalized,
|
|
@@ -410,14 +357,14 @@ function normalizeFailedCapture(screenshot = {}, options = {}) {
|
|
|
410
357
|
}
|
|
411
358
|
|
|
412
359
|
/**
|
|
413
|
-
* Collect failed captures from
|
|
360
|
+
* Collect failed captures from the build context response.
|
|
414
361
|
*
|
|
415
362
|
* @param {Object} context - Raw build context response.
|
|
416
363
|
* @param {Object} options - Normalization options passed to each capture.
|
|
417
364
|
* @returns {Object[]} Deduplicated failed captures in API order.
|
|
418
365
|
*/
|
|
419
366
|
function getFailedCaptures(context = {}, options = {}) {
|
|
420
|
-
let explicit = Array.isArray(context.failed_captures) ? context.failed_captures :
|
|
367
|
+
let explicit = Array.isArray(context.failed_captures) ? context.failed_captures : [];
|
|
421
368
|
let screenshots = (context.screenshots || []).filter(screenshot => ['failed', 'error'].includes(screenshot.status));
|
|
422
369
|
let captures = [...explicit, ...screenshots];
|
|
423
370
|
let seen = new Set();
|
|
@@ -432,7 +379,7 @@ function getFailedCaptures(context = {}, options = {}) {
|
|
|
432
379
|
}
|
|
433
380
|
|
|
434
381
|
/**
|
|
435
|
-
* Adapt raw build context for human presentation
|
|
382
|
+
* Adapt raw build context for human presentation.
|
|
436
383
|
*
|
|
437
384
|
* Raw JSON output intentionally bypasses this lossy presentation boundary.
|
|
438
385
|
*
|
|
@@ -445,7 +392,7 @@ export function normalizeBuildContext(context = {}, options = {}) {
|
|
|
445
392
|
let rawComparisons = context.comparisons || [];
|
|
446
393
|
let comparisons = rawComparisons.map(comparison => normalizeComparisonRecord(comparison, options));
|
|
447
394
|
let joinedGroups = joinGroupComparisons(context.groups || [], rawComparisons);
|
|
448
|
-
let groups = joinedGroups.
|
|
395
|
+
let groups = joinedGroups.map(group => normalizeComparisonGroup(group, options));
|
|
449
396
|
return {
|
|
450
397
|
...context,
|
|
451
398
|
comparisons,
|
package/package.json
CHANGED
package/skills/vizzly/SKILL.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: vizzly
|
|
3
|
-
description: Inspect and explain Vizzly visual regression evidence, verify UI changes, troubleshoot local or cloud builds, adjust screenshot capture, or configure Vizzly CI. Use when a task mentions Vizzly, screenshot baselines, comparisons, Honeydiff, visual review,
|
|
3
|
+
description: Inspect and explain Vizzly visual regression evidence, verify UI changes, troubleshoot local or cloud builds, adjust screenshot capture, or configure Vizzly CI. Use when a task mentions Vizzly, screenshot baselines, comparisons, Honeydiff, visual review, or an existing Vizzly workflow.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Vizzly
|
|
@@ -47,8 +47,8 @@ test workflow in charge of how the UI is exercised.
|
|
|
47
47
|
values.
|
|
48
48
|
- Do not approve, reject, comment on, publish, or replace evidence unless the
|
|
49
49
|
task explicitly asks for that mutation.
|
|
50
|
-
- Preserve thresholds, cluster sizes, signature properties, and
|
|
51
|
-
|
|
50
|
+
- Preserve thresholds, cluster sizes, signature properties, and other capture
|
|
51
|
+
settings unless repeated evidence justifies a change.
|
|
52
52
|
- Prefer deterministic fixtures and existing user journeys over hiding a diff
|
|
53
53
|
with broader tolerances or a new screenshot-only test.
|
|
54
54
|
- Report the comparison or screenshot identity, observed evidence, access
|
|
@@ -60,5 +60,5 @@ test workflow in charge of how the UI is exercised.
|
|
|
60
60
|
discovery, drill-downs, images, and TDD lifecycle.
|
|
61
61
|
- [SDK capture](references/sdks.md): add or change screenshot capture code.
|
|
62
62
|
- [Dynamic content](references/dynamic-content.md): investigate unstable
|
|
63
|
-
content
|
|
63
|
+
content and screenshot-specific tolerances.
|
|
64
64
|
- [Setup and CI](references/setup-ci.md): initialize Vizzly or change CI.
|
|
@@ -14,8 +14,8 @@ can cause a diff. They are possible explanations, not conclusions.
|
|
|
14
14
|
2. Record the visible region, recurrence, render metadata, and whether the same
|
|
15
15
|
change appears across builds or variants.
|
|
16
16
|
3. Prefer deterministic fixtures when they can remove irrelevant variation.
|
|
17
|
-
4.
|
|
18
|
-
|
|
17
|
+
4. Use diff regions only to locate changed pixels; they do not approve a
|
|
18
|
+
comparison or explain why it changed.
|
|
19
19
|
5. Preserve `threshold` and `minClusterSize` unless repeated evidence
|
|
20
20
|
justifies a change.
|
|
21
21
|
|
|
@@ -1,112 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Hotspot Coverage Calculation
|
|
3
|
-
*
|
|
4
|
-
* Pure functions for calculating how much of a visual diff falls within
|
|
5
|
-
* "hotspot" regions - areas of the UI that frequently change due to dynamic
|
|
6
|
-
* content (timestamps, animations, etc.).
|
|
7
|
-
*
|
|
8
|
-
* Uses 1D Y-coordinate matching (same algorithm as cloud).
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Calculate what percentage of diff falls within hotspot regions
|
|
13
|
-
*
|
|
14
|
-
* @param {Array} diffClusters - Array of diff clusters from honeydiff
|
|
15
|
-
* @param {Object} hotspotAnalysis - Hotspot data with regions array
|
|
16
|
-
* @returns {{ coverage: number, linesInHotspots: number, totalLines: number }}
|
|
17
|
-
*/
|
|
18
|
-
export function calculateHotspotCoverage(diffClusters, hotspotAnalysis) {
|
|
19
|
-
if (!diffClusters || diffClusters.length === 0) {
|
|
20
|
-
return {
|
|
21
|
-
coverage: 0,
|
|
22
|
-
linesInHotspots: 0,
|
|
23
|
-
totalLines: 0
|
|
24
|
-
};
|
|
25
|
-
}
|
|
26
|
-
if (!hotspotAnalysis?.regions || hotspotAnalysis.regions.length === 0) {
|
|
27
|
-
return {
|
|
28
|
-
coverage: 0,
|
|
29
|
-
linesInHotspots: 0,
|
|
30
|
-
totalLines: 0
|
|
31
|
-
};
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
// Extract Y-coordinates (diff lines) from clusters
|
|
35
|
-
// Each cluster has a boundingBox with y and height
|
|
36
|
-
let diffLines = [];
|
|
37
|
-
for (let cluster of diffClusters) {
|
|
38
|
-
if (cluster.boundingBox) {
|
|
39
|
-
let {
|
|
40
|
-
y,
|
|
41
|
-
height
|
|
42
|
-
} = cluster.boundingBox;
|
|
43
|
-
// Add all Y lines covered by this cluster
|
|
44
|
-
for (let line = y; line < y + height; line++) {
|
|
45
|
-
diffLines.push(line);
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
if (diffLines.length === 0) {
|
|
50
|
-
return {
|
|
51
|
-
coverage: 0,
|
|
52
|
-
linesInHotspots: 0,
|
|
53
|
-
totalLines: 0
|
|
54
|
-
};
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
// Remove duplicates and sort
|
|
58
|
-
diffLines = [...new Set(diffLines)].sort((a, b) => a - b);
|
|
59
|
-
|
|
60
|
-
// Check how many diff lines fall within hotspot regions
|
|
61
|
-
let linesInHotspots = 0;
|
|
62
|
-
for (let line of diffLines) {
|
|
63
|
-
let inHotspot = hotspotAnalysis.regions.some(region => line >= region.y1 && line <= region.y2);
|
|
64
|
-
if (inHotspot) {
|
|
65
|
-
linesInHotspots++;
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
let coverage = linesInHotspots / diffLines.length;
|
|
69
|
-
return {
|
|
70
|
-
coverage,
|
|
71
|
-
linesInHotspots,
|
|
72
|
-
totalLines: diffLines.length
|
|
73
|
-
};
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
* Determine if a comparison should be filtered as "passed" based on hotspot coverage
|
|
78
|
-
*
|
|
79
|
-
* A diff is filtered when:
|
|
80
|
-
* 1. Coverage is >= 80% (most diff in hotspot regions)
|
|
81
|
-
* 2. Confidence is "high" or confidence score > 0.7
|
|
82
|
-
*
|
|
83
|
-
* @param {Object} hotspotAnalysis - Hotspot data with confidence info
|
|
84
|
-
* @param {{ coverage: number }} coverageResult - Result from calculateHotspotCoverage
|
|
85
|
-
* @returns {boolean} True if diff should be filtered as hotspot noise
|
|
86
|
-
*/
|
|
87
|
-
export function shouldFilterAsHotspot(hotspotAnalysis, coverageResult) {
|
|
88
|
-
if (!hotspotAnalysis || !coverageResult) {
|
|
89
|
-
return false;
|
|
90
|
-
}
|
|
91
|
-
let {
|
|
92
|
-
coverage
|
|
93
|
-
} = coverageResult;
|
|
94
|
-
|
|
95
|
-
// Need at least 80% of diff in hotspot regions
|
|
96
|
-
if (coverage < 0.8) {
|
|
97
|
-
return false;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
// Need high confidence in the hotspot analysis
|
|
101
|
-
let {
|
|
102
|
-
confidence,
|
|
103
|
-
confidenceScore
|
|
104
|
-
} = hotspotAnalysis;
|
|
105
|
-
if (confidence === 'high') {
|
|
106
|
-
return true;
|
|
107
|
-
}
|
|
108
|
-
if (confidenceScore !== undefined && confidenceScore > 0.7) {
|
|
109
|
-
return true;
|
|
110
|
-
}
|
|
111
|
-
return false;
|
|
112
|
-
}
|