@applitools/eyes-results 0.2.1 → 0.2.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # Changelog
2
2
 
3
+ ## [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)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * 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))
9
+
10
+ ## [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)
11
+
12
+
13
+ ### Bug Fixes
14
+
15
+ * discover the project .env so the family read/write keys resolve | AD-15678 ([#4131](https://github.com/Applitools-Dev/sdk/issues/4131)) ([316f39f](https://github.com/Applitools-Dev/sdk/commit/316f39fd15fd516b3d266b8aa8deccddfabd0921))
16
+
3
17
  ## [0.2.1](https://github.com/Applitools-Dev/sdk/compare/js/eyes-results@0.2.0...js/eyes-results@0.2.1) (2026-08-24)
4
18
 
5
19
 
@@ -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
- /** Merges pairs of rects that overlap by more than 10% of the smaller rect's area. */
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
- const overlap = intersectionArea(result[i], result[j]);
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/lib/env.js CHANGED
@@ -5,22 +5,20 @@ export const defaultLoadEnvDeps = {
5
5
  existsSync: fs.existsSync,
6
6
  configDotenv: dotenv.config,
7
7
  };
8
- /**
9
- * Walks up from `startDir` (default: the current working directory) looking for a `.env`
10
- * file, loading the first one found via `dotenv`. Mirrors the behavior every skill's own
11
- * `main.ts` used to inline directly.
12
- */
13
8
  export function loadEnvFromCwd(startDir = process.cwd(), deps = defaultLoadEnvDeps) {
14
9
  let dir = startDir;
15
10
  while (true) {
16
11
  const envPath = path.join(dir, '.env');
17
12
  if (deps.existsSync(envPath)) {
18
- deps.configDotenv({ path: envPath });
19
- return;
13
+ const preset = new Set(Object.keys(process.env));
14
+ const result = deps.configDotenv({ path: envPath });
15
+ const parsed = result?.parsed ?? {};
16
+ const applied = Object.keys(parsed).filter(name => !preset.has(name));
17
+ return { path: envPath, applied, parsed };
20
18
  }
21
19
  const parent = path.dirname(dir);
22
20
  if (parent === dir)
23
- return;
21
+ return undefined;
24
22
  dir = parent;
25
23
  }
26
24
  }
@@ -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
- * Finds the element node in `root` whose rect most closely matches `targetRect` — within
299
- * `tolerancePx` on all four of left/top/width/height — returning the same {domSelector,
300
- * rect} shape as `findNodeById`. Lets a caller upgrade a raw pixel rect (e.g. one eyeballed off
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 best = null;
308
- let bestDelta = Infinity;
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 (delta < bestDelta) {
321
- bestDelta = delta;
322
- best = node;
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
- return best ? findNodeById(root, best.id) : null;
327
+ const found = exact ?? shifted;
328
+ return found ? findNodeById(root, found.id) : null;
326
329
  }
327
330
  // ---------------------------------------------------------------------------
328
331
  // Reading-order sort
@@ -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
- return {
52
- ...imageMatchSettings,
53
- ...pickCategories(annotations),
54
- identifiedRegions: pickCategories(imageMatchSettings),
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.1",
3
+ "version": "0.2.3",
4
4
  "description": "Applitools Eyes REST plumbing, DOM diffing, and session/batch shaping",
5
5
  "keywords": [
6
6
  "applitools",
@@ -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
- /** Merges pairs of rects that overlap by more than 10% of the smaller rect's area. */
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[];
@@ -6,9 +6,9 @@ export interface LoadEnvDeps {
6
6
  }) => unknown;
7
7
  }
8
8
  export declare const defaultLoadEnvDeps: LoadEnvDeps;
9
- /**
10
- * Walks up from `startDir` (default: the current working directory) looking for a `.env`
11
- * file, loading the first one found via `dotenv`. Mirrors the behavior every skill's own
12
- * `main.ts` used to inline directly.
13
- */
14
- export declare function loadEnvFromCwd(startDir?: string, deps?: LoadEnvDeps): void;
9
+ export interface LoadedEnv {
10
+ path: string;
11
+ applied: string[];
12
+ parsed: Record<string, string>;
13
+ }
14
+ export declare function loadEnvFromCwd(startDir?: string, deps?: LoadEnvDeps): LoadedEnv | undefined;
@@ -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
- * Finds the element node in `root` whose rect most closely matches `targetRect` — within
94
- * `tolerancePx` on all four of left/top/width/height — returning the same {domSelector,
95
- * rect} shape as `findNodeById`. Lets a caller upgrade a raw pixel rect (e.g. one eyeballed off
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>;