コンテンツにスキップ

Driver & selectors

The common interface every backend satisfies, and the shared types a selector resolves against.

bajutsu.drivers.base

Driver abstraction — the linchpin shared by every backend (idb / fake).

Frozen first because everything else depends on it: - common types Point / Element / Selector - the Driver Protocol (only the actuator performs actions) - selector resolution (the determinism core): a single action requires a unique match, and an ambiguous match (2+) raises AmbiguousSelector to rule out nondeterminism structurally.

Capability

Capability names returned by Driver.capabilities().

Used to pick the actuator and resolve fallbacks. A backend with SEMANTIC_TAP actuates more stably (no coordinates involved).

Element

Bases: TypedDict

A single on-screen element, normalized from idb output.

Trait

Normalized accessibility traits used by state assertions.

Drivers normalize at least the following to these common tokens.

Selector

Bases: TypedDict

How to address an element. Provided fields are combined with AND.

The stable selector is id (non-localized, data-derived). label / labelMatches are auxiliary; index is a last resort (flaky).

Driver

Bases: Protocol

Common interface for every backend.

Actions (tap/type/swipe/wait/query) are performed by the actuator only. On a backend without semantic tap (e.g. idb), the abstraction resolves the frame center via query() / resolve_unique() and taps by coordinates.

EvidenceProvider

Bases: Protocol

A read-only evidence source from a non-actuator backend (BE-0020).

A multi-backend run keeps actuation on the one actuator and may consult another same-platform backend read-only to fill an evidence gap the actuator lacks (e.g. idb has no native network, so a second iOS actuator supplies it). The narrow surface — capabilities plus observation methods only, never tap / type / swipe / wait / query — makes "the fallback never actuates" a type-level fact rather than a convention.

BackendLifecycle

Bases: Protocol

The full set of lifecycle hooks backends run around a single run (BE-0141).

A run launches, tears down, and resets a backend, but those steps are platform-shaped: the web (Playwright) backend navigates / closes / resets a browser context, the XCUITest backend waits for its on-device runner to answer, and idb needs none of them (its boot / erase / install sequence lives outside the driver in simctl). The four hooks are therefore split disjointly across backends — no single driver implements all four — so this is a typing umbrella for the call sites, not a conformance target: the platform_lifecycle environments reach each hook through cast(BackendLifecycle, driver) under the platform invariant that already scopes the driver, which turns "the hook exists" into a mypy-checked fact (a renamed or dropped hook fails make check instead of at runtime) without forcing idb to stub no-op methods. @runtime_checkable mirrors EvidenceProvider, but a structural isinstance holds only for a class implementing the whole set — which the concrete drivers, owning disjoint subsets, deliberately do not.

SelectorError

Bases: Exception

Selector resolution failed.

UnsupportedAction

Bases: Exception

The actuator backend cannot perform this action.

For example, a multi-touch gesture on idb, which is single-touch. The tool surfaces it as a step failure with a clear reason rather than letting it pass silently.

ManualStepRequired

Bases: UnsupportedAction

A recorded manual takeover step has no deterministic run-time equivalent (BE-0185).

Raised at run time so a human-takeover marker (a CAPTCHA, a biometric prompt) fails loudly and visibly with its label rather than a silent pass or a hang — the honest boundary for an operation only a human can perform. A subclass of UnsupportedAction so the run loop surfaces it as a clean, labeled step failure like any other action the environment cannot perform.

ElementNotFound

Bases: SelectorError

No candidate matched. A wait times out; an immediate action fails.

AmbiguousSelector

Bases: SelectorError

2+ candidates with no way to disambiguate; needs within or index.

Queryable

Bases: Protocol

Just the current-screen read a wait needs — the query surface, not a full Driver.

default_wait_for reads one screen and matches; a shared read base like CoordinateTreeDriver supplies exactly that without implementing the whole actuator surface, so typing the helper to this narrow protocol lets both a full Driver and such a base delegate to it.

permission_capability(service)

The per-service device-control token for a permission service (BE-0276).

One token per vocabulary entry rather than a single deviceControl.permissions token, so a backend that honors only part of the vocabulary (iOS: everything but notifications) can advertise exactly that subset and preflight names the unsupported service individually.

id_candidates(v)

A single id/pattern or a list of OR candidates, normalized to a list (BE-0221).

validate_id_candidates(field, value)

Reject a malformed id / idMatches OR-candidate list; a no-op for a string or None (BE-0221).

Shared by the scenario Selector model and config's readyWhen (a base.Selector) so a candidate list is checked the same way wherever it is authored. A list must be non-empty with no blank entry, and if it contains any dotted (SPEC-form) candidate, that candidate must lead: single-id consumers — the resolver's representative pick, audit coverage bucketing (namespace_of splits on .), the XCUITest / Playwright codegen emitters — take candidate[0], so a dotted-but-not-first list resolves fine at runtime but silently skews them. Failing at load beats debugging a skewed report. An all-underscore list (no dotted candidate) is accepted as-is.

Raises:

Type Description
ValueError

the list is empty / has a blank entry, or a dotted candidate follows a non-dotted first one.

matches(el, sel)

Whether an element satisfies a selector's per-element conditions (all AND-ed).

Parameters:

Name Type Description Default
el Element

One element from a query() snapshot.

required
sel Selector

The selector to test. Only the per-element fields are checked here (id / idMatches / label / labelMatches / traits / value); within (a cross-element spatial constraint, resolved by find_all) and index (a positional pick among matches, applied by resolve_unique) are ignored. id / idMatches may be a list of candidates, satisfied when the element matches any one (BE-0221).

required

Returns:

Type Description
bool

True when every per-element field set on the selector matches the element.

find_all(elements, sel)

Every element matching the selector — backs idMatches resolution and count assertions.

Parameters:

Name Type Description Default
elements list[Element]

One query() snapshot.

required
sel Selector

The selector to match. within scopes the result to elements spatially contained in a container the within selector resolves to: the accessibility tree is flat, so "parent" is geometric — a candidate qualifies when its frame sits inside a container's, and within may nest.

required

Returns:

Type Description
list[Element]

The matching elements, in elements order.

deadline_ticks(timeout, poll_init, poll_max=None)

Yield once per poll to a monotonic deadline, sleeping with capped backoff between ticks.

The one deadline/backoff skeleton the condition waits share (BE-0118, BE-0256): wait_until here and the platform-lifecycle readiness waits (_await_ready / _await_boot) each run their own check body on every yield and decide what to return, while this owns only the monotonic deadline, the exponential backoff (poll_init doubling up to poll_max), and the never-sleep-past-the-deadline sleep — a condition wait with no fixed up-front sleep, so a timeout means the same real seconds regardless of the caller. A fixed interval is poll_max is None (or equal to poll_init); the first yield fires before any sleep.

Parameters:

Name Type Description Default
timeout float

Seconds from the first tick before the deadline passes.

required
poll_init float

The first inter-tick sleep, doubling each tick.

required
poll_max float | None

The backoff ceiling; a fixed poll_init interval when omitted.

None

wait_until(driver, sel, timeout, poll=0.2)

Poll driver.wait_for(sel) against a monotonic deadline until it matches.

A condition wait with no fixed sleep, mirroring the orchestrator's discipline — it turns the backend's single-shot wait_for into a timeout-honouring wait over deadline_ticks, so a timeout means the same real seconds regardless of which backend drives.

Parameters:

Name Type Description Default
driver Driver

The backend whose single-shot wait_for is polled.

required
sel Selector

The selector to wait for.

required
timeout float

Seconds to keep polling before giving up.

required
poll float

Seconds slept between checks.

0.2

Returns:

Type Description
bool

True once the selector matches; False if timeout elapses first.

Raises:

Type Description
ValueError

poll is negative (a caller error surfaced loudly rather than left to time.sleep's opaque exception).

resolve_unique(elements, sel)

Resolve a selector to exactly one element for a single action.

A single action requires a unique match, so an ambiguous selector fails rather than acting on "whatever matched first" — the determinism core (BE-0001).

Parameters:

Name Type Description Default
elements list[Element]

One query() snapshot of the on-screen elements.

required
sel Selector

The selector to resolve. index is honored only as a last resort, picking the nth of several candidates (negative values count from the end).

required

Returns:

Type Description
Element

The one element the selector resolves to.

Raises:

Type Description
ElementNotFound

Nothing matched, or index is out of range.

AmbiguousSelector

Two or more matched and no index disambiguates.

default_wait_for(driver, sel)

The single-shot wait_for body every real backend delegates to (BE-0118, BE-0251).

Whether sel matches the driver's current screen, checked once — the shared wait_until owns the deadline poll, so a backend never loops here. Hoisted into one definition so the identical driver bodies can't silently diverge; a backend that can wait natively still overrides wait_for rather than calling this.

Returns:

Type Description
bool

True when at least one element matches the selector right now.

frame_center(frame)

The center point of an already-resolved element frame (BE-0251).

Takes the resolved (x, y, w, h) so it stays pure geometry — each backend keeps its own selector-to-frame resolution and routes only the arithmetic through here.

gesture_anchor(frame)

A two-finger gesture's center and finger half-distance for a resolved frame (BE-0251).

The half-distance is a quarter of the smaller side, so the two fingers (and a pinch-out up to ~2x) stay within the element's bounds rather than landing on a neighbour.

Returns:

Type Description
tuple[float, float, float]

(cx, cy, half) — the frame center and min(w, h) / 4.