@vizzly-testing/cli 0.35.2 → 0.35.3-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -5
- package/dist/api/client.js +20 -5
- package/dist/api/core.js +17 -3
- package/dist/api/endpoints.js +24 -3
- package/dist/api/index.js +1 -1
- package/dist/cli.js +25 -65
- package/dist/commands/baselines.js +0 -7
- package/dist/commands/comparisons.js +52 -45
- package/dist/commands/config-cmd.js +0 -7
- package/dist/commands/context.js +444 -238
- package/dist/commands/doctor.js +0 -8
- package/dist/commands/init.js +7 -12
- package/dist/commands/login.js +0 -13
- package/dist/commands/logout.js +0 -12
- package/dist/commands/orgs.js +0 -9
- package/dist/commands/run.js +84 -29
- package/dist/commands/status.js +276 -59
- package/dist/commands/tdd.js +19 -3
- package/dist/commands/whoami.js +0 -12
- package/dist/context/local-workspace-provider.js +293 -37
- package/dist/plugin-loader.js +1 -1
- package/dist/reporter/reporter-bundle.css +1 -1
- package/dist/reporter/reporter-bundle.iife.js +17 -17
- package/dist/server/routers/events.js +4 -4
- package/dist/test-runner/operations.js +70 -28
- package/dist/types/index.d.ts +4 -2
- package/dist/uploader/core.js +6 -6
- package/dist/uploader/operations.js +4 -1
- package/dist/utils/visual-context-normalizers.js +455 -0
- package/package.json +13 -13
- package/skills/vizzly/SKILL.md +68 -101
- package/skills/vizzly/agents/openai.yaml +2 -2
- package/skills/vizzly/references/cli-context.md +79 -27
- package/skills/vizzly/references/dynamic-content.md +28 -33
- package/skills/vizzly/references/sdks.md +26 -39
- package/skills/vizzly/references/setup-ci.md +44 -20
- package/dist/config/index.js +0 -13
- package/dist/project/index.js +0 -13
- package/dist/tdd/index.js +0 -19
- package/dist/tdd/services/baseline-downloader.js +0 -151
- package/dist/tdd/services/hotspot-service.js +0 -58
- package/dist/utils/environment.js +0 -109
- package/skills/vizzly/references/public-screenshots.md +0 -44
|
@@ -0,0 +1,455 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read the API's canonical visual-review state with legacy approval fallbacks.
|
|
3
|
+
*
|
|
4
|
+
* @param {Object} record - Comparison or review record from the API.
|
|
5
|
+
* @returns {string|null} The review state when the API supplied one.
|
|
6
|
+
*/
|
|
7
|
+
export function getVisualReviewState(record = {}) {
|
|
8
|
+
return record.visual_review?.state || record.review_state || record.approval_status || null;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Keep visual outcome separate from processing status when both are available.
|
|
13
|
+
*
|
|
14
|
+
* @param {Object} comparison - Comparison record from the API.
|
|
15
|
+
* @returns {string|null} The visual result, or the legacy status fallback.
|
|
16
|
+
*/
|
|
17
|
+
export function getComparisonResult(comparison = {}) {
|
|
18
|
+
return comparison.result || comparison.status || null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Resolve a screenshot name across current and legacy response shapes.
|
|
23
|
+
*
|
|
24
|
+
* @param {Object} comparison - Comparison record from the API.
|
|
25
|
+
* @returns {string|null} The first API-provided name or identity fallback.
|
|
26
|
+
*/
|
|
27
|
+
export function getComparisonName(comparison = {}) {
|
|
28
|
+
return comparison.name || comparison.screenshot_name || comparison.current_screenshot?.name || comparison.screenshot?.name || comparison.current_name || comparison.id || null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Resolve CSS viewport dimensions without confusing them with bitmap size.
|
|
33
|
+
*
|
|
34
|
+
* @param {Object} comparison - Comparison record from the API.
|
|
35
|
+
* @returns {{width: number|null, height: number|null}|null} Viewport dimensions.
|
|
36
|
+
*/
|
|
37
|
+
export function getComparisonViewport(comparison = {}) {
|
|
38
|
+
let current = comparison.current_screenshot || comparison.screenshot || {};
|
|
39
|
+
let viewport = comparison.viewport || current.viewport || {};
|
|
40
|
+
let width = viewport.width ?? comparison.viewport_width ?? current.viewport_width ?? comparison.current_viewport_width ?? null;
|
|
41
|
+
let height = viewport.height ?? comparison.viewport_height ?? current.viewport_height ?? comparison.current_viewport_height ?? null;
|
|
42
|
+
return width != null || height != null ? {
|
|
43
|
+
width,
|
|
44
|
+
height
|
|
45
|
+
} : null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Resolve the browser from nested current screenshot or legacy metadata fields.
|
|
50
|
+
*
|
|
51
|
+
* @param {Object} comparison - Comparison record from the API.
|
|
52
|
+
* @returns {string|null} The browser name when present.
|
|
53
|
+
*/
|
|
54
|
+
export function getComparisonBrowser(comparison = {}) {
|
|
55
|
+
return comparison.browser || comparison.current_screenshot?.browser || comparison.screenshot?.browser || comparison.current_browser || comparison.current_metadata?.browser || comparison.current_metadata?.properties?.browser || null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Resolve bitmap dimensions from nested or flat screenshot fields.
|
|
60
|
+
*
|
|
61
|
+
* @param {Object} record - Preferred screenshot record.
|
|
62
|
+
* @param {Object} fallback - Legacy fields to use when the record is incomplete.
|
|
63
|
+
* @returns {{width: number|null, height: number|null}|null} Bitmap dimensions.
|
|
64
|
+
*/
|
|
65
|
+
function getBitmap(record = {}, fallback = {}) {
|
|
66
|
+
let bitmap = record.bitmap || fallback.bitmap || {};
|
|
67
|
+
let width = bitmap.width ?? record.bitmap_width ?? fallback.bitmap_width ?? null;
|
|
68
|
+
let height = bitmap.height ?? record.bitmap_height ?? fallback.bitmap_height ?? null;
|
|
69
|
+
return width != null || height != null ? {
|
|
70
|
+
width,
|
|
71
|
+
height
|
|
72
|
+
} : null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Resolve viewport dimensions from one screenshot-shaped record.
|
|
77
|
+
*
|
|
78
|
+
* @param {Object} record - Preferred screenshot record.
|
|
79
|
+
* @param {Object} fallback - Legacy fields to use when the record is incomplete.
|
|
80
|
+
* @returns {{width: number|null, height: number|null}|null} Viewport dimensions.
|
|
81
|
+
*/
|
|
82
|
+
function getRecordViewport(record = {}, fallback = {}) {
|
|
83
|
+
let viewport = record.viewport || fallback.viewport || {};
|
|
84
|
+
let width = viewport.width ?? record.viewport_width ?? fallback.viewport_width ?? null;
|
|
85
|
+
let height = viewport.height ?? record.viewport_height ?? fallback.viewport_height ?? null;
|
|
86
|
+
return width != null || height != null ? {
|
|
87
|
+
width,
|
|
88
|
+
height
|
|
89
|
+
} : null;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Normalize current screenshot identity, render facts, metadata, and asset URL.
|
|
94
|
+
*
|
|
95
|
+
* @param {Object} comparison - Comparison record from the API.
|
|
96
|
+
* @returns {Object} A stable current screenshot projection.
|
|
97
|
+
*/
|
|
98
|
+
function getCurrentScreenshot(comparison = {}) {
|
|
99
|
+
let screenshot = comparison.current_screenshot || comparison.screenshot || {};
|
|
100
|
+
return {
|
|
101
|
+
id: screenshot.id || comparison.current_screenshot_id || null,
|
|
102
|
+
name: screenshot.name || getComparisonName(comparison),
|
|
103
|
+
status: screenshot.status || comparison.screenshot_status || null,
|
|
104
|
+
browser: screenshot.browser || getComparisonBrowser(comparison),
|
|
105
|
+
device: screenshot.device || comparison.device || null,
|
|
106
|
+
viewport: getRecordViewport(screenshot, {
|
|
107
|
+
viewport: comparison.viewport,
|
|
108
|
+
viewport_width: comparison.current_viewport_width ?? comparison.viewport_width,
|
|
109
|
+
viewport_height: comparison.current_viewport_height ?? comparison.viewport_height
|
|
110
|
+
}) || getComparisonViewport(comparison),
|
|
111
|
+
bitmap: getBitmap(screenshot, {
|
|
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
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Normalize the baseline screenshot without inventing missing render facts.
|
|
125
|
+
*
|
|
126
|
+
* @param {Object} comparison - Comparison record from the API.
|
|
127
|
+
* @returns {Object} A stable baseline screenshot projection.
|
|
128
|
+
*/
|
|
129
|
+
function getBaselineScreenshot(comparison = {}) {
|
|
130
|
+
let current = comparison.current_screenshot || comparison.screenshot || {};
|
|
131
|
+
let baseline = comparison.baseline_screenshot || comparison.baseline || current.baseline || {};
|
|
132
|
+
return {
|
|
133
|
+
id: baseline.id || comparison.baseline_screenshot_id || null,
|
|
134
|
+
build_id: baseline.build_id || comparison.baseline_build_id || null,
|
|
135
|
+
name: baseline.name || comparison.baseline_name || null,
|
|
136
|
+
browser: baseline.browser || null,
|
|
137
|
+
viewport: getRecordViewport(baseline),
|
|
138
|
+
bitmap: getBitmap(baseline),
|
|
139
|
+
metadata: baseline.metadata || comparison.baseline_metadata || null,
|
|
140
|
+
signature: baseline.signature || comparison.baseline_signature || null,
|
|
141
|
+
url: baseline.url || baseline.original_url || comparison.baseline_url || comparison.baseline_original_url || comparison.baseline_screenshot_url || null
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Project compact Honeydiff facts without making raw geometry the default.
|
|
147
|
+
*
|
|
148
|
+
* Agents need stable counts, fingerprints, URLs, the API projection, and the
|
|
149
|
+
* server's authoritative artifact manifest for first-pass diagnosis. Keeping
|
|
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
|
|
152
|
+
* because they can dominate an otherwise bounded handoff.
|
|
153
|
+
*
|
|
154
|
+
* @param {Object} comparison - Comparison record from the API.
|
|
155
|
+
* @param {boolean} includeDiffs - Whether to include raw Honeydiff diagnostics.
|
|
156
|
+
* @returns {Object} Compact diff evidence suitable for context summaries.
|
|
157
|
+
*/
|
|
158
|
+
function getComparisonDiff(comparison = {}, includeDiffs = false) {
|
|
159
|
+
let diff = comparison.diff || comparison.diff_image || comparison.analysis || {};
|
|
160
|
+
let analysis = comparison.analysis || {};
|
|
161
|
+
let regions = diff.regions || diff.diff_regions || analysis.diff_regions || comparison.diff_regions;
|
|
162
|
+
let projection = diff.projection || diff.analysis_projection || analysis.projection || analysis.analysis_projection || comparison.analysis_projection || comparison.projection || null;
|
|
163
|
+
let artifacts = diff.artifacts ?? analysis.artifacts ?? comparison.artifacts ?? null;
|
|
164
|
+
let compact = {
|
|
165
|
+
percentage: diff.percentage ?? analysis.percentage ?? analysis.diff_percentage ?? comparison.diff_percentage ?? null,
|
|
166
|
+
changed_pixels: diff.changed_pixels ?? analysis.changed_pixels ?? comparison.changed_pixels ?? null,
|
|
167
|
+
total_pixels: diff.total_pixels ?? analysis.total_pixels ?? comparison.total_pixels ?? null,
|
|
168
|
+
threshold: diff.threshold ?? analysis.threshold ?? comparison.threshold ?? null,
|
|
169
|
+
fingerprint_hash: diff.fingerprint_hash || analysis.fingerprint_hash || comparison.fingerprint_hash || null,
|
|
170
|
+
region_count: diff.region_count ?? analysis.region_count ?? comparison.region_count ?? projection?.clusters?.count ?? (Array.isArray(regions) ? regions.length : null),
|
|
171
|
+
projection,
|
|
172
|
+
image_url: diff.image_url || diff.url || analysis.diff_image_url || comparison.diff_url || comparison.diff_image_url || null
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
// Preserve server-owned evidence exactly so agents can verify and download
|
|
176
|
+
// artifacts without the CLI recreating identities, digests, or availability.
|
|
177
|
+
if (artifacts != null) {
|
|
178
|
+
compact.artifacts = artifacts;
|
|
179
|
+
}
|
|
180
|
+
if (includeDiffs) {
|
|
181
|
+
compact.regions = regions || [];
|
|
182
|
+
compact.cluster_metadata = diff.cluster_metadata || analysis.cluster_metadata || comparison.cluster_metadata || null;
|
|
183
|
+
compact.ssim_score = diff.ssim_score ?? analysis.ssim_score ?? comparison.ssim_score ?? null;
|
|
184
|
+
compact.gmsd_score = diff.gmsd_score ?? analysis.gmsd_score ?? comparison.gmsd_score ?? null;
|
|
185
|
+
compact.diff_lines = diff.diff_lines || analysis.diff_lines || comparison.diff_lines || [];
|
|
186
|
+
}
|
|
187
|
+
return compact;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Prefer explicit review need and return null when the API supplied no review fact.
|
|
192
|
+
*
|
|
193
|
+
* @param {Object} comparison - Comparison record from the API.
|
|
194
|
+
* @param {string|null} reviewState - Canonical review state for the record.
|
|
195
|
+
* @returns {boolean|null} Whether the comparison needs review, when known.
|
|
196
|
+
*/
|
|
197
|
+
function comparisonNeedsReview(comparison = {}, reviewState = null) {
|
|
198
|
+
if (comparison.needs_review != null) {
|
|
199
|
+
return comparison.needs_review === true;
|
|
200
|
+
}
|
|
201
|
+
if (reviewState == null) {
|
|
202
|
+
return null;
|
|
203
|
+
}
|
|
204
|
+
return reviewState === 'pending';
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Normalize one comparison across grouped and legacy flat API response shapes.
|
|
209
|
+
*
|
|
210
|
+
* @param {Object} comparison - Comparison record from the API.
|
|
211
|
+
* @param {Object} options - Normalization options.
|
|
212
|
+
* @param {string|null} [options.fallbackName] - Group name for unnamed variants.
|
|
213
|
+
* @param {boolean} [options.includeDiffs] - Include raw Honeydiff diagnostics.
|
|
214
|
+
* @returns {Object} A stable comparison evidence record.
|
|
215
|
+
*/
|
|
216
|
+
export function normalizeComparisonRecord(comparison = {}, options = {}) {
|
|
217
|
+
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
|
+
return {
|
|
220
|
+
id: comparison.id || null,
|
|
221
|
+
name,
|
|
222
|
+
result: getComparisonResult(comparison),
|
|
223
|
+
status: comparison.status || null,
|
|
224
|
+
review_state: reviewState,
|
|
225
|
+
visual_review: comparison.visual_review || null,
|
|
226
|
+
approval_status: comparison.approval_status || null,
|
|
227
|
+
build_branch: comparison.build_branch || null,
|
|
228
|
+
needs_review: comparisonNeedsReview(comparison, reviewState),
|
|
229
|
+
is_flaky: comparison.is_flaky == null ? null : comparison.is_flaky === true,
|
|
230
|
+
browser: getComparisonBrowser(comparison),
|
|
231
|
+
viewport: getComparisonViewport(comparison),
|
|
232
|
+
screenshot: getCurrentScreenshot(comparison),
|
|
233
|
+
baseline: getBaselineScreenshot(comparison),
|
|
234
|
+
diff: getComparisonDiff(comparison, options.includeDiffs === true)
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Read the server's total variant count across supported response shapes.
|
|
240
|
+
*
|
|
241
|
+
* @param {Object} group - Screenshot group from the API.
|
|
242
|
+
* @returns {number|null} The explicit total when present.
|
|
243
|
+
*/
|
|
244
|
+
function getExplicitVariantCount(group = {}) {
|
|
245
|
+
return group.variant_count ?? group.total_variants ?? group.totalVariants ?? null;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Prove whether every variant is available before deriving aggregate facts.
|
|
250
|
+
*
|
|
251
|
+
* @param {Object} group - Screenshot group from the API.
|
|
252
|
+
* @param {Object[]} variants - Returned normalized variants.
|
|
253
|
+
* @param {boolean} forcedComplete - Whether the caller owns the complete set.
|
|
254
|
+
* @returns {boolean} Whether aggregate derivation is safe.
|
|
255
|
+
*/
|
|
256
|
+
function hasCompleteVariants(group = {}, variants = [], forcedComplete = false) {
|
|
257
|
+
if (forcedComplete || group.variants_complete === true) {
|
|
258
|
+
return true;
|
|
259
|
+
}
|
|
260
|
+
let explicitCount = getExplicitVariantCount(group);
|
|
261
|
+
return explicitCount != null && explicitCount === variants.length;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Derive only facts backed by a complete variant set and complete source fields.
|
|
266
|
+
*
|
|
267
|
+
* Unknown review, flaky, result, or diff values stay null instead of becoming
|
|
268
|
+
* client-authored false or zero values.
|
|
269
|
+
*
|
|
270
|
+
* @param {Object[]} variants - Complete normalized variants for one screenshot.
|
|
271
|
+
* @param {boolean} complete - Whether every variant is present.
|
|
272
|
+
* @returns {Object} Exact derived aggregate facts, or an empty object.
|
|
273
|
+
*/
|
|
274
|
+
function deriveAggregateFacts(variants = [], complete = false) {
|
|
275
|
+
if (!complete) {
|
|
276
|
+
return {};
|
|
277
|
+
}
|
|
278
|
+
let results = variants.map(variant => variant.result);
|
|
279
|
+
let reviewStates = variants.map(variant => variant.review_state);
|
|
280
|
+
let needsReview = variants.map(variant => variant.needs_review);
|
|
281
|
+
let flaky = variants.map(variant => variant.is_flaky);
|
|
282
|
+
let percentages = variants.map(variant => variant.diff.percentage).filter(value => value != null);
|
|
283
|
+
let allResultsKnown = results.every(value => value != null);
|
|
284
|
+
let allReviewStatesKnown = reviewStates.every(value => value != null);
|
|
285
|
+
let allNeedsReviewKnown = needsReview.every(value => value != null);
|
|
286
|
+
let allFlakyKnown = flaky.every(value => value != null);
|
|
287
|
+
return {
|
|
288
|
+
has_changes: results.includes('changed') ? true : allResultsKnown ? false : null,
|
|
289
|
+
has_new: results.includes('new') ? true : allResultsKnown ? false : null,
|
|
290
|
+
all_approved: variants.length > 0 && allReviewStatesKnown ? reviewStates.every(state => state === 'approved') : null,
|
|
291
|
+
needs_review: allNeedsReviewKnown ? needsReview.some(value => value === true) : null,
|
|
292
|
+
needs_review_count: allNeedsReviewKnown ? needsReview.filter(value => value === true).length : null,
|
|
293
|
+
failed_count: allResultsKnown ? results.filter(result => ['failed', 'error'].includes(result)).length : null,
|
|
294
|
+
has_rejected: reviewStates.includes('rejected') ? true : allReviewStatesKnown ? false : null,
|
|
295
|
+
has_flaky: flaky.includes(true) ? true : allFlakyKnown ? false : null,
|
|
296
|
+
max_diff_percentage: percentages.length === variants.length && percentages.length > 0 ? Math.max(...percentages) : null
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Normalize a screenshot group while keeping explicit server aggregates authoritative.
|
|
302
|
+
*
|
|
303
|
+
* @param {Object} group - Grouped visual-review record from the API.
|
|
304
|
+
* @param {Object} options - Normalization options.
|
|
305
|
+
* @param {boolean} [options.variantsComplete] - Marks a caller-owned complete set.
|
|
306
|
+
* @returns {Object} A normalized screenshot group and its variants.
|
|
307
|
+
*/
|
|
308
|
+
export function normalizeComparisonGroup(group = {}, options = {}) {
|
|
309
|
+
let rawVariants = group.variants || group.comparisons || [];
|
|
310
|
+
let groupName = group.name || group.test_name || group.testName || null;
|
|
311
|
+
let variants = rawVariants.map(variant => normalizeComparisonRecord(variant, {
|
|
312
|
+
fallbackName: groupName,
|
|
313
|
+
includeDiffs: options.includeDiffs
|
|
314
|
+
}));
|
|
315
|
+
let complete = hasCompleteVariants(group, variants, options.variantsComplete === true);
|
|
316
|
+
let derived = deriveAggregateFacts(variants, complete);
|
|
317
|
+
let aggregate = group.aggregate_status || {};
|
|
318
|
+
let needsReviewCount = aggregate.needs_review_count ?? derived.needs_review_count ?? null;
|
|
319
|
+
return {
|
|
320
|
+
name: groupName || variants[0]?.name || 'unknown screenshot',
|
|
321
|
+
variant_count: getExplicitVariantCount(group) ?? (complete ? variants.length : null),
|
|
322
|
+
variants_complete: complete,
|
|
323
|
+
variants,
|
|
324
|
+
aggregate_status: {
|
|
325
|
+
has_changes: aggregate.has_changes ?? derived.has_changes ?? null,
|
|
326
|
+
has_new: aggregate.has_new ?? derived.has_new ?? null,
|
|
327
|
+
all_approved: aggregate.all_approved ?? derived.all_approved ?? null,
|
|
328
|
+
needs_review: aggregate.needs_review ?? (aggregate.needs_review_count != null ? aggregate.needs_review_count > 0 : derived.needs_review ?? null),
|
|
329
|
+
needs_review_count: needsReviewCount,
|
|
330
|
+
failed_count: aggregate.failed_count ?? derived.failed_count ?? null,
|
|
331
|
+
has_rejected: aggregate.has_rejected ?? derived.has_rejected ?? null,
|
|
332
|
+
has_flaky: aggregate.has_flaky ?? derived.has_flaky ?? null,
|
|
333
|
+
max_diff_percentage: aggregate.max_diff_percentage ?? derived.max_diff_percentage ?? null
|
|
334
|
+
}
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
|
|
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
|
+
/**
|
|
363
|
+
* Join compact group variants to the exact comparison records in the same API
|
|
364
|
+
* response.
|
|
365
|
+
*
|
|
366
|
+
* The build context API deliberately keeps group variants small while also
|
|
367
|
+
* returning complete comparison evidence at the top level. Joining by the
|
|
368
|
+
* server-owned comparison ID keeps the useful group ordering and aggregates
|
|
369
|
+
* without dropping screenshot identity, baseline facts, or Honeydiff data.
|
|
370
|
+
*
|
|
371
|
+
* @param {Object[]} groups - Raw comparison groups from build context.
|
|
372
|
+
* @param {Object[]} comparisons - Exact top-level comparison records.
|
|
373
|
+
* @returns {Object[]} Groups whose variants include exact comparison evidence.
|
|
374
|
+
*/
|
|
375
|
+
function joinGroupComparisons(groups = [], comparisons = []) {
|
|
376
|
+
let comparisonsById = new Map(comparisons.filter(comparison => comparison?.id).map(comparison => [comparison.id, comparison]));
|
|
377
|
+
return groups.map(group => {
|
|
378
|
+
let variantKey = Array.isArray(group.variants) ? 'variants' : 'comparisons';
|
|
379
|
+
let variants = Array.isArray(group[variantKey]) ? group[variantKey] : [];
|
|
380
|
+
return {
|
|
381
|
+
...group,
|
|
382
|
+
[variantKey]: variants.map(variant => ({
|
|
383
|
+
...variant,
|
|
384
|
+
...(comparisonsById.get(variant.id) || {})
|
|
385
|
+
}))
|
|
386
|
+
};
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Keep failed capture identity, render evidence, and the API error together.
|
|
392
|
+
*
|
|
393
|
+
* A capture can fail before any comparison exists, so it must remain useful
|
|
394
|
+
* evidence without inheriting comparison-only assumptions.
|
|
395
|
+
*
|
|
396
|
+
* @param {Object} screenshot - Failed screenshot record from the API.
|
|
397
|
+
* @param {Object} options - Normalization options passed to the record.
|
|
398
|
+
* @returns {Object} A normalized failed-capture record.
|
|
399
|
+
*/
|
|
400
|
+
function normalizeFailedCapture(screenshot = {}, options = {}) {
|
|
401
|
+
let normalized = normalizeComparisonRecord({
|
|
402
|
+
...screenshot,
|
|
403
|
+
screenshot,
|
|
404
|
+
result: screenshot.result || screenshot.status || 'failed'
|
|
405
|
+
}, options);
|
|
406
|
+
return {
|
|
407
|
+
...normalized,
|
|
408
|
+
error_message: screenshot.error_message || normalized.screenshot.error_message || null
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* Collect failed captures from explicit and screenshot-list response shapes.
|
|
414
|
+
*
|
|
415
|
+
* @param {Object} context - Raw build context response.
|
|
416
|
+
* @param {Object} options - Normalization options passed to each capture.
|
|
417
|
+
* @returns {Object[]} Deduplicated failed captures in API order.
|
|
418
|
+
*/
|
|
419
|
+
function getFailedCaptures(context = {}, options = {}) {
|
|
420
|
+
let explicit = Array.isArray(context.failed_captures) ? context.failed_captures : Array.isArray(context.failed_screenshots) ? context.failed_screenshots : [];
|
|
421
|
+
let screenshots = (context.screenshots || []).filter(screenshot => ['failed', 'error'].includes(screenshot.status));
|
|
422
|
+
let captures = [...explicit, ...screenshots];
|
|
423
|
+
let seen = new Set();
|
|
424
|
+
return captures.map(capture => normalizeFailedCapture(capture, options)).filter(capture => {
|
|
425
|
+
let key = capture.id || capture.screenshot.signature || capture.name;
|
|
426
|
+
if (seen.has(key)) {
|
|
427
|
+
return false;
|
|
428
|
+
}
|
|
429
|
+
seen.add(key);
|
|
430
|
+
return true;
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* Adapt raw build context for human presentation across current and legacy APIs.
|
|
436
|
+
*
|
|
437
|
+
* Raw JSON output intentionally bypasses this lossy presentation boundary.
|
|
438
|
+
*
|
|
439
|
+
* @param {Object} context - Raw build context response from the provider.
|
|
440
|
+
* @param {Object} options - Normalization options.
|
|
441
|
+
* @param {boolean} [options.includeDiffs] - Include raw Honeydiff diagnostics.
|
|
442
|
+
* @returns {Object} Normalized comparisons, groups, and failed captures.
|
|
443
|
+
*/
|
|
444
|
+
export function normalizeBuildContext(context = {}, options = {}) {
|
|
445
|
+
let rawComparisons = context.comparisons || [];
|
|
446
|
+
let comparisons = rawComparisons.map(comparison => normalizeComparisonRecord(comparison, options));
|
|
447
|
+
let joinedGroups = joinGroupComparisons(context.groups || [], rawComparisons);
|
|
448
|
+
let groups = joinedGroups.length > 0 ? joinedGroups.map(group => normalizeComparisonGroup(group, options)) : groupFlatComparisons(rawComparisons, options);
|
|
449
|
+
return {
|
|
450
|
+
...context,
|
|
451
|
+
comparisons,
|
|
452
|
+
groups,
|
|
453
|
+
failed_captures: getFailedCaptures(context, options)
|
|
454
|
+
};
|
|
455
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vizzly-testing/cli",
|
|
3
|
-
"version": "0.35.
|
|
3
|
+
"version": "0.35.3-beta.1",
|
|
4
4
|
"description": "Visual regression testing from your terminal",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"llm-context",
|
|
@@ -60,7 +60,8 @@
|
|
|
60
60
|
"LICENSE"
|
|
61
61
|
],
|
|
62
62
|
"scripts": {
|
|
63
|
-
"cli": "
|
|
63
|
+
"cli": "node scripts/run-development-cli.js production",
|
|
64
|
+
"cli:local": "node scripts/run-development-cli.js local",
|
|
64
65
|
"start": "node src/index.js",
|
|
65
66
|
"build": "pnpm run clean && pnpm run compile && pnpm run build:reporter && pnpm run build:reporter-ssr && pnpm run copy-types",
|
|
66
67
|
"clean": "rimraf dist",
|
|
@@ -78,7 +79,6 @@
|
|
|
78
79
|
"test:swift:e2e": "pnpm run build && node clients/swift/scripts/run-e2e.js",
|
|
79
80
|
"test:tui": "node --test --test-reporter=spec tests/tui/*.test.js",
|
|
80
81
|
"test:tui:docker": "./tests/tui/run-tui-tests.sh",
|
|
81
|
-
"smoke:prod:pack": "node scripts/smoke-prod-pack.js",
|
|
82
82
|
"lint": "biome check src tests clients/storybook/src clients/storybook/tests clients/static-site/src clients/static-site/tests clients/vitest/src clients/vitest/tests clients/ember/src clients/ember/tests clients/ember/bin",
|
|
83
83
|
"lint:fix": "biome check --write --unsafe src tests clients/storybook/src clients/storybook/tests clients/static-site/src clients/static-site/tests clients/vitest/src clients/vitest/tests clients/ember/src clients/ember/tests clients/ember/bin",
|
|
84
84
|
"format": "biome format --write src tests clients/storybook/src clients/storybook/tests clients/static-site/src clients/static-site/tests clients/vitest/src clients/vitest/tests clients/ember/src clients/ember/tests clients/ember/bin",
|
|
@@ -93,10 +93,10 @@
|
|
|
93
93
|
"registry": "https://registry.npmjs.org/"
|
|
94
94
|
},
|
|
95
95
|
"dependencies": {
|
|
96
|
-
"@vizzly-testing/bear-den": "0.1.
|
|
97
|
-
"@vizzly-testing/honeydiff": "^0.
|
|
96
|
+
"@vizzly-testing/bear-den": "0.1.4",
|
|
97
|
+
"@vizzly-testing/honeydiff": "^0.15.0",
|
|
98
98
|
"ansis": "^4.2.0",
|
|
99
|
-
"commander": "^
|
|
99
|
+
"commander": "^15.0.0",
|
|
100
100
|
"cosmiconfig": "^9.0.0",
|
|
101
101
|
"dotenv": "^17.2.1",
|
|
102
102
|
"form-data": "^4.0.0",
|
|
@@ -116,18 +116,18 @@
|
|
|
116
116
|
}
|
|
117
117
|
},
|
|
118
118
|
"devDependencies": {
|
|
119
|
-
"@babel/cli": "^
|
|
120
|
-
"@babel/core": "^
|
|
121
|
-
"@babel/preset-env": "^
|
|
122
|
-
"@babel/preset-react": "^
|
|
123
|
-
"@babel/preset-typescript": "^
|
|
119
|
+
"@babel/cli": "^8.0.1",
|
|
120
|
+
"@babel/core": "^8.0.1",
|
|
121
|
+
"@babel/preset-env": "^8.0.2",
|
|
122
|
+
"@babel/preset-react": "^8.0.1",
|
|
123
|
+
"@babel/preset-typescript": "^8.0.1",
|
|
124
124
|
"@biomejs/biome": "^2.3.8",
|
|
125
125
|
"@heroicons/react": "^2.2.0",
|
|
126
126
|
"@modelcontextprotocol/sdk": "^1.0.4",
|
|
127
127
|
"@playwright/test": "^1.55.1",
|
|
128
128
|
"@tailwindcss/postcss": "^4.1.13",
|
|
129
129
|
"@tanstack/react-query": "^5.90.11",
|
|
130
|
-
"@types/node": "^
|
|
130
|
+
"@types/node": "^26.0.0",
|
|
131
131
|
"@vitejs/plugin-react": "^6.0.1",
|
|
132
132
|
"autoprefixer": "^10.4.21",
|
|
133
133
|
"babel-plugin-react-compiler": "^1.0.0",
|
|
@@ -137,7 +137,7 @@
|
|
|
137
137
|
"tailwindcss": "^4.1.13",
|
|
138
138
|
"tsd": "^0.33.0",
|
|
139
139
|
"tui-driver": "github:vizzly-testing/tui-driver",
|
|
140
|
-
"typescript": "^
|
|
140
|
+
"typescript": "^7.0.2",
|
|
141
141
|
"vite": "^8.0.0",
|
|
142
142
|
"wouter": "^3.7.1"
|
|
143
143
|
}
|