refactor(spec): actions.ts returns pure GeneratorNode data trees

This commit is contained in:
pj committed 2026-06-01 16:24:20 +05:30
1 parent b34f73f3f7
commit fe3ebd0484
2 files changed
+60 -78

No files matched your search

+51 -45
View File
@@ -1,7 +1,13 @@
// Author-facing action factories. Each returns plain GeneratorNode /
// ActionDescriptor DATA (see action-tree.ts); the shared picker (pick.ts) walks
// the tree and the runtime entry (runtime-entry.ts) serializes the result. No
// factory forwards to globalThis.__sanderling__ anymore: the same data tree
// drives both the goja verifier and the V8 web runtime.
import type { Pcg } from "./pcg.ts";
import type {
AccessibilityElement,
Action,
ActionGenerator,
Direction,
DoubleTapAction,
InputTextAction,
@@ -16,16 +22,17 @@ import type {
WaitAction,
WeightedEntry,
} from "./types.ts";
import type { ActionDescriptor, GeneratorNode } from "./action-tree.ts";
export function actions(generator: () => Action[]): ActionGenerator {
return globalThis.__sanderling__.actions(generator);
export function actions(generator: () => Action[]): GeneratorNode {
return { kind: "actions", generate: generator as () => ActionDescriptor[] };
}
export function whenRoute(
routeExtractor: { readonly current: string | null },
routes: string | readonly string[],
body: () => Action[],
): ActionGenerator {
): GeneratorNode {
const allowed = typeof routes === "string" ? [routes] : routes;
return actions(() => {
const current = routeExtractor.current;
@@ -34,38 +41,54 @@ export function whenRoute(
});
}
export function weighted(...entries: WeightedEntry[]): ActionGenerator {
return globalThis.__sanderling__.weighted(...entries);
export function weighted(...entries: WeightedEntry[]): GeneratorNode {
return { kind: "weighted", branches: entries };
}
// samplerRng is the picker's Pcg while it evaluates an `actions` node's
// generator (set by pick.ts walkActions). `from(...).generate()` draws from it
// so sampling shares the single deterministic stream. It is null outside a
// walk; eager spec-time generate() calls then fall back to the first item.
let samplerRng: Pcg | null = null;
export function setSamplerRng(rng: Pcg | null): void {
samplerRng = rng;
}
export function from<T>(items: readonly T[]): Sampler<T> {
return globalThis.__sanderling__.from(items);
return {
generate(): T {
if (items.length <= 1) return items[0] as T;
const index = samplerRng ? samplerRng.intN(items.length) : 0;
return items[index] as T;
},
};
}
export function Tap(parameters: { on: string | AccessibilityElement }): TapAction {
return globalThis.__sanderling__.tap(parameters);
return { kind: "Tap", on: parameters.on };
}
export function DoubleTap(parameters: { on: string | AccessibilityElement }): DoubleTapAction {
return globalThis.__sanderling__.doubleTap(parameters);
return { kind: "DoubleTap", on: parameters.on };
}
export function LongPress(parameters: { on: string | AccessibilityElement }): LongPressAction {
return globalThis.__sanderling__.longPress(parameters);
return { kind: "LongPress", on: parameters.on };
}
export function Scroll(parameters: {
direction: Direction;
in?: string | AccessibilityElement;
}): ScrollAction {
return globalThis.__sanderling__.scroll(parameters);
return { kind: "Scroll", direction: parameters.direction, in: parameters.in };
}
export function InputText(parameters: {
into: string | AccessibilityElement;
text: string;
}): InputTextAction {
return globalThis.__sanderling__.inputText(parameters);
return { kind: "InputText", into: parameters.into, text: parameters.text };
}
export function Swipe(parameters: {
@@ -73,44 +96,27 @@ export function Swipe(parameters: {
to: Point | AccessibilityElement;
durationMillis?: number;
}): SwipeAction {
return globalThis.__sanderling__.swipe(parameters);
return {
kind: "Swipe",
from: parameters.from,
to: parameters.to,
durationMillis: parameters.durationMillis,
};
}
export function PressKey(parameters: { key: Key }): PressKeyAction {
return globalThis.__sanderling__.pressKey(parameters);
return { kind: "PressKey", key: parameters.key };
}
export function Wait(parameters: { durationMillis: number }): WaitAction {
return globalThis.__sanderling__.wait(parameters);
return { kind: "Wait", durationMillis: parameters.durationMillis };
}
function builtinGenerator(
name:
| "taps"
| "doubleTaps"
| "longPresses"
| "scrolls"
| "typing"
| "swipes"
| "waitOnce"
| "pressKeys",
): ActionGenerator {
return new Proxy({} as ActionGenerator, {
get(_target, property) {
const runtime = globalThis.__sanderling__[name] as unknown as Record<
string | symbol,
unknown
>;
return runtime[property];
},
});
}
export const taps: ActionGenerator = builtinGenerator("taps");
export const doubleTaps: ActionGenerator = builtinGenerator("doubleTaps");
export const longPresses: ActionGenerator = builtinGenerator("longPresses");
export const scrolls: ActionGenerator = builtinGenerator("scrolls");
export const typing: ActionGenerator = builtinGenerator("typing");
export const swipes: ActionGenerator = builtinGenerator("swipes");
export const waitOnce: ActionGenerator = builtinGenerator("waitOnce");
export const pressKey: ActionGenerator = builtinGenerator("pressKeys");
export const taps: GeneratorNode = { kind: "builtin", verb: "taps" };
export const doubleTaps: GeneratorNode = { kind: "builtin", verb: "doubleTaps" };
export const longPresses: GeneratorNode = { kind: "builtin", verb: "longPresses" };
export const scrolls: GeneratorNode = { kind: "builtin", verb: "scrolls" };
export const typing: GeneratorNode = { kind: "builtin", verb: "typing" };
export const swipes: GeneratorNode = { kind: "builtin", verb: "swipes" };
export const waitOnce: GeneratorNode = { kind: "builtin", verb: "waitOnce" };
export const pressKey: GeneratorNode = { kind: "builtin", verb: "pressKeys" };
+9 -33
View File
@@ -175,10 +175,10 @@ export type Key =
| "left"
| "right";
export interface ActionGenerator {
readonly __sanderlingActionGenerator: true;
generate(): Action[];
}
// ActionGenerator is a node in the action-generator tree (see action-tree.ts).
// Author specs treat it as an opaque handle they compose with weighted(); the
// shared picker walks the underlying GeneratorNode.
export type ActionGenerator = GeneratorNode;
export interface Formula {
readonly __sanderlingFormula: true;
@@ -196,45 +196,21 @@ export interface Sampler<T> {
generate(): T;
}
// SanderlingRuntime is the host-bound surface that stays on
// globalThis.__sanderling__: extract plus the LTL formula constructors. Action
// factories no longer live here; they return plain data trees (see actions.ts).
export interface SanderlingRuntime {
extract: <T>(getter: (state: State) => T, name?: string) => Extracted<T>;
always: (predicateOrFormula: (() => boolean) | Formula) => Formula;
now: (predicate: () => boolean) => Formula;
next: (predicate: () => boolean) => Formula;
eventually: (predicate: () => boolean) => EventuallyFormula;
actions: (generator: () => Action[]) => ActionGenerator;
weighted: (...entries: WeightedEntry[]) => ActionGenerator;
from: <T>(items: readonly T[]) => Sampler<T>;
tap: (parameters: { on: string | AccessibilityElement }) => TapAction;
doubleTap: (parameters: { on: string | AccessibilityElement }) => DoubleTapAction;
longPress: (parameters: { on: string | AccessibilityElement }) => LongPressAction;
scroll: (parameters: {
direction: Direction;
in?: string | AccessibilityElement;
}) => ScrollAction;
inputText: (parameters: {
into: string | AccessibilityElement;
text: string;
}) => InputTextAction;
swipe: (parameters: {
from: Point | AccessibilityElement;
to: Point | AccessibilityElement;
durationMillis?: number;
}) => SwipeAction;
pressKey: (parameters: { key: Key }) => PressKeyAction;
wait: (parameters: { durationMillis: number }) => WaitAction;
taps: ActionGenerator;
doubleTaps: ActionGenerator;
longPresses: ActionGenerator;
scrolls: ActionGenerator;
typing: ActionGenerator;
swipes: ActionGenerator;
waitOnce: ActionGenerator;
pressKeys: ActionGenerator;
}
export type WeightedEntry = readonly [number, ActionGenerator];
import type { GeneratorNode } from "./action-tree.ts";
declare global {
// eslint-disable-next-line no-var
var __sanderling__: SanderlingRuntime;