JavaScript API Reference
Overview
Section titled “Overview”The @crowecawcaw/xa11y package provides cross-platform accessibility queries
and actions for Node.js. All methods that touch the accessibility tree
are asynchronous — they run on the napi tokio worker pool so the Node
event loop stays responsive.
import { App } from '@crowecawcaw/xa11y';
const app = await App.byName('Safari');await app.locator('button[name="OK"]').press();type CheckedState
Section titled “type CheckedState”Checked state of a toggleable element.
type CheckedState = 'on' | 'off' | 'mixed';type EventTypeName
Section titled “type EventTypeName”Accessibility event type names, normalised across platforms.
type EventTypeName = | 'focusChanged' | 'valueChanged' | 'nameChanged' | 'stateChanged' | 'structureChanged' | 'windowOpened' | 'windowClosed' | 'windowActivated' | 'windowDeactivated' | 'selectionChanged' | 'menuOpened' | 'menuClosed' | 'textChanged' | 'announcement';type NativeSubscription
Section titled “type NativeSubscription”type NativeSubscription = _NativeSubscription;type TestActionProbe
Section titled “type TestActionProbe”type TestActionProbe = _TestActionProbe
/** * A running application — the entry point for accessibility queries. * * Construct via {@link App.byName}, {@link App.byPid}, or {@link App.list}. * An `App` is **not** an `Element` — it represents the application as a * whole and provides {@link App.locator} to search its accessibility tree. */export declare class App { /** * Find an application by exact name. * * Polls the accessibility API until the app appears or * `options.timeout` (ms) elapses. When omitted, the process-wide * default applies — 5 seconds unless overridden via * `setDefaultTimeout()` / `XA11Y_DEFAULT_TIMEOUT`. Pass `{ timeout: 0 }` * for a single attempt with no waiting. Only "not found" errors trigger * a retry; permission errors and the like fail fast. * * Rejects with `PermissionDeniedError` if accessibility is not enabled, * or `SelectorNotMatchedError` if no matching app is found. */ static byName(name: string, options?: AppLookupOptions | undefined | null): Promise<App> /** * Find an application by process ID. * * See {@link App.byName} for the `options.timeout` behaviour. */ static byPid(pid: number, options?: AppLookupOptions | undefined | null): Promise<App> /** List all running applications with an accessibility tree. */ static list(): Promise<App[]> /** The application's human-readable name (e.g. `"Safari"`). */ get name(): string /** * The application's process ID, or `null` if the platform does not * expose one for this app. */ get pid(): number | null /** * Create a `Locator` scoped to this application's accessibility tree. * * The locator re-resolves `selector` on every operation, so it always * targets the current UI state — see the `Locator` class for the full * API. */ locator(selector: string): Locator /** Get direct children (typically windows) of this application. */ children(): Promise<Element[]> /** * Get an `Element` handle for the application root. * * Useful for invoking Element-level methods (`children()`, `parent()`, * etc.) without going through a locator. Synchronous — the App already * holds the application's accessibility data. */ asElement(): Element /** Subscribe to accessibility events from this application. */ subscribe(): Promise<_NativeSubscription> /** * Capture this application's accessibility tree as a recursive snapshot, * rooted at the application element. * * `maxDepth` limits traversal depth: `0` = only the application node, * `1` = application + direct children (typically windows), and so on. * Omit for the full subtree. */ tree(maxDepth?: number | null): Promise<TreeNode> /** * Render this application's accessibility tree as an indented string. * * Returns the string without printing it. The primary inspection helper * — call `console.log(await app.dump())` to discover the role and name * of every element in the app before writing selectors. * * For the same output from the shell, use `xa11y tree --app NAME`. */ dump(maxDepth?: number | null): Promise<string>}
/** * A snapshot of a node in the accessibility tree. * * Property getters (`role`, `name`, `value`, state flags, etc.) are * synchronous — they read the snapshot data captured when the element * was fetched. Navigation methods (`children()`, `parent()`) are async * and re-query the provider on every call, so you always see the latest * tree state. * * Elements are cheap to pass around; they share the provider handle * internally. */export declare class Element { /** The element's role, as a snake_case string (e.g. `"button"`, `"check_box"`). */ get role(): string /** Human-readable name (title, label, or ARIA name). */ get name(): string | null /** * Current value — text content for editable fields, stringified slider * position, etc. For numeric controls, prefer `numericValue`. */ get value(): string | null /** Supplementary description (tooltip text, ARIA description). */ get description(): string | null /** Numeric value for sliders, spin buttons, and progress indicators. */ get numericValue(): number | null /** Minimum numeric value for bounded controls (slider, spin button). */ get minValue(): number | null /** Maximum numeric value for bounded controls (slider, spin button). */ get maxValue(): number | null /** * Platform-assigned identifier that is stable across queries for the * same element. Not available on every platform / every widget. */ get stableId(): string | null /** Process ID of the owning application. */ get pid(): number | null /** * Names of actions the element advertises (e.g. `["press", "focus"]`). * Use `Locator.performAction(name)` to invoke a custom action, or the * named convenience methods (`press`, `toggle`, etc.) for the common * ones. */ get actions(): Array<string> /** * Screen-coordinate bounding rectangle, or `null` for virtual / * off-screen elements that do not have a physical position. */ get bounds(): Rect | null /** * Platform-specific raw data attached to this element, as a plain JS * object. Keys are provider-defined (e.g. `ax_role`/`ax_subrole` on macOS, * `uia_control_type` on Windows). Values are JSON-compatible — strings, * numbers, booleans, arrays, nested objects. Intended for debugging and * platform-specific queries. */ get raw(): Record<string, unknown> /** `true` if the element is interactive (not greyed out or disabled). */ get enabled(): boolean /** * `true` if the element is currently rendered on screen (not hidden, * not clipped off the viewport). */ get visible(): boolean /** `true` if the element currently has keyboard focus. */ get focused(): boolean /** * Tri-state checked value for checkboxes, toggle buttons, and menu items: * `"on"`, `"off"`, `"mixed"`, or `null` if the element is not toggleable. */ get checked(): CheckedState | null /** `true` if the element is selected (list item, tab, row). */ get selected(): boolean /** * `true` / `false` for expandable elements (disclosures, menus, tree * items); `null` if the element is not expandable. */ get expanded(): boolean | null /** * `true` if the element accepts text editing (text field, text area, * rich-text region). */ get editable(): boolean /** * `true` if the element can receive keyboard focus (distinct from * `focused`, which reports the current state). */ get focusable(): boolean /** * `true` if the element is a modal dialog that blocks interaction with * the rest of the app. */ get modal(): boolean /** `true` for form fields that are marked required. */ get required(): boolean /** * `true` if the element is loading or otherwise indicating a busy * state (progress indicator, spinner region). */ get busy(): boolean /** Get direct children (lazy — each call re-queries the provider). */ children(): Promise<Element[]> /** Get the parent element, or `null` if this is the root. */ parent(): Promise<Element | null> /** Subscribe to accessibility events for this element (typically an app). */ subscribe(): Promise<_NativeSubscription> /** * Capture the subtree rooted at this element as a recursive snapshot. * * `maxDepth` limits traversal depth: `0` = only this node (no children), * `1` = node + direct children, and so on. Omit for the full subtree. */ tree(maxDepth?: number | null): Promise<TreeNode> /** * Render the subtree rooted at this element as an indented string. * * Returns the string without printing it. Same depth semantics as `tree()`. */ dump(maxDepth?: number | null): Promise<string> /** * Click / invoke this element. Acts on the captured snapshot — does * not re-resolve. */ press(): Promise<void> /** Move keyboard focus to this element. Acts on the captured snapshot. */ focus(): Promise<void> /** * Remove keyboard focus from this element. Acts on the captured * snapshot. * * Not supported on Linux or Windows — on those platforms this rejects * with `ActionNotSupportedError`. */ blur(): Promise<void> /** * Toggle a two- or three-state control (checkbox, switch, toggle * button). Acts on the captured snapshot. */ toggle(): Promise<void> /** * Expand a disclosure, menu, or tree item. Acts on the captured * snapshot. */ expand(): Promise<void> /** * Collapse a disclosure, menu, or tree item. Acts on the captured * snapshot. */ collapse(): Promise<void> /** * Select this element (list item, tab, row). Acts on the captured * snapshot. */ select(): Promise<void> /** Open this element's context menu. Acts on the captured snapshot. */ showMenu(): Promise<void> /** * Scroll this element into the visible area. Acts on the captured * snapshot. * * No-op on macOS — the macOS accessibility API has no equivalent. Uses * `Component.ScrollTo` on Linux and `ScrollItemPattern` on Windows. */ scrollIntoView(): Promise<void> /** * Increment a numeric value (slider, spin button) by its platform step. * Acts on the captured snapshot. */ increment(): Promise<void> /** * Decrement a numeric value (slider, spin button) by its platform step. * Acts on the captured snapshot. */ decrement(): Promise<void> /** * Set the text value of this element. Replaces the entire value rather * than inserting at the caret — use `typeText` for insertion. Acts on * the captured snapshot. */ setValue(value: string): Promise<void> /** * Set the numeric value of this element (slider, spin button). Acts on * the captured snapshot. */ setNumericValue(value: number): Promise<void> /** * Type `text` at the current caret position. Acts on the captured * snapshot. * * Uses the platform accessibility API — never simulates keyboard events. * For synthesised keystrokes (global shortcuts, drag gestures), use the * `InputSim` surface instead. */ typeText(text: string): Promise<void> /** * Select the text range from `start` to `end` (0-based character * offsets). Rejects with `InvalidActionDataError` if `start > end`. * Acts on the captured snapshot. */ selectText(start: number, end: number): Promise<void> /** * Perform a custom action by its snake_case name. Acts on the captured * snapshot. * * Use this for actions the element advertises in its `actions` list * that don't have a dedicated method. Rejects with * `ActionNotSupportedError` if the element does not advertise `action`. */ performAction(action: string): Promise<void>}
/** * An accessibility event delivered to a `Subscription`. * * Events are emitted from the source application — focus changes, value * edits, window lifecycle, structural updates. Attach a listener via * `subscription.on(type, handler)` or await one with * `subscription.waitForEvent(type, opts)`. */export declare class Event { /** Event kind, as a camelCase string (e.g. `"focusChanged"`, `"valueChanged"`). */ get type(): EventTypeName /** * For `stateChanged` events: the flag that changed (e.g. `"checked"`, `"busy"`). * `null` for all other event kinds. */ get stateFlag(): string | null /** * For `stateChanged` events: the new boolean value of the flag. * `null` for all other event kinds. */ get stateValue(): boolean | null /** Name of the application that emitted this event. */ get appName(): string /** Process ID of the application that emitted this event. */ get appPid(): number /** Snapshot of the element that triggered the event, if available. */ get target(): Element | null}
/** * Synthesises OS-level pointer and keyboard events. * * Constructed via the module-level `inputSim()` function. Targets are * either an `[x, y]` tuple in screen pixels, or an `Element` (centred on * its bounds). Key values are strings: printable characters are literal * (`"a"`, `"7"`, `";"`); named keys use their Pascal name (`"Enter"`, * `"ArrowUp"`, `"F5"`); modifiers are `"Shift"`, `"Ctrl"`, `"Alt"`, * `"Meta"`. * * Input simulation is distinct from the accessibility action layer — * prefer `Locator.press` / `Locator.typeText` when the target exposes * the semantic action. Use `InputSim` for gestures with no a11y * equivalent (drag-and-drop, scroll wheels, global shortcuts). * * Methods return `Promise<void>` — the underlying OS input APIs are * synchronous but can block briefly, so they run on the napi worker pool. */export declare class InputSim { /** Left-click once at `target`. */ click(target: Array<number> | Element): Promise<void> /** Left double-click at `target`. */ doubleClick(target: Array<number> | Element): Promise<void> /** Right-click at `target`. */ rightClick(target: Array<number> | Element): Promise<void> /** Move the pointer to `target` without pressing any button. */ moveTo(target: Array<number> | Element): Promise<void> /** Left-drag from `start` to `end`. Default duration (150 ms). */ drag(start: Array<number> | Element, end: Array<number> | Element): Promise<void> /** * Scroll at `target`. `dx` positive → right, `dy` positive → content * scrolls down. Defaults: `0`, `0` (a no-op). */ scroll(target: Array<number> | Element, dx?: number | undefined | null, dy?: number | undefined | null): Promise<void> /** Tap a key (press + release). */ press(key: string): Promise<void> /** Tap `key` while the keys in `held` are held down. */ chord(key: string, held?: Array<string> | undefined | null): Promise<void> /** Type literal text into the currently focused control. */ typeText(text: string): Promise<void>}
/** * A resilient element reference that re-queries on each interaction. * * Locators never hold a live reference to a UI element. Instead, they * store a selector and resolve it on demand, making them immune to * staleness. Action methods (`press`, `typeText`, `toggle`, …) auto-wait * for the element to appear before acting, up to the process-wide default * timeout — 5 seconds unless overridden via `setDefaultTimeout()` or the * `XA11Y_DEFAULT_TIMEOUT` environment variable. * * Locators are cheap to clone — the chaining methods (`child`, `descendant`, * `nth`, `first`) return new locators rather than mutating in place. * * @example * ```ts * const app = await App.byName('MyApp');Errors
Section titled “Errors”All operations throw subclasses of XA11yError. Catch a specific subclass with instanceof and let the rest propagate.
XA11yError
Section titled “XA11yError”Base class for all xa11y errors.
PermissionDeniedError
Section titled “PermissionDeniedError”Accessibility permissions have not been granted.
AccessibilityNotEnabledError
Section titled “AccessibilityNotEnabledError”The target app advertises an accessibility tree but it is empty.
Raised on Linux when a Chromium/Electron app is launched without
--force-renderer-accessibility (or the ACCESSIBILITY_ENABLED=1
environment variable), so the renderer accessibility bridge never
populates the window’s subtree.
SelectorNotMatchedError
Section titled “SelectorNotMatchedError”No element matched the selector (also used for stale elements).
Carries a structured diagnosis so the failure is understandable without
re-running it under manual tree dumps. Every field is always present
(null / [] when not applicable); the same content is rendered into
the message.
Properties
Section titled “Properties”selector
Section titled “selector”Type: string | null
The selector that failed to match.
condition
Section titled “condition”Type: string | null
What the operation was waiting for / trying to find, if known.
lastObserved
Section titled “lastObserved”Type: string | null
What the failing operation last observed.
candidates
Section titled “candidates”Type: string[]
Bounded near-miss candidates (e.g. same-role elements).
Type: string | null
Bounded rendering of the search scope (tree dump or app list).
elapsedMs
Section titled “elapsedMs”Type: number | null
Always null for this class (parity with TimeoutError).
ActionNotSupportedError
Section titled “ActionNotSupportedError”The requested action is not supported on the target element.
TimeoutError
Section titled “TimeoutError”An operation exceeded its timeout.
Carries a structured diagnosis: what the wait was for (condition +
selector), what it last observed, and — when the selector never
matched — bounded scope context (candidates + scope). Every field is
always present (null / [] when not applicable); the same content is
rendered into the message.
Properties
Section titled “Properties”elapsedMs
Section titled “elapsedMs”Type: number | null
Wall-clock milliseconds the operation waited before giving up.
condition
Section titled “condition”Type: string | null
What the wait was for: 'visible', 'attached', 'press target actionable (visible && enabled)', …
selector
Section titled “selector”Type: string | null
The selector being resolved, when the wait had one.
lastObserved
Section titled “lastObserved”Type: string | null
The last poll’s observation (matched-with-states vs never matched).
candidates
Section titled “candidates”Type: string[]
Bounded near-miss candidates; collected only when the selector never matched.
Type: string | null
Bounded rendering of the search scope; collected only when the selector never matched.
InvalidSelectorError
Section titled “InvalidSelectorError”The selector string has invalid syntax.
InvalidActionDataError
Section titled “InvalidActionDataError”The data passed to an action method was rejected (e.g. out-of-range slider value).
PlatformError
Section titled “PlatformError”An OS-level accessibility error occurred.
Classes
Section titled “Classes”Locator
Section titled “Locator”Properties
Section titled “Properties”selector
Section titled “selector”Type: string
The CSS-like selector string for this locator.
Methods
Section titled “Methods”nth(n: number): Locator
Section titled “nth(n: number): Locator”Return a new Locator that selects the n-th match (1-based).
first(): Locator
Section titled “first(): Locator”Return a new Locator that selects the first match.
child(selector: string): Locator
Section titled “child(selector: string): Locator”Return a new Locator scoped to direct children matching selector.
descendant(selector: string): Locator
Section titled “descendant(selector: string): Locator”Return a new Locator scoped to descendants matching selector.
exists(): Promise<boolean>
Section titled “exists(): Promise<boolean>”Check whether a matching element exists (does not throw on miss).
count(): Promise<number>
Section titled “count(): Promise<number>”Count matching elements.
element(): Promise<Element>
Section titled “element(): Promise<Element>”Resolve to a single [Element] snapshot. Throws SelectorNotMatchedError
if no element matches.
elements(): Promise<Element[]>
Section titled “elements(): Promise<Element[]>”Resolve to all matching [Element] snapshots.
tree(maxDepth?: number | null): Promise<TreeNode>
Section titled “tree(maxDepth?: number | null): Promise<TreeNode>”Capture the subtree rooted at the matched element as a recursive snapshot.
maxDepth limits traversal depth: 0 = only this node (no children),
1 = node + direct children, and so on. Omit for the full subtree.
Resolves the selector once; rejects with SelectorNotMatchedError
if no match — does not auto-wait.
dump(maxDepth?: number | null): Promise<string>
Section titled “dump(maxDepth?: number | null): Promise<string>”Render the subtree rooted at the matched element as an indented string.
Returns the string without printing it. Same depth and resolution
semantics as tree().
press(): Promise<void>
Section titled “press(): Promise<void>”Click / invoke the matched element.
Auto-waits for the element to exist before acting. For elements whose
primary activation is toggle or select (checkbox, tab, radio),
press dispatches to that semantic — there is no need to distinguish.
focus(): Promise<void>
Section titled “focus(): Promise<void>”Move keyboard focus to the matched element.
blur(): Promise<void>
Section titled “blur(): Promise<void>”Remove keyboard focus from the matched element.
Not supported on Linux or Windows — on those platforms this rejects
with ActionNotSupportedError.
toggle(): Promise<void>
Section titled “toggle(): Promise<void>”Toggle a two- or three-state control (checkbox, switch, toggle button).
expand(): Promise<void>
Section titled “expand(): Promise<void>”Expand a disclosure, menu, or tree item.
collapse(): Promise<void>
Section titled “collapse(): Promise<void>”Collapse a disclosure, menu, or tree item.
select(): Promise<void>
Section titled “select(): Promise<void>”Select the matched element (list item, tab, row).
showMenu(): Promise<void>
Section titled “showMenu(): Promise<void>”Open the element’s context menu.
scrollIntoView(): Promise<void>
Section titled “scrollIntoView(): Promise<void>”Scroll the element into the visible area.
No-op on macOS — the macOS accessibility API has no equivalent. Uses
Component.ScrollTo on Linux and ScrollItemPattern on Windows.
increment(): Promise<void>
Section titled “increment(): Promise<void>”Increment a numeric value (slider, spin button) by its platform step.
decrement(): Promise<void>
Section titled “decrement(): Promise<void>”Decrement a numeric value (slider, spin button) by its platform step.
setValue(value: string): Promise<void>
Section titled “setValue(value: string): Promise<void>”Set the text value of the matched element. Replaces the entire value
rather than inserting at the caret — use typeText for insertion.
setNumericValue(value: number): Promise<void>
Section titled “setNumericValue(value: number): Promise<void>”Set the numeric value of the matched element (slider, spin button).
typeText(text: string): Promise<void>
Section titled “typeText(text: string): Promise<void>”Type text at the current caret position.
Uses the platform accessibility API — never simulates keyboard events.
For synthesised keystrokes (global shortcuts, drag gestures), use the
InputSim surface instead.
selectText(start: number, end: number): Promise<void>
Section titled “selectText(start: number, end: number): Promise<void>”Select the text range from start to end (0-based character offsets).
Rejects with InvalidActionDataError if start > end.
performAction(action: string): Promise<void>
Section titled “performAction(action: string): Promise<void>”Perform a custom action by its snake_case name.
Use this for actions the element advertises in its actions list
that don’t have a dedicated method. Rejects with
ActionNotSupportedError if the element does not advertise action.
waitVisible(timeoutSeconds?: number): Promise<Element>
Section titled “waitVisible(timeoutSeconds?: number): Promise<Element>”Wait for a matching element to become visible.
Rejects with TimeoutError if still hidden after timeoutSeconds.
waitAttached(timeoutSeconds?: number): Promise<Element>
Section titled “waitAttached(timeoutSeconds?: number): Promise<Element>”Wait for a matching element to exist in the tree (may not be visible).
Rejects with TimeoutError if no match appears within timeoutSeconds.
waitDetached(timeoutSeconds?: number): Promise<void>
Section titled “waitDetached(timeoutSeconds?: number): Promise<void>”Wait for the matching element to be removed from the tree.
waitEnabled(timeoutSeconds?: number): Promise<Element>
Section titled “waitEnabled(timeoutSeconds?: number): Promise<Element>”Wait for the matching element to become enabled (interactive).
waitHidden(timeoutSeconds?: number): Promise<void>
Section titled “waitHidden(timeoutSeconds?: number): Promise<void>”Wait for the matching element to be hidden or removed.
waitDisabled(timeoutSeconds?: number): Promise<Element>
Section titled “waitDisabled(timeoutSeconds?: number): Promise<Element>”Wait for the matching element to become disabled (non-interactive).
waitFocused(timeoutSeconds?: number): Promise<Element>
Section titled “waitFocused(timeoutSeconds?: number): Promise<Element>”Wait for the matching element to receive keyboard focus.
waitUnfocused(timeoutSeconds?: number): Promise<Element>
Section titled “waitUnfocused(timeoutSeconds?: number): Promise<Element>”Wait for the matching element to lose keyboard focus.
Screenshot
Section titled “Screenshot”A captured image: raw RGBA8 pixels plus dimensions and scale.
width and height are in physical pixels. scale is the physical-to-
logical ratio (1.0 on standard displays, 2.0 on typical Retina).
pixels.length equals width * height * 4.
Properties
Section titled “Properties”Type: number
Image width in physical pixels.
height
Section titled “height”Type: number
Image height in physical pixels.
Type: number
Physical-to-logical pixel ratio (1.0 on standard displays, 2.0 on typical Retina, 1.5 / 1.75 / 2.0 on common Windows / Linux HiDPI).
pixels
Section titled “pixels”Type: Buffer
Raw RGBA8 pixel bytes (width * height * 4).
Methods
Section titled “Methods”toPng(): Buffer
Section titled “toPng(): Buffer”Encode the image as a PNG and return the bytes.
savePng(path: string): void
Section titled “savePng(path: string): void”Encode as PNG and write to path.
AppLookupOptions
Section titled “AppLookupOptions”Options for App.byName / App.byPid.
A bounding rectangle in screen coordinates.
Coordinates use the platform’s native coordinate space: points on macOS,
physical pixels on Windows and Linux. Origin is the top-left of the
primary display; negative x / y are valid on multi-monitor setups.
Properties
Section titled “Properties”Type: number
Left edge, in screen coordinates.
Type: number
Top edge, in screen coordinates.
Type: number
Width in screen-coordinate units.
height
Section titled “height”Type: number
Height in screen-coordinate units.
TreeNode
Section titled “TreeNode”A node in a recursive snapshot of the accessibility subtree.
Returned by Element.tree() and Locator.tree(). Each node carries the
role, display name, and value of one element, plus its children recursively.
children is empty when maxDepth was reached or the element is a leaf.
Properties
Section titled “Properties”Type: string
children
Section titled “children”Type: Array<TreeNode>
Functions
Section titled “Functions”getDefaultTimeout(): number
Section titled “getDefaultTimeout(): number”Get the effective process-wide default timeout, in seconds.
Resolution order: the [set_default_timeout] value, else the
XA11Y_DEFAULT_TIMEOUT environment variable, else the built-in 5.0.
inputSim(): InputSim
Section titled “inputSim(): InputSim”Construct an InputSim backed by the platform’s native input path
(CGEvent on macOS, SendInput on Windows, XTest on X11).
Throws PlatformError on a Wayland-only Linux session (no XTest
available). InputSim is cheap to hold; construct one and reuse.
locator(selector: string): Locator
Section titled “locator(selector: string): Locator”Create a top-level Locator that searches from the
system accessibility root (across all applications).
setDefaultTimeout(seconds: number): void
Section titled “setDefaultTimeout(seconds: number): void”Set the process-wide default timeout, in seconds.
Becomes the default for every auto-waiting action method, wait* call,
and app lookup (App.byName / App.byPid) that doesn’t pass an explicit
timeout. An explicit per-call timeout always wins. Takes precedence over
the XA11Y_DEFAULT_TIMEOUT environment variable (seconds, read once on
first use).
Pass 0 for “single attempt, no polling” semantics. Throws for negative
or non-finite values.