mirror of
https://github.com/priyanshujain/sanderling.git
synced 2026-10-02 19:17:10 +00:00
Both hosts routed verbs themselves and both policies enumerated their own actions, and all four drifted. Web sent `swipes` to scrollable containers only, so swipe-to-dismiss on a list row was reachable on native and unreachable on web; the model policy folded gestures its own way and could not reach what the seeded picker drew. A host now reports facts about every element and never decides which verb may act on it: targets.ts acceptsTarget owns that for both. pick.ts builtinCandidates is the single enumeration, and the model policy reads it through __sanderlingEnumerateBuiltin__ instead of reimplementing it in Go. Gesture verbs change with it: scrolls stay vertical over scrollable containers, swipes go free-form in all four directions from any element with real bounds. Claude-Session: https://claude.ai/code/session_01Fj4wJUikdABuMQEETwW55J
89 lines
2.7 KiB
TypeScript
89 lines
2.7 KiB
TypeScript
// A minimal stand-in for the DOM surface the web host reads, shared by the web
|
|
// runtime's own tests and the cross-host eligibility test. The host asks the
|
|
// document for three things -- every element, the tappable set, the editable set
|
|
// -- and reads geometry, `disabled` and the scroll extents off each element, so
|
|
// that is all a fake has to answer.
|
|
|
|
import { __testing__ } from "../src/web-runtime.ts";
|
|
|
|
const { TAPPABLE_SELECTOR, EDITABLE_SELECTOR } = __testing__;
|
|
|
|
export interface FakeElementSpec {
|
|
tag: string;
|
|
x: number;
|
|
y: number;
|
|
width: number;
|
|
height: number;
|
|
// clickable/editable place the element in the selector sets the host queries;
|
|
// the fake answers those queries directly rather than matching CSS.
|
|
clickable?: boolean;
|
|
editable?: boolean;
|
|
disabled?: boolean;
|
|
// overflows makes the element's content taller than its box, which is how the
|
|
// host decides an element is scrollable.
|
|
overflows?: boolean;
|
|
}
|
|
|
|
export interface FakeElement extends FakeElementSpec {
|
|
tagName: string;
|
|
type: string;
|
|
isContentEditable: boolean;
|
|
scrollHeight: number;
|
|
clientHeight: number;
|
|
scrollWidth: number;
|
|
clientWidth: number;
|
|
getBoundingClientRect(): {
|
|
left: number;
|
|
top: number;
|
|
width: number;
|
|
height: number;
|
|
right: number;
|
|
bottom: number;
|
|
};
|
|
}
|
|
|
|
export function fakeElement(spec: FakeElementSpec): FakeElement {
|
|
const editable = spec.editable ?? false;
|
|
return {
|
|
...spec,
|
|
tagName: spec.tag.toUpperCase(),
|
|
type: spec.tag === "input" ? "text" : "",
|
|
isContentEditable: editable && spec.tag !== "input" && spec.tag !== "textarea",
|
|
scrollHeight: spec.overflows ? spec.height * 2 : spec.height,
|
|
clientHeight: spec.height,
|
|
scrollWidth: spec.width,
|
|
clientWidth: spec.width,
|
|
getBoundingClientRect: () => ({
|
|
left: spec.x,
|
|
top: spec.y,
|
|
width: spec.width,
|
|
height: spec.height,
|
|
right: spec.x + spec.width,
|
|
bottom: spec.y + spec.height,
|
|
}),
|
|
};
|
|
}
|
|
|
|
// withFakeDocument installs a document answering the host's three queries over
|
|
// `elements`, resets the host's per-tick cache, and restores the real document
|
|
// afterwards.
|
|
export function withFakeDocument(elements: FakeElement[], run: () => void): void {
|
|
const global = globalThis as Record<string, unknown>;
|
|
const original = global.document;
|
|
const answers: Record<string, FakeElement[]> = {
|
|
"*": elements,
|
|
[TAPPABLE_SELECTOR]: elements.filter((element) => element.clickable),
|
|
[EDITABLE_SELECTOR]: elements.filter((element) => element.editable),
|
|
};
|
|
global.document = {
|
|
querySelectorAll: (selector: string) => answers[selector] ?? [],
|
|
};
|
|
__testing__.resetTargetCache();
|
|
try {
|
|
run();
|
|
} finally {
|
|
__testing__.resetTargetCache();
|
|
global.document = original;
|
|
}
|
|
}
|