@vizzly-testing/cli 0.35.3-beta.2 → 0.35.3-beta.3

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.
@@ -1,203 +0,0 @@
1
- /**
2
- * Region Coverage Calculation
3
- *
4
- * Pure functions for calculating how much of a visual diff falls within
5
- * user-defined "region" areas - 2D bounding boxes that users have confirmed
6
- * as dynamic content areas (e.g., timestamps, animations, user avatars).
7
- *
8
- * Unlike hotspots (1D Y-bands from historical analysis), regions are explicit
9
- * 2D boxes that users have manually confirmed via the cloud UI.
10
- */
11
-
12
- let REGION_CENTER_TOLERANCE = 10;
13
-
14
- /**
15
- * Check if a diff cluster intersects with a region (2D box intersection)
16
- *
17
- * @param {Object} cluster - Diff cluster with boundingBox { x, y, width, height }
18
- * @param {Object} region - Region with { x1, y1, x2, y2 }
19
- * @returns {boolean} True if the cluster overlaps the region
20
- */
21
- export function clusterIntersectsRegion(cluster, region) {
22
- if (!cluster?.boundingBox || !region) {
23
- return false;
24
- }
25
- let {
26
- x,
27
- y,
28
- width,
29
- height
30
- } = cluster.boundingBox;
31
-
32
- // Convert cluster to x1,y1,x2,y2 format
33
- let clusterX1 = x;
34
- let clusterY1 = y;
35
- let clusterX2 = x + width;
36
- let clusterY2 = y + height;
37
-
38
- // Box intersection: NOT (one is completely outside the other)
39
- // A is left of B: clusterX2 < region.x1
40
- // A is right of B: clusterX1 > region.x2
41
- // A is above B: clusterY2 < region.y1
42
- // A is below B: clusterY1 > region.y2
43
- let noOverlap = clusterX2 < region.x1 || clusterX1 > region.x2 || clusterY2 < region.y1 || clusterY1 > region.y2;
44
- return !noOverlap;
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
- }
105
-
106
- /**
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.
113
- *
114
- * @param {Array} diffClusters - Array of diff clusters from honeydiff
115
- * @param {Array} regions - Array of confirmed regions { x1, y1, x2, y2 }
116
- * @returns {{ coverage: number, clustersInRegions: number, totalClusters: number, matchedRegions: string[], pixelsInRegions: number, totalPixels: number }}
117
- */
118
- export function calculateRegionCoverage(diffClusters, regions) {
119
- if (!diffClusters || diffClusters.length === 0) {
120
- return {
121
- coverage: 0,
122
- clustersInRegions: 0,
123
- totalClusters: 0,
124
- matchedRegions: [],
125
- pixelsInRegions: 0,
126
- totalPixels: 0
127
- };
128
- }
129
- if (!regions || regions.length === 0) {
130
- return {
131
- coverage: 0,
132
- clustersInRegions: 0,
133
- totalClusters: diffClusters.length,
134
- matchedRegions: [],
135
- pixelsInRegions: 0,
136
- totalPixels: 0
137
- };
138
- }
139
- let clustersInRegions = 0;
140
- let matchedRegionIds = new Set();
141
- let pixelsInRegions = 0;
142
- let totalPixels = 0;
143
- for (let cluster of diffClusters) {
144
- let pixelCount = estimateClusterPixels(cluster);
145
- totalPixels += pixelCount;
146
- let matchedAnyRegion = false;
147
- for (let region of regions) {
148
- if (clusterMatchesRegion(cluster, region)) {
149
- matchedAnyRegion = true;
150
- let regionLabel = getRegionLabel(region);
151
- if (regionLabel) {
152
- matchedRegionIds.add(regionLabel);
153
- }
154
- }
155
- }
156
- if (matchedAnyRegion) {
157
- clustersInRegions++;
158
- pixelsInRegions += pixelCount;
159
- }
160
- }
161
- let coverage = totalPixels > 0 ? pixelsInRegions / totalPixels : 0;
162
- return {
163
- coverage,
164
- clustersInRegions,
165
- totalClusters: diffClusters.length,
166
- matchedRegions: [...matchedRegionIds],
167
- pixelsInRegions,
168
- totalPixels
169
- };
170
- }
171
-
172
- /**
173
- * Determine if a comparison should auto-pass based on region coverage
174
- *
175
- * Unlike hotspots which require confidence scoring, user-defined regions
176
- * are already confirmed by humans. Cloud still requires strong structural
177
- * similarity so a layout shift cannot hide inside a confirmed box.
178
- *
179
- * @param {Array} regions - Confirmed regions (already filtered to confirmed status)
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
185
- * @returns {boolean} True if diff should auto-pass as region-filtered
186
- */
187
- export function shouldAutoApproveFromRegions(regions, coverageResult, options = {}) {
188
- if (!regions || regions.length === 0 || !coverageResult) {
189
- return false;
190
- }
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;
203
- }
@@ -1,93 +0,0 @@
1
- /**
2
- * Hotspot Metadata I/O
3
- *
4
- * Functions for reading and writing hotspot data files.
5
- * Hotspots identify regions of screenshots that frequently change
6
- * due to dynamic content (timestamps, animations, etc.).
7
- */
8
-
9
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
10
- import { join } from 'node:path';
11
-
12
- /**
13
- * Load hotspot data from disk
14
- *
15
- * @param {string} workingDir - Working directory containing .vizzly folder
16
- * @returns {Object|null} Hotspot data keyed by screenshot name, or null if not found
17
- */
18
- export function loadHotspotMetadata(workingDir) {
19
- let hotspotsPath = join(workingDir, '.vizzly', 'hotspots.json');
20
- if (!existsSync(hotspotsPath)) {
21
- return null;
22
- }
23
- try {
24
- let content = readFileSync(hotspotsPath, 'utf8');
25
- let data = JSON.parse(content);
26
- return data.hotspots || null;
27
- } catch {
28
- // Return null for parse/read errors
29
- return null;
30
- }
31
- }
32
-
33
- /**
34
- * Save hotspot data to disk
35
- *
36
- * @param {string} workingDir - Working directory containing .vizzly folder
37
- * @param {Object} hotspotData - Hotspot data keyed by screenshot name
38
- * @param {Object} summary - Summary information about the hotspots
39
- */
40
- export function saveHotspotMetadata(workingDir, hotspotData, summary = {}) {
41
- let vizzlyDir = join(workingDir, '.vizzly');
42
-
43
- // Ensure directory exists
44
- if (!existsSync(vizzlyDir)) {
45
- mkdirSync(vizzlyDir, {
46
- recursive: true
47
- });
48
- }
49
- let hotspotsPath = join(vizzlyDir, 'hotspots.json');
50
- let content = {
51
- downloadedAt: new Date().toISOString(),
52
- summary,
53
- hotspots: hotspotData
54
- };
55
- writeFileSync(hotspotsPath, JSON.stringify(content, null, 2));
56
- }
57
-
58
- /**
59
- * Get hotspot for a specific screenshot with caching support
60
- *
61
- * This is a pure function that takes a cache object as parameter
62
- * for stateless operation. The cache is mutated if data needs to be loaded.
63
- *
64
- * @param {Object} cache - Cache object { data: Object|null, loaded: boolean }
65
- * @param {string} workingDir - Working directory
66
- * @param {string} screenshotName - Name of the screenshot
67
- * @returns {Object|null} Hotspot analysis or null if not available
68
- */
69
- export function getHotspotForScreenshot(cache, workingDir, screenshotName) {
70
- // Check cache first
71
- if (cache.data?.[screenshotName]) {
72
- return cache.data[screenshotName];
73
- }
74
-
75
- // Load from disk if not yet loaded
76
- if (!cache.loaded) {
77
- cache.data = loadHotspotMetadata(workingDir);
78
- cache.loaded = true;
79
- }
80
- return cache.data?.[screenshotName] || null;
81
- }
82
-
83
- /**
84
- * Create an empty hotspot cache object
85
- *
86
- * @returns {{ data: null, loaded: boolean }}
87
- */
88
- export function createHotspotCache() {
89
- return {
90
- data: null,
91
- loaded: false
92
- };
93
- }
@@ -1,56 +0,0 @@
1
- /**
2
- * Region Metadata I/O
3
- *
4
- * Functions for reading and writing user-defined hotspot region data.
5
- * Regions are 2D bounding boxes that users have confirmed as dynamic content areas.
6
- * Unlike historical hotspots (1D Y-bands), these are explicit definitions.
7
- */
8
-
9
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
10
- import { join } from 'node:path';
11
-
12
- /**
13
- * Load region data from disk
14
- *
15
- * @param {string} workingDir - Working directory containing .vizzly folder
16
- * @returns {Object|null} Region data keyed by screenshot name, or null if not found
17
- */
18
- export function loadRegionMetadata(workingDir) {
19
- let regionsPath = join(workingDir, '.vizzly', 'regions.json');
20
- if (!existsSync(regionsPath)) {
21
- return null;
22
- }
23
- try {
24
- let content = readFileSync(regionsPath, 'utf8');
25
- let data = JSON.parse(content);
26
- return data.regions || null;
27
- } catch {
28
- // Return null for parse/read errors
29
- return null;
30
- }
31
- }
32
-
33
- /**
34
- * Save region data to disk
35
- *
36
- * @param {string} workingDir - Working directory containing .vizzly folder
37
- * @param {Object} regionData - Region data keyed by screenshot name
38
- * @param {Object} summary - Summary information about the regions
39
- */
40
- export function saveRegionMetadata(workingDir, regionData, summary = {}) {
41
- let vizzlyDir = join(workingDir, '.vizzly');
42
-
43
- // Ensure directory exists
44
- if (!existsSync(vizzlyDir)) {
45
- mkdirSync(vizzlyDir, {
46
- recursive: true
47
- });
48
- }
49
- let regionsPath = join(vizzlyDir, 'regions.json');
50
- let content = {
51
- downloadedAt: new Date().toISOString(),
52
- summary,
53
- regions: regionData
54
- };
55
- writeFileSync(regionsPath, JSON.stringify(content, null, 2));
56
- }