Files
sanderling/pkg/spec/test/dom-facts-probe.ts
T
pj 7066b23a22 fix(web-runtime): answer clickable for an element reached through ax
The handle hardcoded true, so every text node and container a spec reached
through state.ax claimed to be a tap target while the enumeration and the
hierarchy dump both resolved it through the tappable selector.

The parity test now compares the handle against the enumeration element by
element in a real browser, which is where the three answers have to agree.

Claude-Session: https://claude.ai/code/session_01A5KmftdEJ49A9z5mF5ESrX
2026-08-15 13:45:30 +05:30

57 lines
2.1 KiB
TypeScript

/// <reference lib="dom" />
// The V8 host's fact producer, read back element by element.
//
// internal/driver/chrome/fact_parity_test.go bundles this probe into a live page
// and compares what it returns against the hierarchy dump the goja host reads
// off the SAME page. Both sides derive their facts here, in production code:
// collectTargets is the shipped walk, and targetElements is the shipped
// enumeration domain, so the id at index i names the element facts[i] describes.
// The dump carries that id as `resource-id`, which is what the Go side joins on.
import { __testing__ } from "../src/web-runtime.ts";
const { collectTargets, targetElements, buildAx } = __testing__;
// The handle facts are read back off the shipped ax element, the way a spec
// reaches them and the way internal/verifier/llm.go handleLabel reads them, so
// the comparison runs over what the model is actually shown rather than over a
// recomputation of it.
type AxHandle = { attrs?: Record<string, string>; clickable?: boolean };
type Ax = { find(selector: unknown): AxHandle | undefined };
function handleOf(ax: Ax, id: string): AxHandle | undefined {
return id ? ax.find({ id }) : undefined;
}
function domFacts(): unknown[] {
const elements = targetElements();
const facts = collectTargets();
const ax = buildAx() as Ax;
if (elements.length !== facts.length) {
throw new Error(
`collectTargets reported ${facts.length} targets over ${elements.length} elements`,
);
}
return elements.map((element, index) => {
const target = facts[index]!;
const handle = handleOf(ax, element.id);
return {
id: element.id,
tag: element.tagName.toLowerCase(),
clickable: target.clickable,
enabled: target.enabled,
editable: target.editable,
scrollable: target.scrollable,
hintText: handle?.attrs?.hintText ?? "",
handleClickable: handle?.clickable ?? false,
width: target.width ?? 0,
height: target.height ?? 0,
};
});
}
type FactsGlobal = { __sanderlingDomFacts__: () => unknown[] };
(globalThis as unknown as FactsGlobal).__sanderlingDomFacts__ = domFacts;