e2e 0.15.1 → 0.15.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. package/README.md +51 -24
  2. package/dist/.build.json +1 -1
  3. package/dist/agent/act.js +3 -3
  4. package/dist/agent/act.js.map +1 -1
  5. package/dist/agent/error.d.ts +16 -3
  6. package/dist/agent/error.d.ts.map +1 -1
  7. package/dist/agent/error.js +25 -6
  8. package/dist/agent/error.js.map +1 -1
  9. package/dist/agent/model/tool-choice.d.ts +24 -1
  10. package/dist/agent/model/tool-choice.d.ts.map +1 -1
  11. package/dist/agent/model/tool-choice.js +27 -1
  12. package/dist/agent/model/tool-choice.js.map +1 -1
  13. package/dist/agent/step-cache.d.ts +14 -3
  14. package/dist/agent/step-cache.d.ts.map +1 -1
  15. package/dist/agent/step-cache.js +13 -3
  16. package/dist/agent/step-cache.js.map +1 -1
  17. package/dist/agent/tool-loop.d.ts.map +1 -1
  18. package/dist/agent/tool-loop.js +62 -9
  19. package/dist/agent/tool-loop.js.map +1 -1
  20. package/dist/cache/context.d.ts +19 -6
  21. package/dist/cache/context.d.ts.map +1 -1
  22. package/dist/cache/context.js +15 -21
  23. package/dist/cache/context.js.map +1 -1
  24. package/dist/cli/init/scaffold.d.ts +1 -0
  25. package/dist/cli/init/scaffold.d.ts.map +1 -1
  26. package/dist/cli/init/scaffold.js +2 -1
  27. package/dist/cli/init/scaffold.js.map +1 -1
  28. package/dist/cli/init/sibling-versions.json +1 -1
  29. package/dist/expect/values.js +1 -1
  30. package/dist/expect/values.js.map +1 -1
  31. package/dist/run/execute.d.ts.map +1 -1
  32. package/dist/run/execute.js +21 -8
  33. package/dist/run/execute.js.map +1 -1
  34. package/dist/types.d.ts +1 -1
  35. package/dist/types.d.ts.map +1 -1
  36. package/docs/agent-steps.mdx +126 -0
  37. package/docs/agents.mdx +217 -0
  38. package/docs/api-testing.mdx +65 -0
  39. package/docs/authentication.mdx +345 -0
  40. package/docs/browser.mdx +440 -0
  41. package/docs/bug-bash.mdx +90 -0
  42. package/docs/cache.mdx +306 -0
  43. package/docs/ci.mdx +210 -0
  44. package/docs/coding-agents.mdx +183 -0
  45. package/docs/debugging.mdx +291 -0
  46. package/docs/executors.mdx +243 -0
  47. package/docs/explore.mdx +174 -0
  48. package/docs/github.mdx +184 -0
  49. package/docs/index.mdx +82 -0
  50. package/docs/integrations/eas.mdx +216 -0
  51. package/docs/integrations/index.mdx +24 -0
  52. package/docs/integrations/kernel.mdx +163 -0
  53. package/docs/migrate/cypress.mdx +124 -0
  54. package/docs/migrate/detox.mdx +140 -0
  55. package/docs/migrate/maestro.mdx +229 -0
  56. package/docs/migrate/playwright.mdx +427 -0
  57. package/docs/migrate/selenium.mdx +110 -0
  58. package/docs/mobile-ci.mdx +567 -0
  59. package/docs/mobile.mdx +555 -0
  60. package/docs/models.mdx +241 -0
  61. package/docs/quickstart.mdx +260 -0
  62. package/docs/reference/agent.mdx +524 -0
  63. package/docs/reference/app.mdx +72 -0
  64. package/docs/reference/cli.mdx +817 -0
  65. package/docs/reference/config.mdx +986 -0
  66. package/docs/reference/engine.mdx +1362 -0
  67. package/docs/reference/environment.mdx +113 -0
  68. package/docs/reference/errors.mdx +143 -0
  69. package/docs/reference/expect.mdx +513 -0
  70. package/docs/reference/mcp.mdx +202 -0
  71. package/docs/reference/mobile.mdx +522 -0
  72. package/docs/reference/reporters.mdx +216 -0
  73. package/docs/reference/screen.mdx +688 -0
  74. package/docs/reference/test.mdx +449 -0
  75. package/docs/reference/web.mdx +611 -0
  76. package/docs/security.mdx +292 -0
  77. package/docs/starting-your-app.mdx +231 -0
  78. package/docs/subscriptions.mdx +157 -0
  79. package/docs/telemetry.mdx +185 -0
  80. package/docs/tools.mdx +150 -0
  81. package/docs/viewports.mdx +146 -0
  82. package/docs/writing-an-engine.mdx +316 -0
  83. package/docs/writing-tests.mdx +256 -0
  84. package/package.json +2 -1
  85. package/skills/e2e/SKILL.md +4 -1
  86. package/skills/e2e/references/agent.md +3 -1
  87. package/skills/e2e/references/setup.md +13 -4
@@ -0,0 +1,1362 @@
1
+ ---
2
+ title: e2e/engine
3
+ description: Build an engine with observations, actions, lifecycle hooks, and fixtures.
4
+ ---
5
+
6
+ Import engine types and helpers from `e2e/engine`. `defineEngine` validates
7
+ and freezes your implementation, derives its capabilities, and returns a
8
+ handle for `targets` in the config.
9
+
10
+ ```ts
11
+ import { defineEngine, type EngineHandle } from 'e2e/engine';
12
+
13
+ export function defineEngine(spec: Engine): EngineHandle;
14
+ export function isEngineHandle(value: unknown): value is EngineHandle;
15
+ ```
16
+
17
+ An engine drives a user interface, such as a browser or simulator. It
18
+ implements shared types for nodes, locators, actions, and errors. Use
19
+ `test.extend` fixtures for resources outside the UI, such as APIs or databases.
20
+
21
+ Validation fails with `INVALID_CONFIG` for:
22
+
23
+ - a spec that is not an object, or a missing or empty `name`;
24
+ - a missing or empty `version`, or a `spiVersion` other than `1`;
25
+ - an empty `platform`, or a `workers` that is not a positive safe integer;
26
+ - a hook (`observe`, `locate`, `perform`, `performAt`, `prepare`, `finish`,
27
+ `init`, `startAttempt`, `settleAttempt`, `endAttempt`, `dispose`,
28
+ `validateApp`) that is not a function;
29
+ - an unknown top-level key on an object literal (a class instance's own
30
+ fields are not checked);
31
+ - `perform`, `locate`, `performAt`, or `keyboard` without `observe`;
32
+ - `perform` without `actions` or the reverse; an `actions` list that is
33
+ empty, repeats a kind, or names one outside `LOCATOR_ACTION_KINDS`;
34
+ - `performAt` without `pointerActions` or the reverse; a `pointerActions`
35
+ list that is empty, repeats a kind, or names one outside
36
+ `POINTER_ACTION_KINDS`;
37
+ - a `tapModifiers` that is not a boolean, or `true` without `perform`;
38
+ - a `keyboard`, `state`, `artifacts`, or `session` manifest with an unknown
39
+ key or a non-function member. `keyboard` needs `type` and `press`; `state`
40
+ needs both `capture` and `restore`; `artifacts` needs `screenshot`;
41
+ `startTrace`/`stopTrace` and `startVideo`/`stopVideo` come in pairs;
42
+ - a `secrets` that is not an array of `secrets.get()` handles;
43
+ - an `app` member: the app under test is declared on the target, and an
44
+ engine checks what it needs in `validateApp`;
45
+ - a `fixtures` that is not an object, a fixture that is not a factory
46
+ function, or a fixture name that is not a lower-camel identifier or shadows
47
+ `agent`, `app`, `screen`, `platform`, or `session`.
48
+
49
+ Every function on the handle is bound to the spec it came from, so a class
50
+ instance is as valid a body as an object literal.
51
+
52
+ ## Engine
53
+
54
+ ```ts
55
+ /** The body of one target: typed, model-free, capability-graded. */
56
+ export interface Engine {
57
+ /** Engine name, for diagnostics. */
58
+ readonly name: string;
59
+ /**
60
+ * Required implementation version, recorded in reports and cache keys.
61
+ * Cached traces cannot replay across engine versions.
62
+ */
63
+ readonly version: string;
64
+ /**
65
+ * Contract version literal. Additive optional members never bump it;
66
+ * changed required semantics do.
67
+ */
68
+ readonly spiVersion: EngineSpiVersion;
69
+ /**
70
+ * Platform label this engine drives (`web`, `ios`, `android`, or a label of
71
+ * the engine's own). Tests filter on it through `platforms` and tool packs
72
+ * are scoped by it; the harness attaches no meaning to any value. A target
73
+ * inherits it; a target that names a platform of its own must agree with it.
74
+ */
75
+ readonly platform?: string;
76
+ /**
77
+ * Maximum workers for this target, such as the number of devices in a
78
+ * pool. The scheduler applies this cap even when `config.workers` is
79
+ * higher. Omit when the engine has no concurrency limit.
80
+ */
81
+ readonly workers?: number;
82
+ /** capability: observation. */
83
+ observe?(context: OperationContext, options?: EngineObserveOptions): Promise<EngineSnapshot>;
84
+ /**
85
+ * Performs one action on a ref from the newest observation or `locate`,
86
+ * with the platform's actionability checks. Both agent tools and locator
87
+ * actions call this method. A `swipe` on the observation root swipes the
88
+ * viewport.
89
+ *
90
+ * For safe trace replay, throw a retryable
91
+ * `NODE_STALE`-coded error when a ref no longer binds, and an
92
+ * `ACTION_MAY_HAVE_COMMITTED`-coded error when input may have reached the
93
+ * app. The runner never repeats an action that may have committed.
94
+ */
95
+ perform?(ref: NodeRef, action: LocatorAction, context: OperationContext): Promise<void>;
96
+ /**
97
+ * The action kinds `perform` honors, required with it. The harness offers
98
+ * the agent and the `screen` tier exactly these: a kind left out is never
99
+ * presented as a tool that can only decline, and `screen`/`Locator` methods
100
+ * for it fail with `UNSUPPORTED_CAPABILITY` before reaching the engine.
101
+ * `perform` still throws `UNSUPPORTED_CAPABILITY` for a declared kind one
102
+ * particular node cannot take (a `check` on a toggle whose state is unknown).
103
+ */
104
+ readonly actions?: readonly LocatorActionKind[];
105
+ /**
106
+ * capability: location - requires observation. Deterministic locator
107
+ * resolution for the `screen`/`expect` tier: resolve one expression to the
108
+ * nodes it currently matches. The harness owns polling, strictness, and
109
+ * staleness; an engine resolves once, immediately.
110
+ */
111
+ locate?(
112
+ expression: LocatorExpression,
113
+ context: OperationContext,
114
+ ): Promise<readonly SemanticNode[]>;
115
+ /**
116
+ * capability: pointer - requires observation. Performs one pointer action
117
+ * at a viewport point, in the CSS pixels of `SemanticNode.rect`, with no
118
+ * node behind it: the agent's `tap_at` verb lands here when the point the
119
+ * model named in a screenshot sits on nothing the tree lists (a shape on a
120
+ * canvas, a pin on a map, a control in a system sheet), and so do a test's
121
+ * `screen.tapAt`, `screen.swipe({ from, to })`, and `tap({ position })`.
122
+ * Dispatch the pointer at the point as given; the harness has clamped an
123
+ * agent's point to the viewport and validated a test's. Throw
124
+ * `UNSUPPORTED_CAPABILITY` for a declared kind the surface cannot deliver
125
+ * at this particular point.
126
+ */
127
+ performAt?(point: ViewportPoint, action: PointerAction, context: OperationContext): Promise<void>;
128
+ /**
129
+ * The pointer action kinds `performAt` honors, required with it. The
130
+ * harness routes a point-addressed action to the engine only for a kind
131
+ * listed here; an undeclared kind fails with `UNSUPPORTED_CAPABILITY`
132
+ * before reaching the engine.
133
+ */
134
+ readonly pointerActions?: readonly PointerActionKind[];
135
+ /**
136
+ * The surface honors `modifiers` on the `tap`, `doubleTap`, and
137
+ * `secondaryTap` actions `perform` receives. Without it the harness fails
138
+ * an action that carries modifiers with `UNSUPPORTED_CAPABILITY` before it
139
+ * reaches the engine.
140
+ */
141
+ readonly tapModifiers?: boolean;
142
+ /** capability: keyboard - requires observation. Input to whatever holds focus; see `EngineKeyboard`. */
143
+ readonly keyboard?: EngineKeyboard;
144
+ /**
145
+ * Named deterministic surfaces this engine contributes to TestFixtures
146
+ * (a device fixture, a document fixture - anything). Keys become fixture and
147
+ * capability names; `requires: ['<name>']` gates at selection.
148
+ */
149
+ readonly fixtures?: Readonly<Record<string, EngineFixtureFactory>>;
150
+ /** capability: state - opaque snapshot capture/restore for session reuse. */
151
+ readonly state?: EngineStateCapability;
152
+ /** capability: artifacts - screenshots and traces under the attempt directory. */
153
+ readonly artifacts?: EngineArtifacts;
154
+ /**
155
+ * Checks the app a target declares against what this engine can drive, at
156
+ * config load, synchronously: throw `INVALID_CONFIG` naming
157
+ * `info.targetName` for a field the platform needs and the target left out
158
+ * (a URL, a bundle id) or one it cannot drive. The harness has already
159
+ * checked every field's shape.
160
+ */
161
+ validateApp?(app: EngineAppDeclaration, info: EngineAppCheckInfo): void;
162
+ /**
163
+ * The `secrets.get()` handles the engine's options hold, such as a
164
+ * basic-auth password. The config load checks each names a configured
165
+ * secret (`INVALID_CONFIG` otherwise), and only these resolve through
166
+ * `EngineAttemptContext.resolveSecret`. The handles carry names only, so
167
+ * no value reaches the config digest.
168
+ */
169
+ readonly secrets?: readonly Secret[];
170
+ /** Steering hooks: open, back, restart, reset. */
171
+ readonly session?: EngineSession;
172
+ /**
173
+ * Runs once per target in the runner, after collection and before workers,
174
+ * app commands, and the run's clock start. Download dependencies or boot
175
+ * devices here; report progress through `info.log`. No launch timeout
176
+ * applies. A failure ends the run as an infrastructure error.
177
+ * The result can lower the target's worker cap, for example after
178
+ * discovering how many devices are available.
179
+ */
180
+ prepare?(info: EnginePrepareInfo): Promise<void | EnginePrepareResult>;
181
+ /**
182
+ * Once per run and target in the runner process, after every worker of the
183
+ * target is gone, on every exit path, whenever `prepare` was called.
184
+ * Release what `prepare` acquired for the run (a leased cloud device).
185
+ * Runs within the cleanup budget, never cancelled by an interrupt; a
186
+ * failure is a cleanup-phase run error.
187
+ */
188
+ finish?(info: EngineFinishInfo): Promise<void>;
189
+ /**
190
+ * Once per worker, before the first step; boot devices here, not in a step
191
+ * budget. The same handle can be booted again after `dispose`: a config-held
192
+ * handle outlives an in-process worker, so init MUST work on a disposed
193
+ * engine as it does on a fresh one.
194
+ */
195
+ init?(info: EngineInitInfo): Promise<void>;
196
+ /** Before each attempt: set up per-test isolation. */
197
+ startAttempt?(context: EngineAttemptContext): Promise<void>;
198
+ /**
199
+ * After the body has settled, and again after teardown, before the verdict,
200
+ * within the cleanup budget: report a failure that landed on a path no step
201
+ * awaited (a callback the surface ran for the test: a request interceptor,
202
+ * a native dialog handler). Wait for one still running, then throw its
203
+ * error; the attempt fails with it the way a step would, an error already
204
+ * classified keeping its code. MUST report each failure once.
205
+ */
206
+ settleAttempt?(context: EngineCleanupContext): Promise<void>;
207
+ /**
208
+ * After each attempt, within the cleanup budget: tear that isolation down.
209
+ * MUST be idempotent, and safe to call after a failed `startAttempt`.
210
+ */
211
+ endAttempt?(context: EngineCleanupContext): Promise<void>;
212
+ /**
213
+ * Worker shutdown, within the cleanup budget; failure is a run error. Runs
214
+ * whether or not `init` ran, so it MUST tolerate a cold engine.
215
+ */
216
+ dispose?(context: EngineCleanupContext): Promise<void>;
217
+ }
218
+ ```
219
+
220
+ ```ts
221
+ /** What every engine call carries: budget, ids, and who is acting. */
222
+ export interface OperationContext {
223
+ /** Aborts with the attempt or when the operation budget is spent. */
224
+ readonly signal: AbortSignal;
225
+ /** Remaining operation budget when the call starts. */
226
+ readonly timeoutMs: number;
227
+ /** The run id. */
228
+ readonly runId: string;
229
+ /** The attempt id. */
230
+ readonly attemptId: string;
231
+ /**
232
+ * Who asked: a test's own deterministic step, or the agent acting on a
233
+ * screen it observed. A deterministic step verifies its outcome with
234
+ * `expect`, so an engine may act as soon as the target holds still; an
235
+ * agent reads the screen right after acting, so an engine may wait for the
236
+ * transition to end first.
237
+ */
238
+ readonly origin: 'test' | 'agent';
239
+ }
240
+ ```
241
+
242
+ ### Capabilities
243
+
244
+ | Member | Capability | Requires | Unlocks |
245
+ | --- | --- | --- | --- |
246
+ | `observe` | `observation` | - | judgment tier, prompt snapshots, trace replay, the viewport swipe's root |
247
+ | `perform` + `actions` | `actions` | `observe` | the agent's grammar verbs (`tap`, `double_tap`, `long_press`, `right_click`, `hover`, `type`, `press`, `select`, `check`, `type_secret`, `scroll`, `scroll_to`, `drag`, `upload`, and `tap_at` or `hover_at` when the point lands on a listed node) and the `screen`/`Locator` action set with platform actionability checks, each only for a kind listed in `actions` |
248
+ | `locate` | `location` | `observe` | `screen`, `Locator`, `expect(locator)` |
249
+ | `tapModifiers: true` | - | `perform` | `modifiers` on `tap`, `doubleTap`, and `secondaryTap` (`row.click({ modifiers: ['Shift'] })`), held for the click; without it such a call is `UNSUPPORTED_CAPABILITY` |
250
+ | `performAt` + `pointerActions` | `pointer` | `observe` | the agent's `tap_at` and `hover_at` verbs when the point lands on nothing the tree lists (the `tap` and `hover` pointer kinds), and the test's `screen.tapAt` and `tap({ position })` (the `tap` kind) and `screen.swipe({ from, to })` (the `swipeTo` kind): one pointer action at a viewport point in the CSS pixels of `SemanticNode.rect`, with no node behind it |
251
+ | `keyboard.{type,press,dismiss?}` | `keyboard` | `observe` | the agent's `type` and `press` without a target, the keyboard fallback of `type_at` and `press_at` on a point the tree lists nothing at, and `dismiss_keyboard` when `dismiss` exists: input to whatever holds focus, for a field drawn on a canvas or flattened out of a platform's tree; with the `focus` action, `locator.pressSequentially`, which the harness composes as `focus` then `keyboard.type` |
252
+ | `fixtures.<name>` | `<name>` | - | a contributed fixture; `requires: ['<name>']` gates at selection |
253
+ | `state` | `state` | - | `test.setup` sessions and the `session:` option |
254
+ | `artifacts` | `artifacts` | - | `app.screenshot()`, assertion evidence, the failure screenshot, and every [`trace` and `video` mode](/reference/config#recording) but `off`, from the config, a target, a test, `--trace`, or `--video` |
255
+ | `validateApp` | - | - | a config-load check of the target's `app` |
256
+ | `session` | - | - | `app.open()` and the agent's `navigate` verb (`open`), `app.back()` and the agent's `back` verb (`back`), `app.restart()` (`restart`), `app.clearState()` (`reset`) |
257
+
258
+ The agent offers the model only what `actions` declares: `tap` needs the
259
+ `tap` kind, `double_tap`, `long_press`, and `right_click` need `doubleTap`,
260
+ `longPress`, and `secondaryTap`; `hover` needs `hover`; `type` and `type_secret` need `fill`;
261
+ `press` needs `press`; `select` needs `selectOption`; `check` needs both
262
+ `check` and `uncheck`, since it sets a state rather than flipping one;
263
+ `scroll` and `scroll_to({ text })` need `swipe`; `scroll_to({ target })` needs `scrollIntoView`; `drag` needs
264
+ `dragTo`; `upload` needs `setInputFiles`; `navigate` needs `session.open`;
265
+ `back` needs `session.back`; `tap_at` and `hover_at` need the `tap` or
266
+ `hover` kind in `actions` or in `pointerActions`. `clear` and `focus` unlock
267
+ no verb. An undeclared verb is absent from the toolset, and a
268
+ `screen`/`Locator` method for an undeclared kind fails with
269
+ `UNSUPPORTED_CAPABILITY` before reaching the engine.
270
+
271
+ `engine.capabilities` is the computed `ReadonlySet` of capability names, and
272
+ is what selection, the report, and `UNSUPPORTED_CAPABILITY` messages read.
273
+ Selection also grades a target by the kind of app it declares, not by its
274
+ engine: `browser` for a target with `app.url`, `native-app` for one with
275
+ `app.bundleId` or `app.appPath`, so a test can `requires: ['native-app']`.
276
+
277
+ ```ts
278
+ /**
279
+ * Capability names: the closed harness capabilities plus one name per
280
+ * contributed fixture. `actions` is `perform`; `location` is `locate`; both
281
+ * require observation, because their refs live in the observation's id space.
282
+ * `pointer` is `performAt`, an action addressed by a viewport point rather
283
+ * than a node, and requires observation because the point is read off its
284
+ * pixels.
285
+ */
286
+ export type EngineCapability =
287
+ | 'observation'
288
+ | 'actions'
289
+ | 'location'
290
+ | 'pointer'
291
+ | 'keyboard'
292
+ | 'state'
293
+ | 'artifacts'
294
+ | (string & {});
295
+
296
+ /**
297
+ * capability: keyboard - requires observation. Input to whatever holds focus,
298
+ * with no node behind it: the agent's `type` and `press` without a target, and
299
+ * the fallback of its point-addressed `type_at` and `press_at` when the point
300
+ * lands on nothing the tree lists (a field drawn on a canvas, an input a
301
+ * platform flattens out of its accessibility tree). Focus is the app's: the
302
+ * harness tapped first, or the model said the field already has it.
303
+ */
304
+ export interface EngineKeyboard {
305
+ /**
306
+ * Types `text` into the focused field as a person typing would, through
307
+ * the platform's keyboard path: a browser dispatches the key events of each
308
+ * character, a device sends the text through its keyboard. Where the
309
+ * platform has key events the app must receive them; setting the value
310
+ * without keystrokes is not a compliant implementation, since
311
+ * `locator.pressSequentially` and the agent's `type` rely on the app
312
+ * reacting as it does to a user. With `replace`, clears the field first
313
+ * (select-all and delete on a keyboard surface; the platform's clear on a
314
+ * device). Throw `NOT_ACTIONABLE` when nothing that accepts text has focus,
315
+ * so the keystrokes are never silently discarded into the document body.
316
+ */
317
+ type(text: string, options: { readonly replace: boolean }, context: OperationContext): Promise<void>;
318
+ /**
319
+ * Sends one key of the contract grammar (`Enter`, `Escape`, `Tab`,
320
+ * `Shift+Tab`, a character) to whatever holds focus. The harness has
321
+ * already validated the key's shape; refuse with `UNSUPPORTED_CAPABILITY`
322
+ * a key the surface cannot deliver.
323
+ */
324
+ press(key: string, context: OperationContext): Promise<void>;
325
+ /** Hides an on-screen keyboard. Platforms without one leave it undefined. */
326
+ dismiss?(context: OperationContext): Promise<void>;
327
+ }
328
+ ```
329
+
330
+ ## Lifecycle
331
+
332
+ | Hook | Budget |
333
+ | --- | --- |
334
+ | `prepare(info)` | none; only `info.signal` (the run's interrupt) cuts it short. Failure ends the run before any test: a runner error class (a `ConfigurationError` for an option the engine cannot honour) keeps its code, an `EngineError` `CANCELLED` stays a cancellation, and anything else, any other `EngineError` or a plain `Error`, is infrastructure `ENGINE_FAILURE` |
335
+ | `finish(info)` | `cleanupTimeout`, shared by every prepared target, never cut short by an interrupt; failure is a cleanup run error |
336
+ | `init(info)` | `launchTimeout`; failure is infrastructure |
337
+ | `startAttempt(context)` | `launchTimeout`; on failure or timeout `context.signal` aborts and `endAttempt` runs |
338
+ | `settleAttempt(context)` | `cleanupTimeout`, after the body and again after teardown; a thrown error is the attempt's failure and keeps its code |
339
+ | `endAttempt(context)` | `cleanupTimeout`; failure marks cleanup failed, the test keeps its status |
340
+ | `dispose(context)` | `cleanupTimeout`; failure is a run error |
341
+
342
+ ```ts
343
+ /**
344
+ * What `prepare` learned that the run must honour. `workers` replaces the
345
+ * engine's declared cap for this target and run: the surfaces the engine
346
+ * actually provisioned, `1` to `info.slots`. Declared `workers` stays the
347
+ * static answer for engines that know it up front.
348
+ */
349
+ export interface EnginePrepareResult {
350
+ /** Lowers the target's worker cap for this run, `1` to `info.slots`. */
351
+ readonly workers?: number;
352
+ /**
353
+ * Variables every worker of this target is spawned with, on top of the
354
+ * run's environment: how a runner-side `prepare` hands what it provisioned
355
+ * (a device pool it discovered) to each worker's `init`. The run's own
356
+ * variables win on a clash.
357
+ */
358
+ readonly env?: Readonly<Record<string, string>>;
359
+ }
360
+
361
+ /**
362
+ * Facts handed to `prepare`, once per run and target in the runner process,
363
+ * before any worker exists.
364
+ */
365
+ export interface EnginePrepareInfo {
366
+ /** The run id. */
367
+ readonly runId: string;
368
+ /** The target being prepared. */
369
+ readonly targetName: string;
370
+ /** Directory relative paths in the config resolve against; see `EngineInitInfo.projectRoot`. */
371
+ readonly projectRoot: string;
372
+ /** The app the target declares, as `init` receives it: what a device installs or warms up here. */
373
+ readonly app: EngineAppInfo;
374
+ /**
375
+ * The worker slots the run will start for this target, `0` to `slots - 1`:
376
+ * the run's worker cap, the engine's declared `workers`, and the work units
377
+ * selected for the target, whichever is smallest; `0` when nothing runs on
378
+ * it. Provision for exactly these slots (boot that many devices of a pool);
379
+ * `init` then finds them ready.
380
+ */
381
+ readonly slots: number;
382
+ /**
383
+ * The run's environment: what every worker is started with. A host may hand
384
+ * the run an environment other than the runner process's own, so anything
385
+ * provisioned here that a worker later looks up by environment (a browser
386
+ * cache location) must be resolved and spawned against this, not
387
+ * `process.env`.
388
+ */
389
+ readonly env: Readonly<Record<string, string | undefined>>;
390
+ /**
391
+ * Aborts on interrupt only. Provisioning has no budget: a first-run
392
+ * download is as long as the network makes it, and cutting it short would
393
+ * fail every test behind it.
394
+ */
395
+ readonly signal: AbortSignal;
396
+ /**
397
+ * Reports one line of progress. The runner streams it as a `notice` run
398
+ * event, so it reaches the reporter and every host sink instead of being
399
+ * written to a worker's stderr underneath the live status block.
400
+ */
401
+ readonly log: (line: string) => void;
402
+ }
403
+
404
+ /** Handed to `finish`, once per run and target after the last worker is gone, with the cleanup budget of one hook. */
405
+ export interface EngineFinishInfo extends EngineCleanupContext {
406
+ /** The run id. */
407
+ readonly runId: string;
408
+ /** The target being finished. */
409
+ readonly targetName: string;
410
+ /** The run's environment, the same `prepare` saw. */
411
+ readonly env: Readonly<Record<string, string | undefined>>;
412
+ /** Reports one line of progress, streamed as a `notice` run event like `prepare`'s. */
413
+ readonly log: (line: string) => void;
414
+ }
415
+
416
+ /** Run identity and harness-resolved facts handed to `init`, once per worker before the first step. */
417
+ export interface EngineInitInfo {
418
+ /** The run id. */
419
+ readonly runId: string;
420
+ /** The target this worker serves. */
421
+ readonly targetName: string;
422
+ /**
423
+ * Directory relative paths in the config resolve against: the config file's
424
+ * directory, or the run's `cwd` for a programmatic config. An engine option
425
+ * naming a file (a build to install) resolves here, never against
426
+ * `process.cwd()`, which an in-process run does not change.
427
+ */
428
+ readonly projectRoot: string;
429
+ /** The app the target declares: the site policy of its URL, and what a device launches. */
430
+ readonly app: EngineAppInfo;
431
+ /**
432
+ * This worker's environment: the run's, plus what this target's `prepare`
433
+ * returned in its result's `env`. Read what `prepare` provisioned from
434
+ * here, never from `process.env`: an in-process worker shares its process
435
+ * with the runner and every other target.
436
+ */
437
+ readonly env: Readonly<Record<string, string | undefined>>;
438
+ /** Whether the run asked for a visible surface (`--headed`). */
439
+ readonly headed: boolean;
440
+ /**
441
+ * This worker's 0-based slot among the target's workers: the lowest slot
442
+ * free when the worker was spawned, so a replacement worker takes over the
443
+ * slot of the one that exited. An engine with several devices hands each
444
+ * slot its own; `config.workers` bounds the slots a target can reach.
445
+ */
446
+ readonly workerSlot: number;
447
+ /** Aborts on interrupt and when init exceeds the launch timeout; init must stop promptly. */
448
+ readonly signal: AbortSignal;
449
+ /**
450
+ * Reports one line of progress. The runner streams it as a `notice` run
451
+ * event, prefixed with the target and worker slot, so it reaches the
452
+ * reporter and every host sink instead of being written to a worker's
453
+ * stderr underneath the live status block. This is where a fact that only
454
+ * exists once the worker is up belongs: the URL a hosted browser or device
455
+ * can be watched at, which simulator a slot got, a version it detected.
456
+ * Meant for `init` itself; a line logged later is still delivered while
457
+ * the worker lives and dropped once it has exited. The line is shown as is
458
+ * in reporters and host sinks, so keep tokens and other secrets out of it
459
+ * (a viewer URL without its credential).
460
+ */
461
+ readonly log: (line: string) => void;
462
+ }
463
+
464
+ /**
465
+ * The app the harness resolved from the target's declaration, handed to
466
+ * `prepare` and `init`: the site policy for the decisions only a surface can
467
+ * make (which nested documents enter an observation, which requests carry
468
+ * injected credentials), and what a device launches.
469
+ */
470
+ export interface EngineAppInfo {
471
+ /**
472
+ * The app URL's registrable domain, as read by `siteOf`. Use `sameSite`
473
+ * to scope secret fills, injected headers, and observed child frames.
474
+ * Absent when the app has no URL. The runner passes navigation URLs
475
+ * separately through `session.open`.
476
+ */
477
+ readonly site?: string;
478
+ /** The installed app to launch, as the target declared it. */
479
+ readonly bundleId?: string;
480
+ /** The build to install, as the target declared it; relative to `projectRoot`. */
481
+ readonly appPath?: string;
482
+ /** Arguments every fresh launch passes the app. */
483
+ readonly launchArguments?: readonly string[];
484
+ /** Permissions the app holds on every fresh launch, by name. */
485
+ readonly permissions?: Readonly<Record<string, AppPermissionState>>;
486
+ }
487
+
488
+ /**
489
+ * Per-attempt isolation context. An engine that must give each test a fresh
490
+ * surface (a fresh isolated context per test, a reset device) sets it up in
491
+ * `startAttempt` and tears it down in `endAttempt`.
492
+ */
493
+ export interface EngineAttemptContext {
494
+ /** The attempt id. */
495
+ readonly attemptId: string;
496
+ /** Absolute directory every artifact of this attempt is written under. */
497
+ readonly artifactsDir: string;
498
+ /**
499
+ * The plaintext of a secret the engine declared in `secrets`, for an
500
+ * option the engine hands the app itself (basic-auth credentials). A
501
+ * provider runs again on every call. The value, and every value
502
+ * `options.derived` computes from it, join the attempt's redaction before
503
+ * this resolves, so reports, logs, and every observation redact them, and
504
+ * the attempt's trace and text downloads are rewritten. The protection is
505
+ * text only: unlike a fill, it withholds no screenshot or model pixels.
506
+ * A secret the engine did not declare is `SECRET_UNAVAILABLE`.
507
+ */
508
+ readonly resolveSecret: (secret: Secret, options?: ResolveSecretOptions) => Promise<string>;
509
+ /**
510
+ * Aborts with the attempt, and the moment `startAttempt` fails or exceeds
511
+ * the launch timeout: a hook still running then must stop, because the
512
+ * harness ends the attempt's isolation right behind it and may retry.
513
+ */
514
+ readonly signal: AbortSignal;
515
+ }
516
+
517
+ /** How `EngineAttemptContext.resolveSecret` treats what the engine makes of the value. */
518
+ export interface ResolveSecretOptions {
519
+ /**
520
+ * The forms of the value the app sees instead of the value itself, for the
521
+ * same redaction under the secret's name: the base64 `user:password` an
522
+ * `Authorization: Basic` header carries, which a page echoing its request
523
+ * headers would otherwise show.
524
+ */
525
+ readonly derived?: (plaintext: string) => readonly string[];
526
+ }
527
+
528
+ /**
529
+ * Budget of one cleanup hook (`settleAttempt`, `endAttempt`, `dispose`). `signal` aborts when
530
+ * the budget is exhausted, so a hook that cannot finish in time stops instead
531
+ * of running on into the next attempt's setup.
532
+ */
533
+ export interface EngineCleanupContext {
534
+ /** Aborts when the cleanup budget is spent. */
535
+ readonly signal: AbortSignal;
536
+ /** Remaining cleanup budget when the call starts. */
537
+ readonly timeoutMs: number;
538
+ }
539
+ ```
540
+
541
+ URLs arrive through `session.open` only; `init` never learns the app URL.
542
+
543
+ ## Observation
544
+
545
+ ```ts
546
+ /**
547
+ * One fresh semantic snapshot of the surface under test. The harness owns
548
+ * everything downstream: revision minting, secret redaction, and the
549
+ * observation byte budget apply to every engine equally.
550
+ */
551
+ export interface EngineSnapshot {
552
+ /**
553
+ * Where the surface is, as an opaque address the platform understands: a
554
+ * URL on a document platform, the foreground screen or activity on a
555
+ * device, the front window on a desktop. The harness shows it to the model
556
+ * and to the report, anchors trace replay on it, and applies the origin
557
+ * policy to it only when it parses as a URL. Omit when the platform has no
558
+ * notion of a current location.
559
+ */
560
+ readonly location?: string;
561
+ /**
562
+ * The observation tree under one root the engine minted. The root's id MUST
563
+ * be stable for the surface across observations: `perform(root, swipe)` is
564
+ * the viewport swipe, and the harness addresses it from an earlier
565
+ * observation, possibly one taken before a `restart` or `reset`, so the
566
+ * root swipe MUST NOT depend on a live handle from that observation. A
567
+ * platform with several top-level elements (a device's windows) wraps them
568
+ * in a root of its own.
569
+ */
570
+ readonly root: SemanticNode;
571
+ /** The viewport `SemanticNode.rect` and `ViewportPoint` are measured in, in CSS pixels. */
572
+ readonly viewport: ViewportSize;
573
+ /**
574
+ * True when `root` leaves out nodes that are on the surface: the engine
575
+ * stopped reading at a node cap or a depth limit, or could not enter a
576
+ * child document. The harness tells the model the listing is incomplete
577
+ * and never calls such a screen unchanged. Omit or set false for a tree
578
+ * read whole.
579
+ */
580
+ readonly truncated?: boolean;
581
+ /** Semantic capture failed; only fresh, independently masked pixels are evidence. */
582
+ readonly treeUnavailable?: true;
583
+ /** Masked pixels, when requested and producible; omitted otherwise. */
584
+ readonly pixels?: ObservationPixels;
585
+ /** Regions masked in `pixels`; the harness checks it covers every secure node. */
586
+ readonly maskedRegionCount?: number;
587
+ }
588
+
589
+ export interface EngineObserveOptions {
590
+ /** Requests masked viewport pixels for the same revision as the tree. */
591
+ readonly pixels?: boolean;
592
+ /** Permits independently masked pixels to replace a failed semantic capture. */
593
+ readonly pixelFallback?: boolean;
594
+ }
595
+ ```
596
+
597
+ Return the surface as it is right now. The runner mints the revision, stamps
598
+ it onto every ref, redacts registered secret values, bounds the byte size, and
599
+ checks that `maskedRegionCount` covers every `secure` node when `pixels` are
600
+ present.
601
+
602
+ Set `truncated` when a node limit or inaccessible subtree leaves content out.
603
+ The runner warns the model that the tree is incomplete and never treats a
604
+ truncated observation as unchanged. Omit it for a complete tree.
605
+
606
+ A valid empty tree may have a childless root and include `pixels`.
607
+
608
+ If semantic capture times out, an engine may recover only when `pixelFallback`
609
+ is true and it can prove screenshot masking without that capture. Return
610
+ `treeUnavailable: true`, fresh `pixels`, and a `root` containing only its
611
+ stable `ref`. That reference supports viewport actions; it supplies no
612
+ semantic node evidence. Retire earlier node references. The flag distinguishes
613
+ missing semantic evidence from a valid empty tree; never infer absence from
614
+ it. The runner validates this shape at the engine boundary and rejects a
615
+ fallback without permitted pixels as `UNSUPPORTED_CAPABILITY`, or one with
616
+ semantic root fields as `INVALID_STATE`. `EngineSnapshot` remains an
617
+ extendable interface; these dependent requirements are checked at runtime.
618
+
619
+ Without permission, pixels, or independent masking proof, propagate the
620
+ capture failure. Cancellation, policy denial, and hard engine failures must
621
+ also propagate.
622
+
623
+ The runner permits fallback for `act` and for judgments with `vision: true`
624
+ or `'only'`, before any secret fill. It includes the screenshot even if the
625
+ initial observation requested only a tree. A step that loses semantic
626
+ evidence cannot record or finish by replaying a trace. Engines whose masks
627
+ depend on their semantic capture cannot provide this fallback.
628
+
629
+ ## Actions
630
+
631
+ ```ts
632
+ /**
633
+ * One deterministic action `perform` carries out on a node. The `swipe`
634
+ * kind on the observation's root node is the viewport swipe: the agent's
635
+ * `scroll` verb and `screen.swipe()` both arrive as `perform(root, swipe)`,
636
+ * so a surface implements scrolling once.
637
+ *
638
+ * `press` takes one key in the `Key` grammar below.
639
+ */
640
+ export type LocatorAction =
641
+ | {
642
+ readonly kind: 'tap' | 'doubleTap' | 'secondaryTap';
643
+ /** Keys held for the click; only an engine that declares `tapModifiers` receives it. */
644
+ readonly modifiers?: readonly KeyModifier[];
645
+ }
646
+ | {
647
+ readonly kind:
648
+ | 'check'
649
+ | 'uncheck'
650
+ | 'clear'
651
+ | 'focus'
652
+ | 'hover'
653
+ | 'scrollIntoView';
654
+ }
655
+ | { readonly kind: 'longPress'; readonly durationMs?: number }
656
+ | { readonly kind: 'fill'; readonly value: string; readonly sensitive: boolean }
657
+ | { readonly kind: 'press'; readonly key: Key }
658
+ | { readonly kind: 'selectOption'; readonly value: SelectOption }
659
+ | { readonly kind: 'setInputFiles'; readonly paths: readonly string[] }
660
+ | { readonly kind: 'dragTo'; readonly target: NodeRef }
661
+ | {
662
+ readonly kind: 'swipe';
663
+ readonly direction: ScrollDirection;
664
+ readonly momentum?: Momentum;
665
+ };
666
+
667
+ /** Every action kind; what an engine declares in `actions`. */
668
+ export type LocatorActionKind = LocatorAction['kind'];
669
+
670
+ /** The closed list of action kinds, in contract order; the type is derived from it. */
671
+ export const LOCATOR_ACTION_KINDS = [
672
+ 'tap',
673
+ 'doubleTap',
674
+ 'secondaryTap',
675
+ 'longPress',
676
+ 'fill',
677
+ 'clear',
678
+ 'press',
679
+ 'check',
680
+ 'uncheck',
681
+ 'focus',
682
+ 'hover',
683
+ 'scrollIntoView',
684
+ 'selectOption',
685
+ 'setInputFiles',
686
+ 'dragTo',
687
+ 'swipe',
688
+ ] as const satisfies readonly LocatorActionKind[];
689
+ ```
690
+
691
+ The grammar is closed. Anything beyond it is an agent-side `defineTool`.
692
+
693
+ ### Pointer actions
694
+
695
+ Pointer actions use viewport coordinates when no semantic node is available.
696
+ `dragTo` ends at another point; `swipe` starts a scroll gesture at the given
697
+ point. `swipeTo` is the one kind with no node form: the finger's path from
698
+ the point to `target`, a touch swipe on a device and a pointer drag on a
699
+ document platform, distinct from `dragTo` where a platform holds before it
700
+ drags. Declare supported kinds in `pointerActions`.
701
+
702
+ ```ts
703
+ export type PointerAction =
704
+ | { readonly kind: 'tap' | 'doubleTap' | 'secondaryTap' | 'hover' }
705
+ | { readonly kind: 'longPress'; readonly durationMs?: number }
706
+ | { readonly kind: 'dragTo'; readonly target: ViewportPoint }
707
+ | {
708
+ readonly kind: 'swipe';
709
+ readonly direction: ScrollDirection;
710
+ readonly momentum?: Momentum;
711
+ }
712
+ | { readonly kind: 'swipeTo'; readonly target: ViewportPoint };
713
+
714
+ /** Every pointer action kind; what an engine declares in `pointerActions`. */
715
+ export type PointerActionKind = PointerAction['kind'];
716
+
717
+ /** The closed list of pointer action kinds, in contract order; the type is derived from it. */
718
+ export const POINTER_ACTION_KINDS = [
719
+ 'tap',
720
+ 'doubleTap',
721
+ 'secondaryTap',
722
+ 'longPress',
723
+ 'hover',
724
+ 'dragTo',
725
+ 'swipe',
726
+ 'swipeTo',
727
+ ] as const satisfies readonly PointerActionKind[];
728
+ ```
729
+
730
+ ### Keys
731
+
732
+ ```ts
733
+ /**
734
+ * The key grammar of `press`, shared by every engine so a test's key names are
735
+ * portable: zero or more modifiers and one key, joined by `+`.
736
+ *
737
+ * - Modifiers: `Shift`, `Control`, `Alt`, `Meta`, `ControlOrMeta` (Control on
738
+ * Windows and Linux, Meta on macOS).
739
+ * - Named keys: those in `KEY_NAMES` (`Enter`, `Escape`, `Tab`, `Backspace`,
740
+ * `Delete`, `ArrowUp`..., `Home`, `End`, `PageUp`, `PageDown`, `Insert`,
741
+ * `Space`, `F1`..`F12`).
742
+ * - Any single printable character (`a`, `A`, `1`, `$`), typed as itself.
743
+ *
744
+ * `Control+a`, `Shift+Tab`, `Enter`, `$`. A platform without a key maps it
745
+ * or throws `UNSUPPORTED_CAPABILITY`; it never reinterprets the spelling.
746
+ * `parseKey` is the one parser, exported so no engine carries its own.
747
+ */
748
+ export type Key = string;
749
+
750
+ export const KEY_MODIFIERS = ['Shift', 'Control', 'Alt', 'Meta', 'ControlOrMeta'] as const;
751
+
752
+ export const KEY_NAMES = [
753
+ 'Enter',
754
+ 'Escape',
755
+ 'Tab',
756
+ 'Backspace',
757
+ 'Delete',
758
+ 'Insert',
759
+ 'Space',
760
+ 'ArrowUp',
761
+ 'ArrowDown',
762
+ 'ArrowLeft',
763
+ 'ArrowRight',
764
+ 'Home',
765
+ 'End',
766
+ 'PageUp',
767
+ 'PageDown',
768
+ 'F1',
769
+ 'F2',
770
+ 'F3',
771
+ 'F4',
772
+ 'F5',
773
+ 'F6',
774
+ 'F7',
775
+ 'F8',
776
+ 'F9',
777
+ 'F10',
778
+ 'F11',
779
+ 'F12',
780
+ ] as const;
781
+
782
+ /** One parsed `press` key: its modifiers and either a named key or a single character. */
783
+ export interface ParsedKey {
784
+ /** Modifiers, in the order written. */
785
+ readonly modifiers: readonly KeyModifier[];
786
+ /** The key itself. */
787
+ readonly key: { readonly kind: 'named'; readonly name: KeyName } | { readonly kind: 'char'; readonly char: string };
788
+ }
789
+
790
+ export function parseKey(key: string): ParsedKey | undefined;
791
+ ```
792
+
793
+ ## App
794
+
795
+ The app under test is declared on the target (`targets: [{ engine, app }]`,
796
+ see [the app under test](/reference/config#the-app-under-test)); an engine
797
+ only drives it. The harness checks the shape of every field, then hands the
798
+ declaration to `validateApp` at config load, so an engine can require what
799
+ its platform needs and refuse what it cannot drive. `@e2e-dev/web` requires
800
+ `url`; `@e2e-dev/mobile` requires `bundleId` or `appPath` and refuses `url`.
801
+
802
+ ```ts
803
+ /**
804
+ * The app a target declares, as its engine reads it at config load: what it
805
+ * is and where it is served. `EngineAppInfo` (below) is what `prepare` and
806
+ * `init` receive; the declaration adds the URL, which a surface never gets.
807
+ */
808
+ export type EngineAppDeclaration = EngineAppInfo & {
809
+ /** Base URL the target opens, as declared; see `TargetApp.url`. */
810
+ readonly url?: string;
811
+ };
812
+
813
+ /** Handed to `Engine.validateApp` beside the declaration. */
814
+ export interface EngineAppCheckInfo {
815
+ /** The target declaring the app, for the message of a refusal. */
816
+ readonly targetName: string;
817
+ }
818
+
819
+ /** A permission's state when the app launches: held, refused, or not asked for yet, so the OS asks again. */
820
+ export type AppPermissionState = 'grant' | 'deny' | 'reset';
821
+ ```
822
+
823
+ ```ts
824
+ validateApp(app, { targetName }) {
825
+ if (app.url === undefined) {
826
+ throw new ConfigurationError('INVALID_CONFIG', `target "${targetName}" needs app.url`);
827
+ }
828
+ },
829
+ ```
830
+
831
+ `validateApp` runs for every target the engine serves, with `{}` for a
832
+ target that declares no `app`. The resolved app comes back to the engine as
833
+ `EnginePrepareInfo.app` and `EngineInitInfo.app`: the site of the URL, which
834
+ `sameSite` from `e2e/engine` applies to a URL, and the device fields as the
835
+ target declared them. The URL itself stays with the harness, which opens it
836
+ through `session.open`. The target's `command` and `readyUrl` never reach
837
+ the engine: the runner starts it. `CommandConfig` is exported from `e2e`, not
838
+ `e2e/engine`.
839
+
840
+ ## Session
841
+
842
+ ```ts
843
+ /**
844
+ * Steering hooks behind the universal `app` fixture and the agent's
845
+ * `navigate` verb. The app itself is the target's `app`, handed over as data;
846
+ * node actions never live here, they are `perform`.
847
+ *
848
+ * `restart` and `reset` open nothing on an addressable surface: they end at
849
+ * a fresh surface with no location shown, and the harness reopens the app
850
+ * through `open` when the target has an address. A surface therefore never
851
+ * needs to know the app's URL, and an unreachable app after a restart is
852
+ * reported the same way as on first open. A surface without `open` (a
853
+ * device with a pinned app) relaunches the app inside `restart` and `reset`
854
+ * itself, since nothing else will.
855
+ */
856
+ export interface EngineSession {
857
+ /**
858
+ * Opens one URL the harness already resolved against the base URL and the
859
+ * origin policy. Absent on a surface without addressable locations.
860
+ */
861
+ open?(url: string, context: OperationContext): Promise<void>;
862
+ /** Navigates back once in the surface's history. */
863
+ back?(context: OperationContext): Promise<void>;
864
+ /** Recreates the execution context, keeping persisted state. */
865
+ restart?(context: OperationContext): Promise<void>;
866
+ /** Clears persisted client state and recreates the execution context. */
867
+ reset?(context: OperationContext): Promise<void>;
868
+ }
869
+ ```
870
+
871
+ `open` is behind `app.open()`, the agent's `navigate` verb, and the reopen
872
+ after `restart` and `reset`; `back`, `restart`, and `reset` are behind
873
+ `app.back()`, `app.restart()`, and `app.clearState()`. An absent hook makes
874
+ its method fail with `UNSUPPORTED_CAPABILITY`.
875
+
876
+ ## Errors
877
+
878
+ ```ts
879
+ export class EngineError extends Error {
880
+ constructor(code: EngineErrorCode, message: string, options: { retryable: boolean; cause?: unknown });
881
+ /** One of `ENGINE_ERROR_CODES`. */
882
+ readonly code: EngineErrorCode;
883
+ /** Whether the runner may re-resolve and retry; only `RETRYABLE_ENGINE_ERROR_CODES` can be. */
884
+ readonly retryable: boolean;
885
+ }
886
+
887
+ /** The closed engine error code set; the type is derived from it, so the two cannot drift. */
888
+ export const ENGINE_ERROR_CODES = [
889
+ 'NODE_STALE',
890
+ 'FRAME_NOT_FOUND',
891
+ 'FRAME_AMBIGUOUS',
892
+ 'NOT_ACTIONABLE',
893
+ 'ACTION_MAY_HAVE_COMMITTED',
894
+ 'OPERATION_TIMEOUT',
895
+ 'CANCELLED',
896
+ 'UNSUPPORTED_CAPABILITY',
897
+ 'INVALID_STATE',
898
+ 'ENGINE_FAILURE',
899
+ ] as const;
900
+
901
+ /** One of `ENGINE_ERROR_CODES`. */
902
+ export type EngineErrorCode = (typeof ENGINE_ERROR_CODES)[number];
903
+
904
+ /**
905
+ * The only codes that may be retryable: both describe a repeatable read. Any
906
+ * other retryable claim, from this module's class or a foreign copy of it, is
907
+ * coerced to a non-retryable `ENGINE_FAILURE`.
908
+ */
909
+ export const RETRYABLE_ENGINE_ERROR_CODES: ReadonlySet<EngineErrorCode> = new Set([
910
+ 'NODE_STALE',
911
+ 'FRAME_NOT_FOUND',
912
+ ]);
913
+ ```
914
+
915
+ | Code | Runner mapping | Retryable |
916
+ | --- | --- | --- |
917
+ | `NODE_STALE` | re-resolve, then `LOCATOR_NOT_FOUND` | yes |
918
+ | `FRAME_NOT_FOUND` | re-resolve, then `LOCATOR_NOT_FOUND` | yes |
919
+ | `FRAME_AMBIGUOUS` | `LOCATOR_AMBIGUOUS` | no |
920
+ | `NOT_ACTIONABLE`, `ACTION_MAY_HAVE_COMMITTED`, `OPERATION_TIMEOUT` | `ACTION_FAILED` | no |
921
+ | `CANCELLED` | infrastructure `CANCELLED` | no |
922
+ | `UNSUPPORTED_CAPABILITY` | configuration `UNSUPPORTED_CAPABILITY` | no |
923
+ | `INVALID_STATE` | `APP_NOT_OPEN` | no |
924
+ | `ENGINE_FAILURE`, any non-`EngineError` | infrastructure `ENGINE_FAILURE` | no |
925
+
926
+ An `EngineError` a contributed fixture method throws is mapped by the table
927
+ above, so a fixture over a session that ended reports `APP_NOT_OPEN` the way
928
+ an observation would. A method can also fail a step with the runner's own
929
+ error classes, exported from the same entry:
930
+
931
+ ```ts
932
+ /** The runner's error base: `category` sets the exit code, `code` names the cause. */
933
+ declare class E2EError extends Error {
934
+ readonly category: 'test' | 'configuration' | 'infrastructure' | 'internal' | 'interrupted';
935
+ readonly code: string;
936
+ readonly retryable: boolean;
937
+ }
938
+
939
+ /** A test failure: ASSERTION_FAILED, INVALID_LOCATOR, ACTION_FAILED, ... */
940
+ export class TestError extends E2EError {
941
+ constructor(code: string, message: string, options?: { retryable?: boolean; cause?: unknown });
942
+ }
943
+ /** POLICY_DENIED, APP_URL_REQUIRED, UNSUPPORTED_CAPABILITY, ... */
944
+ export class ConfigurationError extends E2EError {
945
+ constructor(code: string, message: string, options?: { cause?: unknown });
946
+ }
947
+ /** The surface itself failed: BROWSER_INSTALL_FAILED, ... */
948
+ export class InfrastructureError extends E2EError {
949
+ constructor(code: string, message: string, options?: { cause?: unknown });
950
+ }
951
+ ```
952
+
953
+ `E2EError` itself is not exported; construct one of the three.
954
+
955
+ ## Shared semantics
956
+
957
+ Use these helpers to keep behavior consistent across engines:
958
+
959
+ | Export | Use |
960
+ | --- | --- |
961
+ | `matchesText(actual, pattern)`, `toTextPattern(match, { exact? })`, `describePattern(pattern)` | `TextPattern` matching for `locate` and text matchers; `toTextPattern` throws `INVALID_ARGUMENT` for a `match` that is neither a string nor a `RegExp` |
962
+ | `resolveExpression(expression, nodes, { selector? })` | The reference `locate`. Resolves one `LocatorExpression` over a semantic tree in document order, with `text` and `label` answering the innermost match when a container echoes a descendant's text. An engine whose tree is the whole truth (a device) calls it; a browser engine reproduces it. A `selector` needs the hook, a `frame` is `FRAME_NOT_FOUND` |
963
+ | `urlMatches(current, expected, baseHref)` | `toHaveURL`-style comparison; an `expected` that is neither a string nor a `RegExp` throws `INVALID_ARGUMENT`, as in `toTextPattern` |
964
+ | `pollCondition({ deadline, signal, negated, evaluate, onTimeout })`, `Deadline` | Assertion polling and the negation grace window |
965
+ | `validateJsonValue(value, label, rules?)` | Checks a value is plain JSON: a top-level `undefined` passes; a non-finite number, a cycle, an object that is not a plain object or array, or a function, symbol, bigint, or `undefined` inside is `INVALID_ARGUMENT` naming `label`. A `Secret` is `POLICY_DENIED` unless `rules.onSecret` throws its own; `rules.maxDepth` caps the nesting |
966
+ | `rejectUnknownKeys(label, value, keys, code?)` | Throws `INVALID_CONFIG` for the first own key of `value` outside `keys`, naming the nearest known key when one is a plausible typo (`web() has unknown key "viewprt"; did you mean "viewport"?`) and the known keys otherwise: the check the runner runs on its own config objects, for an engine factory's options. Pass `'INVALID_ARGUMENT'` as `code` for an object a test passes to a fixture method |
967
+ | `obj`, `raceAbort`, `withTimeout`, `withinCleanupBudget` | See [Contributed fixtures](#contributed-fixtures) |
968
+ | `isProviderRecording(value)` | Whether what a provider's `record` resolved to is a recording: a parseable `startedAt` and a `stop`. Check it before keeping one, so a malformed recording fails the start, named, instead of the stop |
969
+ | `stopProviderRecording(recording, { artifactsDir, provider, leaseId, signal })` | Ends a hosted service's recording and checks it into a `VideoSegment`; see [Provider recordings](#provider-recordings) |
970
+
971
+ ## State
972
+
973
+ ```ts
974
+ /**
975
+ * An opaque, restorable snapshot of an engine's state. `data` is JSON the
976
+ * harness never inspects - a document platform's storage state, a device's app state, a
977
+ * desktop's window state all satisfy it identically. `format`/`version` let a
978
+ * engine reject a snapshot it can no longer read.
979
+ */
980
+ export interface EngineState {
981
+ /** Engine-defined format name. */
982
+ readonly format: string;
983
+ /** Format version, for rejecting a snapshot the engine can no longer read. */
984
+ readonly version: number;
985
+ /** Opaque JSON the runner never inspects. */
986
+ readonly data: unknown;
987
+ /** Earliest moment the state is known to be invalid, if the engine knows one. */
988
+ readonly expiresAt?: string;
989
+ }
990
+
991
+ /**
992
+ * Platform-neutral state capture and restore. A setup test captures a
993
+ * snapshot; an ordinary test restores it at launch.
994
+ *
995
+ * Snapshots are credentials: cookies, tokens, and storage that authenticate
996
+ * the app under test. The duties split at the seam. The harness owns the
997
+ * snapshot from the moment `capture` returns: it encrypts it at rest with a
998
+ * per-run AES-256-GCM key that never touches disk, stores it under the run's
999
+ * private session directory, binds it to the run, target, engine, and app
1000
+ * identity, and deletes it when the run ends; it never logs it, reports it,
1001
+ * digests it, or sends it to a model. An engine MUST NOT persist, cache, or
1002
+ * log a snapshot on its own, MUST NOT echo snapshot contents in an error
1003
+ * message, and MUST treat `restore` as a replacement of the surface's whole
1004
+ * persisted state, never a merge, so a restored session cannot leak into the
1005
+ * next attempt.
1006
+ */
1007
+ export interface EngineStateCapability {
1008
+ /** Captures the surface's persisted state. */
1009
+ capture(context: OperationContext): Promise<EngineState>;
1010
+ /** Replaces the surface's persisted state with `state`. */
1011
+ restore(state: EngineState, context: OperationContext): Promise<void>;
1012
+ }
1013
+ ```
1014
+
1015
+ ## Artifacts
1016
+
1017
+ ```ts
1018
+ /**
1019
+ * Evidence capture. Paths are relative to the attempt artifact directory the
1020
+ * engine received in `startAttempt`.
1021
+ */
1022
+ export interface EngineArtifacts {
1023
+ /** Captures a redacted screenshot; secure fields are masked at the source. */
1024
+ screenshot(label: string | undefined, context: OperationContext): Promise<string>;
1025
+ /** Starts recording an execution trace for the attempt. */
1026
+ startTrace?(context: OperationContext): Promise<void>;
1027
+ /**
1028
+ * Stops the trace and returns its relative path, or every archive written,
1029
+ * in order, when the trace had to be cut: a trace bound to one context
1030
+ * closes as a segment when a restart or a state reset replaces the
1031
+ * context, and a new one records on from there. The harness registers and
1032
+ * redacts each returned archive.
1033
+ */
1034
+ stopTrace?(context: OperationContext): Promise<string | readonly string[]>;
1035
+ /**
1036
+ * Starts recording the surface for the attempt. Declared together with
1037
+ * `stopVideo`. A surface that has nothing to show yet (no page open) may
1038
+ * defer the actual capture to the moment it does; the segments returned by
1039
+ * `stopVideo` say when each one began.
1040
+ */
1041
+ startVideo?(context: OperationContext): Promise<void>;
1042
+ /**
1043
+ * Stops recording and returns every segment written, in order. One segment
1044
+ * is the common case; a surface whose recording is bound to a page returns
1045
+ * one per page the attempt showed (a restart or a state reset opens a new
1046
+ * one). An attempt that never showed anything returns none.
1047
+ */
1048
+ stopVideo?(context: OperationContext): Promise<readonly VideoSegment[]>;
1049
+ }
1050
+
1051
+ /**
1052
+ * One recorded video of an attempt: a file the engine wrote into the attempt
1053
+ * artifact directory, or a recording a hosted service keeps and links to.
1054
+ */
1055
+ export type VideoSegment = VideoFile | VideoLink;
1056
+
1057
+ /** A video file in the attempt artifact directory. */
1058
+ export interface VideoFile {
1059
+ /** Path relative to the attempt artifact directory. */
1060
+ readonly path: string;
1061
+ /** When the segment started recording, as an ISO timestamp; its first frame is at or just after it. */
1062
+ readonly startedAt: string;
1063
+ }
1064
+
1065
+ /** A recording a hosted service keeps, by URL: the report links to it and the runner never downloads it. */
1066
+ export interface VideoLink {
1067
+ /** `http(s)` URL of the recording. */
1068
+ readonly url: string;
1069
+ /** Media type of what the URL serves: `video/mp4`, or `text/html` for a player page. */
1070
+ readonly mediaType: string;
1071
+ /** When the recording started, as an ISO timestamp; its first frame is at or just after it. */
1072
+ readonly startedAt: string;
1073
+ }
1074
+ ```
1075
+
1076
+ The runner calls `startVideo` only for an attempt whose
1077
+ [video mode](/reference/config#video) records, then `startTrace` for one whose
1078
+ [trace mode](/reference/config#trace) records, after
1079
+ `startAttempt` and any session restore. Both use the launch budget and stop
1080
+ before `endAttempt`, within the cleanup budget; a `startVideo` that fails
1081
+ fails the launch. Return segments in recording order from `stopVideo` and
1082
+ `stopTrace`. The runner decides which recordings are kept and deletes a file an
1083
+ attempt does not keep.
1084
+
1085
+ ### Provider recordings
1086
+
1087
+ An engine that leases its browser or device from a hosted service can let the
1088
+ service record the attempt in place of its own capture, the way
1089
+ [`BrowserProvider.record`](/browser#recording-on-the-provider) and
1090
+ [`DeviceProvider.record`](/mobile#hosted-devices) do. `e2e/engine` exports
1091
+ the shared shape and `stopProviderRecording`, so every engine turns a
1092
+ provider's recording into a `VideoSegment` the same way.
1093
+
1094
+ ```ts
1095
+ /** Handed to a provider's `record`, once per attempt that records video. */
1096
+ export interface ProviderRecordContext {
1097
+ readonly runId: string;
1098
+ readonly targetName: string;
1099
+ /** The attempt the recording covers. */
1100
+ readonly attemptId: string;
1101
+ /** The run's environment, the same the provider's `acquire` saw. */
1102
+ readonly env: Readonly<Record<string, string | undefined>>;
1103
+ /** Aborts when the attempt is cancelled or its launch budget is spent. */
1104
+ readonly signal: AbortSignal;
1105
+ }
1106
+
1107
+ /** A recording a provider's service makes of the browser or device it leased, as `record` started it. */
1108
+ export interface ProviderRecording {
1109
+ /** When the recording started, as an ISO timestamp; its first frame is at or just after it. */
1110
+ readonly startedAt: string;
1111
+ stop(context: ProviderRecordingStopContext): Promise<ProviderRecordingResult>;
1112
+ }
1113
+
1114
+ /** Handed to `ProviderRecording.stop`, once per recording. */
1115
+ export interface ProviderRecordingStopContext {
1116
+ /** Absolute directory, already created, a recording written as a file goes into. */
1117
+ readonly dir: string;
1118
+ /** Aborts when the cleanup budget is spent. */
1119
+ readonly signal: AbortSignal;
1120
+ }
1121
+
1122
+ /**
1123
+ * What a stopped recording became: a file the provider wrote into the
1124
+ * directory it was given, by name, or a recording its service keeps, by
1125
+ * `http(s)` URL and media type.
1126
+ */
1127
+ export type ProviderRecordingResult =
1128
+ | { readonly file: string }
1129
+ | { readonly url: string; readonly mediaType: string };
1130
+
1131
+ export function stopProviderRecording(
1132
+ recording: ProviderRecording,
1133
+ target: {
1134
+ /** Absolute attempt artifact directory; a file recording goes into its `video` directory. */
1135
+ readonly artifactsDir: string;
1136
+ /** The provider as error messages name it: `browser provider "kernel"`. */
1137
+ readonly provider: string;
1138
+ /** The lease the recording covers, named in error messages. */
1139
+ readonly leaseId: string;
1140
+ readonly signal: AbortSignal;
1141
+ },
1142
+ ): Promise<VideoSegment>;
1143
+ ```
1144
+
1145
+ `stopProviderRecording` creates the attempt's `video` directory, calls
1146
+ `stop` with it, and checks what came back: a file must be a plain name that
1147
+ exists in that directory, a link an `http(s)` URL with a media type. It
1148
+ returns `{ path: 'video/<file>', startedAt }` or `{ url, mediaType,
1149
+ startedAt }` for `stopVideo` to hand back. A `stop` that throws or returns
1150
+ anything else is `ENGINE_FAILURE`, naming the provider and the lease. The
1151
+ target argument's type is exported as `ProviderRecordingTarget`.
1152
+
1153
+ After a secret fill, the runner redacts every text entry in each trace
1154
+ archive and drops its screencast frames: every `screencast/` entry, each
1155
+ `screencast-frame` record in a `.trace` entry, and any image a record names.
1156
+ After `resolveSecret` returns, it redacts the trace the same way and keeps
1157
+ the frames: a trace records the options the engine opened the attempt with,
1158
+ and an engine-held secret is protected as text only.
1159
+ If redaction fails, it deletes the archives and records
1160
+ `TRACE_WITHHELD`. Videos are unmasked and carry `redaction: 'incomplete'`.
1161
+ A `download` an engine attaches is the app's bytes: after a secret fill or
1162
+ once `resolveSecret` returns, the runner rewrites a text one (`.txt`,
1163
+ `.csv`, `.json`, `.html`) through the ledger and labels it `complete`; every
1164
+ other download keeps its bytes and carries `redaction: 'incomplete'`.
1165
+
1166
+ ## Contributed fixtures
1167
+
1168
+ ```ts
1169
+ /**
1170
+ * A contributed fixture: any record of async methods, sync accessors, and
1171
+ * nested namespaces, declared through context.fixture so its metadata controls
1172
+ * labels, deadlines and verification. The factory must return the declared
1173
+ * surface; a plain one is rejected with INVALID_CONFIG.
1174
+ */
1175
+ export type EngineFixtureFactory = (context: EngineFixtureContext) => object;
1176
+
1177
+ /** Context handed to a contributed fixture factory, once per attempt. */
1178
+ export interface EngineFixtureContext {
1179
+ /** The target the fixture serves. */
1180
+ readonly targetName: string;
1181
+ /** Records the declared operations before invoking them; undeclared accessors retain their identity. */
1182
+ fixture<T extends object>(name: string, surface: T, operations: FixtureOperations<T>): T;
1183
+ readonly app: EngineAppInfo & {
1184
+ /**
1185
+ * Resolves a navigation target against the base URL. Throws
1186
+ * `APP_URL_REQUIRED` when the target declares no URL and `POLICY_DENIED`
1187
+ * for a `file:`, `data:`, or `javascript:` scheme, so a fixture never
1188
+ * re-implements the rule the harness owns.
1189
+ */
1190
+ resolveUrl(url: string): string;
1191
+ };
1192
+ /** Harness budgets, for fixtures that poll or navigate. */
1193
+ readonly timeouts: {
1194
+ readonly test: number;
1195
+ readonly action: number;
1196
+ readonly assertion: number;
1197
+ };
1198
+ /**
1199
+ * The running phase's signal: the attempt's through `beforeEach` and the
1200
+ * body, then a fresh one per `afterEach` hook so teardown can still drive
1201
+ * the app after a body timeout. Read it per call; a fixture factory that
1202
+ * captures it once keeps a signal that is dead by teardown.
1203
+ */
1204
+ readonly signal: AbortSignal;
1205
+ /** Per-call operation budget: the action timeout (or an explicit one) plus the attempt signal. */
1206
+ operation(timeoutMs?: number): OperationContext;
1207
+ /** Registers a file the current step produced under the attempt artifact directory. */
1208
+ attachArtifact(
1209
+ kind: 'screenshot' | 'trace' | 'video' | 'download' | 'log',
1210
+ relativePath: string,
1211
+ ): void;
1212
+ /** Records the viewport the current step established. */
1213
+ attachViewport(viewport: { width: number; height: number; scale: number }): void;
1214
+ /** Mints a public locator from a raw expression (a platform selector, say). */
1215
+ locator(expression: LocatorExpression): Locator;
1216
+ /** Mints a screen scope whose every query is wrapped by `scope` (a nested document, say). */
1217
+ screen(scope: (expression: LocatorExpression) => LocatorExpression): Screen;
1218
+ /**
1219
+ * Attaches an expectation surface to a fixture object so `expect(fixture)`
1220
+ * returns it. The harness records every matcher call as an assertion step
1221
+ * named `expect.<matcher>`; the factory only decides what the matchers mean.
1222
+ */
1223
+ expectable<T extends object, E extends object>(target: T, factory: () => E): T & Expectable<E>;
1224
+ }
1225
+ ```
1226
+
1227
+ `context.fixture(name, surface, operations)` declares which methods are
1228
+ recorded steps. Each operation names a `kind: 'resource' | 'assertion'`, an
1229
+ optional `label(...args)`, an optional `timeout` (milliseconds, a function of
1230
+ the arguments, or `false` when the method owns its deadline), and optional
1231
+ `verifies`. The default timeout is `actionTimeout`; assertions verify by
1232
+ default. Nested maps describe namespaces. Recording decorates the object in
1233
+ place, so create a fresh surface in each factory invocation.
1234
+
1235
+ The runner opens the step before calling a declared method, so attachments
1236
+ and failures from synchronous and asynchronous work land on the same step.
1237
+ Work arriving after its step has closed cannot attach to a later one.
1238
+
1239
+ A factory must return the surface it declared through `context.fixture`; a
1240
+ plain surface is `INVALID_CONFIG` the first time a test reaches for it.
1241
+ `expectable` attaches an expectation surface so `expect(fixture)` returns it;
1242
+ declare matcher recording with `context.fixture('expect', matchers,
1243
+ operations)`. Type the fixture with `test.extend<{ name: Type }>()`.
1244
+
1245
+ The entrypoint also exports `obj(value)`, which drops `undefined`-valued keys
1246
+ so a result built from optional inputs satisfies exact optional property
1247
+ types, plus `raceAbort(work, signal, label)`, `withTimeout(promise,
1248
+ timeoutMs, onTimeout)`, and `withinCleanupBudget(promise, { signal,
1249
+ timeoutMs })`. Pass a thunk to `raceAbort` to check cancellation before
1250
+ dispatch. `isSecret(value)` tells a `secrets.get()` handle from a string, for
1251
+ an option that takes either.
1252
+
1253
+ ## Vocabulary
1254
+
1255
+ `OperationContext`, `TextPattern`, `SemanticQuery`, `LocatorExpression`,
1256
+ `NodeRef`, `SemanticNode`, `ViewportPoint`, `ViewportSize`, `ObservationPixels`,
1257
+ `LocatorAction`, `LocatorActionKind`, `PointerAction`, `PointerActionKind`,
1258
+ `ScrollDirection`, `Momentum`, `SelectOption`, `Key`,
1259
+ `KeyModifier`, `KeyName`, `ParsedKey`, `ENGINE_SPI_VERSION`,
1260
+ `LOCATOR_ACTION_KINDS`, `POINTER_ACTION_KINDS`, `KEY_MODIFIERS`,
1261
+ `KEY_NAMES`, `parseKey`, `ENGINE_ERROR_CODES`, `RETRYABLE_ENGINE_ERROR_CODES`,
1262
+ `Secret`, `isSecret`, `TextMatch`, `JsonValue`, `JsonValueRules`,
1263
+ `ResolveExpressionOptions`, `PollConditionOptions`, `WithoutUndefined`,
1264
+ `FixtureOperation`, `OBSERVED_NAME_LIMIT` (256) and `OBSERVED_TEXT_LIMIT`
1265
+ (512) are exported from `e2e/engine`. An engine that cuts an observed field cuts it at exactly its
1266
+ limit: `name` at the name limit, `text` and any `value` or `selection` it
1267
+ cuts at the text limit. So a field exactly at its limit may have been cut; a
1268
+ shorter one, or a longer one the engine left whole, is complete. Keep the start and cut the end: the
1269
+ runner redacts the leading part of a secret that a field exactly at its limit
1270
+ ends with, which a whole-value match cannot see. Single-node reads are
1271
+ unbounded and always carry the full value. The `selector` expression kind carries a
1272
+ platform-native selector string; `frame` scopes a query into a nested
1273
+ document.
1274
+
1275
+ ```ts
1276
+ /** One node of an observation tree. */
1277
+ export interface SemanticNode {
1278
+ /** Node reference, valid against the observation it came from. */
1279
+ readonly ref: NodeRef;
1280
+ /** Semantic role. */
1281
+ readonly role?: string;
1282
+ /** Accessible name, cut at `OBSERVED_NAME_LIMIT`. */
1283
+ readonly name?: string;
1284
+ /** Visible text, cut at `OBSERVED_TEXT_LIMIT`. */
1285
+ readonly text?: string;
1286
+ /** Current input value; never reported for a secure field. */
1287
+ readonly value?: string;
1288
+ /**
1289
+ * Text selected inside the node, for the focused field or editing host
1290
+ * whose selection the platform reports; absent when the selection is
1291
+ * collapsed, and never reported for a secure field. Cut at `OBSERVED_TEXT_LIMIT`.
1292
+ */
1293
+ readonly selection?: string;
1294
+ /**
1295
+ * The node's test id, when the platform gives it one: the value of the
1296
+ * project's test-id attribute on a document platform, an accessibility
1297
+ * identifier on iOS, a resource id on Android, an automation id on a
1298
+ * desktop. The `testId` query resolves against exactly this field, so the
1299
+ * harness never knows where a platform keeps it.
1300
+ */
1301
+ readonly testId?: string;
1302
+ /** What a field takes, deciding which secrets may fill it. */
1303
+ readonly inputPurpose?: 'username' | 'password' | 'one-time-code' | 'generic-secret' | 'none';
1304
+ /**
1305
+ * Boolean states the platform reports. `pressed` is a toggle button's
1306
+ * pressed state; `secure` marks a field whose value is never observed.
1307
+ */
1308
+ readonly states?: Readonly<
1309
+ Partial<
1310
+ Record<
1311
+ 'checked' | 'disabled' | 'selected' | 'expanded' | 'pressed' | 'focused' | 'hidden' | 'secure',
1312
+ boolean
1313
+ >
1314
+ >
1315
+ >;
1316
+ /** Heading level of a heading node (`1` through `6` on a document platform); absent elsewhere. */
1317
+ readonly level?: number;
1318
+ /** Platform attributes, what `getAttribute` reads. */
1319
+ readonly attributes?: Readonly<Record<string, string>>;
1320
+ /**
1321
+ * The node's box in the top-level viewport's CSS pixels (`ViewportPoint`
1322
+ * space), for every node, including those inside nested documents: an
1323
+ * engine that measures a child document against its own viewport shifts
1324
+ * the boxes by the boundary element's before reporting them.
1325
+ */
1326
+ readonly rect?: {
1327
+ readonly x: number;
1328
+ readonly y: number;
1329
+ readonly width: number;
1330
+ readonly height: number;
1331
+ };
1332
+ /**
1333
+ * Platform selector that addresses this node within its own document, when
1334
+ * the platform has one. It is structural, not semantic: it survives the
1335
+ * content changes that rename a node, and a runner may store it to re-find
1336
+ * the node cheaply on a later run. It is never part of the node's identity,
1337
+ * so whatever it resolves to is still checked before it is used.
1338
+ *
1339
+ * It SHOULD be anchored on an attribute naming the node or one of its
1340
+ * ancestors, and SHOULD be absent rather than positional all the way to the
1341
+ * document root: such a path is shifted by anything inserted above the node,
1342
+ * so it does not survive to the later run it exists for.
1343
+ */
1344
+ readonly selector?: string;
1345
+ /**
1346
+ * Enclosing nested-document chain as selectors of each boundary node,
1347
+ * outermost first. Absent for nodes in the main document.
1348
+ */
1349
+ readonly framePath?: readonly string[];
1350
+ /** Child nodes. */
1351
+ readonly children?: readonly SemanticNode[];
1352
+ }
1353
+
1354
+ /** The `screen` query kinds. */
1355
+ export type QueryKind = 'role' | 'label' | 'placeholder' | 'text' | 'displayValue' | 'testId';
1356
+ ```
1357
+
1358
+ A `SemanticQuery` with `visible: true` must match only nodes whose
1359
+ `states.hidden` is false, by the same predicate the engine reports on
1360
+ `SemanticNode`, so `getByText('x', { visible: true })` and `toBeVisible()`
1361
+ never disagree. Apply it wherever the query sits in the expression, under a
1362
+ scope, filter, or index too.