@applitools/eyes-results 0.2.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.
Files changed (72) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/LICENSE +26 -0
  3. package/dist/api/api.js +17 -0
  4. package/dist/api/compare-step.js +20 -0
  5. package/dist/api/diff-fetch.js +13 -0
  6. package/dist/api/dom-diff.js +139 -0
  7. package/dist/api/dom-fetch.js +39 -0
  8. package/dist/api/find-regions.js +24 -0
  9. package/dist/api/fs-cache.js +19 -0
  10. package/dist/api/gzip.js +12 -0
  11. package/dist/api/image-fetch.js +31 -0
  12. package/dist/api/polling.js +117 -0
  13. package/dist/api/req-results.js +77 -0
  14. package/dist/api/same-diff-as.js +25 -0
  15. package/dist/api/selector-mapping.js +15 -0
  16. package/dist/api/session-fetch.js +35 -0
  17. package/dist/api/session-history-fetch.js +73 -0
  18. package/dist/api/session-updates.js +37 -0
  19. package/dist/api/tasks.js +48 -0
  20. package/dist/api/url-parsing.js +33 -0
  21. package/dist/common/common.js +2 -0
  22. package/dist/common/diff-rects.js +64 -0
  23. package/dist/common/region-mapping.js +50 -0
  24. package/dist/geometry/geometry.js +72 -0
  25. package/dist/index.js +14 -0
  26. package/dist/lib/api-key-provider.js +26 -0
  27. package/dist/lib/env.js +26 -0
  28. package/dist/lib/phase-tracker.js +223 -0
  29. package/dist/sessions/batch.js +98 -0
  30. package/dist/sessions/diffs.js +210 -0
  31. package/dist/sessions/dom-history.js +107 -0
  32. package/dist/sessions/dom-search.js +1150 -0
  33. package/dist/sessions/dom.js +513 -0
  34. package/dist/sessions/match.js +56 -0
  35. package/dist/sessions/regions.js +280 -0
  36. package/dist/sessions/session.js +115 -0
  37. package/llm-docs/dom-search.md +164 -0
  38. package/package.json +69 -0
  39. package/types/api/api.d.ts +17 -0
  40. package/types/api/compare-step.d.ts +52 -0
  41. package/types/api/diff-fetch.d.ts +21 -0
  42. package/types/api/dom-diff.d.ts +53 -0
  43. package/types/api/dom-fetch.d.ts +29 -0
  44. package/types/api/find-regions.d.ts +26 -0
  45. package/types/api/fs-cache.d.ts +12 -0
  46. package/types/api/gzip.d.ts +5 -0
  47. package/types/api/image-fetch.d.ts +29 -0
  48. package/types/api/polling.d.ts +41 -0
  49. package/types/api/req-results.d.ts +37 -0
  50. package/types/api/same-diff-as.d.ts +38 -0
  51. package/types/api/selector-mapping.d.ts +28 -0
  52. package/types/api/session-fetch.d.ts +34 -0
  53. package/types/api/session-history-fetch.d.ts +56 -0
  54. package/types/api/session-updates.d.ts +26 -0
  55. package/types/api/tasks.d.ts +33 -0
  56. package/types/api/url-parsing.d.ts +20 -0
  57. package/types/common/common.d.ts +2 -0
  58. package/types/common/diff-rects.d.ts +39 -0
  59. package/types/common/region-mapping.d.ts +26 -0
  60. package/types/geometry/geometry.d.ts +24 -0
  61. package/types/index.d.ts +14 -0
  62. package/types/lib/api-key-provider.d.ts +31 -0
  63. package/types/lib/env.d.ts +14 -0
  64. package/types/lib/phase-tracker.d.ts +146 -0
  65. package/types/sessions/batch.d.ts +67 -0
  66. package/types/sessions/diffs.d.ts +26 -0
  67. package/types/sessions/dom-history.d.ts +119 -0
  68. package/types/sessions/dom-search.d.ts +113 -0
  69. package/types/sessions/dom.d.ts +263 -0
  70. package/types/sessions/match.d.ts +72 -0
  71. package/types/sessions/regions.d.ts +171 -0
  72. package/types/sessions/session.d.ts +97 -0
@@ -0,0 +1,263 @@
1
+ import { type Rect } from '../geometry/geometry.js';
2
+ /**
3
+ * A DOM snapshot as the backend's pruned-DOM endpoint serves it (`api`'s `fetchPrunedDom`) —
4
+ * distinct from the raw dom the raw-DOM endpoint returns. Every step below is the *backend's* —
5
+ * the client-side pruner was removed at the DOM-diff cutover, so no function in this package
6
+ * performs any of them. Relative to a raw dom, a PrunedDom has:
7
+ * - embedded UFG CDN URLs rewritten to their canonical form, so the same resource looks
8
+ * identical whether captured on a baseline or checkpoint run
9
+ * - every element assigned a stable, sequential `id` (the numbering `assignNodeIds` mirrors),
10
+ * against the full unpruned tree, before any cropping happens
11
+ * - top-level metadata fields stripped (`css`, `images`, `version`, `scriptVersion`) — never
12
+ * part of the tree structure itself
13
+ * - whitespace-only `#text` node children removed throughout the tree
14
+ * It has *not* been cropped to any area — that's `getDomArea`'s separate, uncached, per-call
15
+ * job, applied to a PrunedDom after it's already been fetched.
16
+ */
17
+ export type PrunedDom = any;
18
+ /**
19
+ * Extracts the bounding rect from an Applitools DOM snapshot node.
20
+ * Returns null if the node has no rect or its values are not numbers.
21
+ */
22
+ export declare function nodeRect(node: PrunedDom): Rect | null;
23
+ /**
24
+ * Recursively crops a PrunedDom tree to only the branches relevant to the given area.
25
+ *
26
+ * Retention rules:
27
+ * 1. Nodes that overlap the area without fully containing it get full detail — they are
28
+ * direct neighbours of the change.
29
+ * 2. The tightest container (innermost ancestor whose rect strictly contains the area,
30
+ * with no retained child also containing it) gets full detail.
31
+ * 3. Ancestors within ANCESTOR_SIMILARITY_FACTOR× the tightest container's pixel area
32
+ * get full detail — they are likely part of the immediate component structure.
33
+ * 4. All other (distant) ancestors are skipped entirely; their retained children are
34
+ * promoted up, so the output starts at the innermost relevant node.
35
+ * - Text/comment nodes ("#text", "#comment") are passed through with their parent.
36
+ * - Returns null if no node in the tree intersects the area.
37
+ */
38
+ export declare function getDomArea(node: PrunedDom, area: Rect): PrunedDom | PrunedDom[] | null;
39
+ /**
40
+ * Validates that a domId is usable. Returns an error string if not, null if valid.
41
+ * Guards against passing the literal string "null" copied from `info` JSON output.
42
+ */
43
+ export declare function validateDomId(domId: string | null | undefined): string | null;
44
+ /**
45
+ * Assigns a sequential, 1-based id to every element node as a top-level `id` field, via a
46
+ * pre-order DFS over `root`. Mutates nodes in place.
47
+ *
48
+ * Must be called against the full, unpruned tree so a node's id is stable no matter
49
+ * what area a later `dom`/`dom-diff` call happens to crop to.
50
+ */
51
+ export declare function assignNodeIds(root: any): void;
52
+ /** A node located by id or by walking a `domSelector` path — the selector that reaches it, and its rect. */
53
+ export interface DomNode {
54
+ domSelector: string;
55
+ rect: Rect;
56
+ }
57
+ /**
58
+ * Resolves a single node id to its `domSelector` and `rect`. Returns `null` if the id isn't
59
+ * present in the tree, or if the node found has no usable rect.
60
+ */
61
+ export declare function findNodeById(root: PrunedDom, domId: string): DomNode | null;
62
+ /**
63
+ * Resolves a single node id to the node object itself — unlike `findNodeById`, which only
64
+ * returns `{domSelector, rect}`. Needed wherever a caller must inspect or serialize the node
65
+ * itself, not just locate it. Returns `null` if the id isn't present in the tree.
66
+ */
67
+ export declare function findNodeObjectById(root: PrunedDom, domId: string): any | null;
68
+ /**
69
+ * The inverse of `findNodeById`: walks `root` following a `domSelector` path (e.g.
70
+ * `/html/body[0]/div[2]`, as produced by `findNodeById`/`findNodeByRect`) and returns the
71
+ * rect of whatever element it still resolves to — or `null` if it doesn't (the element that
72
+ * path was built against has been removed, or a sibling's tag/order changed enough to shift
73
+ * indices). Returns the same `{domSelector, rect}` shape as `findNodeById`/`findNodeByRect`,
74
+ * so callers can use whichever of the three applies interchangeably.
75
+ *
76
+ * The first path segment is the root's own bare tag name with no `[index]` (see
77
+ * `findNodeById`) — there's only one root, so it's skipped rather than checked. Every
78
+ * segment after that must match `tagName[index]`, `index` being 0-based among `node`'s
79
+ * children sharing that tag.
80
+ */
81
+ export declare function findNodeBySelector(root: PrunedDom, path: string): DomNode | null;
82
+ /**
83
+ * Resolves a batch of node ids against the same already-fetched, already-id-assigned
84
+ * tree, one `findNodeById` walk per id. Pure — takes the tree as-is, does no fetching of
85
+ * its own (callers fetch the DOM snapshot once and reuse it here).
86
+ *
87
+ * Throws if any id can't be resolved, naming all of them, rather than silently omitting
88
+ * an entry from the returned map — a caller indexing the result by id (e.g.
89
+ * `result.get(id)!`) would otherwise get `undefined` for a bad id with no signal why.
90
+ */
91
+ export declare function resolveNodeIdsToNodes(root: PrunedDom, nodeIds: string[]): Map<string, DomNode>;
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.
100
+ */
101
+ export declare function findNodeByRect(root: PrunedDom, targetRect: Rect, tolerancePx?: number): DomNode | null;
102
+ export interface FlatNode {
103
+ node: PrunedDom;
104
+ depth: number;
105
+ /** This node's parent FlatNode in the same tree, or null/undefined at the root or when built
106
+ * without parent tracking (e.g. hand-built test fixtures). Lets a consumer walk up to an
107
+ * ancestor, or find true structural siblings, without re-traversing the tree. */
108
+ parent?: FlatNode | null;
109
+ }
110
+ export interface DisplacedEntry {
111
+ status: 'displaced';
112
+ count: number;
113
+ depth: number;
114
+ rect: {
115
+ left: number;
116
+ top: number;
117
+ width: number;
118
+ height: number;
119
+ };
120
+ baseline: {
121
+ rect: {
122
+ left: number;
123
+ top: number;
124
+ width: number;
125
+ height: number;
126
+ };
127
+ };
128
+ }
129
+ export interface CollapsedMissingEntry {
130
+ status: 'missing';
131
+ tagName: string;
132
+ id: string;
133
+ depth: number;
134
+ count: number;
135
+ attributes?: Record<string, string>;
136
+ rect: {
137
+ left: number;
138
+ top: number;
139
+ width: number;
140
+ height: number;
141
+ };
142
+ }
143
+ /** Common shape shared by `ChangedDiffEntry`/`NewDiffEntry`/`MissingDiffEntry`'s own side of a
144
+ * comparison — never the full `PrunedDom` node, only the properties an entry ever surfaces. */
145
+ interface DiffEntrySide {
146
+ style?: Record<string, any>;
147
+ attributes?: Record<string, string>;
148
+ text?: string;
149
+ }
150
+ export interface ChangedDiffEntry extends DiffEntrySide {
151
+ status: 'changed';
152
+ depth: number;
153
+ tagName: string;
154
+ id: string;
155
+ rect: Rect;
156
+ baseline: DiffEntrySide & {
157
+ tagName?: string;
158
+ id: string;
159
+ rect: Rect;
160
+ };
161
+ }
162
+ export interface NewDiffEntry extends DiffEntrySide {
163
+ status: 'new';
164
+ depth: number;
165
+ tagName: string;
166
+ id: string;
167
+ rect: Rect;
168
+ }
169
+ /** One baseline-side element with no checkpoint counterpart at all — a single removed element,
170
+ * as opposed to `CollapsedMissingEntry`'s whole removed subtree. */
171
+ export interface MissingDiffEntry extends DiffEntrySide {
172
+ status: 'missing';
173
+ depth: number;
174
+ tagName: string;
175
+ id: string;
176
+ rect: Rect;
177
+ }
178
+ /** One entry of an already-computed `DomAlignment.diff` — the same compact, per-element shape
179
+ * `serializeDiff` formats into its final string, carrying only each side's own id/rect/changed
180
+ * properties, never a full `PrunedDom` node or its ancestor chain. */
181
+ export type DiffEntry = ChangedDiffEntry | NewDiffEntry | MissingDiffEntry | DisplacedEntry | CollapsedMissingEntry;
182
+ /** Kept past the DOM-diff backend cutover: `findNodeByRect` walks the tree through it. */
183
+ export declare function flattenTree(root: PrunedDom): FlatNode[];
184
+ /**
185
+ * Returns whether an Applitools DOM capture node could plausibly paint a pixel: it has a
186
+ * non-zero-size rect and isn't hidden via `display:none`, `visibility:hidden`,
187
+ * `opacity:0`, or the `hidden` HTML attribute.
188
+ */
189
+ export declare function isVisible(node: PrunedDom): boolean;
190
+ export declare function filterDiffToArea(diffs: (DiffEntry | null)[], area: Rect): (DiffEntry | null)[];
191
+ export declare function excludeNonVisualNodeDiffs(diffs: (DiffEntry | null)[], baselineDiffs: Rect[], checkpointDiffs: Rect[]): (DiffEntry | null)[];
192
+ export declare function serializeDiff(diffs: (DiffEntry | null)[]): string;
193
+ /** One checkpoint node's own match info, keyed by that node's id in `DomAlignment.matches` — its
194
+ * matched baseline node's own id (`baselineId`) and the checkpoint node's own `checkpointRect`.
195
+ * Never the full node objects on either side — see `DomAlignment.matches`'s own doc for why. A
196
+ * checkpoint node with no baseline counterpart (e.g. a genuinely new element) has no rect worth
197
+ * keeping here either — nothing about it can be aligned further, so `matches` maps that id to
198
+ * `null` instead of a `CompactMatch` (see `DomAlignment.matches`'s own doc for the full
199
+ * three-state shape). */
200
+ export interface CompactMatch {
201
+ baselineId: string;
202
+ checkpointRect: Rect;
203
+ }
204
+ export interface DomAlignment {
205
+ /** One entry per checkpoint node, in one of three states: the key itself absent means the id
206
+ * doesn't exist on the checkpoint side at all; present but mapped to `null` means the id exists
207
+ * but has no baseline counterpart; present and mapped to a `CompactMatch` means it does. Compact
208
+ * by design — never full node objects for the two sides of a match; serializing those (to a
209
+ * cache file, or a network response) would duplicate most of the tree's own data once per node
210
+ * it appears in as an ancestor, large enough on a big page to exhaust memory outright. */
211
+ matches: Record<string, CompactMatch | null>;
212
+ /** One entry per changed/new/missing/displaced element, already narrowed to `DiffEntry`'s
213
+ * compact shape — never a full `PrunedDom` node or its ancestor
214
+ * chain. Cropping to an area (`filterDiffToArea`) or excluding non-visual entries
215
+ * (`excludeNonVisualNodeDiffs`) are separate, later steps a caller applies to this array
216
+ * itself. */
217
+ diff: (DiffEntry | null)[];
218
+ }
219
+ export interface NodeDiffResult {
220
+ /** Whether `targetNodeId` was found at all on the checkpoint side of the alignment. `false`
221
+ * means the id genuinely doesn't exist in whichever tree was aligned as the checkpoint side —
222
+ * distinct from `matchedNodeId: null`, which means the id *was* found but has no baseline
223
+ * counterpart (e.g. it's a genuinely new element). A caller that only cares whether the id it
224
+ * asked about actually exists (rather than fetching a whole tree just to check) can rely on
225
+ * this without an extra call, since it's read off an alignment already being computed. */
226
+ found: boolean;
227
+ /** The id of the baseline-side node aligned to `targetNodeId`, or `null` if the global
228
+ * alignment found no counterpart for it (e.g. it doesn't exist yet on the baseline side).
229
+ * Always `null` when `found` is `false`. */
230
+ matchedNodeId: string | null;
231
+ /** Same compact format `serializeDiff` produces, filtered to the target node's own rect, but
232
+ * without `serializeDiff`'s own leading per-line indentation — that indent communicates
233
+ * nesting depth for direct terminal viewing (`dom-diff`'s own use), which is meaningless
234
+ * once this diff is embedded as a string value inside another JSON structure, and would
235
+ * vary arbitrarily with how deep the target node itself happens to sit in the page. Always
236
+ * `''` when `found` is `false` or `matchedNodeId` is `null` — either way there's no baseline
237
+ * counterpart's own rect left to scope a diff to (`DomAlignment.matches` never keeps one for
238
+ * an unmatched node). */
239
+ diff: string;
240
+ }
241
+ /**
242
+ * Extracts one target node's aligned counterpart and rect-filtered diff from an
243
+ * already-computed alignment (the backend's `fetchDomDiff` result, or an equivalent
244
+ * `DomAlignment` obtained some other way) — the cheap part once the
245
+ * whole-tree alignment is done, so a caller with several target nodes against the same pair of
246
+ * trees runs this once per node without paying for the alignment itself more than once.
247
+ * `targetNodeId` must be a node id assigned (see `assignNodeIds`) on whichever tree was aligned
248
+ * as the checkpoint side — its matched baseline id and its own checkpoint rect are read straight
249
+ * off `alignment.matches` by that id, so a caller never needs to hold onto the actual node object,
250
+ * only its id, and this works just as well against an alignment that was serialized (e.g. read
251
+ * back from a cache or a network response) as against one just computed in-process.
252
+ *
253
+ * Reading a target's own match off a *global*, whole-tree alignment — rather than a standalone
254
+ * best-match search for that one node — is deliberate: an element that occupies the same
255
+ * structural position across captures but now shows entirely different content (e.g. a rotating
256
+ * carousel slot, or a live counter) would otherwise get matched to wherever its *old* content
257
+ * happens to reappear elsewhere on the page, rather than to whatever now occupies its own slot.
258
+ * Aligning the whole page at once keeps a node's in-place replacement — not a content lookalike
259
+ * somewhere else — as its counterpart, because the aligner is scored on overall sequence
260
+ * correspondence, not one node in isolation.
261
+ */
262
+ export declare function extractNodeDiff(alignment: DomAlignment, targetNodeId: string): NodeDiffResult;
263
+ export {};
@@ -0,0 +1,72 @@
1
+ import type { Region } from './regions.js';
2
+ /**
3
+ * The subset of `AnnotationCategory` (`sessions/regions.ts`) Applitools Eyes' own
4
+ * `ImageMatchSettings` schema actually has a field for — a raw session's own
5
+ * `imageMatchSettings` object never has a `mismatching` or `remarks` key, unlike
6
+ * `annotations`/`identifiedRegions`, which do (those two are a dashboard/annotations-only
7
+ * concept, not a real match-settings one).
8
+ */
9
+ export declare const MATCH_SETTINGS_CATEGORIES: readonly ["content", "dynamic", "floating", "ignore", "layout", "strict", "accessibility"];
10
+ export type MatchSettingsCategory = (typeof MATCH_SETTINGS_CATEGORIES)[number];
11
+ /**
12
+ * Applitools Eyes' own `ImageMatchSettings` structure — the same shape both as stored on a raw
13
+ * session's `actualAppOutput[stepIndex].imageMatchSettings` and as accepted by an
14
+ * `images/compare` request body (see `api`'s `compareStep`), built from a raw session via
15
+ * `getEffectiveMatchSettings` below. Every field is optional — a raw session's own
16
+ * `imageMatchSettings` isn't guaranteed to carry all of them (nothing here enforces it did), and
17
+ * a caller building a partial one by hand (e.g. a test fixture) shouldn't have to restate every
18
+ * scalar/category just to exercise the one it cares about.
19
+ */
20
+ export interface ImageMatchSettings {
21
+ matchLevel?: string;
22
+ ignoreCaret?: boolean;
23
+ ignoreDisplacements?: boolean;
24
+ useDom?: boolean;
25
+ useDL?: boolean;
26
+ enablePatterns?: boolean;
27
+ scale?: number;
28
+ remainder?: number;
29
+ splitTopHeight?: number;
30
+ splitBottomHeight?: number;
31
+ content?: Region[];
32
+ dynamic?: Region[];
33
+ floating?: Region[];
34
+ ignore?: Region[];
35
+ layout?: Region[];
36
+ strict?: Region[];
37
+ accessibility?: Region[];
38
+ identifiedRegions?: Record<MatchSettingsCategory, Region[]>;
39
+ /**
40
+ * Only meaningful on an `images/compare` request body — governs what a `Dynamic` matchLevel
41
+ * treats as "the same." Never observed on a raw session's own `imageMatchSettings`; omitted,
42
+ * the server applies its own default.
43
+ */
44
+ dynamicMatchLevelSettings?: {
45
+ ignorePatterns: string[];
46
+ };
47
+ }
48
+ /**
49
+ * Builds the full `ImageMatchSettings` an `images/compare` request body expects for a step —
50
+ * ready to use as-is. The rest of the request (`image1`/`image2`/`sessionId`/`batchId`/
51
+ * `runningSessionId`/`stepIndex`) is the caller's own job, not this function's — none of those
52
+ * are match-settings concerns.
53
+ *
54
+ * - Scalars (`matchLevel`, `scale`, `ignoreCaret`, `ignoreDisplacements`, `useDom`, etc.) come
55
+ * straight from `actualAppOutput[stepIndex].imageMatchSettings` — the match settings actually
56
+ * in effect for this run.
57
+ * - `identifiedRegions` (this run's own coded regions) also comes from that same
58
+ * `imageMatchSettings` object's own per-category arrays — never falling back to the
59
+ * baseline's `identifiedRegions` snapshot: if a category has no entry there, there is no coded
60
+ * region of that kind in the current match, full stop.
61
+ * - The plain, domSelector-anchored region categories are overwritten from
62
+ * `appOutputResolution[stepIndex].annotations` if a resolution exists, else
63
+ * `expectedAppOutput[stepIndex].annotations` (the baseline's own regions) — since
64
+ * `imageMatchSettings`'s own arrays only reflect what was true when the test last ran, not any
65
+ * region a reviewer added since.
66
+ *
67
+ * No baseline/checkpoint side-picking or healing happens here: the raw session already resolves
68
+ * which side a region describes (see `regions.ts`'s `getEffectiveRegions`, used instead for a
69
+ * diagnostic display that needs both sides shown) — this only ever needs the one side already in
70
+ * effect, relayed as-is, by reference.
71
+ */
72
+ export declare function getEffectiveMatchSettings(rawSession: any, stepIndex: number): ImageMatchSettings;
@@ -0,0 +1,171 @@
1
+ import { type PrunedDom } from './dom.js';
2
+ /**
3
+ * A single region, unified across every place this repo encounters one — a dashboard-added,
4
+ * `domSelector`-anchored annotation; a test-code-defined, `regionId`-anchored coded region; or
5
+ * an `images/compare` request's own region entry (per Applitools' own compare-endpoint schema,
6
+ * which is the origin of the `padding*` fields). `domSelector` and `regionId` are each optional
7
+ * and independent: a region may be anchored by one, the other, both, or neither (a plain rect —
8
+ * confirmed live all of domSelector-only/regionId-only/plain-rect/combined work), and padding
9
+ * only ever layers on top of whichever anchor is given.
10
+ */
11
+ export interface Region {
12
+ left: number;
13
+ top: number;
14
+ width: number;
15
+ height: number;
16
+ regionId?: string;
17
+ domSelector?: string;
18
+ paddingLeft?: number;
19
+ paddingTop?: number;
20
+ paddingRight?: number;
21
+ paddingBottom?: number;
22
+ }
23
+ export declare const ANNOTATION_CATEGORIES: readonly ["content", "dynamic", "floating", "ignore", "mismatching", "layout", "remarks", "strict", "accessibility"];
24
+ export type AnnotationCategory = (typeof ANNOTATION_CATEGORIES)[number];
25
+ export declare function emptyAnnotations(): Record<AnnotationCategory, Region[]>;
26
+ export interface RegionEntry {
27
+ category: AnnotationCategory;
28
+ region: Region;
29
+ }
30
+ /**
31
+ * The same logical region as known on each image side — either may be absent, e.g. because the
32
+ * raw session doesn't encode that side's version at all, or the region doesn't exist there.
33
+ * Used both for plain (`annotations`-backed) regions, where either side may be absent, and for
34
+ * coded regions (see `getEffectiveRegions`'s own `pairCodedRegions`), where `checkpoint` is
35
+ * always populated by construction — a coded region is only ever surfaced there when it's
36
+ * defined on the checkpoint side, so a caller working with `codedRegions` can rely on
37
+ * `checkpoint` being present without an extra check, even though the type itself doesn't
38
+ * enforce that narrower guarantee.
39
+ */
40
+ export interface RegionPair {
41
+ category: AnnotationCategory;
42
+ baseline?: Region;
43
+ checkpoint?: Region;
44
+ }
45
+ export interface EffectiveRegions {
46
+ regions: RegionPair[];
47
+ codedRegions: RegionPair[];
48
+ }
49
+ /**
50
+ * Determines which side's DOM a step's regions should reference. `accept`/`reject` always
51
+ * resolve against the side they're about to make the (new) baseline going forward —
52
+ * checkpoint or baseline respectively. `unchanged` means an action that doesn't itself
53
+ * change the step's resolution (e.g. `baseline-regions`/`checkpoint-regions`, or just
54
+ * inspecting current state), so it must defer to whatever the step's *existing* resolution
55
+ * already is: a step accepted (but not yet saved) by an earlier call already has the
56
+ * checkpoint as its baseline-to-be, so a region added now should reference that, not the
57
+ * original baseline. Only an explicit prior reject, or no resolution at all, means the
58
+ * original baseline is still the one to reference. `expectedAppOutput`/`actualAppOutput`
59
+ * never change to reflect a pending resolution — only
60
+ * `appOutputResolution[stepIndex].replaceExpected` does.
61
+ */
62
+ export declare function resolveDomIdSide(action: 'accept' | 'reject' | 'unchanged', rawSession: any, stepIndex: number): 'baseline' | 'checkpoint';
63
+ /** The raw session's own image record (`{ id, domId, size, hasDom, location, rectangle }`) for a step's baseline or checkpoint image. */
64
+ export declare function imageForSide(rawSession: any, stepIndex: number, side: 'baseline' | 'checkpoint'): any;
65
+ /**
66
+ * Extracts every region-shaped structure the raw session encodes for a step, as
67
+ * baseline/checkpoint pairs — either side of a pair may be `undefined`, for a caller to fill
68
+ * in later (e.g. via `checkRegions` and the other functions below):
69
+ *
70
+ * - `regions` — domSelector-backed `Region[]`, from `annotations`. Only one side is ever
71
+ * populated here, since the raw session never encodes an `annotations`-shaped structure for
72
+ * both sides at once — `resolveDomIdSide` determines which side these domSelectors actually
73
+ * describe. `appOutputResolution[stepIndex].annotations`, when present, overrides
74
+ * `expectedAppOutput[stepIndex].annotations` entirely rather than being combined with it —
75
+ * the same way an empty category there means "resolved to have none" even when the
76
+ * baseline has one.
77
+ * - `codedRegions` — `regionId`-backed `Region[]`, paired up from `identifiedRegions` (on
78
+ * `expectedAppOutput`/`appOutputResolution`, same override rule as `annotations` — always the
79
+ * baseline side) and `imageMatchSettings` (on `actualAppOutput` — always the checkpoint side).
80
+ * Only ever includes an entry that's defined on the checkpoint side — a baseline
81
+ * `identifiedRegions` entry with no matching checkpoint `regionId` is dropped entirely (see
82
+ * `pairCodedRegions`), so `checkpoint` is never `undefined` here, only `baseline`.
83
+ *
84
+ * Both `annotations` and `identifiedRegions` deliberately exclude `removedAnnotations` — a
85
+ * historical record of past removals, not part of the current, effective region set.
86
+ */
87
+ export declare function getEffectiveRegions(rawSession: any, stepIndex: number): EffectiveRegions;
88
+ export interface CheckedRegions {
89
+ /** Has a `domSelector` that resolved against `domSource`. */
90
+ anchored: RegionPair[];
91
+ /** No `domSelector`, or one that failed to resolve against `domSource` (including when `domSource` itself is unavailable) — left as-is either way, *not* cleared. */
92
+ nonAnchored: RegionPair[];
93
+ }
94
+ /**
95
+ * Splits every incomplete `RegionPair` (missing one side — see `getEffectiveRegions`) into
96
+ * anchored/non-anchored, verifying each anchored candidate's `domSelector` against `domSource`
97
+ * (the DOM of whichever side is already populated) along the way. Purely a classification —
98
+ * nothing is mutated, so a caller that just wants to inspect a step's regions as currently
99
+ * encoded (e.g. `eyes-inspect`'s `regions` command) can call this without side effects: a
100
+ * stale `domSelector` (one that no longer resolves) lands in `nonAnchored` exactly as encoded,
101
+ * not cleared — only a caller that intends to *heal* it (see `clearStaleSelectors`, below)
102
+ * should clear it. `anchored` means "has a `domSelector` that actually resolved" — if
103
+ * `domSource` is `null` (couldn't be fetched), nothing can be verified, so every pair lands in
104
+ * `nonAnchored`, domSelector or not.
105
+ */
106
+ export declare function checkRegions(regions: RegionPair[], domSource: PrunedDom): CheckedRegions;
107
+ /**
108
+ * Clears the `domSelector` (mutating in place) of every pair in `nonAnchored` that still has
109
+ * one — i.e. a stale one `checkRegions` left untouched — so it can be retried by
110
+ * `healNonAnchoredRegionsBySourceRect`. A stale selector is never sent to
111
+ * get-compatible-selectors as-is: live-confirmed against the real API, a single bad input
112
+ * selector fails the *entire* batched call, not just itself. Returns whether anything was
113
+ * actually cleared. Only a caller that intends to re-anchor a region calls this (e.g.
114
+ * `eyes-resolve`'s `resolveStepFlow`) — a caller that just wants to inspect regions as encoded
115
+ * (`checkRegions`'s other caller) should leave a stale selector alone for diagnosis rather
116
+ * than heal past it.
117
+ */
118
+ export declare function clearStaleSelectors(nonAnchored: RegionPair[]): boolean;
119
+ /**
120
+ * Attempts to anchor each `nonAnchored` pair by finding its region's rect on `domSource` — the
121
+ * same mechanism a brand-new rect-form region uses to upgrade into a domSelector-backed one.
122
+ * Mutates a healed pair's existing (source-side) region in place with the found `domSelector`
123
+ * and accurate rect; a pair with no match is left as a plain rect, unchanged. Returns whether
124
+ * anything was healed. Pure and synchronous — `domSource` must already be fetched.
125
+ */
126
+ export declare function healNonAnchoredRegionsBySourceRect(nonAnchored: RegionPair[], domSource: PrunedDom): boolean;
127
+ /**
128
+ * Fills in whichever side of each `RegionPair` is still missing with a plain, unanchored
129
+ * copy of its populated side — same rect, no `domSelector`. This doesn't try to find or heal
130
+ * anything (unlike every other function above); it just guarantees every pair comes out with
131
+ * both sides present, for a caller (`eyes-inspect`'s `regions` command) that wants a
132
+ * consistently-shaped, fully "mapped" output to inspect rather than an occasionally-partial
133
+ * one. Once this runs, a `domSelector` present on one side but empty on the other is itself
134
+ * diagnostically meaningful — it means that side's `domSelector` is stale, or the anchoring
135
+ * element genuinely doesn't exist there (get-compatible-selectors mapped it to `null`) —
136
+ * rather than being an accident of the pair simply never having been looked at.
137
+ */
138
+ export declare function fillMissingSideWithPlainRect(regions: RegionPair[]): void;
139
+ /**
140
+ * The coded-region counterpart to `fillMissingSideWithPlainRect` — but one-directional, since
141
+ * `checkpoint` is never missing to begin with on a coded `RegionPair` (see
142
+ * `pairCodedRegions`/`getEffectiveRegions`): only `baseline` ever needs filling. Fills it with a
143
+ * plain copy of `checkpoint` at the same rect, with `regionId` cleared to `''`, rather than
144
+ * leaving it `undefined`.
145
+ */
146
+ export declare function fillMissingBaselineForCodedRegions(codedRegions: RegionPair[]): void;
147
+ /**
148
+ * Finds an existing region "the same" as `candidate`: an exact `domSelector` match
149
+ * (checked across all candidates first — the stronger signal, survives rect drift), or
150
+ * else a rect match within `tolerancePx` on all four of left/top/width/height. Searches
151
+ * across every category, not just the one `candidate` belongs to, so a region that's been
152
+ * reclassified (e.g. layout -> ignore) is still found and replaced rather than duplicated.
153
+ */
154
+ export declare function findSimilarExistingRegion(existing: RegionEntry[], candidate: Region, tolerancePx?: number): RegionEntry | null;
155
+ /**
156
+ * Removes (in place) the entry in `entries` "similar" to `candidate` (see
157
+ * `findSimilarExistingRegion`), if any, returning the removed entry — or `null` if nothing
158
+ * matched, so a caller can report exactly what was actually removed (e.g. for a `-<matchLevel>-
159
+ * <target>` regionOp) rather than just whether something was.
160
+ *
161
+ * Live-confirmed against the real API: the `stepUpdates[].annotations` field is a full
162
+ * replacement of the resolution's active regions, not a delta merged against whatever's
163
+ * already stored — and `removedAnnotations` isn't actually used for removal at all (the
164
+ * real Test Manager UI sends it as an empty skeleton even when deleting a region). So the
165
+ * only correct way to add/remove a region is to fetch the current full set, mutate it
166
+ * in memory (this function, plus a plain `.push` for adds), and send the *entire* result
167
+ * back as `annotations` — see eyes-resolve's `resolveStepFlow`.
168
+ */
169
+ export declare function removeSimilarRegion(entries: RegionEntry[], candidate: Region, tolerancePx?: number): RegionEntry | null;
170
+ /** Buckets a flat, category-tagged region list back into the category-keyed shape the API expects. */
171
+ export declare function bucketRegions(entries: RegionEntry[]): Record<AnnotationCategory, Region[]>;
@@ -0,0 +1,97 @@
1
+ export interface ImageInfo {
2
+ id: string | null;
3
+ domId: string | null;
4
+ size: {
5
+ width: number;
6
+ height: number;
7
+ } | null;
8
+ }
9
+ export interface SessionEnvironment {
10
+ browser: string | null;
11
+ device: string | null;
12
+ os: string | null;
13
+ width: number | null;
14
+ height: number | null;
15
+ }
16
+ export interface SessionStep {
17
+ tag: string | undefined;
18
+ baseline: ImageInfo;
19
+ checkpoint: ImageInfo;
20
+ isMatching: boolean;
21
+ resolution: 'accepted' | 'rejected' | 'none';
22
+ /** A pending resolution or region change on this step, not yet committed via `save`. */
23
+ isUnsaved: boolean;
24
+ }
25
+ export interface SimplifiedSession {
26
+ sessionId: string;
27
+ sessionName: string;
28
+ appName: string;
29
+ status: string;
30
+ isUnsaved: boolean;
31
+ isAborted: boolean;
32
+ environment: SessionEnvironment;
33
+ startedAt: string;
34
+ /** `null` when shaped with `{ includeSteps: false }` — see `simplifySession`. */
35
+ steps: SessionStep[] | null;
36
+ }
37
+ /**
38
+ * Whether a raw session currently has any pending, unsaved change — an accept/reject decision
39
+ * or a region edit not yet committed via `save`. Checks every step's own
40
+ * `appOutputResolution[i].isChanged` flag (the same signal each step's own `isUnsaved` below is
41
+ * derived from), not the raw session's own top-level `isModified` field — confirmed live:
42
+ * `isModified` is sticky, staying `true` forever once a session was ever changed, even long
43
+ * after that change was fully cleared (e.g. via a batch-wide reset), so it can't signal "has a
44
+ * pending change right now." Independent of whether `simplifySession` was asked to compute
45
+ * `steps` — this reads `appOutputResolution` directly, so it's correct either way.
46
+ */
47
+ export declare function hasUnsavedChanges(rawData: any): boolean;
48
+ /**
49
+ * Whether `stepIndex`'s checkpoint already carries a human accept/reject decision — the same
50
+ * `resolution` a step gets in `simplifySession` below, pulled out standalone so a caller only
51
+ * needs one step's status without shaping (or paying for) the whole session's `steps` array.
52
+ * A resolution entry can exist purely from a region edit, with no accept/reject decision behind
53
+ * it (no `replaceExpected` field) — only a real boolean there counts as resolved, however each
54
+ * side originally read.
55
+ */
56
+ export declare function getStepResolution(rawData: any, stepIndex: number): 'accepted' | 'rejected' | 'none';
57
+ /**
58
+ * Extracts a raw session's scenario id — the join key for the session-history query (see
59
+ * `api`'s `fetchSessionHistory`) — confirmed live as a top-level `scenarioId` field,
60
+ * distinct from `scenarioName` (already mapped to `SimplifiedSession.sessionName`). Throws
61
+ * rather than returning `undefined` so a caller building a history query fails loudly instead
62
+ * of silently querying with a bad/missing id.
63
+ */
64
+ export declare function extractScenarioId(rawData: any): string;
65
+ /**
66
+ * Throws a clear "step index N is out of range" error if `stepIndex` doesn't correspond to an
67
+ * actual step in the raw session. Callers that work from the raw, unshaped session directly
68
+ * (unlike this module's own `simplifySession`, which already has a bounds-checked `steps`
69
+ * array) would otherwise silently index past the end of `expectedAppOutput`/`actualAppOutput`
70
+ * and read `undefined` — which `validateDomId` then reports as "this step has no DOM capture",
71
+ * indistinguishable from a real step that simply wasn't DOM-captured.
72
+ */
73
+ export declare function validateStepIndex(rawSession: any, stepIndex: number): void;
74
+ /** Total step count for a raw session — the longer of its baseline/checkpoint image arrays. */
75
+ export declare function getStepCount(rawSession: any): number;
76
+ /**
77
+ * Shapes a raw Eyes session API response into a compact summary — one representation
78
+ * shared by both a single-session inspection (`steps` command, full `steps` array) and a
79
+ * batch listing (`batch` command, one entry per session — pass `{ includeSteps: false }`
80
+ * there to skip computing `steps` for every session in the batch, which that listing has
81
+ * no use for since step-level counts are already aggregated separately into `sessions/batch`'s
82
+ * `BatchStats`).
83
+ *
84
+ * Overloaded so the common case (steps included, by default or explicitly) types `steps`
85
+ * as a real array rather than `Array | null` — only `{ includeSteps: false }` narrows it to
86
+ * `null`, matching what the implementation actually returns in each case.
87
+ */
88
+ export declare function simplifySession(rawData: any, options?: {
89
+ includeSteps?: true;
90
+ }): SimplifiedSession & {
91
+ steps: SessionStep[];
92
+ };
93
+ export declare function simplifySession(rawData: any, options: {
94
+ includeSteps: false;
95
+ }): SimplifiedSession & {
96
+ steps: null;
97
+ };