@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.
- package/CHANGELOG.md +8 -0
- package/LICENSE +26 -0
- package/dist/api/api.js +17 -0
- package/dist/api/compare-step.js +20 -0
- package/dist/api/diff-fetch.js +13 -0
- package/dist/api/dom-diff.js +139 -0
- package/dist/api/dom-fetch.js +39 -0
- package/dist/api/find-regions.js +24 -0
- package/dist/api/fs-cache.js +19 -0
- package/dist/api/gzip.js +12 -0
- package/dist/api/image-fetch.js +31 -0
- package/dist/api/polling.js +117 -0
- package/dist/api/req-results.js +77 -0
- package/dist/api/same-diff-as.js +25 -0
- package/dist/api/selector-mapping.js +15 -0
- package/dist/api/session-fetch.js +35 -0
- package/dist/api/session-history-fetch.js +73 -0
- package/dist/api/session-updates.js +37 -0
- package/dist/api/tasks.js +48 -0
- package/dist/api/url-parsing.js +33 -0
- package/dist/common/common.js +2 -0
- package/dist/common/diff-rects.js +64 -0
- package/dist/common/region-mapping.js +50 -0
- package/dist/geometry/geometry.js +72 -0
- package/dist/index.js +14 -0
- package/dist/lib/api-key-provider.js +26 -0
- package/dist/lib/env.js +26 -0
- package/dist/lib/phase-tracker.js +223 -0
- package/dist/sessions/batch.js +98 -0
- package/dist/sessions/diffs.js +210 -0
- package/dist/sessions/dom-history.js +107 -0
- package/dist/sessions/dom-search.js +1150 -0
- package/dist/sessions/dom.js +513 -0
- package/dist/sessions/match.js +56 -0
- package/dist/sessions/regions.js +280 -0
- package/dist/sessions/session.js +115 -0
- package/llm-docs/dom-search.md +164 -0
- package/package.json +69 -0
- package/types/api/api.d.ts +17 -0
- package/types/api/compare-step.d.ts +52 -0
- package/types/api/diff-fetch.d.ts +21 -0
- package/types/api/dom-diff.d.ts +53 -0
- package/types/api/dom-fetch.d.ts +29 -0
- package/types/api/find-regions.d.ts +26 -0
- package/types/api/fs-cache.d.ts +12 -0
- package/types/api/gzip.d.ts +5 -0
- package/types/api/image-fetch.d.ts +29 -0
- package/types/api/polling.d.ts +41 -0
- package/types/api/req-results.d.ts +37 -0
- package/types/api/same-diff-as.d.ts +38 -0
- package/types/api/selector-mapping.d.ts +28 -0
- package/types/api/session-fetch.d.ts +34 -0
- package/types/api/session-history-fetch.d.ts +56 -0
- package/types/api/session-updates.d.ts +26 -0
- package/types/api/tasks.d.ts +33 -0
- package/types/api/url-parsing.d.ts +20 -0
- package/types/common/common.d.ts +2 -0
- package/types/common/diff-rects.d.ts +39 -0
- package/types/common/region-mapping.d.ts +26 -0
- package/types/geometry/geometry.d.ts +24 -0
- package/types/index.d.ts +14 -0
- package/types/lib/api-key-provider.d.ts +31 -0
- package/types/lib/env.d.ts +14 -0
- package/types/lib/phase-tracker.d.ts +146 -0
- package/types/sessions/batch.d.ts +67 -0
- package/types/sessions/diffs.d.ts +26 -0
- package/types/sessions/dom-history.d.ts +119 -0
- package/types/sessions/dom-search.d.ts +113 -0
- package/types/sessions/dom.d.ts +263 -0
- package/types/sessions/match.d.ts +72 -0
- package/types/sessions/regions.d.ts +171 -0
- package/types/sessions/session.d.ts +97 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export interface Rect {
|
|
2
|
+
left: number;
|
|
3
|
+
top: number;
|
|
4
|
+
width: number;
|
|
5
|
+
height: number;
|
|
6
|
+
}
|
|
7
|
+
export declare function parseRect(s: string): Rect;
|
|
8
|
+
/** Inverse of `parseRect` — formats a rect back into `"left,top,width,height"`. */
|
|
9
|
+
export declare function formatRect(rect: Rect): string;
|
|
10
|
+
/**
|
|
11
|
+
* Returns true if outer completely contains inner (all edges of inner are within outer).
|
|
12
|
+
* Equal rectangles are considered to contain each other.
|
|
13
|
+
*/
|
|
14
|
+
export declare function rectContains(outer: Rect, inner: Rect): boolean;
|
|
15
|
+
export declare function rectsIntersect(a: Rect, b: Rect): boolean;
|
|
16
|
+
export declare function intersectionArea(a: Rect, b: Rect): number;
|
|
17
|
+
export declare function boundingRect(a: Rect, b: Rect): Rect;
|
|
18
|
+
/**
|
|
19
|
+
* Expands a rect by `padding` pixels on every side.
|
|
20
|
+
* When `imageWidth`/`imageHeight` are provided the result is clipped to the image bounds.
|
|
21
|
+
*/
|
|
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. */
|
|
24
|
+
export declare function unifyRects(rects: Rect[]): Rect[];
|
package/types/index.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export * from './lib/api-key-provider.js';
|
|
2
|
+
export * from './lib/env.js';
|
|
3
|
+
export * from './lib/phase-tracker.js';
|
|
4
|
+
export * from './geometry/geometry.js';
|
|
5
|
+
export * from './sessions/batch.js';
|
|
6
|
+
export * from './sessions/diffs.js';
|
|
7
|
+
export * from './sessions/dom-history.js';
|
|
8
|
+
export * from './sessions/dom-search.js';
|
|
9
|
+
export * from './sessions/dom.js';
|
|
10
|
+
export * from './sessions/match.js';
|
|
11
|
+
export * from './sessions/regions.js';
|
|
12
|
+
export * from './sessions/session.js';
|
|
13
|
+
export * from './api/api.js';
|
|
14
|
+
export * from './common/common.js';
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A source of API keys, so a caller threads one object through a call chain instead of choosing
|
|
3
|
+
* and passing the right raw key string itself at every call site — the read/write (and, for a
|
|
4
|
+
* future run-triggering flow, run) distinction is made once, by whoever constructs the provider,
|
|
5
|
+
* not re-decided by every function that happens to need one. A function that only ever needs one
|
|
6
|
+
* specific kind of key calls that one getter and never sees the others, so passing a provider
|
|
7
|
+
* built for the wrong purpose fails at the point of use rather than silently substituting the
|
|
8
|
+
* wrong credential.
|
|
9
|
+
*/
|
|
10
|
+
export interface ApiKeyProvider {
|
|
11
|
+
getReadKey(): string;
|
|
12
|
+
getWriteKey(): string;
|
|
13
|
+
getRunKey(): string;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Wraps already-resolved key strings. A getter throws a clear error if its key wasn't supplied —
|
|
17
|
+
* e.g. a write key deliberately withheld when building a provider for a read-only call — rather
|
|
18
|
+
* than returning `undefined` and letting a missing credential fail confusingly further down.
|
|
19
|
+
*/
|
|
20
|
+
export declare class StaticApiKeyProvider implements ApiKeyProvider {
|
|
21
|
+
private readonly keys;
|
|
22
|
+
constructor(keys: {
|
|
23
|
+
readKey?: string;
|
|
24
|
+
writeKey?: string;
|
|
25
|
+
runKey?: string;
|
|
26
|
+
});
|
|
27
|
+
getReadKey(): string;
|
|
28
|
+
getWriteKey(): string;
|
|
29
|
+
getRunKey(): string;
|
|
30
|
+
private require;
|
|
31
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
export interface LoadEnvDeps {
|
|
3
|
+
existsSync: typeof fs.existsSync;
|
|
4
|
+
configDotenv: (options: {
|
|
5
|
+
path: string;
|
|
6
|
+
}) => unknown;
|
|
7
|
+
}
|
|
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;
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A generic, eyes-agnostic completion registry backing eyes-inspect's workflow tools (and,
|
|
3
|
+
* eventually, eyes-resolve's own batch-review phases) — persisted to the filesystem so a
|
|
4
|
+
* restarted server picks an in-progress investigation back up with no special "resume"
|
|
5
|
+
* handling. Every id is an opaque string a caller constructs deterministically from its own
|
|
6
|
+
* natural identifiers (a batch/session URL, a step index, a rect) — this module has no
|
|
7
|
+
* knowledge of what an id represents, only that the same input always hashes to the same
|
|
8
|
+
* record.
|
|
9
|
+
*
|
|
10
|
+
* Alongside that registry sits a second, unrelated facility on the same filesystem substrate:
|
|
11
|
+
* `log`, a generic call-trace log any caller can append a labeled entry to — see its own doc
|
|
12
|
+
* comment.
|
|
13
|
+
*/
|
|
14
|
+
export interface PhaseTrackerDeps {
|
|
15
|
+
readFile: (filePath: string) => string | null;
|
|
16
|
+
writeFile: (filePath: string, contents: string) => void;
|
|
17
|
+
/** Appends `contents` to `filePath`, creating it if absent — never reads first. */
|
|
18
|
+
appendFile: (filePath: string, contents: string) => void;
|
|
19
|
+
/** Removes `filePath` if it exists — a no-op if it doesn't. */
|
|
20
|
+
deleteFile: (filePath: string) => void;
|
|
21
|
+
mkdir: (dirPath: string) => void;
|
|
22
|
+
/** Blocks the current call for `ms` milliseconds — swapped out in tests to skip real delays. */
|
|
23
|
+
sleep: (ms: number) => void;
|
|
24
|
+
/** Root directory every record is written under, partitioned into completions/child-findings/invocations/modes/children/unready-children subdirectories, plus a debug subdirectory holding one log file per scope id a caller has logged under. */
|
|
25
|
+
stateDir: string;
|
|
26
|
+
}
|
|
27
|
+
export declare const defaultPhaseTrackerDeps: PhaseTrackerDeps;
|
|
28
|
+
export interface Completion<T = unknown> {
|
|
29
|
+
payload: T;
|
|
30
|
+
completedAt: string;
|
|
31
|
+
}
|
|
32
|
+
/** The recorded completion for `id`, or `undefined` if `<level>_end` was never (successfully) called for it. */
|
|
33
|
+
export declare function getCompletion<T = unknown>(id: string, deps?: PhaseTrackerDeps): Completion<T> | undefined;
|
|
34
|
+
export declare function isCompleted(id: string, deps?: PhaseTrackerDeps): boolean;
|
|
35
|
+
/**
|
|
36
|
+
* Persists `payload` as `id`'s completion, stamped with the current time — overwriting
|
|
37
|
+
* whatever was recorded for `id` before, so revising a summary (or refreshing a stale
|
|
38
|
+
* completion) is just calling this again.
|
|
39
|
+
*/
|
|
40
|
+
export declare function recordCompletion<T>(id: string, payload: T, deps?: PhaseTrackerDeps): Completion<T>;
|
|
41
|
+
/** `parentId`'s full accumulated child-findings record, read as-is with no live-data filtering. `{}` if no child has ever reported into it. */
|
|
42
|
+
export declare function getChildFindings<T = unknown>(parentId: string, deps?: PhaseTrackerDeps): Record<string, T>;
|
|
43
|
+
/**
|
|
44
|
+
* Writes (or overwrites) `childId`'s entry in `parentId`'s accumulated findings record,
|
|
45
|
+
* creating that record the first time any child reports into it. Called once a child's own
|
|
46
|
+
* `_end` succeeds — independent of whether `childId` is still among `parentId`'s currently-live
|
|
47
|
+
* children, so a child that later drops out of the live set never loses its findings.
|
|
48
|
+
*/
|
|
49
|
+
export declare function appendChildFinding<T>(parentId: string, childId: string, payload: T, deps?: PhaseTrackerDeps): void;
|
|
50
|
+
export interface InvocationLogEntry {
|
|
51
|
+
toolName: string;
|
|
52
|
+
args: unknown;
|
|
53
|
+
at: string;
|
|
54
|
+
}
|
|
55
|
+
/** Appends one entry to `scopeId`'s invocation log — called as a side effect of every eyes-inspect data tool. */
|
|
56
|
+
export declare function logInvocation(scopeId: string, toolName: string, args: unknown, deps?: PhaseTrackerDeps): void;
|
|
57
|
+
/** Whether `scopeId`'s invocation log contains a call to `toolName` whose `args` satisfy `matcher` — the per-item invocation-verification check backing `step_end`/`diff_end`'s hard gates. */
|
|
58
|
+
export declare function verifyInvocation(scopeId: string, toolName: string, matcher: (args: unknown) => boolean, deps?: PhaseTrackerDeps): boolean;
|
|
59
|
+
/** Persists `mode` as the mode `scopeId` is currently being reviewed in — overwriting whatever was recorded before, so re-entering with the same mode simply re-states it. */
|
|
60
|
+
export declare function recordScopeMode(scopeId: string, mode: string, deps?: PhaseTrackerDeps): void;
|
|
61
|
+
/** The mode last recorded for `scopeId` via `recordScopeMode`, or `undefined` if never recorded. */
|
|
62
|
+
export declare function getScopeMode(scopeId: string, deps?: PhaseTrackerDeps): string | undefined;
|
|
63
|
+
/** Discards `id`'s completion, as if `recordCompletion` had never been called for it. */
|
|
64
|
+
export declare function clearCompletion(id: string, deps?: PhaseTrackerDeps): void;
|
|
65
|
+
/** Discards `parentId`'s entire accumulated child-findings record — every child's entry, not just one. */
|
|
66
|
+
export declare function clearChildFindings(parentId: string, deps?: PhaseTrackerDeps): void;
|
|
67
|
+
/** Discards `scopeId`'s recorded mode, as if `recordScopeMode` had never been called for it. */
|
|
68
|
+
export declare function clearScopeMode(scopeId: string, deps?: PhaseTrackerDeps): void;
|
|
69
|
+
/** Persists the keys of `scopeId`'s children still unready to be finalized, as of its own latest live check — overwriting whatever was recorded before, so a child that has since become ready (or a newly-discovered unready one) is reflected exactly, never accumulated across checks. */
|
|
70
|
+
export declare function recordUnreadyChildren(scopeId: string, childKeys: string[], deps?: PhaseTrackerDeps): void;
|
|
71
|
+
/** The child keys `recordUnreadyChildren` last recorded as unready for `scopeId`, or `[]` if never recorded. */
|
|
72
|
+
export declare function getUnreadyChildren(scopeId: string, deps?: PhaseTrackerDeps): string[];
|
|
73
|
+
/** Discards `scopeId`'s recorded unready-children snapshot, as if `recordUnreadyChildren` had never been called for it. */
|
|
74
|
+
export declare function clearUnreadyChildren(scopeId: string, deps?: PhaseTrackerDeps): void;
|
|
75
|
+
/** `scopeId`'s queued child keys — the stable, append-only set of children discovered live so far, investigated from regardless of what a later live fetch would show. `[]` if none have ever been queued. */
|
|
76
|
+
export declare function getQueuedChildKeys(scopeId: string, deps?: PhaseTrackerDeps): string[];
|
|
77
|
+
/**
|
|
78
|
+
* Appends any of `liveKeys` not already queued, preserving the existing queue's order with
|
|
79
|
+
* newcomers appended in their given order — never removes a key, so a child that later drops out
|
|
80
|
+
* of a live fetch (resolved via propagation, or otherwise) stays queued and gets visited. Returns
|
|
81
|
+
* the full merged queue; writes only when something actually changed.
|
|
82
|
+
*/
|
|
83
|
+
export declare function queueChildKeys(scopeId: string, liveKeys: string[], deps?: PhaseTrackerDeps): string[];
|
|
84
|
+
/** Discards `scopeId`'s entire queued-children record, as if nothing had ever been queued for it. */
|
|
85
|
+
export declare function clearQueuedChildKeys(scopeId: string, deps?: PhaseTrackerDeps): void;
|
|
86
|
+
/**
|
|
87
|
+
* Persists `payload` as everything `scopeId`'s own live fetch most recently discovered — e.g. a
|
|
88
|
+
* batch's full session list, expensive to fetch and shared by everything nested under it —
|
|
89
|
+
* overwriting whatever was recorded before, so a caller that decides to refresh simply calls this
|
|
90
|
+
* again. Unlike `queueChildKeys`/`getQueuedChildKeys` (a stable, append-only set of keys a caller
|
|
91
|
+
* has committed to investigating), this is the raw discovery result itself — including entries
|
|
92
|
+
* (e.g. still-`Running` sessions) that may not be ready to investigate at all — so a lower-level
|
|
93
|
+
* scope nested underneath `scopeId` can re-derive its own children from it without repeating the
|
|
94
|
+
* fetch.
|
|
95
|
+
*/
|
|
96
|
+
export declare function recordDiscoveredChildren<T>(scopeId: string, payload: T, deps?: PhaseTrackerDeps): void;
|
|
97
|
+
/** What `recordDiscoveredChildren` last recorded for `scopeId`, or `undefined` if never recorded. */
|
|
98
|
+
export declare function getDiscoveredChildren<T>(scopeId: string, deps?: PhaseTrackerDeps): T | undefined;
|
|
99
|
+
/** Discards `scopeId`'s recorded discovery, as if `recordDiscoveredChildren` had never been called for it. */
|
|
100
|
+
export declare function clearDiscoveredChildren(scopeId: string, deps?: PhaseTrackerDeps): void;
|
|
101
|
+
/**
|
|
102
|
+
* The exact filesystem path `log` appends `scopeId`'s entries to — a caller (or a human trying to
|
|
103
|
+
* find the right file by hand) never has to reimplement this module's own hashing scheme just to
|
|
104
|
+
* locate it.
|
|
105
|
+
*/
|
|
106
|
+
export declare function getLogFilePath(scopeId: string, deps?: PhaseTrackerDeps): string;
|
|
107
|
+
export interface DebugLogEntry {
|
|
108
|
+
at: string;
|
|
109
|
+
label: string;
|
|
110
|
+
input?: unknown;
|
|
111
|
+
output?: unknown;
|
|
112
|
+
error?: unknown;
|
|
113
|
+
message?: unknown;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* A generic call-trace log: any caller can record one labeled entry, with no fixed vocabulary for
|
|
117
|
+
* what a call even is — `label` is whatever convention the caller adopts (today, resolution.ts
|
|
118
|
+
* labels each entry with the traced function's own name; a wholly different caller could start
|
|
119
|
+
* labeling its own entries tomorrow with no change here). Appends to `scopeId`'s own log file,
|
|
120
|
+
* partitioned by hashed id the same way every other record in this module is — `scopeId` is just
|
|
121
|
+
* as opaque to this module as everywhere else it appears, so it's entirely the caller's own choice
|
|
122
|
+
* whether to pass the same id (e.g. an investigation's root) on every call, consolidating every
|
|
123
|
+
* entry raised anywhere within it into one file, or a more granular one per call, splitting the
|
|
124
|
+
* trace up instead. `label` is the only other required field; `input`/`output`/`error`/`message`
|
|
125
|
+
* are all optional and independent — a call-and-response entry sets `input`/`output`, a failed one
|
|
126
|
+
* sets `error` (instead of stuffing it into `output`), and anything that isn't a call at all (a
|
|
127
|
+
* one-off event, a snapshot) just sets `message` to whatever shape fits. Every entry lands as one
|
|
128
|
+
* line (a JSON-encoded `DebugLogEntry`) in a single NDJSON file at `getLogFilePath(scopeId, deps)`.
|
|
129
|
+
* There is no reader function: this is meant to be opened directly (as a file on disk, or via
|
|
130
|
+
* `deps.readFile` in a test) rather than queried back through this module.
|
|
131
|
+
*
|
|
132
|
+
* Safe under concurrent callers by construction, not by locking: this never reads the file before
|
|
133
|
+
* appending (so there's no read-modify-write window for two callers to race), and each entry is
|
|
134
|
+
* written as a single `appendFile` call — opening for append and writing is one atomic
|
|
135
|
+
* seek-to-end-and-write at the OS level, so concurrent appends interleave whole lines and never
|
|
136
|
+
* corrupt or split one another's. The only remaining failure mode is a transient lock on the file
|
|
137
|
+
* itself (e.g. a concurrent antivirus scan) — `mkdir`/`appendFile` are wrapped in the same
|
|
138
|
+
* lock-retry backoff (`withRetry`) as every other record this module writes, so a caller never
|
|
139
|
+
* has to poll for the file to become available itself; a momentarily-busy append just retries.
|
|
140
|
+
*/
|
|
141
|
+
export declare function log(scopeId: string, label: string, fields?: {
|
|
142
|
+
input?: unknown;
|
|
143
|
+
output?: unknown;
|
|
144
|
+
error?: unknown;
|
|
145
|
+
message?: unknown;
|
|
146
|
+
}, deps?: PhaseTrackerDeps): void;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { SimplifiedSession } from './session.js';
|
|
2
|
+
/**
|
|
3
|
+
* The slice of the raw batch entity's fields `toBatchStats` reads. The real response has
|
|
4
|
+
* many more fields (name, timing, `properties`, etc.) — irrelevant here, so left untyped.
|
|
5
|
+
*/
|
|
6
|
+
export interface BatchInfo {
|
|
7
|
+
completedCount: number;
|
|
8
|
+
runningCount: number;
|
|
9
|
+
abortedCount: number;
|
|
10
|
+
passedCount: number;
|
|
11
|
+
failedCount: number;
|
|
12
|
+
unresolvedCount: number;
|
|
13
|
+
changedCount: number;
|
|
14
|
+
stepsCount: number;
|
|
15
|
+
unresolvedStepsCount: number;
|
|
16
|
+
changedStepsCount: number;
|
|
17
|
+
missingStepsCount: number;
|
|
18
|
+
newStepsCount: number;
|
|
19
|
+
}
|
|
20
|
+
export interface BatchStats {
|
|
21
|
+
total: number;
|
|
22
|
+
unresolved: number;
|
|
23
|
+
passed: number;
|
|
24
|
+
failed: number;
|
|
25
|
+
aborted: number;
|
|
26
|
+
unsaved: number;
|
|
27
|
+
totalSteps: number;
|
|
28
|
+
unresolvedSteps: number;
|
|
29
|
+
unsavedSteps: number;
|
|
30
|
+
missingSteps: number;
|
|
31
|
+
newSteps: number;
|
|
32
|
+
}
|
|
33
|
+
/** Shapes a raw batch entity's fields (see `BatchInfo` — fetched via `api`'s `fetchBatchInfo`) into `BatchStats`. */
|
|
34
|
+
export declare function toBatchStats(raw: Partial<BatchInfo>): BatchStats;
|
|
35
|
+
/**
|
|
36
|
+
* Computes the same `BatchStats` shape as `toBatchStats`, but by counting over a set of
|
|
37
|
+
* already-`simplifySession`-shaped sessions (`sessions/session`) instead of reading the
|
|
38
|
+
* (eventually consistent) batch aggregate — so it's always current, at the cost of only
|
|
39
|
+
* reflecting whatever subset of sessions `sessions` contains (the whole batch, or an
|
|
40
|
+
* already-filtered slice of it).
|
|
41
|
+
*
|
|
42
|
+
* Takes `SimplifiedSession[]`, not raw sessions, for two reasons: it reuses whatever
|
|
43
|
+
* `simplifySession` call the caller already made (e.g. to also build a session listing)
|
|
44
|
+
* instead of re-simplifying every session a second time just for this; and it counts a
|
|
45
|
+
* step's `resolution`/`isUnsaved` exactly as `simplifySession` derived them, with no second,
|
|
46
|
+
* possibly-diverging notion of what "resolved" means computed from the raw
|
|
47
|
+
* `appOutputResolution` fields directly. Every entry in `sessions` must have been simplified
|
|
48
|
+
* with `steps` included (the default) — one shaped with `{ includeSteps: false }` (`steps:
|
|
49
|
+
* null`) would silently contribute 0 to every step-level count.
|
|
50
|
+
*/
|
|
51
|
+
export declare function computeBatchStats(sessions: SimplifiedSession[]): BatchStats;
|
|
52
|
+
export declare const BATCH_FILTERS: readonly ["unresolved", "unsaved", "mismatching"];
|
|
53
|
+
export type BatchFilter = (typeof BATCH_FILTERS)[number];
|
|
54
|
+
/**
|
|
55
|
+
* Narrows `sessions` to those matching `filter` — every session, unfiltered, when `filter` is
|
|
56
|
+
* omitted. `"mismatching"` keeps only sessions with at least one step that's `isMatching: false`,
|
|
57
|
+
* missing a checkpoint (`checkpoint.id === null` — the test stopped capturing this checkpoint),
|
|
58
|
+
* or new (`baseline.id === null` — the test started capturing a checkpoint with no prior baseline
|
|
59
|
+
* to compare against) — a missing/new step still needs a decision even though its own
|
|
60
|
+
* `isMatching` isn't a reliable signal either way, so it counts regardless of what `isMatching`
|
|
61
|
+
* reads. Also regardless of a step's own `resolution`: `isMatching` is fixed at comparison time
|
|
62
|
+
* and never changes afterward, including once a step is accepted/rejected and saved to the
|
|
63
|
+
* baseline, so it stays a reliable "did this ever differ" signal rather than one a decision
|
|
64
|
+
* clears. Requires `sessions` to have been simplified with `steps` included (the default); one
|
|
65
|
+
* shaped with `{ includeSteps: false }` never matches (`steps` is `null`).
|
|
66
|
+
*/
|
|
67
|
+
export declare function filterSessions(sessions: SimplifiedSession[], filter?: BatchFilter): SimplifiedSession[];
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { type Rect } from '../geometry/geometry.js';
|
|
2
|
+
export interface DiffArea extends Rect {
|
|
3
|
+
diffCount: number;
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* Returns coarse page areas containing diffs. Each rect is expanded to at least
|
|
7
|
+
* `hPad`×`vPad` (centered) before checking for overlap; overlapping inflated rects
|
|
8
|
+
* are merged into the bounding rect of their originals (not the inflated ones).
|
|
9
|
+
*/
|
|
10
|
+
export declare function diffAreas(rects: Rect[], hPad?: number, vPad?: number): DiffArea[];
|
|
11
|
+
/**
|
|
12
|
+
* Condenses a large number of DiffAreas into grouped ContainerDiffArea objects.
|
|
13
|
+
* When the number of areas is <= threshold, returns the original array unchanged.
|
|
14
|
+
*
|
|
15
|
+
* Each area is assigned to every grid cell it overlaps. A cell is dense when the
|
|
16
|
+
* total area of diff pixels covering it meets DENSE_COVERAGE (ratio to cell area).
|
|
17
|
+
* Dense cells are flood-filled into connected components, each producing a single
|
|
18
|
+
* ContainerDiffArea bounding box. Adjacent containers (gap ≤ 1 cell, or one contains
|
|
19
|
+
* the other) are merged. Finally, any unclaimed area whose rect falls completely inside
|
|
20
|
+
* a container is absorbed into it.
|
|
21
|
+
*/
|
|
22
|
+
export declare function condenseDiffAreas(areas: DiffArea[], threshold?: number): DiffArea[];
|
|
23
|
+
export declare function condenseDiffAreasDetailed(areas: DiffArea[], threshold?: number): {
|
|
24
|
+
containers: DiffArea[];
|
|
25
|
+
individuals: DiffArea[];
|
|
26
|
+
};
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { type DomAlignment } from './dom.js';
|
|
2
|
+
/** One older DOM capture to compare a suspect node against — labeled and dated, but otherwise
|
|
3
|
+
* agnostic to where it came from (a past test session today; possibly a past baseline version
|
|
4
|
+
* later — see this package's README). Despite the name, this is always a *checkpoint* DOM in
|
|
5
|
+
* the Visual Testing sense (the actual rendering of some past run), never a "baseline" (the
|
|
6
|
+
* expected/reference image) in that sense — "historic" here is about *when* the capture
|
|
7
|
+
* happened, an entirely different axis. `step` is an opaque, caller-constructed reference back
|
|
8
|
+
* to where this capture came from — e.g. `eyes-inspect`'s `history.ts` builds it as
|
|
9
|
+
* `<batchId>/<sessionId>/<stepIndex>` so the reader can navigate back to that exact step; this
|
|
10
|
+
* module never inspects or parses it, only carries it through to the output. `domId` identifies
|
|
11
|
+
* this capture to `FetchAlignmentDeps.fetchAlignment` — always passed as its baseline-role
|
|
12
|
+
* argument — see `HistoricDomNodeDiffEntry`, whose own `baselineStep`/`baselineCapturedAt`
|
|
13
|
+
* fields are copied from here and named to match. */
|
|
14
|
+
export interface HistoricDom {
|
|
15
|
+
step: string;
|
|
16
|
+
capturedAt: string;
|
|
17
|
+
domId: string;
|
|
18
|
+
}
|
|
19
|
+
/** One non-empty diff for a suspect node against one older historic version. `checkpointStep`/
|
|
20
|
+
* `checkpointCapturedAt` and `baselineStep`/`baselineCapturedAt` identify and date the two sides
|
|
21
|
+
* actually compared for this entry — the tracker's anchor at the time (the current step, on the
|
|
22
|
+
* first hop that ever produces a diff; an intermediate historic version thereafter) and the
|
|
23
|
+
* `HistoricDom` it was diffed against, respectively. `baselineStep`/`baselineCapturedAt` are
|
|
24
|
+
* named to match the `diff` string's own embedded `"baseline"` key (`serializeDiff`
|
|
25
|
+
* always labels its first/older argument that way), so the two agree rather than
|
|
26
|
+
* describing the same referent with two different words — but neither side is a literal Visual
|
|
27
|
+
* Testing baseline; both are checkpoint DOMs, just from different points in time. */
|
|
28
|
+
export interface HistoricDomNodeDiffEntry {
|
|
29
|
+
checkpointStep: string;
|
|
30
|
+
checkpointCapturedAt: string;
|
|
31
|
+
baselineStep: string;
|
|
32
|
+
baselineCapturedAt: string;
|
|
33
|
+
diff: string;
|
|
34
|
+
}
|
|
35
|
+
export interface HistoricDomNodeEvidence {
|
|
36
|
+
nodeId: string;
|
|
37
|
+
diffs: HistoricDomNodeDiffEntry[];
|
|
38
|
+
}
|
|
39
|
+
/** Mutable per-node accumulator threaded through repeated `applyHistoricDom` calls. */
|
|
40
|
+
export interface HistoricDomNodeTracker {
|
|
41
|
+
nodeId: string;
|
|
42
|
+
/** The most recently matched node id/domId pair — the chain's current position. Seeded from
|
|
43
|
+
* the current step; only ever moves when a hop actually finds a counterpart for it. A hop
|
|
44
|
+
* that finds none leaves this untouched, so the same anchor is retried against the next
|
|
45
|
+
* (older) historic candidate rather than falling back to some other reference point. Kept as
|
|
46
|
+
* a plain id, not the node object itself — the object is never needed again once
|
|
47
|
+
* `extractNodeDiff` has used it to read the match/rect it needs off the alignment. */
|
|
48
|
+
anchorDomId: string;
|
|
49
|
+
anchorNodeId: string;
|
|
50
|
+
/** Identifies/dates whatever `anchorNodeId` currently refers to — the current step at first,
|
|
51
|
+
* then whichever historic version the anchor most recently advanced to. Copied onto a diff
|
|
52
|
+
* entry's `checkpointStep`/`checkpointCapturedAt` so the newer side of a comparison is
|
|
53
|
+
* traceable even when it isn't the current step (e.g. after the anchor has advanced past one
|
|
54
|
+
* or more hops, or held through a skipped miss). */
|
|
55
|
+
anchorStep: string;
|
|
56
|
+
anchorCapturedAt: string;
|
|
57
|
+
diffs: HistoricDomNodeDiffEntry[];
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* The one piece of I/O `applyHistoricDom` needs — aligning two already-identified DOM captures
|
|
61
|
+
* by `domId` — injected rather than imported directly, since this package has no network or
|
|
62
|
+
* filesystem awareness of its own. A caller wires this to whatever aligns-and-caches two domIds
|
|
63
|
+
* for it (e.g. `api`'s `fetchDomDiff`/`checkDomDiffTask` driven to a ready alignment, closed over
|
|
64
|
+
* its own `baseUrl`/`keys`); no default is provided here, since no such implementation is within
|
|
65
|
+
* this package's own reach.
|
|
66
|
+
*/
|
|
67
|
+
export interface FetchAlignmentDeps {
|
|
68
|
+
fetchAlignment: (baselineDomId: string, checkpointDomId: string) => Promise<DomAlignment>;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Seeds one tracker per suspect nodeId, anchored at `currentDomId`. Doesn't verify the nodeId
|
|
72
|
+
* actually exists in the current DOM — that would need fetching the whole tree just to check one
|
|
73
|
+
* id — so a bad id is simply carried through and only surfaces once (if) a real historic
|
|
74
|
+
* candidate is actually walked (`applyHistoricDom` catches it there for free, off an alignment it
|
|
75
|
+
* already has to compute). `currentStep`/`currentCapturedAt` identify and date the tracker's
|
|
76
|
+
* initial anchor, so the very first diff entry (if any) correctly attributes its checkpoint side
|
|
77
|
+
* to the current step rather than leaving it implicit.
|
|
78
|
+
*/
|
|
79
|
+
export declare function createHistoricDomNodeTrackers(currentDomId: string, nodeIds: string[], currentStep: string, currentCapturedAt: string): HistoricDomNodeTracker[];
|
|
80
|
+
/**
|
|
81
|
+
* Applies exactly one older `HistoricDom` to every tracker that still has fewer than
|
|
82
|
+
* `targetDiffCount` non-empty diffs, mutating each in place — the unit of lazy evaluation a
|
|
83
|
+
* caller feeds historic DOMs into one at a time as it discovers them (see `history.ts`'s
|
|
84
|
+
* `walkHistoricSources`), so it can stop pulling further history the moment every tracker has
|
|
85
|
+
* enough evidence, rather than pre-fetching a fixed batch before knowing how much is needed.
|
|
86
|
+
*
|
|
87
|
+
* Each hop diffs a tracker's current anchor against `historicDom.domId` via a *global* alignment
|
|
88
|
+
* between the two full trees (`deps.fetchAlignment`/`extractNodeDiff`), not a standalone search
|
|
89
|
+
* for the anchor alone — a single-node content search would chase a content lookalike elsewhere
|
|
90
|
+
* on the page (e.g. a rotating carousel slot, or a live counter) rather than the element that
|
|
91
|
+
* actually took its place. When the alignment finds a counterpart, that counterpart becomes the
|
|
92
|
+
* anchor for the next (older) hop, so the walk tracks the same structural slot across a long
|
|
93
|
+
* history one hop at a time. Trackers sharing the same anchor domId are aligned against
|
|
94
|
+
* `historicDom.domId` together, once, via `groupByAnchorDomId` — the alignment itself doesn't
|
|
95
|
+
* depend on which specific node is being asked about, only on the pair of domIds, so paying for
|
|
96
|
+
* it once per distinct pair rather than once per tracker avoids pure waste (and, since
|
|
97
|
+
* `deps.fetchAlignment` is typically itself cached by domId pair, this also means a hop already
|
|
98
|
+
* paid for elsewhere — e.g. by `eyes_inspect_dom_diff`'s own two-sided diff — is never
|
|
99
|
+
* recomputed here either).
|
|
100
|
+
*
|
|
101
|
+
* When a hop's alignment finds no counterpart for the current anchor, that doesn't mean the
|
|
102
|
+
* anchor itself is wrong — it may just mean this one historic candidate is a poor comparison
|
|
103
|
+
* point (an unrelated element elsewhere on the page stealing the alignment's matching capacity,
|
|
104
|
+
* a transient loading state, and so on). Rather than reverting to some other reference node,
|
|
105
|
+
* a miss is simply skipped: nothing is recorded, and the anchor is left untouched, so the very
|
|
106
|
+
* same anchor is retried against the next (older) candidate the next time this is called. The
|
|
107
|
+
* chain resumes forward from wherever it next finds a match.
|
|
108
|
+
*
|
|
109
|
+
* A tracker whose `anchorNodeId` doesn't exist at all in whichever tree just played the
|
|
110
|
+
* checkpoint role (`extractNodeDiff`'s `found: false`) is different from a poor-comparison miss —
|
|
111
|
+
* that's `createHistoricDomNodeTrackers` never having verified the caller's nodeId, surfacing now
|
|
112
|
+
* for the first time. Every such tracker across the whole call is collected and reported in one
|
|
113
|
+
* combined error, rather than silently left to retry forever.
|
|
114
|
+
*/
|
|
115
|
+
export declare function applyHistoricDom(trackers: HistoricDomNodeTracker[], historicDom: HistoricDom, targetDiffCount: number, deps: FetchAlignmentDeps): Promise<void>;
|
|
116
|
+
/** Strips tracker-only bookkeeping (the chain anchor) down to the plain
|
|
117
|
+
* `{nodeId, diffs: [{checkpointStep, checkpointCapturedAt, baselineStep, baselineCapturedAt, diff}]}`
|
|
118
|
+
* output shape. */
|
|
119
|
+
export declare function finalizeHistoricDomNodeEvidence(trackers: HistoricDomNodeTracker[]): HistoricDomNodeEvidence[];
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
type RelKind = 'descendant' | 'ancestor' | 'child' | 'parent' | 'sibling' | 'nthChild';
|
|
2
|
+
type RelTarget = {
|
|
3
|
+
type: 'id';
|
|
4
|
+
id: string;
|
|
5
|
+
} | {
|
|
6
|
+
type: 'expr';
|
|
7
|
+
ast: PredicateAST;
|
|
8
|
+
};
|
|
9
|
+
type BoundsTarget = {
|
|
10
|
+
type: 'id';
|
|
11
|
+
id: string;
|
|
12
|
+
} | {
|
|
13
|
+
type: 'point';
|
|
14
|
+
x: number;
|
|
15
|
+
y: number;
|
|
16
|
+
} | {
|
|
17
|
+
type: 'rect';
|
|
18
|
+
rect: {
|
|
19
|
+
left: number;
|
|
20
|
+
top: number;
|
|
21
|
+
width: number;
|
|
22
|
+
height: number;
|
|
23
|
+
};
|
|
24
|
+
};
|
|
25
|
+
type SizeOpKind = 'larger' | 'smaller' | 'wider' | 'narrower' | 'taller' | 'shorter';
|
|
26
|
+
type SizeTarget = {
|
|
27
|
+
type: 'id';
|
|
28
|
+
id: string;
|
|
29
|
+
} | {
|
|
30
|
+
type: 'rect';
|
|
31
|
+
rect: {
|
|
32
|
+
left: number;
|
|
33
|
+
top: number;
|
|
34
|
+
width: number;
|
|
35
|
+
height: number;
|
|
36
|
+
};
|
|
37
|
+
} | {
|
|
38
|
+
type: 'size';
|
|
39
|
+
width: number;
|
|
40
|
+
height: number;
|
|
41
|
+
} | {
|
|
42
|
+
type: 'value';
|
|
43
|
+
value: number;
|
|
44
|
+
};
|
|
45
|
+
type DirOpKind = 'above' | 'below' | 'leftOf' | 'rightOf';
|
|
46
|
+
type DirTarget = {
|
|
47
|
+
type: 'id';
|
|
48
|
+
id: string;
|
|
49
|
+
} | {
|
|
50
|
+
type: 'rect';
|
|
51
|
+
rect: {
|
|
52
|
+
left: number;
|
|
53
|
+
top: number;
|
|
54
|
+
width: number;
|
|
55
|
+
height: number;
|
|
56
|
+
};
|
|
57
|
+
};
|
|
58
|
+
type CompareOpKey = 'eq' | 'neq' | 'lt' | 'lte' | 'gt' | 'gte' | 'startsWith' | 'endsWith' | 'contains' | 'in' | 'regex';
|
|
59
|
+
export type PredicateAST = {
|
|
60
|
+
type: 'or';
|
|
61
|
+
left: PredicateAST;
|
|
62
|
+
right: PredicateAST;
|
|
63
|
+
} | {
|
|
64
|
+
type: 'and';
|
|
65
|
+
left: PredicateAST;
|
|
66
|
+
right: PredicateAST;
|
|
67
|
+
} | {
|
|
68
|
+
type: 'not';
|
|
69
|
+
inner: PredicateAST;
|
|
70
|
+
} | {
|
|
71
|
+
type: 'bounds';
|
|
72
|
+
op: 'contains' | 'intersects' | 'containedBy';
|
|
73
|
+
target: BoundsTarget;
|
|
74
|
+
} | {
|
|
75
|
+
type: 'rel';
|
|
76
|
+
kind: RelKind;
|
|
77
|
+
ordinal?: number;
|
|
78
|
+
target: RelTarget;
|
|
79
|
+
} | {
|
|
80
|
+
type: 'size';
|
|
81
|
+
kind: SizeOpKind;
|
|
82
|
+
target: SizeTarget;
|
|
83
|
+
} | {
|
|
84
|
+
type: 'dir';
|
|
85
|
+
kind: DirOpKind;
|
|
86
|
+
target: DirTarget;
|
|
87
|
+
} | {
|
|
88
|
+
type: 'prop';
|
|
89
|
+
path: string[];
|
|
90
|
+
op: CompareOpKey;
|
|
91
|
+
value: string | string[] | RegExp;
|
|
92
|
+
} | {
|
|
93
|
+
type: 'nullary';
|
|
94
|
+
name: string;
|
|
95
|
+
} | {
|
|
96
|
+
type: 'hasAttr';
|
|
97
|
+
name: string;
|
|
98
|
+
};
|
|
99
|
+
export declare function parsePredicate(source: string): PredicateAST;
|
|
100
|
+
export declare function astNeedsIndex(ast: PredicateAST): boolean;
|
|
101
|
+
/**
|
|
102
|
+
* Searches a DOM capture for elements matching a boolean predicate. See `llm-docs/dom-search.md`
|
|
103
|
+
* in this package (shipped in the tarball) for the full predicate language documentation (grammar,
|
|
104
|
+
* operators, examples) — that file is the canonical, copy-paste-friendly source; don't re-describe
|
|
105
|
+
* the grammar here or anywhere else, keep it in sync by editing it and copying it where it's
|
|
106
|
+
* needed instead.
|
|
107
|
+
*
|
|
108
|
+
* @param domCaptureJson Raw JSON text of a DOM capture file (as written by eyes-inspect's `dom` command) — a single node, or an array of nodes.
|
|
109
|
+
* @param predicateSource The predicate string described in `llm-docs/dom-search.md`.
|
|
110
|
+
* @returns One line per matching element, most indented lines the deepest — each line is that element's own properties (minus `childNodes`) as JSON, with a trailing `# <text>` comment when it has non-empty text.
|
|
111
|
+
*/
|
|
112
|
+
export declare function searchDom(domCaptureJson: string, predicateSource: string): string;
|
|
113
|
+
export {};
|