e2e 0.21.1 → 0.21.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 (133) hide show
  1. package/README.md +2 -0
  2. package/dist/.build.json +1 -1
  3. package/dist/agent/act-validation.js +1 -0
  4. package/dist/agent/act.js +7 -3
  5. package/dist/agent/action-dispatcher.js +30 -8
  6. package/dist/agent/actions.d.ts +2 -0
  7. package/dist/agent/actions.js +5 -2
  8. package/dist/agent/error.js +1 -0
  9. package/dist/agent/executor.d.ts +14 -6
  10. package/dist/agent/index.js +5 -1
  11. package/dist/agent/invocation.d.ts +3 -0
  12. package/dist/agent/model/sdk.d.ts +5 -0
  13. package/dist/agent/model/sdk.js +18 -1
  14. package/dist/agent/observation.js +1 -0
  15. package/dist/agent/primitives.js +14 -3
  16. package/dist/agent/replay.js +9 -3
  17. package/dist/agent/step-cache.d.ts +3 -2
  18. package/dist/agent/step-cache.js +7 -4
  19. package/dist/agent/tool-loop.js +4 -1
  20. package/dist/cache/context.js +1 -1
  21. package/dist/cache/recorder.d.ts +2 -1
  22. package/dist/cache/recorder.js +11 -2
  23. package/dist/cache/trace-fields.d.ts +10 -0
  24. package/dist/cache/trace-fields.js +17 -0
  25. package/dist/cache/trace.d.ts +9 -1
  26. package/dist/cache/trace.js +26 -7
  27. package/dist/cli/feedback.js +2 -1
  28. package/dist/cli/init/sibling-versions.json +1 -1
  29. package/dist/collect/select.js +12 -2
  30. package/dist/config/agent.d.ts +1 -1
  31. package/dist/config/diagnose.js +9 -3
  32. package/dist/config/esm-hooks.js +3 -2
  33. package/dist/config/resolve.js +3 -2
  34. package/dist/config/typescript.js +52 -2
  35. package/dist/engine/contract.d.ts +3 -2
  36. package/dist/engine/manifest.js +2 -2
  37. package/dist/internal/errors.js +5 -2
  38. package/dist/internal/source.js +2 -0
  39. package/dist/internal/unserved-model.d.ts +5 -0
  40. package/dist/internal/unserved-model.js +22 -0
  41. package/dist/locator/screen.js +1 -2
  42. package/dist/oauth/copilot.js +23 -5
  43. package/dist/oauth/device-code.js +35 -19
  44. package/dist/oauth/errors.d.ts +3 -0
  45. package/dist/oauth/errors.js +26 -11
  46. package/dist/oauth/providers/github-copilot.d.ts +4 -0
  47. package/dist/oauth/providers/github-copilot.js +64 -16
  48. package/dist/oauth/providers/openai.d.ts +2 -2
  49. package/dist/oauth/providers/openai.js +47 -19
  50. package/dist/oauth/providers/opencode-console.js +36 -15
  51. package/dist/oauth/providers/xai.js +18 -9
  52. package/dist/oauth/token-endpoint.d.ts +15 -6
  53. package/dist/oauth/token-endpoint.js +18 -6
  54. package/dist/report/code-frame.js +9 -3
  55. package/dist/report/failure-text.d.ts +10 -2
  56. package/dist/report/failure-text.js +24 -8
  57. package/dist/report/list-model.d.ts +13 -0
  58. package/dist/report/list-steps.d.ts +6 -1
  59. package/dist/report/list-steps.js +29 -0
  60. package/dist/report/list.d.ts +1 -0
  61. package/dist/report/list.js +16 -7
  62. package/dist/report/live-window.js +2 -1
  63. package/dist/report/markdown.js +1 -1
  64. package/dist/run/fixtures.js +2 -0
  65. package/dist/run/host-trace.d.ts +3 -2
  66. package/dist/run/host-trace.js +2 -1
  67. package/dist/run/last-run.d.ts +9 -0
  68. package/dist/run/last-run.js +28 -0
  69. package/dist/run/open-session.d.ts +3 -1
  70. package/dist/run/open-session.js +1 -1
  71. package/dist/run/runner.js +6 -2
  72. package/dist/run/sessions.js +3 -3
  73. package/dist/run/standalone-attempt.d.ts +2 -1
  74. package/dist/run/standalone-attempt.js +3 -1
  75. package/dist/run/steps.d.ts +3 -1
  76. package/dist/run/units.d.ts +8 -0
  77. package/dist/run/units.js +14 -0
  78. package/dist/run/worker-models.d.ts +2 -0
  79. package/dist/run/worker-models.js +7 -0
  80. package/dist/types.d.ts +2 -2
  81. package/docs/authentication.mdx +26 -23
  82. package/docs/browser.mdx +7 -4
  83. package/docs/bug-bash.mdx +2 -2
  84. package/docs/cache.mdx +6 -1
  85. package/docs/ci/bitrise.mdx +1 -1
  86. package/docs/ci/codemagic.mdx +2 -2
  87. package/docs/ci/eas-workflows.mdx +1 -1
  88. package/docs/coding-agents.mdx +7 -35
  89. package/docs/debugging.mdx +24 -36
  90. package/docs/executors.mdx +1 -1
  91. package/docs/github.mdx +2 -1
  92. package/docs/integrations/eas.mdx +1 -1
  93. package/docs/integrations/index.mdx +6 -0
  94. package/docs/integrations/lightpanda.mdx +192 -0
  95. package/docs/integrations/testmu.mdx +152 -0
  96. package/docs/mcp.mdx +123 -0
  97. package/docs/migrate/detox.mdx +1 -1
  98. package/docs/migrate/maestro.mdx +101 -105
  99. package/docs/migrate/playwright.mdx +65 -70
  100. package/docs/mobile.mdx +18 -3
  101. package/docs/models.mdx +4 -2
  102. package/docs/quickstart.mdx +7 -0
  103. package/docs/reference/agent.mdx +369 -277
  104. package/docs/reference/app.mdx +141 -32
  105. package/docs/reference/cli.mdx +674 -558
  106. package/docs/reference/config.mdx +1406 -752
  107. package/docs/reference/engine.mdx +1062 -256
  108. package/docs/reference/environment.mdx +156 -84
  109. package/docs/reference/errors.mdx +106 -72
  110. package/docs/reference/expect.mdx +752 -372
  111. package/docs/reference/mcp.mdx +366 -141
  112. package/docs/reference/mobile.mdx +844 -403
  113. package/docs/reference/reporters.mdx +312 -106
  114. package/docs/reference/runner.mdx +373 -174
  115. package/docs/reference/screen.mdx +931 -398
  116. package/docs/reference/security.mdx +106 -40
  117. package/docs/reference/test.mdx +626 -273
  118. package/docs/reference/web.mdx +956 -533
  119. package/docs/security.mdx +6 -5
  120. package/docs/snippets/model-provider.jsx +763 -0
  121. package/docs/snippets/setup-prompt-card.jsx +22 -0
  122. package/docs/snippets/show-more.jsx +22 -0
  123. package/docs/snippets/video-card.jsx +34 -0
  124. package/docs/snippets/video.jsx +25 -0
  125. package/docs/subscriptions.mdx +10 -3
  126. package/docs/telemetry.mdx +39 -39
  127. package/package.json +3 -2
  128. package/schema/report-v1.schema.json +6 -0
  129. package/skills/e2e/references/agent.md +8 -3
  130. package/skills/e2e/references/debugging.md +1 -1
  131. package/skills/e2e/references/mcp.md +1 -1
  132. package/skills/e2e/references/setup.md +12 -3
  133. package/skills/e2e/references/writing-tests.md +1 -1
@@ -6,95 +6,242 @@ icon: "display"
6
6
  "twitter:image": "https://e2e.tester.army/docs/images/og/reference/screen.png?v=1b21c45d37"
7
7
  ---
8
8
 
9
- A `Locator` stores a query and resolves it when an action, read, or assertion
10
- runs. Every `Screen` query method is also available on a locator, scoped to
11
- its subtree. `tapAt` is not scoped: on a locator it still taps a viewport
12
- point.
13
-
14
- ```ts
15
- export interface Screen {
16
- /** Creates a lazy role query. An alias such as `img` is rewritten to its role. */
17
- getByRole(role: Role | RoleAlias, options?: RoleOptions): Locator;
18
- /** Creates a lazy role query narrowed to an accessible name: `getByRole('button', 'Sign in')`. */
19
- getByRole(role: Role | RoleAlias, name: TextMatch, options?: Omit<RoleOptions, 'name'>): Locator;
20
- /** Creates a lazy accessible-label query. */
21
- getByLabel(text: TextMatch, options?: TextMatchOptions): Locator;
22
- /** Creates a lazy placeholder query. */
23
- getByPlaceholder(text: TextMatch, options?: TextMatchOptions): Locator;
24
- /** Creates a lazy visible-text query. */
25
- getByText(text: TextMatch, options?: TextMatchOptions): Locator;
26
- /** Creates a lazy displayed-value query. */
27
- getByDisplayValue(value: TextMatch, options?: TextMatchOptions): Locator;
28
- /** Creates a lazy test-id query: a string matches the whole id, case-sensitive; a RegExp tests it. */
29
- getByTestId(id: TextMatch, options?: { visible?: boolean }): Locator;
30
- /** Taps a viewport point, with no node behind it. */
31
- tapAt(point: Point, options?: ActionOptions): Promise<void>;
32
- /** Performs a viewport-level swipe: in a direction, or along a path from one point to another. */
33
- swipe(options: SwipeOptions | SwipePathOptions): Promise<void>;
34
- /** Scrolls until a locator resolves visibly or times out. */
35
- scrollUntilVisible(
36
- target: Locator,
37
- options?: { direction?: ScrollDirection; momentum?: Momentum; timeout?: number },
38
- ): Promise<void>;
39
- }
9
+ The `screen` fixture builds locators. A `Locator` stores a query and resolves
10
+ it when an action, read, or assertion runs. `Locator` extends `Screen`, so
11
+ every query also runs on a locator, scoped to its subtree.
12
+
13
+ ```ts title="tests/invoices.e2e.ts"
14
+ import { expect, test } from 'e2e';
15
+
16
+ test('void an invoice', async ({ app, screen }) => {
17
+ await app.open('/invoices');
18
+ const row = screen.getByRole('listitem').filter({ hasText: 'Invoice 42' });
19
+ await row.getByRole('button', 'Void').tap();
20
+ await expect(row.getByText('Voided')).toBeVisible();
21
+ });
40
22
  ```
41
23
 
42
- Most operations require exactly one match. A read polls for its one match
43
- until the action timeout, as an action does, then returns the current value
44
- without waiting for it to change. A node the engine reports stale (a resolve
45
- that raced a navigation, or a match the app replaced while it was read) is
46
- re-resolved within the same timeout. Use [`expect`](/reference/expect) to
47
- wait for a value.
24
+ ## Reference
48
25
 
49
- | Operation | Zero matches | Multiple matches |
50
- | --- | --- | --- |
51
- | Actions | Poll until the timeout, then `LOCATOR_NOT_FOUND` | `LOCATOR_AMBIGUOUS` |
52
- | Reads | Poll until the timeout, then `LOCATOR_NOT_FOUND` | `LOCATOR_AMBIGUOUS` |
53
- | `count()`, `all()`, `allTextContents()` | `0`, `[]`, `[]` | The count, one locator per match, one text per match |
54
- | `isVisible()`, `isHidden()` | `false`, `true` | `LOCATOR_AMBIGUOUS` |
55
- | Assertions | Poll until the timeout | `LOCATOR_AMBIGUOUS` |
56
- | Any of these under a frame that is not in the document | As zero matches for `count()`, `all()`, `allTextContents()`, `isVisible()`, and `isHidden()`; actions, reads, and assertions poll for the frame until the timeout, then `LOCATOR_NOT_FOUND` | `LOCATOR_AMBIGUOUS` when several frames match |
26
+ ### Queries
57
27
 
58
- ## Queries
28
+ | Method | Returns |
29
+ | --- | --- |
30
+ | [`getByRole`](#getbyrole) | `Locator` |
31
+ | [`getByLabel`](#getbylabel) | `Locator` |
32
+ | [`getByPlaceholder`](#getbyplaceholder) | `Locator` |
33
+ | [`getByText`](#getbytext) | `Locator` |
34
+ | [`getByDisplayValue`](#getbydisplayvalue) | `Locator` |
35
+ | [`getByTestId`](#getbytestid) | `Locator` |
59
36
 
60
37
  Every query returns a `Locator` synchronously and performs no work. An
61
38
  option key the query does not take (`{ nam: 'Save' }`) throws
62
39
  `INVALID_LOCATOR` naming the keys it does.
63
40
 
64
- `getByRole` takes the accessible name as its second argument, a string or a
65
- `RegExp`, with the other options after it:
66
- `getByRole('button', 'Save', { exact: false })`. The object form,
67
- `getByRole('button', { name: 'Save', disabled: true })`, is the same query.
68
-
69
41
  A string passed to `getByRole` as the name, `getByLabel`, `getByPlaceholder`,
70
42
  `getByText`, or `getByDisplayValue` matches the whole text, case-sensitive,
71
- after whitespace normalization: `getByRole('button', 'Save')` does
72
- not match `Save changes`. `{ exact: false }` makes it a case-insensitive
73
- substring match, and a `RegExp` matches as written.
43
+ after whitespace normalization: `getByRole('button', 'Save')` does not match
44
+ `Save changes`. `{ exact: false }` makes it a case-insensitive substring
45
+ match, and a `RegExp` matches as written.
46
+
47
+ Every query accepts `visible: true`; see
48
+ [Hidden duplicates](#hidden-duplicates).
74
49
 
75
- `getByTestId` matches the node's `testId`: on the web the value of the
76
- engine's test-id attribute (`web({ testIdAttribute })`, default
77
- `data-testid`), on a device the accessibility identifier or resource id. A
78
- string matches the whole id, case-sensitive; a `RegExp` such as
79
- `/total-budgeted$/` matches as written.
50
+ #### `getByRole`
51
+
52
+ Matches nodes by [role](#types), optionally narrowed to an accessible name
53
+ and to states.
54
+
55
+ ```ts
56
+ /** Creates a lazy role query. An alias such as `img` is rewritten to its role. */
57
+ getByRole(role: Role | RoleAlias, options?: RoleOptions): Locator;
58
+ /** Creates a lazy role query narrowed to an accessible name: `getByRole('button', 'Sign in')`. */
59
+ getByRole(role: Role | RoleAlias, name: TextMatch, options?: Omit<RoleOptions, 'name'>): Locator;
60
+ ```
80
61
 
81
62
  ```ts
82
63
  screen.getByRole('button', 'Save');
64
+ screen.getByRole('button', 'Save', { exact: false });
65
+ screen.getByRole('button', { name: 'Save', disabled: true }); // the object form
83
66
  screen.getByRole('heading', /welcome/i, { level: 1 });
84
- screen.getByLabel('Email');
67
+ ```
68
+
69
+ The accessible name goes second, a string or a `RegExp`, with the other
70
+ options after it. The object form, `{ name: 'Save' }`, is the same query.
71
+ The options are [`RoleOptions`](#types).
72
+
73
+ <Note>
74
+ A role query never matches a node hidden from the accessibility tree: one
75
+ that does not render, or one under `aria-hidden`. On the other query kinds,
76
+ `visible: true` keeps only nodes that render, the same check `toBeVisible()`
77
+ makes, so an `aria-hidden` node that still paints counts as visible. See
78
+ [Hidden duplicates](#hidden-duplicates).
79
+ </Note>
80
+
81
+ ##### `img` alias
82
+
83
+ `img` is an alias, not a role ([`RoleAlias`](#types)).
84
+ `getByRole('img', { name })` builds the same query as
85
+ `getByRole('image', { name })`, so a test ported from Playwright keeps its
86
+ spelling. On the web, `image` covers `<img>` with alt text and any element
87
+ with `role="img"`. An `<img alt="">` is decoration and matches nothing.
88
+
89
+ ##### Roles on the web
90
+
91
+ The web engine derives the roles from HTML the way HTML-AAM does:
92
+
93
+ - `<progress>` is `progressbar`, `<meter>` is `meter`, `<hr>` is `separator`.
94
+ - `<fieldset>` and `<details>` are `group`.
95
+ - `<input type="number">` is `spinbutton`. `<input type="file">` is `button`,
96
+ as Chrome and Playwright read it.
97
+ - `<select size="3">` is a `listbox`, like a `multiple` one. An
98
+ `<input list>` naming a `<datalist>` is a `combobox`.
99
+ - A `<td>` of a `role="grid"` table is a `gridcell`.
100
+ - `<thead>` and `<tbody>` are `rowgroup`.
101
+ - `<th scope="row">` is `rowheader`. A scope-less `<th>` beside a data cell
102
+ with text or children is also a `rowheader`; header-only rows keep
103
+ `columnheader`. A lone scope-less header in a one-row table has no header
104
+ role, but an explicit `scope="col"` or `scope="colgroup"` keeps
105
+ `columnheader`.
106
+ - `<figure>` is `figure`, `<article>` is `article`.
107
+ - `<header>` and `<footer>` are `banner` and `contentinfo` only outside
108
+ sectioning content or an element with a sectioning role (`article`,
109
+ `complementary`, `main`, `navigation`, `region`).
110
+ - `<form>` and `<section>` are `form` and `region` only when they carry an
111
+ `aria-label`, `aria-labelledby`, or `title`.
112
+ - `<menu>` is a `list`; only `role="menu"` is a `menu`.
113
+
114
+ Names follow the same rules:
115
+
116
+ - A `<legend>`, `<figcaption>`, or `<caption>` names its fieldset, figure, or
117
+ table, so `toHaveAccessibleName` reads what `getByRole('group', { name })`
118
+ matched.
119
+ - A role that accname names from content (a button, link, heading, tab, menu
120
+ item, option, checkbox, radio, switch, cell, row or column header, row, tree
121
+ item, tooltip) takes its name from its descendants, a custom element's
122
+ shadow tree included. So `getByRole('checkbox', { name })` and
123
+ `toHaveAccessibleName(name)` agree on a `<div role="checkbox">`.
124
+ - An `<input type="submit">` or `type="reset"` with no `value` reads `Submit`
125
+ or `Reset`.
126
+
127
+ ##### Roles on a device
128
+
129
+ A device engine maps the platform types it has: a tab bar or segmented
130
+ control is a `tablist`, a progress indicator a `progressbar`, a stepper a
131
+ `spinbutton`. It keeps any other type verbatim, so a role with no native
132
+ widget, such as `tooltip` or `figure`, matches nothing on a phone.
133
+
134
+ ##### Editing hosts
135
+
136
+ A `contenteditable` editing host, the root a TipTap, ProseMirror, Lexical, or
137
+ Slate editor renders into, is a `textbox`.
138
+
139
+ - Its value is its text as rendered. While it has focus, its `selection` is
140
+ the text selected inside it. The value keeps spaces and newlines, so
141
+ `toHaveValue` and `inputValue()` read a `white-space: pre-wrap` editor like
142
+ a textarea.
143
+ - An empty editor reads as `''`: one holding only the `<p><br></p>` or the
144
+ zero-width no-break space its editor pads an empty line with. That padding
145
+ is never part of a value.
146
+ - It is named by its `aria-label`, `aria-placeholder`, or the
147
+ `data-placeholder` its editor paints. The blocks inside it are content, not
148
+ textboxes.
149
+ - The tag does not matter: an `<article contenteditable>` is the editor, not
150
+ an article. A native control or a drawn surface made editable keeps its own
151
+ role.
152
+ - A name from a block's `data-placeholder` (TipTap, ProseMirror) goes once
153
+ the editor has content, as the painted hint does. From then on the host is
154
+ an unnamed `textbox`. A name from the host's own `aria-label`,
155
+ `aria-labelledby`, `aria-placeholder`, or `data-placeholder` (Quill) stays.
156
+
157
+ `getByRole` resolves through Playwright's role selector, which has no such
158
+ rule. So `getByRole` finds an editor only when the host carries
159
+ `role="textbox"`; `getByLabel` and `getByTestId` reach it either way. Anchor
160
+ a test on a label or a test id, not on the placeholder.
161
+
162
+ #### `getByLabel`
163
+
164
+ ```ts
165
+ /** Creates a lazy accessible-label query. */
166
+ getByLabel(text: TextMatch, options?: TextMatchOptions): Locator;
167
+ ```
168
+
169
+ ```ts
170
+ await screen.getByLabel('Email').fill('ada@example.test');
171
+ ```
172
+
173
+ #### `getByPlaceholder`
174
+
175
+ ```ts
176
+ /** Creates a lazy placeholder query. */
177
+ getByPlaceholder(text: TextMatch, options?: TextMatchOptions): Locator;
178
+ ```
179
+
180
+ ```ts
181
+ await screen.getByPlaceholder('Search memory...').fill('launch');
182
+ ```
183
+
184
+ #### `getByText`
185
+
186
+ ```ts
187
+ /** Creates a lazy visible-text query. */
188
+ getByText(text: TextMatch, options?: TextMatchOptions): Locator;
189
+ ```
190
+
191
+ ```ts
85
192
  screen.getByText('Welcome back', { exact: false });
86
- screen.getByTestId('todo-3');
87
193
  ```
88
194
 
89
- ### Echoed text
195
+ ##### Echoed text
90
196
 
91
197
  `getByText` answers with the innermost matching node, so a container that
92
- echoes its child's text counts once. Browsers do this natively; on a device
198
+ echoes its child's text counts once. Browsers do this natively. On a device
93
199
  the engine applies the same rule, since iOS reports a React Native `Text` as
94
200
  a host view plus a `StaticText` child with the same label. Two matches that
95
201
  do not contain each other remain `LOCATOR_AMBIGUOUS`.
96
202
 
97
- ### Hidden duplicates
203
+ #### `getByDisplayValue`
204
+
205
+ Matches controls by their displayed value.
206
+
207
+ ```ts
208
+ /** Creates a lazy displayed-value query. */
209
+ getByDisplayValue(value: TextMatch, options?: TextMatchOptions): Locator;
210
+ ```
211
+
212
+ ```ts
213
+ await screen.getByDisplayValue('ada@example.test').clear();
214
+ ```
215
+
216
+ <Note>
217
+ In the web engine a `getByDisplayValue` locator cannot be the scope of a
218
+ child query or a `has` filter; both fail with `UNSUPPORTED_CAPABILITY`.
219
+ Playwright has no selector for a control's current value, so the engine
220
+ filters candidates after the fact.
221
+ </Note>
222
+
223
+ #### `getByTestId`
224
+
225
+ Matches the node's `testId`.
226
+
227
+ ```ts
228
+ /** Creates a lazy test-id query: a string matches the whole id, case-sensitive; a RegExp tests it. An engine may also match an id without its platform's namespacing. */
229
+ getByTestId(id: TextMatch, options?: { visible?: boolean }): Locator;
230
+ ```
231
+
232
+ ```ts
233
+ screen.getByTestId('todo-3');
234
+ screen.getByTestId(/total-budgeted$/);
235
+ ```
236
+
237
+ - On the web, `testId` is the value of the engine's test-id attribute
238
+ (`web({ testIdAttribute })`, default `data-testid`).
239
+ - On a device, it is the accessibility identifier or resource id. For
240
+ namespacing, see the mobile engine's
241
+ [Android resource ids](/reference/mobile#capabilities).
242
+ - `visible` is the only option.
243
+
244
+ #### Hidden duplicates
98
245
 
99
246
  Every query accepts `visible: true`, which drops nodes the platform reports
100
247
  as hidden before the exactly-one rule runs. Frameworks often keep a hidden
@@ -110,91 +257,142 @@ await screen.getByTestId('memory-list', { visible: true }).getByRole('listitem')
110
257
  ```
111
258
 
112
259
  `visible` uses the same visibility check as `toBeVisible()`, `isVisible()`,
113
- and `waitFor()`: what renders, as Playwright reads it. `display: none`,
114
- `visibility: hidden`, a box with no size, a closed `<details>` body, and
115
- content under `content-visibility: hidden` are hidden; `aria-hidden` is not,
116
- since a decorative spinner marked with it still paints, so
117
- `toBeHidden()` on one waits until it leaves. A child that sets
118
- `visibility: visible` under a hidden parent is visible. On the web, text
119
- directly under a `display: contents` element with `visibility: hidden` is
120
- hidden, where Playwright calls it visible. It works with scopes, `filter`, `first`, `last`, and `nth`:
121
- `getByText('Save', { visible: true }).first()` is the first visible match.
260
+ and `waitFor()`: what renders, as Playwright reads it.
261
+
262
+ - Hidden: `display: none`, `visibility: hidden`, a box with no size, a closed
263
+ `<details>` body, and content under `content-visibility: hidden`.
264
+ - Not hidden: `aria-hidden`, since a decorative spinner marked with it still
265
+ paints, so `toBeHidden()` on one waits until it leaves.
266
+ - A child that sets `visibility: visible` under a hidden parent is visible.
267
+ - On the web, text directly under a `display: contents` element with
268
+ `visibility: hidden` is hidden, where Playwright calls it visible.
269
+ - It works with scopes, `filter`, `first`, `last`, and `nth`:
270
+ `getByText('Save', { visible: true }).first()` is the first visible match.
271
+
122
272
  Prefer it over `browser.locator('p:visible')`, which ties the test to CSS.
123
273
 
124
- ## Refinement
274
+ ### Refinement
125
275
 
126
- Refinement returns a new locator without querying the app. `hasText` with a
127
- string is a case-insensitive substring match over the match and its
128
- descendants, unlike a query text, which matches exactly; pass a RegExp for
129
- anything stricter. `has` keeps a match whose subtree resolves the inner
130
- locator.
276
+ | Method | Returns |
277
+ | --- | --- |
278
+ | [`filter`](#filter) | `Locator` |
279
+ | [`first`](#first) | `Locator` |
280
+ | [`last`](#last) | `Locator` |
281
+ | [`nth`](#nth) | `Locator` |
131
282
 
132
- Throws `INVALID_LOCATOR` when `filter` gets neither `hasText` nor `has`, when
133
- it gets any other key, even beside them (`hasNot`, `hasNotText`, and
134
- `visible` are not filters here), when `has` is not a runner locator or was
135
- made by another target's screen, or when `nth` gets a negative or
136
- non-integer index.
283
+ Refinement returns a new locator without querying the app.
284
+
285
+ #### `filter`
286
+
287
+ ```ts
288
+ /** Adds deterministic locator filters. */
289
+ filter(options: { hasText?: TextMatch; has?: Locator }): Locator;
290
+ ```
137
291
 
138
292
  ```ts
139
293
  const row = screen.getByRole('listitem').filter({ hasText: 'Invoice 42' });
140
294
  await row.getByRole('button', 'Void').tap();
141
295
  ```
142
296
 
143
- <Note>
144
- In the web engine a `getByDisplayValue` locator cannot be the scope
145
- of a child query or a `has` filter; both fail with `UNSUPPORTED_CAPABILITY`.
146
- Playwright has no selector for a control's current value, so the engine
147
- filters candidates after the fact.
148
- </Note>
297
+ - `hasText` with a string is a case-insensitive substring match over the
298
+ match and its descendants, unlike a query text, which matches exactly. Pass
299
+ a `RegExp` for anything stricter.
300
+ - `has` keeps a match whose subtree resolves the inner locator.
301
+
302
+ | Throws | When |
303
+ | --- | --- |
304
+ | `INVALID_LOCATOR` | Neither `hasText` nor `has` is given. |
305
+ | `INVALID_LOCATOR` | Any other key, even beside them: `hasNot`, `hasNotText`, and `visible` are not filters here. |
306
+ | `INVALID_LOCATOR` | `has` is not a runner locator, or was made by another target's screen. |
149
307
 
150
- ## screen.tapAt
308
+ #### `first`
151
309
 
152
310
  ```ts
153
- tapAt(point: Point, options?: ActionOptions): Promise<void>;
311
+ /** Selects the first current match. */
312
+ first(): Locator;
313
+ ```
314
+
315
+ ```ts
316
+ await screen.getByRole('listitem').first().tap();
154
317
  ```
155
318
 
156
- Taps a viewport point in CSS pixels, the space `boundingBox()` reports in,
157
- with no node behind it: a pin on a canvas, a control the tree does not
158
- list. Nothing is resolved or waited for; the page decides what the tap
159
- lands on. Uses `config.actionTimeout`. Throws `INVALID_ARGUMENT` for a
160
- negative or non-finite coordinate and `UNSUPPORTED_CAPABILITY` when the
161
- engine does not list `tap` in its pointer actions.
319
+ #### `last`
162
320
 
163
321
  ```ts
164
- const map = await screen.getByRole('image', 'Map').boundingBox();
322
+ /** Selects the last current match. */
323
+ last(): Locator;
324
+ ```
325
+
326
+ ```ts
327
+ await screen.getByRole('listitem').last().tap();
328
+ ```
329
+
330
+ #### `nth`
331
+
332
+ ```ts
333
+ /** Selects one zero-based current match. */
334
+ nth(index: number): Locator;
335
+ ```
336
+
337
+ ```ts
338
+ await screen.getByRole('row').nth(2).getByRole('checkbox').check();
339
+ ```
340
+
341
+ | Throws | When |
342
+ | --- | --- |
343
+ | `INVALID_LOCATOR` | A negative or non-integer index. |
344
+
345
+ ### Screen actions
346
+
347
+ | Method | Returns |
348
+ | --- | --- |
349
+ | [`screen.tapAt`](#screen-tapat) | `Promise<void>` |
350
+ | [`screen.swipe`](#screen-swipe) | `Promise<void>` |
351
+ | [`screen.scrollUntilVisible`](#screen-scrolluntilvisible) | `Promise<void>` |
352
+
353
+ `tapAt` and `swipe` act on the viewport.
354
+
355
+ #### `screen.tapAt`
356
+
357
+ Use it for a pin on a canvas or a control the tree does not list.
358
+
359
+ ```ts
360
+ /** Taps a viewport point, with no node behind it. */
361
+ tapAt(point: Point, options?: ActionOptions): Promise<void>;
362
+ ```
363
+
364
+ ```ts
365
+ const map = (await screen.getByRole('image', 'Map').boundingBox())!;
165
366
  await screen.tapAt({ x: map.x + 360, y: map.y + 70 });
166
367
  ```
167
368
 
168
- Prefer a locator when the tree lists the control; a point moves with the
169
- layout. For a point inside a node, `tap({ position })` reads the node's box
170
- for you.
369
+ - The point is in CSS pixels, the space `boundingBox()` reports in.
370
+ - Nothing is resolved or waited for; the page decides what the tap lands on.
371
+ - Uses `config.actionTimeout`.
372
+ - Prefer a locator when the tree lists the control, since a point moves with
373
+ the layout. For a point inside a node, `tap({ position })` reads the node's
374
+ box for you.
171
375
 
172
- ## screen.swipe
376
+ | Throws | When |
377
+ | --- | --- |
378
+ | `INVALID_ARGUMENT` | A negative or non-finite coordinate. |
379
+ | `UNSUPPORTED_CAPABILITY` | The engine does not list `tap` in its pointer actions. |
380
+
381
+ <Note>
382
+ `tapAt` is not scoped: on a locator it still taps a viewport point.
383
+ </Note>
384
+
385
+ #### `screen.swipe`
173
386
 
174
387
  ```ts
388
+ /** Performs a viewport-level swipe: in a direction, or along a path from one point to another. */
175
389
  swipe(options: SwipeOptions | SwipePathOptions): Promise<void>;
176
390
  ```
177
391
 
178
- Viewport-level gesture, in one of two shapes. `{ direction, momentum? }` is
179
- a scroll gesture on the screen. `direction` is the way the content scrolls,
180
- not the finger: `'down'` reveals what is below. `momentum` is its fling
181
- strength, `'none'` by default: `'none'` a plain drag of half the surface,
182
- `'slow'` a short fling, `'fast'` a long one. How far each carries is the
183
- engine's.
184
- `{ from, to, duration? }` is a swipe along a path between two viewport points:
185
- a touch swipe on a device, a pointer drag on a document platform, which is
186
- what reveals a row action, dismisses a sheet, or drags a card between
187
- columns. `duration` sets how long the gesture takes in milliseconds; omitted
188
- leaves the duration to the engine's default. Uses `config.actionTimeout`.
189
- Throws `ACTION_FAILED` when the gesture is rejected, `INVALID_ARGUMENT` for
190
- a mix of the two shapes, an out-of-range duration, or a coordinate off the
191
- plane, and `UNSUPPORTED_CAPABILITY` when the engine does not list `swipe` in
192
- its actions (a direction) or `swipeTo` in its pointer actions (a path).
193
-
194
392
  ```ts
195
393
  await screen.swipe({ direction: 'up', momentum: 'fast' });
196
394
 
197
- const row = await screen.getByText('Design review').boundingBox();
395
+ const row = (await screen.getByText('Design review').boundingBox())!;
198
396
  await screen.swipe({
199
397
  from: { x: row.x + row.width - 10, y: row.y + row.height / 2 },
200
398
  to: { x: row.x + 10, y: row.y + row.height / 2 },
@@ -202,263 +400,663 @@ await screen.swipe({
202
400
  });
203
401
  ```
204
402
 
205
- ## screen.scrollUntilVisible
403
+ `{ direction, momentum? }` is a scroll gesture on the screen:
404
+
405
+ - `direction` is the way the content scrolls, not the finger: `'down'`
406
+ reveals what is below.
407
+ - `momentum` is its fling strength, `'none'` by default. `'none'` is a
408
+ plain drag of half the surface, `'slow'` a short fling, `'fast'` a long
409
+ one. The engine decides how far each carries.
410
+
411
+ `{ from, to, duration? }` is a swipe along a path between two viewport
412
+ points: a touch swipe on a device, a pointer drag on a document platform.
413
+ Use it to reveal a row action, dismiss a sheet, or drag a card between
414
+ columns.
415
+
416
+ - `duration` is how long the gesture takes, an integer from 16 through
417
+ 10000 ms. Omitted, it is the engine's default.
418
+
419
+ Uses `config.actionTimeout` and takes no `timeout` option.
420
+
421
+ | Throws | When |
422
+ | --- | --- |
423
+ | `ACTION_FAILED` | The gesture is rejected. |
424
+ | `INVALID_ARGUMENT` | A mix of the two shapes, an out-of-range duration, a coordinate off the plane, or a `timeout` key. |
425
+ | `UNSUPPORTED_CAPABILITY` | The engine does not list `swipe` in its actions (a direction) or `swipeTo` in its pointer actions (a path). |
426
+
427
+ #### `screen.scrollUntilVisible`
206
428
 
207
429
  ```ts
430
+ /**
431
+ * Scrolls until a locator resolves visibly or times out: the viewport on
432
+ * `screen`, the node itself on a locator. `momentum` is the stride of each
433
+ * step, `slow` unless told otherwise.
434
+ */
208
435
  scrollUntilVisible(
209
436
  target: Locator,
210
437
  options?: { direction?: ScrollDirection; momentum?: Momentum; timeout?: number },
211
438
  ): Promise<void>;
212
439
  ```
213
440
 
214
- | Name | Type | Default |
441
+ ```ts
442
+ await screen.scrollUntilVisible(screen.getByRole('button', 'Accept'));
443
+ await screen.getByTestId('feed').scrollUntilVisible(row);
444
+ ```
445
+
446
+ | Option | Type | Default |
215
447
  | --- | --- | --- |
216
448
  | `target` | `Locator` | - |
217
449
  | `options.direction` | `ScrollDirection` | `'down'` |
218
- | `options.momentum` | `Momentum` | `'slow'`; the gesture of each step, as [`screen.swipe`](#screen-swipe) defines it. A longer step is faster but must not carry the target past the surface unseen, which is what a virtualized list does |
219
- | `options.timeout` | `number` | `30000`, its own default, not `config.actionTimeout`; it bounds the whole scroll, each swipe included |
450
+ | `options.momentum` | `Momentum` | `'slow'` |
451
+ | `options.timeout` | `number` | `30000` |
452
+
453
+ - `momentum` is as [`screen.swipe`](#screen-swipe) defines it. A longer step
454
+ is faster, but it must not carry the target past the surface unseen, as a
455
+ virtualized list does.
456
+ - `timeout` is its own default, not `config.actionTimeout`. It bounds the
457
+ whole scroll, each swipe included.
458
+ - On a locator each step swipes that node. So a feed or a table that is its
459
+ own scroll container pages without the pointer hovering it first.
460
+
461
+ | Throws | When |
462
+ | --- | --- |
463
+ | `INVALID_LOCATOR` | `target` is not a runner locator, or was made by another target's screen. |
464
+ | `INVALID_ARGUMENT` | An option it does not take. |
465
+ | `LOCATOR_NOT_FOUND` | The deadline expires first, a swipe it cut short included. |
466
+
467
+ ### Locator actions
468
+
469
+ | Method | Returns |
470
+ | --- | --- |
471
+ | [`tap`](#tap) | `Promise<void>` |
472
+ | [`click`](#tap) | `Promise<void>` |
473
+ | [`doubleTap`](#doubletap) | `Promise<void>` |
474
+ | [`dblclick`](#doubletap) | `Promise<void>` |
475
+ | [`secondaryTap`](#secondarytap) | `Promise<void>` |
476
+ | [`longPress`](#longpress) | `Promise<void>` |
477
+ | [`fill`](#fill) | `Promise<void>` |
478
+ | [`pressSequentially`](#presssequentially) | `Promise<void>` |
479
+ | [`clear`](#clear) | `Promise<void>` |
480
+ | [`press`](#press) | `Promise<void>` |
481
+ | [`check`](#check) | `Promise<void>` |
482
+ | [`uncheck`](#uncheck) | `Promise<void>` |
483
+ | [`selectOption`](#selectoption) | `Promise<void>` |
484
+ | [`focus`](#focus) | `Promise<void>` |
485
+ | [`hover`](#hover) | `Promise<void>` |
486
+ | [`setInputFiles`](#setinputfiles) | `Promise<void>` |
487
+ | [`dragTo`](#dragto) | `Promise<void>` |
488
+ | [`scrollIntoView`](#scrollintoview) | `Promise<void>` |
489
+ | [`swipe`](#swipe) | `Promise<void>` |
220
490
 
221
- On `screen` each step is a viewport swipe. On a locator the step is a swipe
222
- of that node, so a feed or a table that is its own scroll container pages
223
- without the pointer having to hover it first:
224
- `screen.getByRole('feed').scrollUntilVisible(row)`.
491
+ Each action resolves one node, waits for actionability, and performs one
492
+ operation. The timeout is
493
+ [`config.actionTimeout`](/reference/config#actiontimeout), and every action
494
+ takes `{ timeout }` to override it. An action takes only the options its
495
+ signature names.
225
496
 
226
- Throws `INVALID_LOCATOR` for a non-runner locator or one made by another
227
- target's screen, `INVALID_ARGUMENT` for an option it does not take,
228
- `LOCATOR_NOT_FOUND` when the deadline expires first, a swipe it cut short
229
- included.
497
+ | Throws | When |
498
+ | --- | --- |
499
+ | `UNSUPPORTED_CAPABILITY` | The engine does not list the action's kind in `actions`; thrown before reaching the engine. |
500
+ | `INVALID_ARGUMENT` | An option key the action does not take, Playwright's `trial`, `force`, or `noWaitAfter` included. Thrown before the node is resolved, so the control is never touched. |
501
+ | `LOCATOR_NOT_FOUND` | No match before the timeout. |
502
+ | `LOCATOR_AMBIGUOUS` | More than one match. |
503
+ | `ACTION_FAILED` | The action may already have committed; it is never repeated. |
504
+
505
+ #### `tap`
230
506
 
231
507
  ```ts
232
- await screen.scrollUntilVisible(screen.getByRole('button', 'Accept'));
508
+ /** Taps exactly one matching actionable node; with `position`, taps that point of its box once it is in view, with no other actionability check. */
509
+ tap(options?: TapOptions): Promise<void>;
510
+ /** Alias of tap. */
511
+ click(options?: TapOptions): Promise<void>;
233
512
  ```
234
513
 
235
- ## Locator actions
514
+ ```ts
515
+ await screen.getByRole('button', 'Save').tap({ timeout: 10_000 });
516
+ await screen.getByRole('image', 'Seat map').tap({ position: { x: 120, y: 48 } });
517
+ await row.click({ modifiers: ['Shift'] });
518
+ ```
236
519
 
237
- Each action resolves one node, waits for actionability, and performs one
238
- operation. Default timeout `config.actionTimeout`, 30000 ms, overridable
239
- with `{ timeout }`.
240
-
241
- ```ts
242
- export interface Locator extends Screen {
243
- /** Taps exactly one matching actionable node; with `position`, taps that point of its box once it is in view, with no other actionability check. */
244
- tap(options?: TapOptions): Promise<void>;
245
- /** Alias of tap. */
246
- click(options?: TapOptions): Promise<void>;
247
- /** Double-taps exactly one matching actionable node. */
248
- doubleTap(options?: ClickOptions): Promise<void>;
249
- /** Alias of doubleTap. */
250
- dblclick(options?: ClickOptions): Promise<void>;
251
- /** Performs a secondary tap, such as a right click or two-finger tap. */
252
- secondaryTap(options?: ClickOptions): Promise<void>;
253
- /** Long-presses exactly one matching actionable node. */
254
- longPress(options?: LongPressOptions): Promise<void>;
255
- /** Fills exactly one input. Secret values are never logged. */
256
- fill(value: string | Secret, options?: ActionOptions): Promise<void>;
257
- /**
258
- * Focuses exactly one input and types `text` through the keyboard, one
259
- * character at a time, so the app receives key events. A `Secret` is
260
- * refused; it goes through `fill`.
261
- */
262
- pressSequentially(text: string, options?: PressSequentiallyOptions): Promise<void>;
263
- /** Clears exactly one input. */
264
- clear(options?: ActionOptions): Promise<void>;
265
- /** Sends one key to exactly one node. */
266
- press(key: string, options?: ActionOptions): Promise<void>;
267
- /** Checks exactly one node. */
268
- check(options?: ActionOptions): Promise<void>;
269
- /** Unchecks exactly one node. */
270
- uncheck(options?: ActionOptions): Promise<void>;
271
- /** Selects one option from exactly one control. */
272
- selectOption(value: SelectOption, options?: ActionOptions): Promise<void>;
273
- /** Focuses exactly one node. */
274
- focus(options?: ActionOptions): Promise<void>;
275
- /** Hovers over exactly one matching actionable node. */
276
- hover(options?: ActionOptions): Promise<void>;
277
- /** Sets the files of exactly one file input; paths resolve from the project root. */
278
- setInputFiles(paths: string | readonly string[], options?: ActionOptions): Promise<void>;
279
- /** Drags exactly one node to exactly one target. */
280
- dragTo(target: Locator, options?: ActionOptions): Promise<void>;
281
- /** Scrolls exactly one node into view. */
282
- scrollIntoView(options?: ActionOptions): Promise<void>;
283
- /** Performs a swipe scoped to exactly one node. */
284
- swipe(options: SwipeOptions & ActionOptions): Promise<void>;
285
- /** Reads normalized text once a match exists, without retrying for a particular value. */
286
- textContent(): Promise<string | null>;
287
- /** Reads the current input value. */
288
- inputValue(): Promise<string>;
289
- /** Reads one attribute. */
290
- getAttribute(name: string): Promise<string | null>;
291
- /** Reads current visibility. */
292
- isVisible(): Promise<boolean>;
293
- /** Reads current hidden or absent state, the negation of `isVisible`. */
294
- isHidden(): Promise<boolean>;
295
- /** Reads current enabled state. */
296
- isEnabled(): Promise<boolean>;
297
- /** Reads current disabled state, the negation of `isEnabled`. */
298
- isDisabled(): Promise<boolean>;
299
- /** Reads current checked state. */
300
- isChecked(): Promise<boolean>;
301
- /** Reads the current viewport-relative rectangle. */
302
- boundingBox(): Promise<{ x: number; y: number; width: number; height: number } | null>;
303
- /** Counts current matches without auto-waiting. */
304
- count(): Promise<number>;
305
- /** One `nth(i)` locator per current match, without auto-waiting; empty when nothing matches. */
306
- all(): Promise<Locator[]>;
307
- /** Reads the normalized text of every current match, without auto-waiting; empty when nothing matches. */
308
- allTextContents(): Promise<string[]>;
309
- /**
310
- * Waits for the requested locator state, `visible` by default: `attached`
311
- * for one match, visible or not; `detached` for none; `hidden` for none or
312
- * a hidden one.
313
- */
314
- waitFor(options?: { state?: 'attached' | 'detached' | 'visible' | 'hidden'; timeout?: number }): Promise<void>;
315
- /** Adds deterministic locator filters. */
316
- filter(options: { hasText?: TextMatch; has?: Locator }): Locator;
317
- /** Selects the first current match. */
318
- first(): Locator;
319
- /** Selects the last current match. */
320
- last(): Locator;
321
- /** Selects one zero-based current match. */
322
- nth(index: number): Locator;
323
- }
520
+ The options are [`TapOptions`](#types):
521
+
522
+ - `modifiers`: the browser engine holds them.
523
+ - `position`: an offset from the node's top-left corner, in CSS pixels. The
524
+ engine scrolls the node into view when it lists `scrollIntoView`. Then it
525
+ reads the node's box and dispatches the pointer at the point. No node is
526
+ behind the gesture, so the platform's other actionability checks (enabled,
527
+ not covered) do not run.
528
+
529
+ | Throws | When |
530
+ | --- | --- |
531
+ | `INVALID_ARGUMENT` | An unknown or repeated modifier, or `modifiers` beside `position`. |
532
+ | `UNSUPPORTED_CAPABILITY` | `modifiers` on an engine that does not declare `tapModifiers`, the mobile engine among them; thrown before it acts. |
533
+ | `UNSUPPORTED_CAPABILITY` | `position` on an engine without the `tap` pointer action. |
534
+ | `ACTION_FAILED` | `position` on a node the browser engine cannot bring into view; its scroll waits for visibility. Other engines fail as they report it. |
535
+ | `LOCATOR_NOT_FOUND` | `position` on a node that resolves but never shows a visible box within the timeout. |
536
+
537
+ #### `doubleTap`
538
+
539
+ ```ts
540
+ /** Double-taps exactly one matching actionable node. */
541
+ doubleTap(options?: ClickOptions): Promise<void>;
542
+ /** Alias of doubleTap. */
543
+ dblclick(options?: ClickOptions): Promise<void>;
544
+ ```
545
+
546
+ ```ts
547
+ await screen.getByRole('cell', 'Q3 budget').doubleTap();
324
548
  ```
325
549
 
326
- - An action whose kind the engine does not list in `actions` throws
327
- `UNSUPPORTED_CAPABILITY` before reaching the engine.
328
- - An action takes only the options its signature names. Any other key,
329
- Playwright's `trial`, `force`, or `noWaitAfter` included, throws
330
- `INVALID_ARGUMENT` before the node is resolved, so the control is never
331
- touched.
332
- - `click` is an alias of `tap`, and `dblclick` of `doubleTap`.
333
- - `tap`, `doubleTap`, `dblclick`, and `secondaryTap` take `modifiers`, the keys held for
334
- the click, as a Shift-click extends a selection:
335
- `row.click({ modifiers: ['Shift'] })`. `ControlOrMeta` is Control on Windows
336
- and Linux, Meta on macOS. An unknown or repeated modifier is
337
- `INVALID_ARGUMENT`, and so is `modifiers` beside `position`. The browser
338
- engine holds them; an engine that does not declare `tapModifiers`, the
339
- mobile engine among them, fails with `UNSUPPORTED_CAPABILITY` before it
340
- acts.
341
- - `tap({ position })` taps at an offset of the node's top-left corner, in
342
- CSS pixels. The node is scrolled into view when the engine lists
343
- `scrollIntoView`, then its box is read and the pointer dispatched at the
344
- point, with no node behind the gesture, so the platform's other
345
- actionability checks (enabled, not covered) do not run. A node the engine
346
- cannot bring into view fails as the engine reports it (`ACTION_FAILED` on
347
- the browser engine, whose scroll waits for visibility); one that resolves
348
- but never shows a visible box within the timeout is `LOCATOR_NOT_FOUND`.
349
- An engine without the `tap` pointer action is `UNSUPPORTED_CAPABILITY`.
350
- - `press` takes zero or more modifiers (`Shift`, `Control`, `Alt`, `Meta`,
351
- `ControlOrMeta`) and one key, joined with `+`: a named key (`Enter`,
352
- `Escape`, `Tab`, `Backspace`, `Delete`, `Insert`, `Space`, the arrows,
353
- `Home`, `End`, `PageUp`, `PageDown`, `F1` to `F12`) or one printable
354
- character. `Shift++` presses `+`. A key the platform lacks throws
355
- `UNSUPPORTED_CAPABILITY`. See the [key grammar](/reference/engine#keys).
356
- - `longPress` holds for `{ duration }` milliseconds, 100 through 10000, else
357
- for the engine's default: 500 ms on the browser engine, one second on the
358
- mobile engine, past a React Native `Pressable`'s `delayLongPress`.
359
- - `fill` replaces the current content. A `Secret` is resolved on the host
360
- right before the call and never logged, reported, or sent to a model.
361
- - `pressSequentially` focuses the input and sends the text as keystrokes
362
- through the engine's keyboard, appending to the current content. `fill`
363
- sets the value directly and fires no key events, so an autocomplete, a
364
- search-as-you-type box, or a masked input wired to `keydown` or `keyup`
365
- never reacts to it; this is the action for those. `{ delay }` waits that
366
- many milliseconds between characters, each sent in its own keyboard call;
367
- without it the whole text goes in one call. Focus, pauses, and keystrokes
368
- share the one action deadline: a pause is cut short at it and no character
369
- is sent past it, and the action fails with `ACTION_FAILED` naming how many
370
- characters landed. It needs the engine's `focus` action and `keyboard`
371
- capability, else `UNSUPPORTED_CAPABILITY` before any node resolves, and it
372
- relies on the [keyboard contract](/reference/engine#engine): `type`
373
- must deliver text as a person typing would, so key events reach the app
374
- where the platform has them. A `Secret` is a type error and
375
- `INVALID_ARGUMENT` at runtime: a secret is filled, never typed as
376
- keystrokes. The step records the locator, not the text, like `fill`.
377
- - `setInputFiles` paths resolve from the project root. An empty list or blank
378
- entry throws `INVALID_ARGUMENT`.
379
- - `dragTo` with a non-runner target, or one made by another target's screen,
380
- throws `INVALID_LOCATOR`.
381
- - `swipe` on a locator is a gesture on that one node: `{ direction,
382
- momentum? }` as [`screen.swipe`](#screen-swipe) defines them, plus
383
- `timeout`. It has no `{ from, to }` form. `screen.swipe` takes no
384
- `timeout`: a `timeout` key there is `INVALID_ARGUMENT`.
385
- - An action that may already have committed surfaces as `ACTION_FAILED` and
386
- is never repeated.
550
+ Takes `timeout` and `modifiers`, with the same rules and errors as
551
+ [`tap`](#tap).
552
+
553
+ #### `secondaryTap`
554
+
555
+ ```ts
556
+ /** Secondary-taps exactly one matching actionable node: a right click, a two-finger tap. */
557
+ secondaryTap(options?: ClickOptions): Promise<void>;
558
+ ```
559
+
560
+ ```ts
561
+ await screen.getByRole('row', /Invoice 42/).secondaryTap();
562
+ ```
563
+
564
+ Takes `timeout` and `modifiers`, with the same rules and errors as
565
+ [`tap`](#tap).
566
+
567
+ #### `longPress`
568
+
569
+ ```ts
570
+ /** Long-presses exactly one matching actionable node. */
571
+ longPress(options?: LongPressOptions): Promise<void>;
572
+ ```
573
+
574
+ ```ts
575
+ await screen.getByText('Design review').longPress({ duration: 800 });
576
+ ```
577
+
578
+ - `duration` is the hold time, an integer from 100 through 10000 ms.
579
+ Anything else is `INVALID_ARGUMENT`.
580
+ - Without it the engine's default applies: 500 ms on the browser engine, one
581
+ second on the mobile engine, past a React Native `Pressable`'s
582
+ `delayLongPress`.
583
+
584
+ #### `fill`
585
+
586
+ Replaces the input's current content.
587
+
588
+ ```ts
589
+ /** Fills exactly one input. Secret values are never logged. */
590
+ fill(value: string | Secret, options?: ActionOptions): Promise<void>;
591
+ ```
387
592
 
388
593
  ```ts
389
594
  await screen.getByLabel('Email').fill('ada@example.test');
595
+ await screen.getByLabel('Password').fill(credentials.user('member').password);
596
+ ```
597
+
598
+ A `Secret` is resolved on the host right before the call and never logged,
599
+ reported, or sent to a model.
600
+
601
+ <Note>
602
+ `fill` sets the value directly and fires no key events. An autocomplete, a
603
+ search-as-you-type box, or a masked input wired to `keydown` or `keyup`
604
+ never reacts to it. Use [`pressSequentially`](#presssequentially) for those.
605
+ </Note>
606
+
607
+ #### `pressSequentially`
608
+
609
+ Types through the engine's keyboard and appends to the current content.
610
+
611
+ ```ts
612
+ /**
613
+ * Focuses exactly one input and types `text` through the keyboard, one
614
+ * character at a time, so the app receives key events. A `Secret` is
615
+ * refused; it goes through `fill`.
616
+ */
617
+ pressSequentially(text: string, options?: PressSequentiallyOptions): Promise<void>;
618
+ ```
619
+
620
+ ```ts
621
+ await screen.getByLabel('City').pressSequentially('War', { delay: 50 });
622
+ ```
623
+
624
+ - `delay` waits that many milliseconds between characters, each sent in its
625
+ own keyboard call. Without it the whole text goes in one call.
626
+ - Focus, pauses, and keystrokes share the one action deadline: a pause is cut
627
+ short at it and no character is sent past it.
628
+ - It relies on the [keyboard contract](/reference/engine#engine): `type` must
629
+ deliver text as a person typing would, so key events reach the app where
630
+ the platform has them.
631
+ - The step records the locator, not the text, like `fill`.
632
+
633
+ | Throws | When |
634
+ | --- | --- |
635
+ | `ACTION_FAILED` | The deadline passed mid-text; the error names how many characters landed. |
636
+ | `UNSUPPORTED_CAPABILITY` | The engine lacks the `focus` action or the `keyboard` capability; thrown before any node resolves. |
637
+ | `INVALID_ARGUMENT` | A `Secret` at runtime (a type error at compile time): a secret is filled, never typed as keystrokes. |
638
+
639
+ #### `clear`
640
+
641
+ ```ts
642
+ /** Clears exactly one input. */
643
+ clear(options?: ActionOptions): Promise<void>;
644
+ ```
645
+
646
+ ```ts
647
+ await screen.getByLabel('Search').clear();
648
+ ```
649
+
650
+ #### `press`
651
+
652
+ ```ts
653
+ /** Sends one key to exactly one node. */
654
+ press(key: string, options?: ActionOptions): Promise<void>;
655
+ ```
656
+
657
+ ```ts
658
+ await screen.getByRole('textbox', 'Search').press('Enter');
659
+ await screen.getByRole('textbox', 'Notes').press('ControlOrMeta+a');
660
+ ```
661
+
662
+ A key is zero or more modifiers and one key, joined with `+`, as the
663
+ [key grammar](/reference/engine#keys) lists them. `Shift++` presses `+`.
664
+
665
+ | Throws | When |
666
+ | --- | --- |
667
+ | `UNSUPPORTED_CAPABILITY` | A key the platform lacks. |
668
+
669
+ #### `check`
670
+
671
+ ```ts
672
+ /** Checks exactly one node. */
673
+ check(options?: ActionOptions): Promise<void>;
674
+ ```
675
+
676
+ ```ts
390
677
  await screen.getByRole('checkbox', 'Remember me').check();
678
+ ```
679
+
680
+ #### `uncheck`
681
+
682
+ ```ts
683
+ /** Unchecks exactly one node. */
684
+ uncheck(options?: ActionOptions): Promise<void>;
685
+ ```
686
+
687
+ ```ts
688
+ await screen.getByRole('checkbox', 'Remember me').uncheck();
689
+ ```
690
+
691
+ #### `selectOption`
692
+
693
+ ```ts
694
+ /** Selects one option from exactly one control. */
695
+ selectOption(value: SelectOption, options?: ActionOptions): Promise<void>;
696
+ ```
697
+
698
+ ```ts
699
+ await screen.getByLabel('Plan').selectOption('Pro');
391
700
  await screen.getByLabel('Plan').selectOption({ label: 'Pro' });
392
701
  await screen.getByLabel('Plan').selectOption({ value: 'pro' });
393
- await screen.getByRole('button', 'Save').tap({ timeout: 10_000 });
394
- await screen.getByRole('image', 'Seat map').tap({ position: { x: 120, y: 48 } });
395
- await screen.getByRole('textbox', 'Search').press('Enter');
396
- await screen.getByRole('textbox', 'Notes').press('ControlOrMeta+a');
397
- await screen.getByLabel('City').pressSequentially('War', { delay: 50 });
398
702
  ```
399
703
 
400
- ## Locator reads
704
+ Pass a label string, or an object with exactly one of `label`, `value` (the
705
+ value attribute, which the form submits), or `index` (zero-based).
401
706
 
402
- `waitFor` defaults `state` to `'visible'` and `timeout` to
403
- `config.actionTimeout`, and throws `LOCATOR_NOT_FOUND` on timeout. `'attached'`
404
- waits for one match, hidden or shown, `'detached'` for none, and `'hidden'` for
405
- a hidden match or none. Several matches are `LOCATOR_AMBIGUOUS` in every
406
- state. Any other state, or an option key besides `state` and `timeout`, is
407
- `INVALID_ARGUMENT` before anything is resolved.
707
+ | Throws | When |
708
+ | --- | --- |
709
+ | `INVALID_ARGUMENT` | More than one option per call: an array, or an object with more than one of those keys. Also a negative or fractional `index`. Thrown before the engine acts. |
408
710
 
409
- `textContent` and `allTextContents` return the node's rendered text with
410
- whitespace collapsed to single spaces. On the web that is what `innerText`
411
- reads, not the DOM's `textContent`: CSS `text-transform` applies,
412
- `display: none` descendants are left out, and a `<br>` reads as a space. A
413
- node that is not rendered itself, such as a `display: none` one, reads its
414
- DOM text, as `innerText` does.
711
+ #### `focus`
712
+
713
+ ```ts
714
+ /** Focuses exactly one node. */
715
+ focus(options?: ActionOptions): Promise<void>;
716
+ ```
717
+
718
+ ```ts
719
+ await screen.getByLabel('Email').focus();
720
+ ```
415
721
 
416
- `isChecked` is `false` on a node with no checked state, such as a button or
417
- a heading; it does not throw.
722
+ #### `hover`
418
723
 
419
- `textContent`, `inputValue`, `getAttribute`, and `allTextContents` throw
420
- `POLICY_DENIED` on a secure field, and so do `expect(locator).toHaveValue`,
421
- `toHaveText`, `toContainText`, and `toHaveAttribute`.
422
- `isEnabled`, `isDisabled`, `isChecked`, and `boundingBox` are allowed there.
724
+ ```ts
725
+ /** Hovers over exactly one matching actionable node. */
726
+ hover(options?: ActionOptions): Promise<void>;
727
+ ```
728
+
729
+ ```ts
730
+ await screen.getByRole('button', 'Account').hover();
731
+ ```
732
+
733
+ #### `setInputFiles`
734
+
735
+ ```ts
736
+ /** Sets the files of exactly one file input; paths resolve from the project root. */
737
+ setInputFiles(paths: string | readonly string[], options?: ActionOptions): Promise<void>;
738
+ ```
739
+
740
+ ```ts
741
+ await screen.getByLabel('Receipt').setInputFiles('fixtures/receipt.pdf');
742
+ ```
423
743
 
424
- `boundingBox()` is `null` for a node that lays out no box (`display: none`
425
- on it or an ancestor), and a box for one `visibility: hidden` keeps.
426
- `inputValue()` and `toHaveValue` on a checkbox or radio read its value
427
- attribute, `on` when it has none, whatever its checked state; `isChecked()`
744
+ | Throws | When |
745
+ | --- | --- |
746
+ | `INVALID_ARGUMENT` | An empty list or a blank entry. |
747
+
748
+ #### `dragTo`
749
+
750
+ ```ts
751
+ /** Drags exactly one node to exactly one target. */
752
+ dragTo(target: Locator, options?: ActionOptions): Promise<void>;
753
+ ```
754
+
755
+ ```ts
756
+ await screen.getByText('Design review').dragTo(screen.getByRole('region', 'Done'));
757
+ ```
758
+
759
+ | Throws | When |
760
+ | --- | --- |
761
+ | `INVALID_LOCATOR` | `target` is not a runner locator, or was made by another target's screen. |
762
+
763
+ #### `scrollIntoView`
764
+
765
+ ```ts
766
+ /** Scrolls exactly one node into view. */
767
+ scrollIntoView(options?: ActionOptions): Promise<void>;
768
+ ```
769
+
770
+ ```ts
771
+ await screen.getByRole('contentinfo').scrollIntoView();
772
+ ```
773
+
774
+ #### `swipe`
775
+
776
+ ```ts
777
+ /** Performs a swipe scoped to exactly one node. */
778
+ swipe(options: SwipeOptions & ActionOptions): Promise<void>;
779
+ ```
780
+
781
+ ```ts
782
+ await screen.getByTestId('feed').swipe({ direction: 'down', momentum: 'slow' });
783
+ ```
784
+
785
+ It takes `{ direction, momentum? }` as [`screen.swipe`](#screen-swipe)
786
+ defines them, plus `timeout`, which `screen.swipe` refuses. It has no
787
+ `{ from, to }` form.
788
+
789
+ ### Locator reads
790
+
791
+ | Method | Returns |
792
+ | --- | --- |
793
+ | [`textContent`](#textcontent) | `Promise<string \| null>` |
794
+ | [`inputValue`](#inputvalue) | `Promise<string>` |
795
+ | [`getAttribute`](#getattribute) | `Promise<string \| null>` |
796
+ | [`isVisible`](#isvisible) | `Promise<boolean>` |
797
+ | [`isHidden`](#ishidden) | `Promise<boolean>` |
798
+ | [`isEnabled`](#isenabled) | `Promise<boolean>` |
799
+ | [`isDisabled`](#isdisabled) | `Promise<boolean>` |
800
+ | [`isChecked`](#ischecked) | `Promise<boolean>` |
801
+ | [`boundingBox`](#boundingbox) | `Promise<{ x, y, width, height } \| null>` |
802
+ | [`count`](#count) | `Promise<number>` |
803
+ | [`all`](#all) | `Promise<Locator[]>` |
804
+ | [`allTextContents`](#alltextcontents) | `Promise<string[]>` |
805
+ | [`waitFor`](#waitfor) | `Promise<void>` |
806
+
807
+ Reads follow the [Match rules](#match-rules). `count`, `all`, and
808
+ `allTextContents` take a snapshot and do not wait.
809
+
810
+ On a secure field, `textContent`, `inputValue`, `getAttribute`, and
811
+ `allTextContents` throw `POLICY_DENIED`, as the matching
812
+ [`expect` matchers](/reference/expect#secure-fields) do. `isEnabled`,
813
+ `isDisabled`, `isChecked`, and `boundingBox` are allowed there.
814
+
815
+ #### `textContent`
816
+
817
+ Reads the node's rendered text with whitespace collapsed to single spaces.
818
+
819
+ ```ts
820
+ /** Reads normalized text once a match exists, without retrying for a particular value. */
821
+ textContent(): Promise<string | null>;
822
+ ```
823
+
824
+ ```ts
825
+ const message = await screen.getByRole('alert').textContent();
826
+ ```
827
+
828
+ - On the web that is what `innerText` reads, not the DOM's `textContent`:
829
+ CSS `text-transform` applies, `display: none` descendants are left out, and
830
+ a `<br>` reads as a space.
831
+ - A node that is not rendered itself, such as a `display: none` one, reads
832
+ its DOM text, as `innerText` does.
833
+
834
+ #### `inputValue`
835
+
836
+ ```ts
837
+ /** Reads the current input value. */
838
+ inputValue(): Promise<string>;
839
+ ```
840
+
841
+ ```ts
842
+ const email = await screen.getByLabel('Email').inputValue();
843
+ ```
844
+
845
+ On a checkbox or radio, `inputValue()` and `toHaveValue` read its value
846
+ attribute, `on` when it has none, whatever its checked state. `isChecked()`
428
847
  reads the state.
429
848
 
430
- `isHidden` is `!isVisible()` and `isDisabled` is `!isEnabled()`, with the
431
- same strictness: `isHidden` is `true` for zero matches, `isDisabled` is
432
- `LOCATOR_NOT_FOUND` there, and both are `LOCATOR_AMBIGUOUS` on several.
849
+ #### `getAttribute`
433
850
 
434
851
  ```ts
435
- if (await screen.getByRole('alert').isVisible()) {
436
- const message = await screen.getByRole('alert').textContent();
437
- }
438
- await screen.getByTestId('spinner').waitFor({ state: 'hidden' });
852
+ /** Reads one attribute. */
853
+ getAttribute(name: string): Promise<string | null>;
854
+ ```
855
+
856
+ ```ts
857
+ const href = await screen.getByRole('link', 'Docs').getAttribute('href');
858
+ ```
859
+
860
+ #### `isVisible`
861
+
862
+ ```ts
863
+ /** Reads current visibility. */
864
+ isVisible(): Promise<boolean>;
865
+ ```
866
+
867
+ ```ts
868
+ const open = await screen.getByRole('dialog').isVisible();
869
+ ```
870
+
871
+ - `false` for zero matches, and for a `frameLocator` whose frame is not in
872
+ the document.
873
+ - Uses the same visibility check as `visible: true` (see
874
+ [Hidden duplicates](#hidden-duplicates)).
875
+
876
+ | Throws | When |
877
+ | --- | --- |
878
+ | `LOCATOR_AMBIGUOUS` | Several matches. |
879
+
880
+ #### `isHidden`
881
+
882
+ ```ts
883
+ /** Reads current hidden or absent state, the negation of `isVisible`. */
884
+ isHidden(): Promise<boolean>;
885
+ ```
886
+
887
+ ```ts
888
+ const gone = await screen.getByTestId('spinner').isHidden();
889
+ ```
890
+
891
+ It has the same strictness as `isVisible`: `true` for zero matches.
892
+
893
+ | Throws | When |
894
+ | --- | --- |
895
+ | `LOCATOR_AMBIGUOUS` | Several matches. |
896
+
897
+ #### `isEnabled`
898
+
899
+ ```ts
900
+ /** Reads current enabled state. */
901
+ isEnabled(): Promise<boolean>;
902
+ ```
903
+
904
+ ```ts
905
+ const ready = await screen.getByRole('button', 'Pay').isEnabled();
906
+ ```
907
+
908
+ #### `isDisabled`
909
+
910
+ ```ts
911
+ /** Reads current disabled state, the negation of `isEnabled`. */
912
+ isDisabled(): Promise<boolean>;
439
913
  ```
440
914
 
441
- ### Every match at once
915
+ ```ts
916
+ const blocked = await screen.getByRole('button', 'Pay').isDisabled();
917
+ ```
918
+
919
+ It has the same strictness as `isEnabled`.
920
+
921
+ | Throws | When |
922
+ | --- | --- |
923
+ | `LOCATOR_NOT_FOUND` | Zero matches. |
924
+ | `LOCATOR_AMBIGUOUS` | Several matches. |
442
925
 
443
- `all()` resolves the query once and hands back one `nth(i)` locator per
444
- match, so a loop can act on each row; `allTextContents()` reads every match's
445
- normalized text in one call. Neither waits or enforces exactly-one: zero
446
- matches is an empty array, the same snapshot rule as `count()`. A
447
- `frameLocator` whose frame is not in the document counts as zero matches here
448
- and in `isVisible()`, where an action or assertion polls for it. Each locator
449
- `all()` returns re-resolves when used, so a list that changes under the loop
450
- can make a later `nth(i)` point at a different row, or at nothing. Follow the
451
- loop with an assertion on the list, and use
452
- [`toHaveText([...])`](/reference/expect#locator-matchers) instead of a loop
453
- when the point is to check the texts.
926
+ #### `isChecked`
927
+
928
+ ```ts
929
+ /** Reads current checked state. */
930
+ isChecked(): Promise<boolean>;
931
+ ```
932
+
933
+ ```ts
934
+ const remembered = await screen.getByRole('checkbox', 'Remember me').isChecked();
935
+ ```
936
+
937
+ `false` on a node with no checked state, such as a button or a heading. It
938
+ does not throw.
939
+
940
+ #### `boundingBox`
941
+
942
+ ```ts
943
+ /** Reads the current viewport-relative rectangle. */
944
+ boundingBox(): Promise<{ x: number; y: number; width: number; height: number } | null>;
945
+ ```
946
+
947
+ ```ts
948
+ const map = (await screen.getByRole('image', 'Map').boundingBox())!;
949
+ ```
950
+
951
+ The box is in CSS pixels. It is `null` for a node that lays out no box
952
+ (`display: none` on it or an ancestor). A `visibility: hidden` node keeps
953
+ its box.
954
+
955
+ #### `count`
956
+
957
+ ```ts
958
+ /** Counts current matches without auto-waiting. */
959
+ count(): Promise<number>;
960
+ ```
961
+
962
+ ```ts
963
+ const rows = await screen.getByRole('row').count();
964
+ ```
965
+
966
+ `0` for zero matches, and for a `frameLocator` whose frame is not in the
967
+ document.
968
+
969
+ #### `all`
970
+
971
+ Resolves the query one time, so a loop can act on each row.
972
+
973
+ ```ts
974
+ /** One `nth(i)` locator per current match, without auto-waiting; empty when nothing matches. */
975
+ all(): Promise<Locator[]>;
976
+ ```
454
977
 
455
978
  ```ts
456
979
  for (const row of await screen.getByRole('row').all()) {
457
980
  await row.getByRole('checkbox').check();
458
981
  }
982
+ ```
983
+
984
+ - It neither waits nor enforces exactly-one: zero matches is an empty array,
985
+ the same snapshot rule as `count()`. A `frameLocator` whose frame is not in
986
+ the document counts as zero matches here, where an action or assertion
987
+ polls for it.
988
+ - Each locator that `all()` returns re-resolves when used. A list that changes
989
+ under the loop can make a later `nth(i)` point at a different row, or at
990
+ nothing. Follow the loop with an assertion on the list.
991
+
992
+ #### `allTextContents`
993
+
994
+ ```ts
995
+ /** Reads the normalized text of every current match, without auto-waiting; empty when nothing matches. */
996
+ allTextContents(): Promise<string[]>;
997
+ ```
998
+
999
+ ```ts
459
1000
  expect(await screen.getByRole('listitem').allTextContents()).toEqual(['Alpha', 'Beta']);
460
1001
  ```
461
1002
 
1003
+ - Text reads as in [`textContent`](#textcontent).
1004
+ - It neither waits nor enforces exactly-one: zero matches is an empty array,
1005
+ as for [`all`](#all).
1006
+ - To check the texts, use
1007
+ [`toHaveText([...])`](/reference/expect#locator-matchers) instead of a
1008
+ loop.
1009
+
1010
+ #### `waitFor`
1011
+
1012
+ ```ts
1013
+ /**
1014
+ * Waits for the requested locator state, `visible` by default: `attached`
1015
+ * for one match, visible or not; `detached` for none; `hidden` for none or
1016
+ * a hidden one.
1017
+ */
1018
+ waitFor(options?: { state?: 'attached' | 'detached' | 'visible' | 'hidden'; timeout?: number }): Promise<void>;
1019
+ ```
1020
+
1021
+ ```ts
1022
+ await screen.getByTestId('spinner').waitFor({ state: 'hidden' }); // default state: 'visible'
1023
+ ```
1024
+
1025
+ | State | Waits for |
1026
+ | --- | --- |
1027
+ | `'visible'` (default) | One visible match. |
1028
+ | `'attached'` | One match, hidden or shown. |
1029
+ | `'detached'` | No match. |
1030
+ | `'hidden'` | A hidden match, or none. |
1031
+
1032
+ `timeout` defaults to `config.actionTimeout`.
1033
+
1034
+ | Throws | When |
1035
+ | --- | --- |
1036
+ | `LOCATOR_NOT_FOUND` | The timeout expires. |
1037
+ | `LOCATOR_AMBIGUOUS` | Several matches, in every state. |
1038
+ | `INVALID_ARGUMENT` | Any other state, or an option key besides `state` and `timeout`; thrown before anything is resolved. |
1039
+
1040
+ ## Match rules
1041
+
1042
+ Most operations require exactly one match. A read polls for its one match
1043
+ until the action timeout, as an action does. Then it returns the current
1044
+ value without waiting for it to change. Use [`expect`](/reference/expect) to
1045
+ wait for a value.
1046
+
1047
+ A node the engine reports stale is re-resolved within the same timeout:
1048
+ a resolve that raced a navigation, or a match the app replaced while it was
1049
+ read.
1050
+
1051
+ | Operation | Zero matches | Multiple matches |
1052
+ | --- | --- | --- |
1053
+ | Actions | Poll until the timeout, then `LOCATOR_NOT_FOUND` | `LOCATOR_AMBIGUOUS` |
1054
+ | Reads | Poll until the timeout, then `LOCATOR_NOT_FOUND` | `LOCATOR_AMBIGUOUS` |
1055
+ | `count()`, `all()`, `allTextContents()` | `0`, `[]`, `[]` | The count, one locator per match, one text per match |
1056
+ | `isVisible()`, `isHidden()` | `false`, `true` | `LOCATOR_AMBIGUOUS` |
1057
+ | Assertions | Poll until the timeout, then `ASSERTION_FAILED` | `LOCATOR_AMBIGUOUS` |
1058
+ | Any of these under a frame that is not in the document | As zero matches for `count()`, `all()`, `allTextContents()`, `isVisible()`, and `isHidden()`; actions and reads poll for the frame until the timeout, then `LOCATOR_NOT_FOUND`; assertions, then `ASSERTION_FAILED` | `LOCATOR_AMBIGUOUS` when several frames match |
1059
+
462
1060
  ## Types
463
1061
 
464
1062
  ```ts
@@ -601,7 +1199,7 @@ export interface SwipePathOptions {
601
1199
  from: Point;
602
1200
  /** Where it lifts. */
603
1201
  to: Point;
604
- /** Gesture duration in milliseconds; omitted = the engine's default. */
1202
+ /** Gesture duration, an integer from 16 through 10000 ms; omitted = the engine's default. */
605
1203
  duration?: number;
606
1204
  direction?: never;
607
1205
  momentum?: never;
@@ -654,72 +1252,7 @@ export type ScrollDirection = 'up' | 'down' | 'left' | 'right';
654
1252
  export type Momentum = 'none' | 'slow' | 'fast';
655
1253
  ```
656
1254
 
657
- A role query never matches a node hidden from the accessibility tree: one
658
- that does not render, or one under `aria-hidden`. `visible` narrows the
659
- other query kinds to the nodes that render, the check `toBeVisible()` makes,
660
- so an `aria-hidden` node that still paints counts as visible there. See
661
- [Hidden duplicates](#hidden-duplicates).
662
-
663
- `img` is an alias, not a role: `getByRole('img', { name })` builds the same
664
- query as `getByRole('image', { name })`, so a test ported from Playwright keeps
665
- its spelling while engines, the replay cache, and reports see `image`. On the
666
- web, `image` covers `<img>` with alt text and any element with `role="img"`;
667
- an `<img alt="">` is decoration and matches nothing.
668
-
669
- The web engine derives the roles from HTML the way HTML-AAM does: `<progress>`
670
- is `progressbar`, `<meter>` is `meter`, `<hr>` is `separator`, `<fieldset>`
671
- and `<details>` are `group`, `<input type="number">` is `spinbutton`,
672
- `<input type="file">` is `button`, as Chrome and Playwright read it,
673
- `<select size="3">` is a `listbox` like a `multiple` one, an `<input list>`
674
- naming a `<datalist>` is a `combobox`, a `<td>` of a `role="grid"` table is a
675
- `gridcell`,
676
- `<thead>` and `<tbody>` are `rowgroup`, `<th scope="row">` is `rowheader`.
677
- A scope-less `<th>` beside a data cell with text or children is also a
678
- `rowheader`; header-only rows keep `columnheader`. A lone scope-less header in a one-row
679
- table has no header role, while `scope="col"` and `scope="colgroup"` explicitly
680
- keep `columnheader`.
681
- `<figure>` is `figure`, `<article>` is `article`, `<header>` and `<footer>` are
682
- `banner` and `contentinfo` only outside sectioning content or an element with
683
- a sectioning role (`article`, `complementary`, `main`, `navigation`, `region`),
684
- and `<form>` and `<section>`
685
- are `form` and `region` only when they carry an `aria-label`,
686
- `aria-labelledby`, or `title`. A `<legend>`, `<figcaption>`, or `<caption>`
687
- names its fieldset, figure, or table, so `toHaveAccessibleName` reads what
688
- `getByRole('group', { name })` matched. A role accname names from content (a
689
- button, link, heading, tab, menu item, option, checkbox, radio, switch, cell,
690
- row or column header, row, tree item, tooltip) is named by its descendants, a
691
- custom element's shadow tree included, so `getByRole('checkbox', { name })`
692
- and `toHaveAccessibleName(name)` agree on a `<div role="checkbox">`. An
693
- `<input type="submit">` or `type="reset"` with no `value` reads `Submit` or
694
- `Reset`. `<menu>` is a `list`; only `role="menu"` is a
695
- `menu`. A device engine maps the platform types it has (a tab bar or segmented
696
- control is a `tablist`, a progress indicator a `progressbar`, a stepper a
697
- `spinbutton`) and keeps any other type verbatim, so a role with no native
698
- widget, such as `tooltip` or `figure`, matches nothing on a phone.
1255
+ ## Related
699
1256
 
700
- A `contenteditable` editing host, the root a TipTap, ProseMirror, Lexical, or
701
- Slate editor renders into, is a `textbox` whose value is its text as rendered
702
- (and, while it has focus, whose `selection` is the text selected inside it).
703
- The value keeps spaces and newlines, so `toHaveValue` and `inputValue()` read
704
- a `white-space: pre-wrap` editor like a textarea. An empty editor, holding only
705
- the `<p><br></p>` or the zero-width no-break space its editor pads an empty
706
- line with, reads as `''`, and that padding is never part of a value. It is named by its
707
- `aria-label`, `aria-placeholder`, or the `data-placeholder` its editor paints;
708
- the blocks inside it are content, not textboxes. The tag does not matter (an
709
- `<article contenteditable>` is the editor, not an article), while a native
710
- control or a drawn surface made editable keeps its own role. `getByRole` resolves through
711
- Playwright's role selector, which has no such rule, so a test addresses an
712
- editor by role only when the host carries `role="textbox"`; `getByLabel` and
713
- `getByTestId` reach it either way. A name taken from a block's
714
- `data-placeholder` (TipTap, ProseMirror) goes once the editor has content, as
715
- the painted hint does, so the host reads as an unnamed `textbox` from then on;
716
- a name from the host's own `aria-label`, `aria-labelledby`, `aria-placeholder`,
717
- or `data-placeholder` (Quill) stays. Anchor a test on a label or a test id, not
718
- on the placeholder.
719
-
720
-
721
- For `selectOption`, pass a label string or exactly one of `label`, `value`,
722
- or `index`. `value` matches the option's value attribute, which is what the
723
- form submits. One option per call: an array, an object with more than one of
724
- those keys, or a negative or fractional `index` fails with `INVALID_ARGUMENT`
725
- before the engine acts.
1257
+ [`expect`](/reference/expect) waits for a locator's state. The
1258
+ [`browser`](/reference/web) fixture adds CSS locators and frames.