test(spec): assert data-tree shapes for action factories

This commit is contained in:
pj committed 2026-06-01 16:27:29 +05:30
1 parent 804cee70d2
commit 51200ed829
2 files changed
+131 -226

No files matched your search

+21 -92
View File
@@ -2,99 +2,29 @@ import assert from "node:assert/strict";
import { test } from "node:test";
import type {
Action,
ActionGenerator,
EventuallyFormula,
Extracted,
Formula,
Sampler,
State,
SanderlingRuntime,
WeightedEntry,
} from "../src/types.ts";
interface RecordedRuntime extends SanderlingRuntime {
currentState: State;
extractors: Array<(state: State) => unknown>;
alwaysArgs: Array<(() => boolean) | Formula>;
lastPredicate: (() => boolean) | undefined;
}
function installRuntime(initialState: State): RecordedRuntime {
const extractors: Array<(state: State) => unknown> = [];
const extracted: Array<{ value: unknown }> = [];
const alwaysArgs: Array<(() => boolean) | Formula> = [];
let lastPredicate: (() => boolean) | undefined;
const runtime = {
extract: <T>(getter: (state: State) => T): Extracted<T> => {
extractors.push(getter as (state: State) => unknown);
const slot = { value: getter(state.currentState) };
extracted.push(slot);
return {
get current(): T {
return slot.value as T;
},
previous: undefined,
};
},
always: (predicateOrFormula: (() => boolean) | Formula): Formula => {
alwaysArgs.push(predicateOrFormula);
if (typeof predicateOrFormula === "function") {
lastPredicate = predicateOrFormula;
}
return { __sanderlingFormula: true } as Formula;
},
// The defaults bundle still calls extract() and the LTL constructors, which
// stay host-bound on __sanderling__. Action factories now return plain data
// trees, so the fake runtime only needs the extract + formula surface.
function installRuntime(initialState: State): void {
const state = { current: initialState };
const runtime: SanderlingRuntime = {
extract: <T>(getter: (s: State) => T): Extracted<T> => ({
current: getter(state.current),
previous: undefined,
}),
always: () => ({ __sanderlingFormula: true } as Formula),
now: () => ({ __sanderlingFormula: true } as Formula),
next: () => ({ __sanderlingFormula: true } as Formula),
eventually: () => ({ __sanderlingFormula: true } as EventuallyFormula),
actions: (generator: () => Action[]): ActionGenerator => ({
__sanderlingActionGenerator: true,
generate: generator,
}),
weighted: (..._entries: WeightedEntry[]): ActionGenerator => ({
__sanderlingActionGenerator: true,
generate: () => [],
}),
from: <T>(_items: readonly T[]): Sampler<T> => ({ generate: () => _items[0] as T }),
tap: ({ on }) => ({ kind: "Tap", on }),
doubleTap: ({ on }) => ({ kind: "DoubleTap", on }),
longPress: ({ on }) => ({ kind: "LongPress", on }),
scroll: ({ direction, in: container }) => ({ kind: "Scroll", direction, in: container }),
inputText: ({ into, text }) => ({ kind: "InputText", into, text }),
swipe: (p) => ({ kind: "Swipe", from: p.from, to: p.to, durationMillis: p.durationMillis }),
pressKey: ({ key }) => ({ kind: "PressKey", key }),
wait: ({ durationMillis }) => ({ kind: "Wait", durationMillis }),
taps: { __sanderlingActionGenerator: true, generate: () => [] } as ActionGenerator,
doubleTaps: { __sanderlingActionGenerator: true, generate: () => [] } as ActionGenerator,
longPresses: { __sanderlingActionGenerator: true, generate: () => [] } as ActionGenerator,
scrolls: { __sanderlingActionGenerator: true, generate: () => [] } as ActionGenerator,
typing: { __sanderlingActionGenerator: true, generate: () => [] } as ActionGenerator,
swipes: { __sanderlingActionGenerator: true, generate: () => [] } as ActionGenerator,
waitOnce: { __sanderlingActionGenerator: true, generate: () => [] } as ActionGenerator,
pressKeys: { __sanderlingActionGenerator: true, generate: () => [] } as ActionGenerator,
} satisfies SanderlingRuntime;
const state = { currentState: initialState };
const recorded = Object.assign(runtime, {
currentState: initialState,
extractors,
alwaysArgs,
get lastPredicate() {
return lastPredicate;
},
}) as unknown as RecordedRuntime;
globalThis.__sanderling__ = recorded;
// Re-bind state ref so subsequent extract() calls read the up-to-date state.
Object.defineProperty(recorded, "currentState", {
get() {
return state.currentState;
},
set(next: State) {
state.currentState = next;
},
});
return recorded;
};
globalThis.__sanderling__ = runtime;
}
const emptyState: State = {
@@ -116,29 +46,28 @@ test("defaults bundle exports formulas tagged as LTL properties", async () => {
assert.equal(defaults.noLogcatErrors.__sanderlingFormula, true);
});
test("defaults bundle exports defaultActions as a valid ActionGenerator", async () => {
test("defaults bundle exports defaultActions as a weighted node", async () => {
installRuntime(emptyState);
const defaults = await import("../src/defaults/actions.ts");
assert.equal(defaults.defaultActions.__sanderlingActionGenerator, true);
assert.equal(typeof defaults.defaultActions.generate, "function");
assert.equal(defaults.defaultActions.kind, "weighted");
});
test("typing resolves as a valid ActionGenerator", async () => {
test("typing resolves as a builtin node", async () => {
installRuntime(emptyState);
const { typing } = await import("../src/defaults/actions.ts");
assert.equal(typing.__sanderlingActionGenerator, true);
assert.deepEqual(typing, { kind: "builtin", verb: "typing" });
});
test("longPresses and scrolls resolve as valid ActionGenerators", async () => {
test("longPresses and scrolls resolve as builtin nodes", async () => {
installRuntime(emptyState);
const { longPresses, scrolls } = await import("../src/defaults/actions.ts");
assert.equal(longPresses.__sanderlingActionGenerator, true);
assert.equal(scrolls.__sanderlingActionGenerator, true);
assert.deepEqual(longPresses, { kind: "builtin", verb: "longPresses" });
assert.deepEqual(scrolls, { kind: "builtin", verb: "scrolls" });
});
test("defaults barrel re-exports both properties and actions", async () => {
installRuntime(emptyState);
const barrel = await import("../src/defaults/index.ts");
assert.equal(barrel.defaultActions.__sanderlingActionGenerator, true);
assert.equal(barrel.defaultActions.kind, "weighted");
assert.equal(barrel.noUncaughtExceptions.__sanderlingFormula, true);
});