From 4a1f8cb522a4bb6c641616eccada203181dd55fc Mon Sep 17 00:00:00 2001 From: PJ Date: Fri, 17 Apr 2026 23:45:36 +0700 Subject: [PATCH] feat(spec-api): typed surface for spec authors Thin wrappers over a globalThis.__uatu__ runtime that the verifier provides as host bindings. Spec authors get IntelliSense for extract/always/Tap/InputText/actions/weighted/taps/swipes, but the bundle stays small because runtime logic lives in goja. --- pkg/spec-api/src/actions.ts | 36 +++++++++++++++++++++++++ pkg/spec-api/src/extract.ts | 5 ++++ pkg/spec-api/src/index.ts | 17 ++++++++++++ pkg/spec-api/src/ltl.ts | 5 ++++ pkg/spec-api/src/types.ts | 53 +++++++++++++++++++++++++++++++++++++ 5 files changed, 116 insertions(+) create mode 100644 pkg/spec-api/src/actions.ts create mode 100644 pkg/spec-api/src/extract.ts create mode 100644 pkg/spec-api/src/index.ts create mode 100644 pkg/spec-api/src/ltl.ts create mode 100644 pkg/spec-api/src/types.ts diff --git a/pkg/spec-api/src/actions.ts b/pkg/spec-api/src/actions.ts new file mode 100644 index 0000000..2d0a758 --- /dev/null +++ b/pkg/spec-api/src/actions.ts @@ -0,0 +1,36 @@ +import type { + AccessibilityElement, + Action, + ActionGenerator, + InputTextAction, + TapAction, + WeightedEntry, +} from "./types.ts"; + +export function actions(generator: () => Action[]): ActionGenerator { + return globalThis.__uatu__.actions(generator); +} + +export function weighted(...entries: WeightedEntry[]): ActionGenerator { + return globalThis.__uatu__.weighted(...entries); +} + +export function Tap(parameters: { on: string | AccessibilityElement }): TapAction { + return globalThis.__uatu__.tap(parameters); +} + +export function InputText(parameters: { into: string | AccessibilityElement; text: string }): InputTextAction { + return globalThis.__uatu__.inputText(parameters); +} + +export const taps: ActionGenerator = new Proxy({} as ActionGenerator, { + get(_target, property) { + return (globalThis.__uatu__.taps as unknown as Record)[property]; + }, +}); + +export const swipes: ActionGenerator = new Proxy({} as ActionGenerator, { + get(_target, property) { + return (globalThis.__uatu__.swipes as unknown as Record)[property]; + }, +}); diff --git a/pkg/spec-api/src/extract.ts b/pkg/spec-api/src/extract.ts new file mode 100644 index 0000000..a0181d7 --- /dev/null +++ b/pkg/spec-api/src/extract.ts @@ -0,0 +1,5 @@ +import type { Extracted, State } from "./types.ts"; + +export function extract(getter: (state: State) => T): Extracted { + return globalThis.__uatu__.extract(getter); +} diff --git a/pkg/spec-api/src/index.ts b/pkg/spec-api/src/index.ts new file mode 100644 index 0000000..b74ca53 --- /dev/null +++ b/pkg/spec-api/src/index.ts @@ -0,0 +1,17 @@ +export type { + AccessibilityElement, + AccessibilityTree, + Action, + ActionGenerator, + Extracted, + Formula, + InputTextAction, + Snapshots, + State, + TapAction, + WeightedEntry, +} from "./types.ts"; + +export { extract } from "./extract.ts"; +export { always } from "./ltl.ts"; +export { Tap, InputText, actions, weighted, taps, swipes } from "./actions.ts"; diff --git a/pkg/spec-api/src/ltl.ts b/pkg/spec-api/src/ltl.ts new file mode 100644 index 0000000..381e3af --- /dev/null +++ b/pkg/spec-api/src/ltl.ts @@ -0,0 +1,5 @@ +import type { Formula } from "./types.ts"; + +export function always(predicate: () => boolean): Formula { + return globalThis.__uatu__.always(predicate); +} diff --git a/pkg/spec-api/src/types.ts b/pkg/spec-api/src/types.ts new file mode 100644 index 0000000..1b02c16 --- /dev/null +++ b/pkg/spec-api/src/types.ts @@ -0,0 +1,53 @@ +export type Snapshots = Record; + +export interface AccessibilityElement { + id?: string; + text?: string; + bounds?: { left: number; top: number; right: number; bottom: number }; +} + +export interface AccessibilityTree { + find(selector: string): AccessibilityElement | undefined; + findAll(selector: string): AccessibilityElement[]; +} + +export interface State { + snapshots: Snapshots; + ax: AccessibilityTree; +} + +export interface Extracted { + readonly current: T; + readonly previous: T | undefined; +} + +export type TapAction = { kind: "Tap"; on: string | AccessibilityElement }; +export type InputTextAction = { kind: "InputText"; into: string | AccessibilityElement; text: string }; +export type Action = TapAction | InputTextAction; + +export interface ActionGenerator { + readonly __uatuActionGenerator: true; + generate(): Action[]; +} + +export interface Formula { + readonly __uatuFormula: true; +} + +export interface UatuRuntime { + extract: (getter: (state: State) => T) => Extracted; + always: (predicate: () => boolean) => Formula; + actions: (generator: () => Action[]) => ActionGenerator; + weighted: (...entries: WeightedEntry[]) => ActionGenerator; + tap: (parameters: { on: string | AccessibilityElement }) => TapAction; + inputText: (parameters: { into: string | AccessibilityElement; text: string }) => InputTextAction; + taps: ActionGenerator; + swipes: ActionGenerator; +} + +export type WeightedEntry = readonly [number, ActionGenerator]; + +declare global { + // eslint-disable-next-line no-var + var __uatu__: UatuRuntime; +}