@vizzly-testing/cli 0.31.0 → 0.33.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 +67 -50
- package/dist/cli.js +8 -8
- package/dist/client/index.js +2 -1
- package/dist/commands/doctor.js +4 -4
- package/dist/commands/init.js +1 -1
- package/dist/commands/preview.js +3 -3
- package/dist/commands/run.js +1 -1
- package/dist/commands/tdd-daemon.js +1 -1
- package/dist/reporter/reporter-bundle.css +1 -1
- package/dist/reporter/reporter-bundle.iife.js +118 -105
- package/dist/reporter-ssr/ssr-entry.js +88 -88
- package/dist/services/static-report-generator.js +2 -2
- package/dist/tdd/core/region-coverage.js +109 -21
- package/dist/tdd/services/comparison-service.js +10 -1
- package/dist/tdd/tdd-service.js +76 -39
- package/dist/utils/colors.js +28 -45
- package/dist/utils/global-config.js +1 -1
- package/dist/utils/output.js +16 -16
- package/dist/utils/screenshot-options.js +1 -0
- package/docs/assets/vizzly-mascot-inspector.png +0 -0
- package/package.json +13 -11
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
* 2D boxes that users have manually confirmed via the cloud UI.
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
+
let REGION_CENTER_TOLERANCE = 10;
|
|
13
|
+
|
|
12
14
|
/**
|
|
13
15
|
* Check if a diff cluster intersects with a region (2D box intersection)
|
|
14
16
|
*
|
|
@@ -41,13 +43,77 @@ export function clusterIntersectsRegion(cluster, region) {
|
|
|
41
43
|
let noOverlap = clusterX2 < region.x1 || clusterX1 > region.x2 || clusterY2 < region.y1 || clusterY1 > region.y2;
|
|
42
44
|
return !noOverlap;
|
|
43
45
|
}
|
|
46
|
+
function normalizeBoundingBox(cluster) {
|
|
47
|
+
let box = cluster?.boundingBox || cluster;
|
|
48
|
+
if (!box) {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
if (Number.isFinite(box.x) && Number.isFinite(box.y) && Number.isFinite(box.width) && Number.isFinite(box.height)) {
|
|
52
|
+
return {
|
|
53
|
+
x1: box.x,
|
|
54
|
+
y1: box.y,
|
|
55
|
+
x2: box.x + box.width,
|
|
56
|
+
y2: box.y + box.height,
|
|
57
|
+
width: Math.abs(box.width),
|
|
58
|
+
height: Math.abs(box.height)
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
if (Number.isFinite(box.x1) && Number.isFinite(box.y1) && Number.isFinite(box.x2) && Number.isFinite(box.y2)) {
|
|
62
|
+
let width = Math.abs(box.x2 - box.x1);
|
|
63
|
+
let height = Math.abs(box.y2 - box.y1);
|
|
64
|
+
return {
|
|
65
|
+
x1: box.x1,
|
|
66
|
+
y1: box.y1,
|
|
67
|
+
x2: box.x2,
|
|
68
|
+
y2: box.y2,
|
|
69
|
+
width,
|
|
70
|
+
height
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
function getBoundingBoxCenter(box) {
|
|
76
|
+
return {
|
|
77
|
+
x: (box.x1 + box.x2) / 2,
|
|
78
|
+
y: (box.y1 + box.y2) / 2
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
function getRegionLabel(region) {
|
|
82
|
+
return region.id || region.label || null;
|
|
83
|
+
}
|
|
84
|
+
function clusterMatchesRegion(cluster, region) {
|
|
85
|
+
let clusterBox = normalizeBoundingBox(cluster);
|
|
86
|
+
let regionBox = normalizeBoundingBox(region);
|
|
87
|
+
if (!clusterBox || !regionBox) {
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
let clusterCenter = getBoundingBoxCenter(clusterBox);
|
|
91
|
+
let regionCenter = getBoundingBoxCenter(regionBox);
|
|
92
|
+
let distance = Math.sqrt((clusterCenter.x - regionCenter.x) ** 2 + (clusterCenter.y - regionCenter.y) ** 2);
|
|
93
|
+
return distance <= REGION_CENTER_TOLERANCE;
|
|
94
|
+
}
|
|
95
|
+
function estimateClusterPixels(cluster) {
|
|
96
|
+
if (Number.isFinite(cluster?.pixelCount)) {
|
|
97
|
+
return cluster.pixelCount;
|
|
98
|
+
}
|
|
99
|
+
let box = normalizeBoundingBox(cluster);
|
|
100
|
+
if (!box) {
|
|
101
|
+
return 0;
|
|
102
|
+
}
|
|
103
|
+
return box.width * box.height * 0.5;
|
|
104
|
+
}
|
|
44
105
|
|
|
45
106
|
/**
|
|
46
|
-
* Calculate what percentage of
|
|
107
|
+
* Calculate what percentage of changed pixels match confirmed regions.
|
|
108
|
+
*
|
|
109
|
+
* This mirrors cloud dynamic-region approval: clusters match confirmed regions
|
|
110
|
+
* by center proximity, then coverage is weighted by changed pixels. A huge
|
|
111
|
+
* unmatched change should not be hidden just because several tiny clusters
|
|
112
|
+
* match confirmed regions.
|
|
47
113
|
*
|
|
48
114
|
* @param {Array} diffClusters - Array of diff clusters from honeydiff
|
|
49
115
|
* @param {Array} regions - Array of confirmed regions { x1, y1, x2, y2 }
|
|
50
|
-
* @returns {{ coverage: number, clustersInRegions: number, totalClusters: number, matchedRegions: string[] }}
|
|
116
|
+
* @returns {{ coverage: number, clustersInRegions: number, totalClusters: number, matchedRegions: string[], pixelsInRegions: number, totalPixels: number }}
|
|
51
117
|
*/
|
|
52
118
|
export function calculateRegionCoverage(diffClusters, regions) {
|
|
53
119
|
if (!diffClusters || diffClusters.length === 0) {
|
|
@@ -55,7 +121,9 @@ export function calculateRegionCoverage(diffClusters, regions) {
|
|
|
55
121
|
coverage: 0,
|
|
56
122
|
clustersInRegions: 0,
|
|
57
123
|
totalClusters: 0,
|
|
58
|
-
matchedRegions: []
|
|
124
|
+
matchedRegions: [],
|
|
125
|
+
pixelsInRegions: 0,
|
|
126
|
+
totalPixels: 0
|
|
59
127
|
};
|
|
60
128
|
}
|
|
61
129
|
if (!regions || regions.length === 0) {
|
|
@@ -63,35 +131,41 @@ export function calculateRegionCoverage(diffClusters, regions) {
|
|
|
63
131
|
coverage: 0,
|
|
64
132
|
clustersInRegions: 0,
|
|
65
133
|
totalClusters: diffClusters.length,
|
|
66
|
-
matchedRegions: []
|
|
134
|
+
matchedRegions: [],
|
|
135
|
+
pixelsInRegions: 0,
|
|
136
|
+
totalPixels: 0
|
|
67
137
|
};
|
|
68
138
|
}
|
|
69
139
|
let clustersInRegions = 0;
|
|
70
140
|
let matchedRegionIds = new Set();
|
|
141
|
+
let pixelsInRegions = 0;
|
|
142
|
+
let totalPixels = 0;
|
|
71
143
|
for (let cluster of diffClusters) {
|
|
72
|
-
|
|
73
|
-
|
|
144
|
+
let pixelCount = estimateClusterPixels(cluster);
|
|
145
|
+
totalPixels += pixelCount;
|
|
146
|
+
let matchedAnyRegion = false;
|
|
74
147
|
for (let region of regions) {
|
|
75
|
-
if (
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
if (
|
|
79
|
-
matchedRegionIds.add(
|
|
80
|
-
} else if (region.label) {
|
|
81
|
-
matchedRegionIds.add(region.label);
|
|
148
|
+
if (clusterMatchesRegion(cluster, region)) {
|
|
149
|
+
matchedAnyRegion = true;
|
|
150
|
+
let regionLabel = getRegionLabel(region);
|
|
151
|
+
if (regionLabel) {
|
|
152
|
+
matchedRegionIds.add(regionLabel);
|
|
82
153
|
}
|
|
83
154
|
}
|
|
84
155
|
}
|
|
85
|
-
if (
|
|
156
|
+
if (matchedAnyRegion) {
|
|
86
157
|
clustersInRegions++;
|
|
158
|
+
pixelsInRegions += pixelCount;
|
|
87
159
|
}
|
|
88
160
|
}
|
|
89
|
-
let coverage =
|
|
161
|
+
let coverage = totalPixels > 0 ? pixelsInRegions / totalPixels : 0;
|
|
90
162
|
return {
|
|
91
163
|
coverage,
|
|
92
164
|
clustersInRegions,
|
|
93
165
|
totalClusters: diffClusters.length,
|
|
94
|
-
matchedRegions: [...matchedRegionIds]
|
|
166
|
+
matchedRegions: [...matchedRegionIds],
|
|
167
|
+
pixelsInRegions,
|
|
168
|
+
totalPixels
|
|
95
169
|
};
|
|
96
170
|
}
|
|
97
171
|
|
|
@@ -99,17 +173,31 @@ export function calculateRegionCoverage(diffClusters, regions) {
|
|
|
99
173
|
* Determine if a comparison should auto-pass based on region coverage
|
|
100
174
|
*
|
|
101
175
|
* Unlike hotspots which require confidence scoring, user-defined regions
|
|
102
|
-
* are already confirmed by humans
|
|
176
|
+
* are already confirmed by humans. Cloud still requires strong structural
|
|
177
|
+
* similarity so a layout shift cannot hide inside a confirmed box.
|
|
103
178
|
*
|
|
104
179
|
* @param {Array} regions - Confirmed regions (already filtered to confirmed status)
|
|
105
180
|
* @param {{ coverage: number }} coverageResult - Result from calculateRegionCoverage
|
|
181
|
+
* @param {Object} options - Approval thresholds
|
|
182
|
+
* @param {number|null} options.ssimScore - Honeydiff SSIM/perceptual score
|
|
183
|
+
* @param {number} options.coverageThreshold - Required region coverage
|
|
184
|
+
* @param {number} options.ssimThreshold - Required structural similarity
|
|
106
185
|
* @returns {boolean} True if diff should auto-pass as region-filtered
|
|
107
186
|
*/
|
|
108
|
-
export function shouldAutoApproveFromRegions(regions, coverageResult) {
|
|
187
|
+
export function shouldAutoApproveFromRegions(regions, coverageResult, options = {}) {
|
|
109
188
|
if (!regions || regions.length === 0 || !coverageResult) {
|
|
110
189
|
return false;
|
|
111
190
|
}
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
191
|
+
let {
|
|
192
|
+
ssimScore = null,
|
|
193
|
+
coverageThreshold = 0.9,
|
|
194
|
+
ssimThreshold = 0.95
|
|
195
|
+
} = options;
|
|
196
|
+
if (coverageResult.coverage < coverageThreshold) {
|
|
197
|
+
return false;
|
|
198
|
+
}
|
|
199
|
+
if (ssimScore === null || ssimScore === undefined) {
|
|
200
|
+
return false;
|
|
201
|
+
}
|
|
202
|
+
return ssimScore >= ssimThreshold;
|
|
115
203
|
}
|
|
@@ -32,6 +32,9 @@ export async function compareImages(baselinePath, currentPath, diffPath, options
|
|
|
32
32
|
diffPath,
|
|
33
33
|
overwrite: true,
|
|
34
34
|
includeClusters: true,
|
|
35
|
+
includeSSIM: true,
|
|
36
|
+
includeGMSD: true,
|
|
37
|
+
clusterMerge: true,
|
|
35
38
|
minClusterSize
|
|
36
39
|
});
|
|
37
40
|
}
|
|
@@ -141,6 +144,7 @@ export function buildFailedComparison(params) {
|
|
|
141
144
|
regionData
|
|
142
145
|
} = params;
|
|
143
146
|
let diffClusters = honeydiffResult.diffClusters || [];
|
|
147
|
+
let ssimScore = honeydiffResult.perceptualScore ?? honeydiffResult.ssimScore ?? honeydiffResult.ssim_score ?? null;
|
|
144
148
|
let isFiltered = false;
|
|
145
149
|
let filterReason = 'pixel-diff';
|
|
146
150
|
|
|
@@ -150,7 +154,9 @@ export function buildFailedComparison(params) {
|
|
|
150
154
|
let confirmedRegions = regionData?.confirmed || [];
|
|
151
155
|
if (confirmedRegions.length > 0 && diffClusters.length > 0) {
|
|
152
156
|
regionCoverage = calculateRegionCoverage(diffClusters, confirmedRegions);
|
|
153
|
-
if (shouldAutoApproveFromRegions(confirmedRegions, regionCoverage
|
|
157
|
+
if (shouldAutoApproveFromRegions(confirmedRegions, regionCoverage, {
|
|
158
|
+
ssimScore
|
|
159
|
+
})) {
|
|
154
160
|
isRegionFiltered = true;
|
|
155
161
|
isFiltered = true;
|
|
156
162
|
filterReason = 'region-filtered';
|
|
@@ -198,8 +204,11 @@ export function buildFailedComparison(params) {
|
|
|
198
204
|
coverage: regionCoverage.coverage,
|
|
199
205
|
clustersInRegions: regionCoverage.clustersInRegions,
|
|
200
206
|
totalClusters: regionCoverage.totalClusters,
|
|
207
|
+
pixelsInRegions: regionCoverage.pixelsInRegions,
|
|
208
|
+
totalPixels: regionCoverage.totalPixels,
|
|
201
209
|
matchedRegions: regionCoverage.matchedRegions,
|
|
202
210
|
confirmedCount: confirmedRegions.length,
|
|
211
|
+
ssimScore,
|
|
203
212
|
isFiltered: isRegionFiltered
|
|
204
213
|
} : null,
|
|
205
214
|
// Include confirmed regions for visualization in UI
|
package/dist/tdd/tdd-service.js
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import { existsSync as defaultExistsSync, mkdirSync as defaultMkdirSync, readFileSync as defaultReadFileSync, writeFileSync as defaultWriteFileSync } from 'node:fs';
|
|
12
|
-
import { createApiClient as defaultCreateApiClient, getBatchHotspots as defaultGetBatchHotspots, getBuilds as defaultGetBuilds, getComparison as defaultGetComparison, getTddBaselines as defaultGetTddBaselines } from '../api/index.js';
|
|
12
|
+
import { createApiClient as defaultCreateApiClient, getBatchHotspots as defaultGetBatchHotspots, getBuilds as defaultGetBuilds, getComparison as defaultGetComparison, getComparisonContext as defaultGetComparisonContext, getTddBaselines as defaultGetTddBaselines } from '../api/index.js';
|
|
13
13
|
import { NetworkError } from '../errors/vizzly-error.js';
|
|
14
14
|
import { colors as defaultColors } from '../utils/colors.js';
|
|
15
15
|
import { fetchWithTimeout as defaultFetchWithTimeout } from '../utils/fetch-utils.js';
|
|
@@ -58,6 +58,7 @@ export class TddService {
|
|
|
58
58
|
getTddBaselines: defaultGetTddBaselines,
|
|
59
59
|
getBuilds: defaultGetBuilds,
|
|
60
60
|
getComparison: defaultGetComparison,
|
|
61
|
+
getComparisonContext: defaultGetComparisonContext,
|
|
61
62
|
getBatchHotspots: defaultGetBatchHotspots,
|
|
62
63
|
fetchWithTimeout: defaultFetchWithTimeout,
|
|
63
64
|
getDefaultBranch: defaultGetDefaultBranch
|
|
@@ -169,6 +170,7 @@ export class TddService {
|
|
|
169
170
|
getTddBaselines,
|
|
170
171
|
getBuilds,
|
|
171
172
|
getComparison,
|
|
173
|
+
getComparisonContext,
|
|
172
174
|
clearBaselineData,
|
|
173
175
|
generateScreenshotSignature,
|
|
174
176
|
generateBaselineFilename,
|
|
@@ -221,10 +223,19 @@ export class TddService {
|
|
|
221
223
|
output.warn(`Build ${buildId} has status: ${baselineBuild.status} (expected: completed)`);
|
|
222
224
|
}
|
|
223
225
|
baselineBuild.screenshots = apiResponse.screenshots;
|
|
226
|
+
this.saveDynamicMetadataFromBaselineResponse(apiResponse);
|
|
224
227
|
} else if (comparisonId) {
|
|
225
228
|
// Handle specific comparison download
|
|
226
229
|
output.info(`Using comparison: ${comparisonId}`);
|
|
227
230
|
let comparison = await getComparison(this.client, comparisonId);
|
|
231
|
+
let comparisonContext = null;
|
|
232
|
+
if (!comparison.dynamic_content && getComparisonContext) {
|
|
233
|
+
try {
|
|
234
|
+
comparisonContext = await getComparisonContext(this.client, comparisonId);
|
|
235
|
+
} catch (error) {
|
|
236
|
+
output.debug('tdd', `comparison context unavailable for ${comparisonId}: ${error.message}`);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
228
239
|
if (!comparison.baseline_screenshot) {
|
|
229
240
|
throw new Error(`Comparison ${comparisonId} has no baseline screenshot. This comparison may be a "new" screenshot.`);
|
|
230
241
|
}
|
|
@@ -255,6 +266,11 @@ export class TddService {
|
|
|
255
266
|
}
|
|
256
267
|
}
|
|
257
268
|
let screenshotName = comparison.baseline_name || comparison.current_name;
|
|
269
|
+
let dynamicContent = comparison.dynamic_content || {
|
|
270
|
+
hotspot_analysis: comparisonContext?.history?.hotspot_analysis,
|
|
271
|
+
confirmed_regions: comparisonContext?.history?.confirmed_regions
|
|
272
|
+
};
|
|
273
|
+
this.saveDynamicMetadataForScreenshot(screenshotName, dynamicContent);
|
|
258
274
|
let signature = generateScreenshotSignature(screenshotName, screenshotProperties, this.signatureProperties);
|
|
259
275
|
let filename = generateBaselineFilename(screenshotName, signature);
|
|
260
276
|
baselineBuild = {
|
|
@@ -294,24 +310,15 @@ export class TddService {
|
|
|
294
310
|
}
|
|
295
311
|
baselineBuild = apiResponse.build;
|
|
296
312
|
baselineBuild.screenshots = apiResponse.screenshots;
|
|
297
|
-
|
|
298
|
-
// Store bundled hotspots and regions from API response
|
|
299
|
-
if (apiResponse.hotspots && Object.keys(apiResponse.hotspots).length > 0) {
|
|
300
|
-
this.hotspotData = apiResponse.hotspots;
|
|
301
|
-
saveHotspotMetadata(this.workingDir, apiResponse.hotspots, apiResponse.summary);
|
|
302
|
-
}
|
|
303
|
-
if (apiResponse.regions && Object.keys(apiResponse.regions).length > 0) {
|
|
304
|
-
this.regionData = apiResponse.regions;
|
|
305
|
-
saveRegionMetadata(this.workingDir, apiResponse.regions, apiResponse.summary);
|
|
306
|
-
}
|
|
313
|
+
this.saveDynamicMetadataFromBaselineResponse(apiResponse);
|
|
307
314
|
}
|
|
308
315
|
let buildDetails = baselineBuild;
|
|
309
316
|
if (!buildDetails.screenshots || buildDetails.screenshots.length === 0) {
|
|
310
317
|
output.warn('No screenshots found in baseline build');
|
|
311
318
|
return null;
|
|
312
319
|
}
|
|
313
|
-
output.info(`Using baseline from build: ${colors.
|
|
314
|
-
output.info(`Checking ${colors.
|
|
320
|
+
output.info(`Using baseline from build: ${colors.info(baselineBuild.name || 'Unknown')} (${baselineBuild.id || 'Unknown ID'})`);
|
|
321
|
+
output.info(`Checking ${colors.info(buildDetails.screenshots.length)} baseline screenshots...`);
|
|
315
322
|
|
|
316
323
|
// Check existing baseline metadata for SHA comparison
|
|
317
324
|
let existingBaseline = await this.loadBaseline();
|
|
@@ -488,6 +495,49 @@ export class TddService {
|
|
|
488
495
|
throw error;
|
|
489
496
|
}
|
|
490
497
|
}
|
|
498
|
+
saveDynamicMetadataFromBaselineResponse(apiResponse = {}) {
|
|
499
|
+
let {
|
|
500
|
+
saveHotspotMetadata,
|
|
501
|
+
saveRegionMetadata
|
|
502
|
+
} = this._deps;
|
|
503
|
+
let hasHotspots = Object.keys(apiResponse.hotspots || {}).length > 0;
|
|
504
|
+
let hasRegions = Object.keys(apiResponse.regions || {}).length > 0;
|
|
505
|
+
if (hasHotspots) {
|
|
506
|
+
this.hotspotData = apiResponse.hotspots;
|
|
507
|
+
saveHotspotMetadata(this.workingDir, apiResponse.hotspots, apiResponse.summary);
|
|
508
|
+
}
|
|
509
|
+
if (hasRegions) {
|
|
510
|
+
this.regionData = apiResponse.regions;
|
|
511
|
+
saveRegionMetadata(this.workingDir, apiResponse.regions, apiResponse.summary);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
saveDynamicMetadataForScreenshot(screenshotName, dynamicContent = {}) {
|
|
515
|
+
if (!screenshotName) {
|
|
516
|
+
return;
|
|
517
|
+
}
|
|
518
|
+
let hotspotAnalysis = dynamicContent.hotspot_analysis;
|
|
519
|
+
let confirmedRegions = dynamicContent.confirmed_regions || [];
|
|
520
|
+
let hasHotspotAnalysis = hotspotAnalysis?.total_builds_analyzed > 0;
|
|
521
|
+
let hasConfirmedRegions = confirmedRegions.length > 0;
|
|
522
|
+
let summary = {
|
|
523
|
+
screenshots: 1,
|
|
524
|
+
hotspots: hasHotspotAnalysis ? 1 : 0,
|
|
525
|
+
regions: hasConfirmedRegions ? 1 : 0,
|
|
526
|
+
total_regions: confirmedRegions.length
|
|
527
|
+
};
|
|
528
|
+
this.saveDynamicMetadataFromBaselineResponse({
|
|
529
|
+
hotspots: hasHotspotAnalysis ? {
|
|
530
|
+
[screenshotName]: hotspotAnalysis
|
|
531
|
+
} : {},
|
|
532
|
+
regions: hasConfirmedRegions ? {
|
|
533
|
+
[screenshotName]: {
|
|
534
|
+
confirmed: confirmedRegions,
|
|
535
|
+
candidates: []
|
|
536
|
+
}
|
|
537
|
+
} : {},
|
|
538
|
+
summary
|
|
539
|
+
});
|
|
540
|
+
}
|
|
491
541
|
|
|
492
542
|
/**
|
|
493
543
|
* Process already-fetched baseline data (for use when caller handles auth)
|
|
@@ -526,20 +576,7 @@ export class TddService {
|
|
|
526
576
|
}
|
|
527
577
|
}
|
|
528
578
|
let baselineBuild = apiResponse.build;
|
|
529
|
-
|
|
530
|
-
// Store bundled hotspots and regions from API response
|
|
531
|
-
let {
|
|
532
|
-
saveHotspotMetadata,
|
|
533
|
-
saveRegionMetadata
|
|
534
|
-
} = this._deps;
|
|
535
|
-
if (apiResponse.hotspots && Object.keys(apiResponse.hotspots).length > 0) {
|
|
536
|
-
this.hotspotData = apiResponse.hotspots;
|
|
537
|
-
saveHotspotMetadata(this.workingDir, apiResponse.hotspots, apiResponse.summary);
|
|
538
|
-
}
|
|
539
|
-
if (apiResponse.regions && Object.keys(apiResponse.regions).length > 0) {
|
|
540
|
-
this.regionData = apiResponse.regions;
|
|
541
|
-
saveRegionMetadata(this.workingDir, apiResponse.regions, apiResponse.summary);
|
|
542
|
-
}
|
|
579
|
+
this.saveDynamicMetadataFromBaselineResponse(apiResponse);
|
|
543
580
|
if (baselineBuild.status === 'failed') {
|
|
544
581
|
output.warn(`Build ${buildId} is marked as FAILED - falling back to local baselines`);
|
|
545
582
|
return await this.handleLocalBaselines();
|
|
@@ -552,8 +589,8 @@ export class TddService {
|
|
|
552
589
|
output.warn('No screenshots found in baseline build');
|
|
553
590
|
return null;
|
|
554
591
|
}
|
|
555
|
-
output.info(`Using baseline from build: ${colors.
|
|
556
|
-
output.info(`Checking ${colors.
|
|
592
|
+
output.info(`Using baseline from build: ${colors.info(baselineBuild.name || 'Unknown')} (${baselineBuild.id || 'Unknown ID'})`);
|
|
593
|
+
output.info(`Checking ${colors.info(buildDetails.screenshots.length)} baseline screenshots...`);
|
|
557
594
|
|
|
558
595
|
// Check existing baseline metadata for SHA comparison
|
|
559
596
|
let existingBaseline = await this.loadBaseline();
|
|
@@ -862,7 +899,7 @@ export class TddService {
|
|
|
862
899
|
}
|
|
863
900
|
return null;
|
|
864
901
|
} else {
|
|
865
|
-
output.info(`Using existing baseline: ${colors.
|
|
902
|
+
output.info(`Using existing baseline: ${colors.info(baseline.buildName)}`);
|
|
866
903
|
return baseline;
|
|
867
904
|
}
|
|
868
905
|
}
|
|
@@ -1132,9 +1169,9 @@ export class TddService {
|
|
|
1132
1169
|
output.print(`🐻 ${colors.bold(results.total)} screenshot${results.total !== 1 ? 's' : ''} compared`);
|
|
1133
1170
|
output.blank();
|
|
1134
1171
|
|
|
1135
|
-
// Passed section - use
|
|
1172
|
+
// Passed section - use BearDen success color
|
|
1136
1173
|
if (results.passed > 0) {
|
|
1137
|
-
let successColor = colors.brand?.success || colors.
|
|
1174
|
+
let successColor = colors.brand?.success || colors.success;
|
|
1138
1175
|
if (output.isVerbose()) {
|
|
1139
1176
|
// Verbose mode: show each screenshot
|
|
1140
1177
|
for (let comp of passedComparisons) {
|
|
@@ -1147,10 +1184,10 @@ export class TddService {
|
|
|
1147
1184
|
output.blank();
|
|
1148
1185
|
}
|
|
1149
1186
|
|
|
1150
|
-
// Failed comparisons with diff bars - use
|
|
1187
|
+
// Failed comparisons with diff bars - use BearDen warning/danger colors
|
|
1151
1188
|
if (failedComparisons.length > 0) {
|
|
1152
1189
|
let warningColor = colors.brand?.warning || colors.yellow;
|
|
1153
|
-
let dangerColor = colors.brand?.danger || colors.
|
|
1190
|
+
let dangerColor = colors.brand?.danger || colors.error;
|
|
1154
1191
|
output.print(` ${warningColor('◐')} ${warningColor(failedComparisons.length)} visual change${failedComparisons.length !== 1 ? 's' : ''} detected`);
|
|
1155
1192
|
|
|
1156
1193
|
// Find longest name for alignment
|
|
@@ -1171,9 +1208,9 @@ export class TddService {
|
|
|
1171
1208
|
output.blank();
|
|
1172
1209
|
}
|
|
1173
1210
|
|
|
1174
|
-
// New screenshots - use
|
|
1211
|
+
// New screenshots - use BearDen info color
|
|
1175
1212
|
if (newComparisons.length > 0) {
|
|
1176
|
-
let infoColor = colors.brand?.info || colors.
|
|
1213
|
+
let infoColor = colors.brand?.info || colors.info;
|
|
1177
1214
|
let textMuted = colors.brand?.textMuted || colors.dim;
|
|
1178
1215
|
output.print(` ${infoColor('+')} ${infoColor(newComparisons.length)} new screenshot${newComparisons.length !== 1 ? 's' : ''}`);
|
|
1179
1216
|
for (let comp of newComparisons) {
|
|
@@ -1182,9 +1219,9 @@ export class TddService {
|
|
|
1182
1219
|
output.blank();
|
|
1183
1220
|
}
|
|
1184
1221
|
|
|
1185
|
-
// Errors - use
|
|
1222
|
+
// Errors - use BearDen danger color
|
|
1186
1223
|
if (results.errors > 0) {
|
|
1187
|
-
let dangerColor = colors.brand?.danger || colors.
|
|
1224
|
+
let dangerColor = colors.brand?.danger || colors.error;
|
|
1188
1225
|
let errorComparisons = this.comparisons.filter(c => c.status === 'error');
|
|
1189
1226
|
output.print(` ${dangerColor('!')} ${dangerColor(results.errors)} error${results.errors !== 1 ? 's' : ''}`);
|
|
1190
1227
|
for (let comp of errorComparisons) {
|
|
@@ -1195,7 +1232,7 @@ export class TddService {
|
|
|
1195
1232
|
|
|
1196
1233
|
// Dashboard link with prominent styling - detect if server is running
|
|
1197
1234
|
if (hasChanges) {
|
|
1198
|
-
let infoColor = colors.brand?.info || colors.
|
|
1235
|
+
let infoColor = colors.brand?.info || colors.info;
|
|
1199
1236
|
let textTertiary = colors.brand?.textTertiary || colors.dim;
|
|
1200
1237
|
|
|
1201
1238
|
// Check if TDD server is already running
|
package/dist/utils/colors.js
CHANGED
|
@@ -4,56 +4,47 @@
|
|
|
4
4
|
import { Ansis } from 'ansis';
|
|
5
5
|
|
|
6
6
|
// =============================================================================
|
|
7
|
-
// Vizzly
|
|
8
|
-
// Aligned with
|
|
7
|
+
// Vizzly BearDen Design System Colors
|
|
8
|
+
// Aligned with the reporter design system color tokens.
|
|
9
9
|
// =============================================================================
|
|
10
10
|
|
|
11
11
|
export let brand = {
|
|
12
|
-
// Primary brand color -
|
|
13
|
-
amber: '#
|
|
12
|
+
// Primary brand color - restrained brass on graphite
|
|
13
|
+
amber: '#D69A35',
|
|
14
14
|
// Primary brand, actions, highlights
|
|
15
|
-
amberLight: '#
|
|
15
|
+
amberLight: '#E0AD55',
|
|
16
16
|
// Hover states, emphasis
|
|
17
17
|
|
|
18
18
|
// Accent colors (semantic)
|
|
19
|
-
success: '#
|
|
19
|
+
success: '#7FD990',
|
|
20
20
|
// Approved, passed, active (--accent-success)
|
|
21
|
-
warning: '#
|
|
21
|
+
warning: '#DCAD5F',
|
|
22
22
|
// Pending, attention (--accent-warning)
|
|
23
|
-
danger: '#
|
|
23
|
+
danger: '#D77782',
|
|
24
24
|
// Rejected, failed, errors (--accent-danger)
|
|
25
|
-
info: '#
|
|
25
|
+
info: '#B7BDC6',
|
|
26
26
|
// Processing, informational (--accent-info)
|
|
27
27
|
|
|
28
28
|
// Surface colors (dark theme)
|
|
29
|
-
bg: '#
|
|
29
|
+
bg: '#07080A',
|
|
30
30
|
// Page background (--vz-bg)
|
|
31
|
-
surface: '#
|
|
31
|
+
surface: '#101216',
|
|
32
32
|
// Cards, panels (--vz-surface)
|
|
33
|
-
elevated: '#
|
|
33
|
+
elevated: '#111318',
|
|
34
34
|
// Dropdowns, modals (--vz-elevated)
|
|
35
|
-
border: '#
|
|
35
|
+
border: '#30343B',
|
|
36
36
|
// Primary borders (--vz-border)
|
|
37
|
-
borderSubtle: '#
|
|
37
|
+
borderSubtle: '#252930',
|
|
38
38
|
// Subtle dividers (--vz-border-subtle)
|
|
39
39
|
|
|
40
40
|
// Text hierarchy
|
|
41
|
-
textPrimary: '#
|
|
41
|
+
textPrimary: '#F4F6F8',
|
|
42
42
|
// Headings, important (--text-primary)
|
|
43
|
-
textSecondary: '#
|
|
43
|
+
textSecondary: '#C8CDD3',
|
|
44
44
|
// Body text (--text-secondary)
|
|
45
|
-
textTertiary: '#
|
|
45
|
+
textTertiary: '#A0A6AF',
|
|
46
46
|
// Captions, metadata (--text-tertiary)
|
|
47
|
-
textMuted: '#
|
|
48
|
-
// Disabled, placeholders (--text-muted)
|
|
49
|
-
|
|
50
|
-
// Legacy aliases (for backward compatibility)
|
|
51
|
-
green: '#10B981',
|
|
52
|
-
red: '#EF4444',
|
|
53
|
-
cyan: '#06B6D4',
|
|
54
|
-
// Still useful for links in terminals
|
|
55
|
-
slate: '#64748B',
|
|
56
|
-
dark: '#1E293B'
|
|
47
|
+
textMuted: '#7A828D' // Disabled, placeholders (--text-muted)
|
|
57
48
|
};
|
|
58
49
|
function supportsColorDefault() {
|
|
59
50
|
// Respect NO_COLOR: https://no-color.org/
|
|
@@ -124,7 +115,7 @@ export function createColors(options = {}) {
|
|
|
124
115
|
hex: () => noop,
|
|
125
116
|
bgRgb: () => noop,
|
|
126
117
|
bgHex: () => noop,
|
|
127
|
-
//
|
|
118
|
+
// BearDen brand colors (noop versions)
|
|
128
119
|
brand: {
|
|
129
120
|
// Primary
|
|
130
121
|
amber: noop,
|
|
@@ -133,6 +124,7 @@ export function createColors(options = {}) {
|
|
|
133
124
|
success: noop,
|
|
134
125
|
warning: noop,
|
|
135
126
|
danger: noop,
|
|
127
|
+
error: noop,
|
|
136
128
|
info: noop,
|
|
137
129
|
// Text hierarchy
|
|
138
130
|
textPrimary: noop,
|
|
@@ -144,12 +136,7 @@ export function createColors(options = {}) {
|
|
|
144
136
|
bgSuccess: noop,
|
|
145
137
|
bgWarning: noop,
|
|
146
138
|
bgDanger: noop,
|
|
147
|
-
bgInfo: noop
|
|
148
|
-
// Legacy aliases
|
|
149
|
-
green: noop,
|
|
150
|
-
red: noop,
|
|
151
|
-
cyan: noop,
|
|
152
|
-
slate: noop
|
|
139
|
+
bgInfo: noop
|
|
153
140
|
}
|
|
154
141
|
};
|
|
155
142
|
}
|
|
@@ -173,16 +160,16 @@ export function createColors(options = {}) {
|
|
|
173
160
|
gray: colors.gray,
|
|
174
161
|
black: colors.black,
|
|
175
162
|
// Semantic aliases (basic)
|
|
176
|
-
success: colors.
|
|
177
|
-
error: colors.
|
|
178
|
-
warning: colors.
|
|
179
|
-
info: colors.
|
|
163
|
+
success: colors.hex(brand.success),
|
|
164
|
+
error: colors.hex(brand.danger),
|
|
165
|
+
warning: colors.hex(brand.warning),
|
|
166
|
+
info: colors.hex(brand.info),
|
|
180
167
|
// Extended colors for rich styling
|
|
181
168
|
rgb: colors.rgb.bind(colors),
|
|
182
169
|
hex: colors.hex.bind(colors),
|
|
183
170
|
bgRgb: colors.bgRgb.bind(colors),
|
|
184
171
|
bgHex: colors.bgHex.bind(colors),
|
|
185
|
-
//
|
|
172
|
+
// BearDen brand colors (Truecolor) - aligned with design system
|
|
186
173
|
brand: {
|
|
187
174
|
// Primary brand color
|
|
188
175
|
amber: colors.hex(brand.amber),
|
|
@@ -191,6 +178,7 @@ export function createColors(options = {}) {
|
|
|
191
178
|
success: colors.hex(brand.success),
|
|
192
179
|
warning: colors.hex(brand.warning),
|
|
193
180
|
danger: colors.hex(brand.danger),
|
|
181
|
+
error: colors.hex(brand.danger),
|
|
194
182
|
info: colors.hex(brand.info),
|
|
195
183
|
// Text hierarchy
|
|
196
184
|
textPrimary: colors.hex(brand.textPrimary),
|
|
@@ -202,12 +190,7 @@ export function createColors(options = {}) {
|
|
|
202
190
|
bgSuccess: colors.bgHex(brand.success),
|
|
203
191
|
bgWarning: colors.bgHex(brand.warning),
|
|
204
192
|
bgDanger: colors.bgHex(brand.danger),
|
|
205
|
-
bgInfo: colors.bgHex(brand.info)
|
|
206
|
-
// Legacy aliases (backward compatibility)
|
|
207
|
-
green: colors.hex(brand.green),
|
|
208
|
-
red: colors.hex(brand.red),
|
|
209
|
-
cyan: colors.hex(brand.cyan),
|
|
210
|
-
slate: colors.hex(brand.slate)
|
|
193
|
+
bgInfo: colors.bgHex(brand.info)
|
|
211
194
|
}
|
|
212
195
|
};
|
|
213
196
|
}
|
|
@@ -121,7 +121,7 @@ export async function clearGlobalConfig() {
|
|
|
121
121
|
|
|
122
122
|
/**
|
|
123
123
|
* Save user's PATH for menubar app to use
|
|
124
|
-
* This auto-configures the menubar app so it can find
|
|
124
|
+
* This auto-configures the menubar app so it can find package runners/node
|
|
125
125
|
* @returns {Promise<void>}
|
|
126
126
|
*/
|
|
127
127
|
export async function saveUserPath() {
|