@applitools/eyes-results 0.2.2 → 0.2.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/CHANGELOG.md +14 -0
- package/dist/api/url-parsing.js +1 -2
- package/dist/geometry/geometry.js +37 -4
- package/dist/sessions/dom.js +19 -16
- package/dist/sessions/match.js +29 -6
- package/package.json +5 -4
- package/types/geometry/geometry.d.ts +18 -1
- package/types/sessions/dom.d.ts +3 -7
- package/types/sessions/match.d.ts +14 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.2.4](https://github.com/Applitools-Dev/sdk/compare/js/eyes-results@0.2.3...js/eyes-results@0.2.4) (2026-09-14)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
* lower the eyes-results node floor to 18 ([#4188](https://github.com/Applitools-Dev/sdk/issues/4188)) ([cbd832d](https://github.com/Applitools-Dev/sdk/commit/cbd832db46366d33c4686bf224634d8b17f85845))
|
|
9
|
+
|
|
10
|
+
## [0.2.3](https://github.com/Applitools-Dev/sdk/compare/js/eyes-results@0.2.2...js/eyes-results@0.2.3) (2026-09-10)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Bug Fixes
|
|
14
|
+
|
|
15
|
+
* replace overlapping match regions and refuse no-op or dynamic masks | AD-15903 ([#4163](https://github.com/Applitools-Dev/sdk/issues/4163)) ([78bdcf1](https://github.com/Applitools-Dev/sdk/commit/78bdcf10aa321252fcd613a685e9a8749a8215ce))
|
|
16
|
+
|
|
3
17
|
## [0.2.2](https://github.com/Applitools-Dev/sdk/compare/js/eyes-results@0.2.1...js/eyes-results@0.2.2) (2026-08-25)
|
|
4
18
|
|
|
5
19
|
|
package/dist/api/url-parsing.js
CHANGED
|
@@ -49,8 +49,7 @@ export function parseApplitoolsUrl(url) {
|
|
|
49
49
|
export function parseSessionUrl(url) {
|
|
50
50
|
const parsed = parseApplitoolsUrl(url);
|
|
51
51
|
if (!parsed.sessionId) {
|
|
52
|
-
throw new Error('The input URL is a batch URL while a session URL is expected.
|
|
53
|
-
'Use the eyes_fetch_visual_results MCP tool to obtain URLs of sessions in this batch.');
|
|
52
|
+
throw new Error('The input URL is a batch URL while a session URL is expected.');
|
|
54
53
|
}
|
|
55
54
|
return parsed;
|
|
56
55
|
}
|
|
@@ -45,7 +45,42 @@ export function padRect(rect, padding, imageWidth = Infinity, imageHeight = Infi
|
|
|
45
45
|
const bottom = Math.min(imageHeight, rect.top + rect.height + padding);
|
|
46
46
|
return { left, top, width: right - left, height: bottom - top };
|
|
47
47
|
}
|
|
48
|
-
/**
|
|
48
|
+
/** A region under this covers a diff artefact, not content, and as a search source it matches almost everywhere. */
|
|
49
|
+
export const MIN_REGION_AREA_PX = 16;
|
|
50
|
+
export function isTooSmallRegion(rect) {
|
|
51
|
+
return rect.width * rect.height < MIN_REGION_AREA_PX;
|
|
52
|
+
}
|
|
53
|
+
export function sameRect(a, b) {
|
|
54
|
+
return a.left === b.left && a.top === b.top && a.width === b.width && a.height === b.height;
|
|
55
|
+
}
|
|
56
|
+
/** Intersection area as a fraction of the smaller rect's area — IoU would punish a thin rect for a few px of drift. */
|
|
57
|
+
export function overlapCoverage(a, b) {
|
|
58
|
+
const smaller = Math.min(a.width * a.height, b.width * b.height);
|
|
59
|
+
// A rect with no area covers nothing, and would divide by zero.
|
|
60
|
+
if (smaller === 0)
|
|
61
|
+
return 0;
|
|
62
|
+
return intersectionArea(a, b) / smaller;
|
|
63
|
+
}
|
|
64
|
+
/** Below this, two rects merely graze each other at the edges and are treated as unrelated. */
|
|
65
|
+
export const SIGNIFICANT_OVERLAP_MIN_FRACTION = 0.1;
|
|
66
|
+
/** Below this on either axis the two rects are different shapes, however much they overlap. */
|
|
67
|
+
export const SIMILAR_SIZE_MIN_RATIO = 0.85;
|
|
68
|
+
export const SIMILAR_COVERAGE_MIN_FRACTION = 0.7;
|
|
69
|
+
export function isSignificantOverlap(a, b) {
|
|
70
|
+
return rectsIntersect(a, b) && overlapCoverage(a, b) > SIGNIFICANT_OVERLAP_MIN_FRACTION;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Similar size on both axes and mostly overlapping: one rect is the other after a drift of a few px.
|
|
74
|
+
* A rect that moved a whole row away shares no area with where it was, so it is not nearly the same.
|
|
75
|
+
*/
|
|
76
|
+
export function isNearlySameRect(a, b) {
|
|
77
|
+
if (!isSignificantOverlap(a, b))
|
|
78
|
+
return false;
|
|
79
|
+
const ratio = (x, y) => Math.min(x, y) / Math.max(x, y);
|
|
80
|
+
const sizeRatio = Math.min(ratio(a.width, b.width), ratio(a.height, b.height));
|
|
81
|
+
return sizeRatio >= SIMILAR_SIZE_MIN_RATIO && overlapCoverage(a, b) >= SIMILAR_COVERAGE_MIN_FRACTION;
|
|
82
|
+
}
|
|
83
|
+
/** Merges pairs of rects that overlap by more than `SIGNIFICANT_OVERLAP_MIN_FRACTION` of the smaller rect's area. */
|
|
49
84
|
export function unifyRects(rects) {
|
|
50
85
|
let result = [...rects];
|
|
51
86
|
let merged = true;
|
|
@@ -53,9 +88,7 @@ export function unifyRects(rects) {
|
|
|
53
88
|
merged = false;
|
|
54
89
|
outer: for (let i = 0; i < result.length; i++) {
|
|
55
90
|
for (let j = i + 1; j < result.length; j++) {
|
|
56
|
-
|
|
57
|
-
const smallerArea = Math.min(result[i].width * result[i].height, result[j].width * result[j].height);
|
|
58
|
-
if (overlap / smallerArea > 0.1) {
|
|
91
|
+
if (isSignificantOverlap(result[i], result[j])) {
|
|
59
92
|
result = [
|
|
60
93
|
...result.slice(0, i),
|
|
61
94
|
...result.slice(i + 1, j),
|
package/dist/sessions/dom.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { rectsIntersect, rectContains } from '../geometry/geometry.js';
|
|
1
|
+
import { rectsIntersect, rectContains, isNearlySameRect } from '../geometry/geometry.js';
|
|
2
2
|
/**
|
|
3
3
|
* Extracts the bounding rect from an Applitools DOM snapshot node.
|
|
4
4
|
* Returns null if the node has no rect or its values are not numbers.
|
|
@@ -295,17 +295,15 @@ export function resolveNodeIdsToNodes(root, nodeIds) {
|
|
|
295
295
|
return result;
|
|
296
296
|
}
|
|
297
297
|
/**
|
|
298
|
-
*
|
|
299
|
-
*
|
|
300
|
-
*
|
|
301
|
-
* a screenshot) into a domSelector-backed region when a matching element actually exists,
|
|
302
|
-
* so the region tracks that element rather than a fixed set of pixels. Returns `null` if no
|
|
303
|
-
* node is within tolerance. When multiple nodes qualify (e.g. a wrapper and its one child
|
|
304
|
-
* share nearly the same rect), the one with the smallest total deviation wins.
|
|
298
|
+
* The element `targetRect` denotes: a match within `tolerancePx` on every edge, else the element
|
|
299
|
+
* whose box is nearly the same rect — a changed-area rect bounds the differing pixels, which rarely
|
|
300
|
+
* lands within a few px of the element's own box. Within a tier the smallest total deviation wins.
|
|
305
301
|
*/
|
|
306
302
|
export function findNodeByRect(root, targetRect, tolerancePx = 5) {
|
|
307
|
-
let
|
|
308
|
-
let
|
|
303
|
+
let exact = null;
|
|
304
|
+
let exactDelta = Infinity;
|
|
305
|
+
let shifted = null;
|
|
306
|
+
let shiftedDelta = Infinity;
|
|
309
307
|
for (const { node } of flattenTree(root)) {
|
|
310
308
|
const r = nodeRect(node);
|
|
311
309
|
if (!r)
|
|
@@ -314,15 +312,20 @@ export function findNodeByRect(root, targetRect, tolerancePx = 5) {
|
|
|
314
312
|
const dTop = Math.abs(r.top - targetRect.top);
|
|
315
313
|
const dWidth = Math.abs(r.width - targetRect.width);
|
|
316
314
|
const dHeight = Math.abs(r.height - targetRect.height);
|
|
317
|
-
if (dLeft > tolerancePx || dTop > tolerancePx || dWidth > tolerancePx || dHeight > tolerancePx)
|
|
318
|
-
continue;
|
|
319
315
|
const delta = dLeft + dTop + dWidth + dHeight;
|
|
320
|
-
if (
|
|
321
|
-
|
|
322
|
-
|
|
316
|
+
if (dLeft <= tolerancePx && dTop <= tolerancePx && dWidth <= tolerancePx && dHeight <= tolerancePx) {
|
|
317
|
+
if (delta < exactDelta) {
|
|
318
|
+
exactDelta = delta;
|
|
319
|
+
exact = node;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
else if (isNearlySameRect(r, targetRect) && delta < shiftedDelta) {
|
|
323
|
+
shiftedDelta = delta;
|
|
324
|
+
shifted = node;
|
|
323
325
|
}
|
|
324
326
|
}
|
|
325
|
-
|
|
327
|
+
const found = exact ?? shifted;
|
|
328
|
+
return found ? findNodeById(root, found.id) : null;
|
|
326
329
|
}
|
|
327
330
|
// ---------------------------------------------------------------------------
|
|
328
331
|
// Reading-order sort
|
package/dist/sessions/match.js
CHANGED
|
@@ -38,7 +38,9 @@ function pickCategories(bucket) {
|
|
|
38
38
|
* `appOutputResolution[stepIndex].annotations` if a resolution exists, else
|
|
39
39
|
* `expectedAppOutput[stepIndex].annotations` (the baseline's own regions) — since
|
|
40
40
|
* `imageMatchSettings`'s own arrays only reflect what was true when the test last ran, not any
|
|
41
|
-
* region a reviewer added since.
|
|
41
|
+
* region a reviewer added since. The coded regions among them (`regionId`) stay in those arrays
|
|
42
|
+
* ahead of the annotations: the compare endpoint applies the arrays and not `identifiedRegions`,
|
|
43
|
+
* so leaving them out reports diffs inside regions the real match never checks.
|
|
42
44
|
*
|
|
43
45
|
* No baseline/checkpoint side-picking or healing happens here: the raw session already resolves
|
|
44
46
|
* which side a region describes (see `regions.ts`'s `getEffectiveRegions`, used instead for a
|
|
@@ -48,9 +50,30 @@ function pickCategories(bucket) {
|
|
|
48
50
|
export function getEffectiveMatchSettings(rawSession, stepIndex) {
|
|
49
51
|
const imageMatchSettings = rawSession?.actualAppOutput?.[stepIndex]?.imageMatchSettings ?? {};
|
|
50
52
|
const annotations = rawSession?.appOutputResolution?.[stepIndex]?.annotations ?? rawSession?.expectedAppOutput?.[stepIndex]?.annotations;
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
53
|
+
const coded = pickCategories(imageMatchSettings);
|
|
54
|
+
const annotated = pickCategories(annotations);
|
|
55
|
+
const regions = {};
|
|
56
|
+
for (const category of MATCH_SETTINGS_CATEGORIES) {
|
|
57
|
+
regions[category] = [...coded[category].filter(isCodedRegion), ...annotated[category]];
|
|
58
|
+
}
|
|
59
|
+
return { ...imageMatchSettings, ...regions, identifiedRegions: coded };
|
|
56
60
|
}
|
|
61
|
+
function isCodedRegion(region) {
|
|
62
|
+
return typeof region.regionId === 'string' && region.regionId !== '';
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* The five match levels a match region can name — a subset of `MATCH_SETTINGS_CATEGORIES`, ordered
|
|
66
|
+
* as the `regionOp` wire syntax and the MCP tool schemas list them.
|
|
67
|
+
*/
|
|
68
|
+
export const REGION_TYPES = ['ignore', 'strict', 'ignorecolors', 'dynamic', 'layout'];
|
|
69
|
+
/**
|
|
70
|
+
* Higher masks less: `strict` checks almost everything, `ignore` checks nothing. Read to refuse a
|
|
71
|
+
* replacement that would leave a region weaker than it was.
|
|
72
|
+
*/
|
|
73
|
+
export const REGION_TYPE_STRENGTH = {
|
|
74
|
+
ignore: 0,
|
|
75
|
+
layout: 1,
|
|
76
|
+
dynamic: 2,
|
|
77
|
+
ignorecolors: 3,
|
|
78
|
+
strict: 4,
|
|
79
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@applitools/eyes-results",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.4",
|
|
4
4
|
"description": "Applitools Eyes REST plumbing, DOM diffing, and session/batch shaping",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"applitools",
|
|
@@ -51,10 +51,10 @@
|
|
|
51
51
|
"devDependencies": {
|
|
52
52
|
"@types/node": "^22",
|
|
53
53
|
"typescript": "~5.9.3",
|
|
54
|
-
"vitest": "^
|
|
54
|
+
"vitest": "^3.2.4"
|
|
55
55
|
},
|
|
56
56
|
"engines": {
|
|
57
|
-
"node": ">=
|
|
57
|
+
"node": ">=18.0.0"
|
|
58
58
|
},
|
|
59
59
|
"publishConfig": {
|
|
60
60
|
"access": "public"
|
|
@@ -63,7 +63,8 @@
|
|
|
63
63
|
"allowScripts": {
|
|
64
64
|
"vitest>jsdom>ws>bufferutil": false,
|
|
65
65
|
"vitest>jsdom>ws>utf-8-validate": false,
|
|
66
|
-
"vitest>vite>esbuild": false
|
|
66
|
+
"vitest>vite>esbuild": false,
|
|
67
|
+
"vitest>vite-node>vite>esbuild": false
|
|
67
68
|
}
|
|
68
69
|
}
|
|
69
70
|
}
|
|
@@ -20,5 +20,22 @@ export declare function boundingRect(a: Rect, b: Rect): Rect;
|
|
|
20
20
|
* When `imageWidth`/`imageHeight` are provided the result is clipped to the image bounds.
|
|
21
21
|
*/
|
|
22
22
|
export declare function padRect(rect: Rect, padding: number, imageWidth?: number, imageHeight?: number): Rect;
|
|
23
|
-
/**
|
|
23
|
+
/** A region under this covers a diff artefact, not content, and as a search source it matches almost everywhere. */
|
|
24
|
+
export declare const MIN_REGION_AREA_PX = 16;
|
|
25
|
+
export declare function isTooSmallRegion(rect: Rect): boolean;
|
|
26
|
+
export declare function sameRect(a: Rect, b: Rect): boolean;
|
|
27
|
+
/** Intersection area as a fraction of the smaller rect's area — IoU would punish a thin rect for a few px of drift. */
|
|
28
|
+
export declare function overlapCoverage(a: Rect, b: Rect): number;
|
|
29
|
+
/** Below this, two rects merely graze each other at the edges and are treated as unrelated. */
|
|
30
|
+
export declare const SIGNIFICANT_OVERLAP_MIN_FRACTION = 0.1;
|
|
31
|
+
/** Below this on either axis the two rects are different shapes, however much they overlap. */
|
|
32
|
+
export declare const SIMILAR_SIZE_MIN_RATIO = 0.85;
|
|
33
|
+
export declare const SIMILAR_COVERAGE_MIN_FRACTION = 0.7;
|
|
34
|
+
export declare function isSignificantOverlap(a: Rect, b: Rect): boolean;
|
|
35
|
+
/**
|
|
36
|
+
* Similar size on both axes and mostly overlapping: one rect is the other after a drift of a few px.
|
|
37
|
+
* A rect that moved a whole row away shares no area with where it was, so it is not nearly the same.
|
|
38
|
+
*/
|
|
39
|
+
export declare function isNearlySameRect(a: Rect, b: Rect): boolean;
|
|
40
|
+
/** Merges pairs of rects that overlap by more than `SIGNIFICANT_OVERLAP_MIN_FRACTION` of the smaller rect's area. */
|
|
24
41
|
export declare function unifyRects(rects: Rect[]): Rect[];
|
package/types/sessions/dom.d.ts
CHANGED
|
@@ -90,13 +90,9 @@ export declare function findNodeBySelector(root: PrunedDom, path: string): DomNo
|
|
|
90
90
|
*/
|
|
91
91
|
export declare function resolveNodeIdsToNodes(root: PrunedDom, nodeIds: string[]): Map<string, DomNode>;
|
|
92
92
|
/**
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
* a screenshot) into a domSelector-backed region when a matching element actually exists,
|
|
97
|
-
* so the region tracks that element rather than a fixed set of pixels. Returns `null` if no
|
|
98
|
-
* node is within tolerance. When multiple nodes qualify (e.g. a wrapper and its one child
|
|
99
|
-
* share nearly the same rect), the one with the smallest total deviation wins.
|
|
93
|
+
* The element `targetRect` denotes: a match within `tolerancePx` on every edge, else the element
|
|
94
|
+
* whose box is nearly the same rect — a changed-area rect bounds the differing pixels, which rarely
|
|
95
|
+
* lands within a few px of the element's own box. Within a tier the smallest total deviation wins.
|
|
100
96
|
*/
|
|
101
97
|
export declare function findNodeByRect(root: PrunedDom, targetRect: Rect, tolerancePx?: number): DomNode | null;
|
|
102
98
|
export interface FlatNode {
|
|
@@ -62,7 +62,9 @@ export interface ImageMatchSettings {
|
|
|
62
62
|
* `appOutputResolution[stepIndex].annotations` if a resolution exists, else
|
|
63
63
|
* `expectedAppOutput[stepIndex].annotations` (the baseline's own regions) — since
|
|
64
64
|
* `imageMatchSettings`'s own arrays only reflect what was true when the test last ran, not any
|
|
65
|
-
* region a reviewer added since.
|
|
65
|
+
* region a reviewer added since. The coded regions among them (`regionId`) stay in those arrays
|
|
66
|
+
* ahead of the annotations: the compare endpoint applies the arrays and not `identifiedRegions`,
|
|
67
|
+
* so leaving them out reports diffs inside regions the real match never checks.
|
|
66
68
|
*
|
|
67
69
|
* No baseline/checkpoint side-picking or healing happens here: the raw session already resolves
|
|
68
70
|
* which side a region describes (see `regions.ts`'s `getEffectiveRegions`, used instead for a
|
|
@@ -70,3 +72,14 @@ export interface ImageMatchSettings {
|
|
|
70
72
|
* effect, relayed as-is, by reference.
|
|
71
73
|
*/
|
|
72
74
|
export declare function getEffectiveMatchSettings(rawSession: any, stepIndex: number): ImageMatchSettings;
|
|
75
|
+
/**
|
|
76
|
+
* The five match levels a match region can name — a subset of `MATCH_SETTINGS_CATEGORIES`, ordered
|
|
77
|
+
* as the `regionOp` wire syntax and the MCP tool schemas list them.
|
|
78
|
+
*/
|
|
79
|
+
export declare const REGION_TYPES: readonly ["ignore", "strict", "ignorecolors", "dynamic", "layout"];
|
|
80
|
+
export type RegionType = (typeof REGION_TYPES)[number];
|
|
81
|
+
/**
|
|
82
|
+
* Higher masks less: `strict` checks almost everything, `ignore` checks nothing. Read to refuse a
|
|
83
|
+
* replacement that would leave a region weaker than it was.
|
|
84
|
+
*/
|
|
85
|
+
export declare const REGION_TYPE_STRENGTH: Record<RegionType, number>;
|