@unotest/grounder-client 0.10.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 +66 -0
- package/LICENSE +21 -0
- package/README.md +38 -0
- package/dist/dom-walker/index.d.ts +115 -0
- package/dist/dom-walker/index.js +717 -0
- package/dist/index.d.ts +182 -0
- package/dist/index.js +124 -0
- package/dist/types-CMkcL3q9.d.ts +69 -0
- package/package.json +49 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to `@unotest/grounder-client` are documented in this file.
|
|
4
|
+
|
|
5
|
+
## [0.10.0] - 2026-08-06
|
|
6
|
+
|
|
7
|
+
### Minor Changes
|
|
8
|
+
|
|
9
|
+
- 7324966: `createDomHelpers()` grows shared accname/state building blocks, so injected algorithms with divergent naming policies stop hand-copying the identical parts: `makeAccname(hostLanguageName)` (fixed prologue: cycle guard → aria-labelledby → aria-label → form-control labels; the per-tag chain is supplied by the caller), plus `nameFromLabelledby`, `svgAccname`, `formControlName`, `formFieldRole`, `ariaStateFlags`, `headingLevel` and the `AriaStateFlags` type. `accname`/`collectState`/`implicitRole` behavior is unchanged — they are now built on the same blocks.
|
|
10
|
+
- b5b9710: Assert steps ground as whole units. `Intent.prefer` gains `"unit"`: the
|
|
11
|
+
grounder returns the semantic unit itself (heading / record / text) and
|
|
12
|
+
suppresses the record→control hop — the profile for read-only targets.
|
|
13
|
+
Web assert actions (`assert_text/visible/hidden/value/count`) declare
|
|
14
|
+
`intentTarget: "unit"` on their plugins, and the intent resolver
|
|
15
|
+
materializes a DOM ref for unit resolutions via the new
|
|
16
|
+
`materializeRefScript(nodeId)` (grounder-client dom-walker): one lazy
|
|
17
|
+
attribute write on the unit's own element, reused by later captures.
|
|
18
|
+
Previously an assert intent like "the page heading" was rejected with a
|
|
19
|
+
RECORD error and forced a `find_element` detour. Control-target errors
|
|
20
|
+
now name the unit kind correctly (heading / record / text block) and
|
|
21
|
+
point at assert steps for check-not-click intents.
|
|
22
|
+
|
|
23
|
+
Materialized record containers now resolve to a stable locator. A record
|
|
24
|
+
row / card / tree-item (`<li>`, `<tr>`, `<article>`) carries no accessible
|
|
25
|
+
name of its own — its text lives in children — so `RefResolver` gains a
|
|
26
|
+
container strategy: `getByRole(role).filter({hasText})`, keyed off the
|
|
27
|
+
container's short descendant text (capped so a whole table is never
|
|
28
|
+
captured) and verified for uniqueness + ref identity. Fixes `assert_visible`
|
|
29
|
+
on a grounded tree item failing with `no stable identifier`.
|
|
30
|
+
|
|
31
|
+
- 574841f: Dedup pass across the tree (jscpd 0 clones, ratchet threshold 0). New public export in `@unotest/grounder-client`: `intEnvInRange(env, name, def, min, max)` — the `UNOTEST_GROUNDER_*` integer-env parser shared by the consumer side and the grounder server. Everything else is internal refactoring with no behavior change: protocol gains a shared fs-safe-name validator behind `validateCollectionName` / `validateSegmentName` and a shared env-file assignment iterator; core's `RuntimeInspection` becomes a `Pick` of `RuntimeStateFile`; dsl's `ExecutionWalker` threads one `WalkCtx` object instead of an eight-argument clump and AST nodes use constructor parameter properties; web deduplicates the MCP tool scaffolds, action plugins, DSL contracts, perception LRU caches, and semantic-dom grouping passes.
|
|
32
|
+
- 65eb77c: Shared injected DOM helpers: one `createDomHelpers()` factory (visibility, ARIA roles, W3C accname, state flags, ref minting) now backs the grounder-client DOM walker and the web snapshot/find algorithms, composed into the page by `composeInjectedScript` — replaces four hand-maintained copies of the same in-page logic.
|
|
33
|
+
|
|
34
|
+
**Breaking (`@unotest/grounder-client`):** `captureRawTree` takes the helpers object as its first parameter, so passing it straight to `page.evaluate(captureRawTree)` no longer works (evaluate arguments are JSON-serialized and cannot carry the helpers). Migration: inject the serialized script instead — `page.evaluate(domWalkerScript())` — which composes the helpers automatically. New exports: `createDomHelpers`, `composeInjectedScript`, `InjectedDomHelpers`, `DomHelperOptions`, `InjectedScriptOptions`.
|
|
35
|
+
|
|
36
|
+
`@unotest/web` behavior deltas from unifying on the walker's ARIA mapping: `get_aria_snapshot` now maps `dl` to `list` (`dt`/`dd` stay name-only) and `tbody`/`thead`/`tfoot` to `rowgroup`, and never names structural containers from their text content; `find_element` matches against the full W3C accname (label-for lookup, composed multi-child names) instead of a reduced approximation.
|
|
37
|
+
|
|
38
|
+
- f106cf8: The DOM walker records `fontWeight` on captured elements when it deviates
|
|
39
|
+
from normal (400), looking a few levels down so emphasis wrapped inside
|
|
40
|
+
the element counts (`<a><b>Technic</b></a>` reports 700).
|
|
41
|
+
|
|
42
|
+
Weight is a LEVEL cue: in a flat list the bold entry heads the plain ones
|
|
43
|
+
below it. Unlike indentation it survives multi-column layouts, where a
|
|
44
|
+
subtree continues in the next column at the same offset — the case that
|
|
45
|
+
made five same-named links in a 1200-link catalog tree indistinguishable.
|
|
46
|
+
Consumers reading `RawCapture` see one optional numeric field; nothing
|
|
47
|
+
else changes.
|
|
48
|
+
|
|
49
|
+
- 856f087: `ResolveDiagnostics` gains `nameEvidence` — the page strings an intent
|
|
50
|
+
quoted verbatim plus whether the resolved unit owned one of them. The
|
|
51
|
+
grounder now answers NONE when an intent quotes page text that the
|
|
52
|
+
picked unit does not own (its own name/value or its record's identity
|
|
53
|
+
and fields); a collection label or region name counts as context, not
|
|
54
|
+
ownership. Consumers that surface grounding diagnostics can report the
|
|
55
|
+
new field; the shape is additive and optional.
|
|
56
|
+
- 78f850f: `ResolveDiagnostics` gains `ordinalSubset` — set when a typed ordinal was
|
|
57
|
+
resolved deterministically inside a value-filtered subset ("the last
|
|
58
|
+
Pending row" over the 48 Pending rows of a 295-row table) instead of by
|
|
59
|
+
the picker. Carries the matched value, the subset size and the 0-based
|
|
60
|
+
index taken; absent when the picker decided the position.
|
|
61
|
+
|
|
62
|
+
### Patch Changes
|
|
63
|
+
|
|
64
|
+
- 1bd55ba: `ResolveDiagnostics.recordHop` gains an optional `prompt` — the exact
|
|
65
|
+
user message the record→control hop picker saw. Debug surface: the eval
|
|
66
|
+
console prints it verbatim; no behavior change.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ivan Volkov
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# @unotest/grounder-client
|
|
2
|
+
|
|
3
|
+
Consumer-side contract of the unotest **semantic UI grounder** — the service
|
|
4
|
+
that resolves a typed intent ("the edit button in the R-00042 row", "the last
|
|
5
|
+
pending row") to element ref(s) on a captured page.
|
|
6
|
+
|
|
7
|
+
This package is everything a consumer needs to capture a page and talk to the
|
|
8
|
+
grounding service:
|
|
9
|
+
|
|
10
|
+
- **Raw capture contract** — `RawCapture` and friends: the dumb visible-DOM
|
|
11
|
+
tree the grounder consumes. Map any accessibility tree to it, or use the
|
|
12
|
+
bundled walker.
|
|
13
|
+
- **DOM walker** (`@unotest/grounder-client/dom-walker`) — a self-contained
|
|
14
|
+
in-page capture function plus evaluate-ready scripts (ref highlight,
|
|
15
|
+
node-registry lookups).
|
|
16
|
+
- **Content hash** — `hashRawCapture`, the "is the page still the same
|
|
17
|
+
state?" cache key. Semantics-only: bounds/scroll don't flip it.
|
|
18
|
+
- **Typed intent → resolution** — `Intent` (text + typed `ordinal` /
|
|
19
|
+
`predicate`), `Resolution` (`ref | set | none` + diagnostics).
|
|
20
|
+
- **Wire protocol + HTTP client** — `GrounderHttpClient` over
|
|
21
|
+
`/index`, `/resolve`, `/session/close`; per-session index isolation.
|
|
22
|
+
|
|
23
|
+
The grounding engine itself (graph perception, embedding recall, picker
|
|
24
|
+
models) runs as a service and is not part of this package.
|
|
25
|
+
|
|
26
|
+
## Usage
|
|
27
|
+
|
|
28
|
+
```ts
|
|
29
|
+
import { GrounderHttpClient, hashRawCapture, type RawCapture } from "@unotest/grounder-client";
|
|
30
|
+
import { domWalkerScript } from "@unotest/grounder-client/dom-walker";
|
|
31
|
+
|
|
32
|
+
const raw = (await page.evaluate(domWalkerScript())) as RawCapture;
|
|
33
|
+
const hash = hashRawCapture(raw);
|
|
34
|
+
|
|
35
|
+
const client = new GrounderHttpClient({ baseUrl, token });
|
|
36
|
+
await client.index(raw, hash);
|
|
37
|
+
const { resolution } = await client.resolve({ text: "the login field" }, hash);
|
|
38
|
+
```
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { R as RawState, a as RawCapture } from '../types-CMkcL3q9.js';
|
|
2
|
+
|
|
3
|
+
interface DomHelperOptions {
|
|
4
|
+
/** Accname length cap (default 80). */
|
|
5
|
+
maxName?: number;
|
|
6
|
+
/** Where `aria-hidden="true"` hides an element: "ancestors" walks the
|
|
7
|
+
* composed tree up (snapshot captures), "self" checks only the element
|
|
8
|
+
* itself — the walker's recursive visibility gate already prunes hidden
|
|
9
|
+
* subtrees, and the per-element ancestor climb would be O(depth²) on
|
|
10
|
+
* its hot path. Default "ancestors". */
|
|
11
|
+
ariaHidden?: "ancestors" | "self";
|
|
12
|
+
}
|
|
13
|
+
/** Subset of accessibility state shared by the walker's `RawState` and
|
|
14
|
+
* outline capture in @unotest/web (`disabled` and `level` handling
|
|
15
|
+
* deliberately differ per side and stay out). */
|
|
16
|
+
interface AriaStateFlags {
|
|
17
|
+
checked?: true | "mixed";
|
|
18
|
+
pressed?: true | "mixed";
|
|
19
|
+
expanded?: true;
|
|
20
|
+
selected?: true;
|
|
21
|
+
active?: true;
|
|
22
|
+
}
|
|
23
|
+
declare function createDomHelpers(opts?: DomHelperOptions): {
|
|
24
|
+
parentOrHost: (node: Node) => Element | null;
|
|
25
|
+
rootForScope: (el: Node) => Document | ShadowRoot;
|
|
26
|
+
getElementByIdInScope: (el: Element, id: string) => Element | null;
|
|
27
|
+
deepWalk: (root: Document | Element | ShadowRoot) => Generator<Element>;
|
|
28
|
+
isVisible: (el: Element) => boolean;
|
|
29
|
+
trim: (s: string | null | undefined) => string;
|
|
30
|
+
cap: (s: string) => string;
|
|
31
|
+
cssEscape: (s: string) => string;
|
|
32
|
+
textContentVisible: (el: Element) => string;
|
|
33
|
+
interactiveRoles: Set<string>;
|
|
34
|
+
formFieldRole: (el: Element, tag: string) => string | null;
|
|
35
|
+
implicitRole: (el: Element) => string | null;
|
|
36
|
+
computeRole: (el: Element) => string | null;
|
|
37
|
+
nameFromLabelledby: (el: Element, seen: Set<Element>, resolve: (ref: Element, seen: Set<Element>) => string) => string;
|
|
38
|
+
svgAccname: (svg: Element, seen: Set<Element>, resolve: (ref: Element, seen: Set<Element>) => string) => string;
|
|
39
|
+
formControlName: (el: Element) => string;
|
|
40
|
+
makeAccname: (hostLanguageName: (el: Element, tag: string, seen: Set<Element>, self: (el: Element, visited?: Set<Element>) => string) => string) => (el: Element, visited?: Set<Element>) => string;
|
|
41
|
+
accname: (el: Element, visited?: Set<Element>) => string;
|
|
42
|
+
ariaStateFlags: (el: Element) => AriaStateFlags;
|
|
43
|
+
headingLevel: (el: Element) => number | undefined;
|
|
44
|
+
collectState: (el: Element, role: string) => RawState | undefined;
|
|
45
|
+
pageOrigin: string;
|
|
46
|
+
normaliseHref: (raw: string | null) => string | undefined;
|
|
47
|
+
isInteractive: (el: Element, role: string | null) => boolean;
|
|
48
|
+
idStable: (id: string) => boolean;
|
|
49
|
+
stableDataAttr: (el: Element) => {
|
|
50
|
+
name: string;
|
|
51
|
+
value: string;
|
|
52
|
+
} | undefined;
|
|
53
|
+
clickAffordance: (el: Element) => boolean;
|
|
54
|
+
testIdOf: (el: Element) => string | undefined;
|
|
55
|
+
fontWeight: (el: Element) => number;
|
|
56
|
+
refRegex: RegExp;
|
|
57
|
+
ensureRef: (el: Element, kind: "i" | "n") => string;
|
|
58
|
+
persistRefCounter: () => void;
|
|
59
|
+
};
|
|
60
|
+
type InjectedDomHelpers = ReturnType<typeof createDomHelpers>;
|
|
61
|
+
interface InjectedScriptOptions {
|
|
62
|
+
helperOpts?: DomHelperOptions;
|
|
63
|
+
/** JS expression literals appended as algorithm arguments after the
|
|
64
|
+
* helpers object (already-serialized, e.g. `JSON.stringify(args)`). */
|
|
65
|
+
argLiterals?: string[];
|
|
66
|
+
}
|
|
67
|
+
/** Evaluate-ready expression running `algorithm(h, ...args)` in the page.
|
|
68
|
+
* The `__name` shim neutralizes esbuild/tsx keepNames helper calls that
|
|
69
|
+
* may be injected into the serialized function bodies. */
|
|
70
|
+
declare function composeInjectedScript(algorithm: (h: InjectedDomHelpers, ...rest: never[]) => unknown, opts?: InjectedScriptOptions): string;
|
|
71
|
+
|
|
72
|
+
declare function captureRawTree(h: InjectedDomHelpers): RawCapture;
|
|
73
|
+
|
|
74
|
+
/** DOM attribute carrying an element's `eN` ref (interactive elements). */
|
|
75
|
+
declare const REF_ATTRIBUTE = "data-unotest-ref";
|
|
76
|
+
/** <html> attribute persisting the ref counter across captures. */
|
|
77
|
+
declare const REF_COUNTER_ATTRIBUTE = "data-unotest-ref-counter";
|
|
78
|
+
/** window global holding the live-node registry of the LAST capture:
|
|
79
|
+
* `window.__unotestGrounderNodes[nodeId] === Element`. This is the
|
|
80
|
+
* highlight handle for graph units — including `rN` record roots, which
|
|
81
|
+
* have no DOM ref attribute. Replaced wholesale on every capture.
|
|
82
|
+
* Must match the literal used inside walker.ts (the function is
|
|
83
|
+
* serialized and cannot reference module scope). */
|
|
84
|
+
declare const NODE_REGISTRY_GLOBAL = "__unotestGrounderNodes";
|
|
85
|
+
/** Serialized walker as an evaluate-ready expression. `ariaHidden: "self"`
|
|
86
|
+
* keeps the walker's original visibility semantics: its recursive walk
|
|
87
|
+
* already prunes hidden subtrees, so the per-element ancestor climb is
|
|
88
|
+
* redundant on this hot path. */
|
|
89
|
+
declare function domWalkerScript(): string;
|
|
90
|
+
/** Evaluate-ready expression resolving a nodeId to its live element —
|
|
91
|
+
* building block for consumer-side highlight overlays. Returns null when
|
|
92
|
+
* the registry is stale (page re-rendered) or the id is unknown. */
|
|
93
|
+
declare function nodeByIdScript(nodeId: number): string;
|
|
94
|
+
/** Evaluate-ready script materializing a `data-unotest-ref` (eN) on the
|
|
95
|
+
* element behind `nodeId` — the DOM handle for graph units that never get
|
|
96
|
+
* a ref at capture time (headings, record roots, text blocks; the walker
|
|
97
|
+
* only stamps interactive elements). Lazy by design: one attribute write
|
|
98
|
+
* per resolve, instead of stamping thousands of non-interactive elements
|
|
99
|
+
* on every capture. Mirrors the walker's `ensureRef` conventions exactly
|
|
100
|
+
* (same attribute, same <html> counter, owner kind `n` = named
|
|
101
|
+
* non-interactive), so the next capture reuses the ref instead of
|
|
102
|
+
* re-minting. Returns the eN, or null when the registry is stale (page
|
|
103
|
+
* re-rendered since the capture). */
|
|
104
|
+
declare function materializeRefScript(nodeId: number): string;
|
|
105
|
+
/** Evaluate-ready script drawing outline overlays over the elements behind
|
|
106
|
+
* `nodeIds` (live-node registry of the LAST capture). Overlays are
|
|
107
|
+
* position:fixed and TRACK their elements — recomputed on every scroll
|
|
108
|
+
* (capture phase, so inner scroll containers count) and resize; the first
|
|
109
|
+
* target is scrolled into view BEFORE the boxes are placed. Re-running
|
|
110
|
+
* replaces the previous highlight. Returns the number of elements found. */
|
|
111
|
+
declare function highlightByNodeIdScript(nodeIds: number[], color?: string): string;
|
|
112
|
+
/** Evaluate-ready script removing the highlight overlay, if any. */
|
|
113
|
+
declare function clearHighlightScript(): string;
|
|
114
|
+
|
|
115
|
+
export { type DomHelperOptions, type InjectedDomHelpers, type InjectedScriptOptions, NODE_REGISTRY_GLOBAL, REF_ATTRIBUTE, REF_COUNTER_ATTRIBUTE, RawCapture, captureRawTree, clearHighlightScript, composeInjectedScript, createDomHelpers, domWalkerScript, highlightByNodeIdScript, materializeRefScript, nodeByIdScript };
|