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.
- package/README.md +51 -24
- package/dist/.build.json +1 -1
- package/dist/agent/act.js +3 -3
- package/dist/agent/act.js.map +1 -1
- package/dist/agent/error.d.ts +16 -3
- package/dist/agent/error.d.ts.map +1 -1
- package/dist/agent/error.js +25 -6
- package/dist/agent/error.js.map +1 -1
- package/dist/agent/model/tool-choice.d.ts +24 -1
- package/dist/agent/model/tool-choice.d.ts.map +1 -1
- package/dist/agent/model/tool-choice.js +27 -1
- package/dist/agent/model/tool-choice.js.map +1 -1
- package/dist/agent/step-cache.d.ts +14 -3
- package/dist/agent/step-cache.d.ts.map +1 -1
- package/dist/agent/step-cache.js +13 -3
- package/dist/agent/step-cache.js.map +1 -1
- package/dist/agent/tool-loop.d.ts.map +1 -1
- package/dist/agent/tool-loop.js +62 -9
- package/dist/agent/tool-loop.js.map +1 -1
- package/dist/cache/context.d.ts +19 -6
- package/dist/cache/context.d.ts.map +1 -1
- package/dist/cache/context.js +15 -21
- package/dist/cache/context.js.map +1 -1
- package/dist/cli/init/scaffold.d.ts +1 -0
- package/dist/cli/init/scaffold.d.ts.map +1 -1
- package/dist/cli/init/scaffold.js +2 -1
- package/dist/cli/init/scaffold.js.map +1 -1
- package/dist/cli/init/sibling-versions.json +1 -1
- package/dist/expect/values.js +1 -1
- package/dist/expect/values.js.map +1 -1
- package/dist/run/execute.d.ts.map +1 -1
- package/dist/run/execute.js +21 -8
- package/dist/run/execute.js.map +1 -1
- package/dist/types.d.ts +1 -1
- package/dist/types.d.ts.map +1 -1
- package/docs/agent-steps.mdx +126 -0
- package/docs/agents.mdx +217 -0
- package/docs/api-testing.mdx +65 -0
- package/docs/authentication.mdx +345 -0
- package/docs/browser.mdx +440 -0
- package/docs/bug-bash.mdx +90 -0
- package/docs/cache.mdx +306 -0
- package/docs/ci.mdx +210 -0
- package/docs/coding-agents.mdx +183 -0
- package/docs/debugging.mdx +291 -0
- package/docs/executors.mdx +243 -0
- package/docs/explore.mdx +174 -0
- package/docs/github.mdx +184 -0
- package/docs/index.mdx +82 -0
- package/docs/integrations/eas.mdx +216 -0
- package/docs/integrations/index.mdx +24 -0
- package/docs/integrations/kernel.mdx +163 -0
- package/docs/migrate/cypress.mdx +124 -0
- package/docs/migrate/detox.mdx +140 -0
- package/docs/migrate/maestro.mdx +229 -0
- package/docs/migrate/playwright.mdx +427 -0
- package/docs/migrate/selenium.mdx +110 -0
- package/docs/mobile-ci.mdx +567 -0
- package/docs/mobile.mdx +555 -0
- package/docs/models.mdx +241 -0
- package/docs/quickstart.mdx +260 -0
- package/docs/reference/agent.mdx +524 -0
- package/docs/reference/app.mdx +72 -0
- package/docs/reference/cli.mdx +817 -0
- package/docs/reference/config.mdx +986 -0
- package/docs/reference/engine.mdx +1362 -0
- package/docs/reference/environment.mdx +113 -0
- package/docs/reference/errors.mdx +143 -0
- package/docs/reference/expect.mdx +513 -0
- package/docs/reference/mcp.mdx +202 -0
- package/docs/reference/mobile.mdx +522 -0
- package/docs/reference/reporters.mdx +216 -0
- package/docs/reference/screen.mdx +688 -0
- package/docs/reference/test.mdx +449 -0
- package/docs/reference/web.mdx +611 -0
- package/docs/security.mdx +292 -0
- package/docs/starting-your-app.mdx +231 -0
- package/docs/subscriptions.mdx +157 -0
- package/docs/telemetry.mdx +185 -0
- package/docs/tools.mdx +150 -0
- package/docs/viewports.mdx +146 -0
- package/docs/writing-an-engine.mdx +316 -0
- package/docs/writing-tests.mdx +256 -0
- package/package.json +2 -1
- package/skills/e2e/SKILL.md +4 -1
- package/skills/e2e/references/agent.md +3 -1
- package/skills/e2e/references/setup.md +13 -4
|
@@ -0,0 +1,688 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: screen and Locator
|
|
3
|
+
description: Queries, refinement, actions, and reads.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
A `Locator` stores a query and resolves it when an action, read, or assertion
|
|
7
|
+
runs. Every `Screen` query method is also available on a locator, scoped to
|
|
8
|
+
its subtree. `tapAt` is not scoped: on a locator it still taps a viewport
|
|
9
|
+
point.
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
export interface Screen {
|
|
13
|
+
/** Creates a lazy role query. An alias such as `img` is rewritten to its role. */
|
|
14
|
+
getByRole(role: Role | RoleAlias, options?: RoleOptions): Locator;
|
|
15
|
+
/** Creates a lazy role query narrowed to an accessible name: `getByRole('button', 'Sign in')`. */
|
|
16
|
+
getByRole(role: Role | RoleAlias, name: TextMatch, options?: Omit<RoleOptions, 'name'>): Locator;
|
|
17
|
+
/** Creates a lazy accessible-label query. */
|
|
18
|
+
getByLabel(text: TextMatch, options?: TextMatchOptions): Locator;
|
|
19
|
+
/** Creates a lazy placeholder query. */
|
|
20
|
+
getByPlaceholder(text: TextMatch, options?: TextMatchOptions): Locator;
|
|
21
|
+
/** Creates a lazy visible-text query. */
|
|
22
|
+
getByText(text: TextMatch, options?: TextMatchOptions): Locator;
|
|
23
|
+
/** Creates a lazy displayed-value query. */
|
|
24
|
+
getByDisplayValue(value: TextMatch, options?: TextMatchOptions): Locator;
|
|
25
|
+
/** Creates a lazy test-id query: a string matches the whole id, case-sensitive; a RegExp tests it. */
|
|
26
|
+
getByTestId(id: TextMatch, options?: { visible?: boolean }): Locator;
|
|
27
|
+
/** Taps a viewport point, with no node behind it. */
|
|
28
|
+
tapAt(point: Point, options?: ActionOptions): Promise<void>;
|
|
29
|
+
/** Performs a viewport-level swipe: in a direction, or along a path from one point to another. */
|
|
30
|
+
swipe(options: SwipeOptions | SwipePathOptions): Promise<void>;
|
|
31
|
+
/** Scrolls until a locator resolves visibly or times out. */
|
|
32
|
+
scrollUntilVisible(
|
|
33
|
+
target: Locator,
|
|
34
|
+
options?: { direction?: ScrollDirection; momentum?: Momentum; timeout?: number },
|
|
35
|
+
): Promise<void>;
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Most operations require exactly one match. Reads return the current value
|
|
40
|
+
without waiting for it to change: a frame that is not in the document answers
|
|
41
|
+
at once, and a node the engine reports stale (a resolve that raced a
|
|
42
|
+
navigation, or a match the app replaced while it was read) is re-resolved
|
|
43
|
+
within the action timeout, which repairs the race without waiting for a
|
|
44
|
+
match. Use [`expect`](/reference/expect) to wait.
|
|
45
|
+
|
|
46
|
+
| Operation | Zero matches | Multiple matches |
|
|
47
|
+
| --- | --- | --- |
|
|
48
|
+
| Actions | Poll until the timeout, then `LOCATOR_NOT_FOUND` | `LOCATOR_AMBIGUOUS` |
|
|
49
|
+
| Reads | `LOCATOR_NOT_FOUND` | `LOCATOR_AMBIGUOUS` |
|
|
50
|
+
| `count()`, `all()`, `allTextContents()` | `0`, `[]`, `[]` | The count, one locator per match, one text per match |
|
|
51
|
+
| `isVisible()`, `isHidden()` | `false`, `true` | `LOCATOR_AMBIGUOUS` |
|
|
52
|
+
| Assertions | Poll until the timeout | `LOCATOR_AMBIGUOUS` |
|
|
53
|
+
| 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 |
|
|
54
|
+
|
|
55
|
+
## Queries
|
|
56
|
+
|
|
57
|
+
Every query returns a `Locator` synchronously and performs no work. An
|
|
58
|
+
option key the query does not take (`{ nam: 'Save' }`) throws
|
|
59
|
+
`INVALID_LOCATOR` naming the keys it does.
|
|
60
|
+
|
|
61
|
+
`getByRole` takes the accessible name as its second argument, a string or a
|
|
62
|
+
`RegExp`, with the other options after it:
|
|
63
|
+
`getByRole('button', 'Save', { exact: false })`. The object form,
|
|
64
|
+
`getByRole('button', { name: 'Save', disabled: true })`, is the same query.
|
|
65
|
+
|
|
66
|
+
A string passed to `getByRole` as the name, `getByLabel`, `getByPlaceholder`,
|
|
67
|
+
`getByText`, or `getByDisplayValue` matches the whole text, case-sensitive,
|
|
68
|
+
after whitespace normalization: `getByRole('button', 'Save')` does
|
|
69
|
+
not match `Save changes`. `{ exact: false }` makes it a case-insensitive
|
|
70
|
+
substring match, and a `RegExp` matches as written.
|
|
71
|
+
|
|
72
|
+
`getByTestId` matches the node's `testId`: on the web the value of the
|
|
73
|
+
engine's test-id attribute (`web({ testIdAttribute })`, default
|
|
74
|
+
`data-testid`), on a device the accessibility identifier or resource id. A
|
|
75
|
+
string matches the whole id, case-sensitive; a `RegExp` such as
|
|
76
|
+
`/total-budgeted$/` matches as written.
|
|
77
|
+
|
|
78
|
+
```ts
|
|
79
|
+
screen.getByRole('button', 'Save');
|
|
80
|
+
screen.getByRole('heading', /welcome/i, { level: 1 });
|
|
81
|
+
screen.getByLabel('Email');
|
|
82
|
+
screen.getByText('Welcome back', { exact: false });
|
|
83
|
+
screen.getByTestId('todo-3');
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### Echoed text
|
|
87
|
+
|
|
88
|
+
`getByText` answers with the innermost matching node, so a container that
|
|
89
|
+
echoes its child's text counts once. Browsers do this natively; on a device
|
|
90
|
+
the engine applies the same rule, since iOS reports a React Native `Text` as
|
|
91
|
+
a host view plus a `StaticText` child with the same label. Two matches that
|
|
92
|
+
do not contain each other remain `LOCATOR_AMBIGUOUS`.
|
|
93
|
+
|
|
94
|
+
### Hidden duplicates
|
|
95
|
+
|
|
96
|
+
Every query accepts `visible: true`, which drops nodes the platform reports
|
|
97
|
+
as hidden before the exactly-one rule runs. Frameworks often keep a hidden
|
|
98
|
+
copy of content in the document, such as a closed drawer or an inactive tab
|
|
99
|
+
panel, and without `visible` both copies match.
|
|
100
|
+
|
|
101
|
+
```ts
|
|
102
|
+
await browser.reload();
|
|
103
|
+
// Two "No memories yet" nodes exist for a moment; one is display:none.
|
|
104
|
+
await expect(screen.getByText('No memories yet', { visible: true })).toBeVisible();
|
|
105
|
+
await screen.getByPlaceholder('Search memory...', { visible: true }).fill('launch');
|
|
106
|
+
await screen.getByTestId('memory-list', { visible: true }).getByRole('listitem').first().tap();
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
`visible` uses the same visibility check as `toBeVisible()`, `isVisible()`,
|
|
110
|
+
and `waitFor()`. It works with scopes, `filter`, `first`, `last`, and `nth`:
|
|
111
|
+
`getByText('Save', { visible: true }).first()` is the first visible match.
|
|
112
|
+
Prefer it over `browser.locator('p:visible')`, which ties the test to CSS.
|
|
113
|
+
|
|
114
|
+
## Refinement
|
|
115
|
+
|
|
116
|
+
Refinement returns a new locator without querying the app. `hasText` with a
|
|
117
|
+
string is a case-insensitive substring match over the match and its
|
|
118
|
+
descendants, unlike a query text, which matches exactly; pass a RegExp for
|
|
119
|
+
anything stricter. `has` keeps a match whose subtree resolves the inner
|
|
120
|
+
locator.
|
|
121
|
+
|
|
122
|
+
Throws `INVALID_LOCATOR` when `filter` gets neither `hasText` nor `has`, when
|
|
123
|
+
it gets any other key, even beside them (`hasNot`, `hasNotText`, and
|
|
124
|
+
`visible` are not filters here), when `has` is not a runner locator or was
|
|
125
|
+
made by another target's screen, or when `nth` gets a negative or
|
|
126
|
+
non-integer index.
|
|
127
|
+
|
|
128
|
+
```ts
|
|
129
|
+
const row = screen.getByRole('listitem').filter({ hasText: 'Invoice 42' });
|
|
130
|
+
await row.getByRole('button', 'Void').tap();
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
<Note>
|
|
134
|
+
In the web engine a `getByDisplayValue` locator cannot be the scope
|
|
135
|
+
of a child query or a `has` filter; both fail with `UNSUPPORTED_CAPABILITY`.
|
|
136
|
+
Playwright has no selector for a control's current value, so the engine
|
|
137
|
+
filters candidates after the fact.
|
|
138
|
+
</Note>
|
|
139
|
+
|
|
140
|
+
## screen.tapAt
|
|
141
|
+
|
|
142
|
+
```ts
|
|
143
|
+
tapAt(point: Point, options?: ActionOptions): Promise<void>;
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
Taps a viewport point in CSS pixels, the space `boundingBox()` reports in,
|
|
147
|
+
with no node behind it: a pin on a canvas, a control the tree does not
|
|
148
|
+
list. Nothing is resolved or waited for; the page decides what the tap
|
|
149
|
+
lands on. Uses `config.actionTimeout`. Throws `INVALID_ARGUMENT` for a
|
|
150
|
+
negative or non-finite coordinate and `UNSUPPORTED_CAPABILITY` when the
|
|
151
|
+
engine does not list `tap` in its pointer actions.
|
|
152
|
+
|
|
153
|
+
```ts
|
|
154
|
+
const map = await screen.getByRole('image', 'Map').boundingBox();
|
|
155
|
+
await screen.tapAt({ x: map.x + 360, y: map.y + 70 });
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
Prefer a locator when the tree lists the control; a point moves with the
|
|
159
|
+
layout. For a point inside a node, `tap({ position })` reads the node's box
|
|
160
|
+
for you.
|
|
161
|
+
|
|
162
|
+
## screen.swipe
|
|
163
|
+
|
|
164
|
+
```ts
|
|
165
|
+
swipe(options: SwipeOptions | SwipePathOptions): Promise<void>;
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
Viewport-level gesture, in one of two shapes. `{ direction, momentum? }` is
|
|
169
|
+
a scroll gesture on the screen. `direction` is the way the content scrolls,
|
|
170
|
+
not the finger: `'down'` reveals what is below. `momentum` is its fling
|
|
171
|
+
strength, `'none'` by default: `'none'` a plain drag of half the surface,
|
|
172
|
+
`'slow'` a short fling, `'fast'` a long one. How far each carries is the
|
|
173
|
+
engine's.
|
|
174
|
+
`{ from, to }` is a swipe along a path between two viewport points: a touch
|
|
175
|
+
swipe on a device, a pointer drag on a document platform, which is what
|
|
176
|
+
reveals a row action, dismisses a sheet, or drags a card between columns.
|
|
177
|
+
Uses `config.actionTimeout`. Throws
|
|
178
|
+
`ACTION_FAILED` when the gesture is rejected, `INVALID_ARGUMENT` for a mix
|
|
179
|
+
of the two shapes or a coordinate off the plane, and `UNSUPPORTED_CAPABILITY`
|
|
180
|
+
when the engine does not list `swipe` in its actions (a direction) or
|
|
181
|
+
`swipeTo` in its pointer actions (a path).
|
|
182
|
+
|
|
183
|
+
```ts
|
|
184
|
+
await screen.swipe({ direction: 'up', momentum: 'fast' });
|
|
185
|
+
|
|
186
|
+
const row = await screen.getByText('Design review').boundingBox();
|
|
187
|
+
await screen.swipe({
|
|
188
|
+
from: { x: row.x + row.width - 10, y: row.y + row.height / 2 },
|
|
189
|
+
to: { x: row.x + 10, y: row.y + row.height / 2 },
|
|
190
|
+
});
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
## screen.scrollUntilVisible
|
|
194
|
+
|
|
195
|
+
```ts
|
|
196
|
+
scrollUntilVisible(
|
|
197
|
+
target: Locator,
|
|
198
|
+
options?: { direction?: ScrollDirection; momentum?: Momentum; timeout?: number },
|
|
199
|
+
): Promise<void>;
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
| Name | Type | Default |
|
|
203
|
+
| --- | --- | --- |
|
|
204
|
+
| `target` | `Locator` | - |
|
|
205
|
+
| `options.direction` | `ScrollDirection` | `'down'` |
|
|
206
|
+
| `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 |
|
|
207
|
+
| `options.timeout` | `number` | `30000`, its own default, not `config.actionTimeout`; it bounds the whole scroll, each swipe included |
|
|
208
|
+
|
|
209
|
+
On `screen` each step is a viewport swipe. On a locator the step is a swipe
|
|
210
|
+
of that node, so a feed or a table that is its own scroll container pages
|
|
211
|
+
without the pointer having to hover it first:
|
|
212
|
+
`screen.getByRole('feed').scrollUntilVisible(row)`.
|
|
213
|
+
|
|
214
|
+
Throws `INVALID_LOCATOR` for a non-runner locator or one made by another
|
|
215
|
+
target's screen, `INVALID_ARGUMENT` for an option it does not take,
|
|
216
|
+
`LOCATOR_NOT_FOUND` when the deadline expires first, a swipe it cut short
|
|
217
|
+
included.
|
|
218
|
+
|
|
219
|
+
```ts
|
|
220
|
+
await screen.scrollUntilVisible(screen.getByRole('button', 'Accept'));
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
## Locator actions
|
|
224
|
+
|
|
225
|
+
Each action resolves one node, waits for actionability, and performs one
|
|
226
|
+
operation. Default timeout `config.actionTimeout`, 30000 ms, overridable
|
|
227
|
+
with `{ timeout }`.
|
|
228
|
+
|
|
229
|
+
```ts
|
|
230
|
+
export interface Locator extends Screen {
|
|
231
|
+
/** Taps exactly one matching actionable node; with `position`, taps that point of its box once it is in view, with no other actionability check. */
|
|
232
|
+
tap(options?: TapOptions): Promise<void>;
|
|
233
|
+
/** Alias of tap. */
|
|
234
|
+
click(options?: TapOptions): Promise<void>;
|
|
235
|
+
/** Double-taps exactly one matching actionable node. */
|
|
236
|
+
doubleTap(options?: ClickOptions): Promise<void>;
|
|
237
|
+
/** Performs a secondary tap, such as a right click or two-finger tap. */
|
|
238
|
+
secondaryTap(options?: ClickOptions): Promise<void>;
|
|
239
|
+
/** Long-presses exactly one matching actionable node. */
|
|
240
|
+
longPress(options?: LongPressOptions): Promise<void>;
|
|
241
|
+
/** Fills exactly one input. Secret values are never logged. */
|
|
242
|
+
fill(value: string | Secret, options?: ActionOptions): Promise<void>;
|
|
243
|
+
/**
|
|
244
|
+
* Focuses exactly one input and types `text` through the keyboard, one
|
|
245
|
+
* character at a time, so the app receives key events. A `Secret` is
|
|
246
|
+
* refused; it goes through `fill`.
|
|
247
|
+
*/
|
|
248
|
+
pressSequentially(text: string, options?: PressSequentiallyOptions): Promise<void>;
|
|
249
|
+
/** Clears exactly one input. */
|
|
250
|
+
clear(options?: ActionOptions): Promise<void>;
|
|
251
|
+
/** Sends one key to exactly one node. */
|
|
252
|
+
press(key: string, options?: ActionOptions): Promise<void>;
|
|
253
|
+
/** Checks exactly one node. */
|
|
254
|
+
check(options?: ActionOptions): Promise<void>;
|
|
255
|
+
/** Unchecks exactly one node. */
|
|
256
|
+
uncheck(options?: ActionOptions): Promise<void>;
|
|
257
|
+
/** Selects one option from exactly one control. */
|
|
258
|
+
selectOption(value: SelectOption, options?: ActionOptions): Promise<void>;
|
|
259
|
+
/** Focuses exactly one node. */
|
|
260
|
+
focus(options?: ActionOptions): Promise<void>;
|
|
261
|
+
/** Hovers over exactly one matching actionable node. */
|
|
262
|
+
hover(options?: ActionOptions): Promise<void>;
|
|
263
|
+
/** Sets the files of exactly one file input; paths resolve from the project root. */
|
|
264
|
+
setInputFiles(paths: string | readonly string[], options?: ActionOptions): Promise<void>;
|
|
265
|
+
/** Drags exactly one node to exactly one target. */
|
|
266
|
+
dragTo(target: Locator, options?: ActionOptions): Promise<void>;
|
|
267
|
+
/** Scrolls exactly one node into view. */
|
|
268
|
+
scrollIntoView(options?: ActionOptions): Promise<void>;
|
|
269
|
+
/** Performs a swipe scoped to exactly one node. */
|
|
270
|
+
swipe(options: SwipeOptions & ActionOptions): Promise<void>;
|
|
271
|
+
/** Reads normalized text without retrying for a particular value. */
|
|
272
|
+
textContent(): Promise<string | null>;
|
|
273
|
+
/** Reads the current input value. */
|
|
274
|
+
inputValue(): Promise<string>;
|
|
275
|
+
/** Reads one attribute. */
|
|
276
|
+
getAttribute(name: string): Promise<string | null>;
|
|
277
|
+
/** Reads current visibility. */
|
|
278
|
+
isVisible(): Promise<boolean>;
|
|
279
|
+
/** Reads current hidden or absent state, the negation of `isVisible`. */
|
|
280
|
+
isHidden(): Promise<boolean>;
|
|
281
|
+
/** Reads current enabled state. */
|
|
282
|
+
isEnabled(): Promise<boolean>;
|
|
283
|
+
/** Reads current disabled state, the negation of `isEnabled`. */
|
|
284
|
+
isDisabled(): Promise<boolean>;
|
|
285
|
+
/** Reads current checked state. */
|
|
286
|
+
isChecked(): Promise<boolean>;
|
|
287
|
+
/** Reads the current viewport-relative rectangle. */
|
|
288
|
+
boundingBox(): Promise<{ x: number; y: number; width: number; height: number } | null>;
|
|
289
|
+
/** Counts current matches without auto-waiting. */
|
|
290
|
+
count(): Promise<number>;
|
|
291
|
+
/** One `nth(i)` locator per current match, without auto-waiting; empty when nothing matches. */
|
|
292
|
+
all(): Promise<Locator[]>;
|
|
293
|
+
/** Reads the normalized text of every current match, without auto-waiting; empty when nothing matches. */
|
|
294
|
+
allTextContents(): Promise<string[]>;
|
|
295
|
+
/** Waits for the requested locator state. */
|
|
296
|
+
waitFor(options?: { state?: 'visible' | 'hidden'; timeout?: number }): Promise<void>;
|
|
297
|
+
/** Adds deterministic locator filters. */
|
|
298
|
+
filter(options: { hasText?: TextMatch; has?: Locator }): Locator;
|
|
299
|
+
/** Selects the first current match. */
|
|
300
|
+
first(): Locator;
|
|
301
|
+
/** Selects the last current match. */
|
|
302
|
+
last(): Locator;
|
|
303
|
+
/** Selects one zero-based current match. */
|
|
304
|
+
nth(index: number): Locator;
|
|
305
|
+
}
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
- An action whose kind the engine does not list in `actions` throws
|
|
309
|
+
`UNSUPPORTED_CAPABILITY` before reaching the engine.
|
|
310
|
+
- An action takes only the options its signature names. Any other key,
|
|
311
|
+
Playwright's `trial`, `force`, or `noWaitAfter` included, throws
|
|
312
|
+
`INVALID_ARGUMENT` before the node is resolved, so the control is never
|
|
313
|
+
touched.
|
|
314
|
+
- `click` is an alias of `tap`.
|
|
315
|
+
- `tap`, `doubleTap`, and `secondaryTap` take `modifiers`, the keys held for
|
|
316
|
+
the click, as a Shift-click extends a selection:
|
|
317
|
+
`row.click({ modifiers: ['Shift'] })`. `ControlOrMeta` is Control on Windows
|
|
318
|
+
and Linux, Meta on macOS. An unknown or repeated modifier is
|
|
319
|
+
`INVALID_ARGUMENT`, and so is `modifiers` beside `position`. The browser
|
|
320
|
+
engine holds them; an engine that does not declare `tapModifiers`, the
|
|
321
|
+
mobile engine among them, fails with `UNSUPPORTED_CAPABILITY` before it
|
|
322
|
+
acts.
|
|
323
|
+
- `tap({ position })` taps at an offset of the node's top-left corner, in
|
|
324
|
+
CSS pixels. The node is scrolled into view when the engine lists
|
|
325
|
+
`scrollIntoView`, then its box is read and the pointer dispatched at the
|
|
326
|
+
point, with no node behind the gesture, so the platform's other
|
|
327
|
+
actionability checks (enabled, not covered) do not run. A node the engine
|
|
328
|
+
cannot bring into view fails as the engine reports it (`ACTION_FAILED` on
|
|
329
|
+
the browser engine, whose scroll waits for visibility); one that resolves
|
|
330
|
+
but never shows a visible box within the timeout is `LOCATOR_NOT_FOUND`.
|
|
331
|
+
An engine without the `tap` pointer action is `UNSUPPORTED_CAPABILITY`.
|
|
332
|
+
- `press` takes zero or more modifiers (`Shift`, `Control`, `Alt`, `Meta`,
|
|
333
|
+
`ControlOrMeta`) and one key, joined with `+`: a named key (`Enter`,
|
|
334
|
+
`Escape`, `Tab`, `Backspace`, `Delete`, `Insert`, `Space`, the arrows,
|
|
335
|
+
`Home`, `End`, `PageUp`, `PageDown`, `F1` to `F12`) or one printable
|
|
336
|
+
character. `Shift++` presses `+`. A key the platform lacks throws
|
|
337
|
+
`UNSUPPORTED_CAPABILITY`. See the [key grammar](/reference/engine#keys).
|
|
338
|
+
- `longPress` holds for `{ duration }` milliseconds, 100 through 10000, else
|
|
339
|
+
for the engine's default: 500 ms on the browser engine, one second on the
|
|
340
|
+
mobile engine, past a React Native `Pressable`'s `delayLongPress`.
|
|
341
|
+
- `fill` replaces the current content. A `Secret` is resolved on the host
|
|
342
|
+
right before the call and never logged, reported, or sent to a model.
|
|
343
|
+
- `pressSequentially` focuses the input and sends the text as keystrokes
|
|
344
|
+
through the engine's keyboard, appending to the current content. `fill`
|
|
345
|
+
sets the value directly and fires no key events, so an autocomplete, a
|
|
346
|
+
search-as-you-type box, or a masked input wired to `keydown` or `keyup`
|
|
347
|
+
never reacts to it; this is the action for those. `{ delay }` waits that
|
|
348
|
+
many milliseconds between characters, each sent in its own keyboard call;
|
|
349
|
+
without it the whole text goes in one call. Focus, pauses, and keystrokes
|
|
350
|
+
share the one action deadline: a pause is cut short at it and no character
|
|
351
|
+
is sent past it, and the action fails with `ACTION_FAILED` naming how many
|
|
352
|
+
characters landed. It needs the engine's `focus` action and `keyboard`
|
|
353
|
+
capability, else `UNSUPPORTED_CAPABILITY` before any node resolves, and it
|
|
354
|
+
relies on the [keyboard contract](/reference/engine#engine): `type`
|
|
355
|
+
must deliver text as a person typing would, so key events reach the app
|
|
356
|
+
where the platform has them. A `Secret` is a type error and
|
|
357
|
+
`INVALID_ARGUMENT` at runtime: a secret is filled, never typed as
|
|
358
|
+
keystrokes. The step records the locator, not the text, like `fill`.
|
|
359
|
+
- `setInputFiles` paths resolve from the project root. An empty list or blank
|
|
360
|
+
entry throws `INVALID_ARGUMENT`.
|
|
361
|
+
- `dragTo` with a non-runner target, or one made by another target's screen,
|
|
362
|
+
throws `INVALID_LOCATOR`.
|
|
363
|
+
- `swipe` on a locator is a gesture on that one node: `{ direction,
|
|
364
|
+
momentum? }` as [`screen.swipe`](#screen-swipe) defines them, plus
|
|
365
|
+
`timeout`. It has no `{ from, to }` form. `screen.swipe` takes no
|
|
366
|
+
`timeout`: a `timeout` key there is `INVALID_ARGUMENT`.
|
|
367
|
+
- An action that may already have committed surfaces as `ACTION_FAILED` and
|
|
368
|
+
is never repeated.
|
|
369
|
+
|
|
370
|
+
```ts
|
|
371
|
+
await screen.getByLabel('Email').fill('ada@example.test');
|
|
372
|
+
await screen.getByRole('checkbox', 'Remember me').check();
|
|
373
|
+
await screen.getByLabel('Plan').selectOption({ label: 'Pro' });
|
|
374
|
+
await screen.getByLabel('Plan').selectOption({ value: 'pro' });
|
|
375
|
+
await screen.getByRole('button', 'Save').tap({ timeout: 10_000 });
|
|
376
|
+
await screen.getByRole('image', 'Seat map').tap({ position: { x: 120, y: 48 } });
|
|
377
|
+
await screen.getByRole('textbox', 'Search').press('Enter');
|
|
378
|
+
await screen.getByRole('textbox', 'Notes').press('ControlOrMeta+a');
|
|
379
|
+
await screen.getByLabel('City').pressSequentially('War', { delay: 50 });
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
## Locator reads
|
|
383
|
+
|
|
384
|
+
`waitFor` defaults `state` to `'visible'` and `timeout` to
|
|
385
|
+
`config.actionTimeout`, and throws `LOCATOR_NOT_FOUND` on timeout.
|
|
386
|
+
|
|
387
|
+
`textContent` and `allTextContents` return the node's rendered text with
|
|
388
|
+
whitespace collapsed to single spaces. On the web that is what `innerText`
|
|
389
|
+
reads, not the DOM's `textContent`: CSS `text-transform` applies,
|
|
390
|
+
`display: none` descendants are left out, and a `<br>` reads as a space. A
|
|
391
|
+
node that is not rendered itself, such as a `display: none` one, reads its
|
|
392
|
+
DOM text, as `innerText` does.
|
|
393
|
+
|
|
394
|
+
`isChecked` is `false` on a node with no checked state, such as a button or
|
|
395
|
+
a heading; it does not throw.
|
|
396
|
+
|
|
397
|
+
`textContent`, `inputValue`, `getAttribute`, and `allTextContents` throw
|
|
398
|
+
`POLICY_DENIED` on a secure field, and so do `expect(locator).toHaveValue`,
|
|
399
|
+
`toHaveText`, `toContainText`, and `toHaveAttribute`.
|
|
400
|
+
`isEnabled`, `isDisabled`, `isChecked`, and `boundingBox` are allowed there.
|
|
401
|
+
|
|
402
|
+
`isHidden` is `!isVisible()` and `isDisabled` is `!isEnabled()`, with the
|
|
403
|
+
same strictness: `isHidden` is `true` for zero matches, `isDisabled` is
|
|
404
|
+
`LOCATOR_NOT_FOUND` there, and both are `LOCATOR_AMBIGUOUS` on several.
|
|
405
|
+
|
|
406
|
+
```ts
|
|
407
|
+
if (await screen.getByRole('alert').isVisible()) {
|
|
408
|
+
const message = await screen.getByRole('alert').textContent();
|
|
409
|
+
}
|
|
410
|
+
await screen.getByTestId('spinner').waitFor({ state: 'hidden' });
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
### Every match at once
|
|
414
|
+
|
|
415
|
+
`all()` resolves the query once and hands back one `nth(i)` locator per
|
|
416
|
+
match, so a loop can act on each row; `allTextContents()` reads every match's
|
|
417
|
+
normalized text in one call. Neither waits or enforces exactly-one: zero
|
|
418
|
+
matches is an empty array, the same snapshot rule as `count()`. A
|
|
419
|
+
`frameLocator` whose frame is not in the document counts as zero matches here
|
|
420
|
+
and in `isVisible()`, where an action or assertion polls for it. Each locator
|
|
421
|
+
`all()` returns re-resolves when used, so a list that changes under the loop
|
|
422
|
+
can make a later `nth(i)` point at a different row, or at nothing. Follow the
|
|
423
|
+
loop with an assertion on the list, and use
|
|
424
|
+
[`toHaveText([...])`](/reference/expect#locator-matchers) instead of a loop
|
|
425
|
+
when the point is to check the texts.
|
|
426
|
+
|
|
427
|
+
```ts
|
|
428
|
+
for (const row of await screen.getByRole('row').all()) {
|
|
429
|
+
await row.getByRole('checkbox').check();
|
|
430
|
+
}
|
|
431
|
+
expect(await screen.getByRole('listitem').allTextContents()).toEqual(['Alpha', 'Beta']);
|
|
432
|
+
```
|
|
433
|
+
|
|
434
|
+
## Types
|
|
435
|
+
|
|
436
|
+
```ts
|
|
437
|
+
/**
|
|
438
|
+
* The closed role vocabulary; an unsupported role is a type error. Every
|
|
439
|
+
* engine maps its platform's element types onto these names, so a role query
|
|
440
|
+
* reads the same against a browser and a device. A role the platform has no
|
|
441
|
+
* widget for matches nothing there; it is never a type error.
|
|
442
|
+
*/
|
|
443
|
+
export type Role =
|
|
444
|
+
| 'button'
|
|
445
|
+
| 'link'
|
|
446
|
+
| 'textbox'
|
|
447
|
+
| 'searchbox'
|
|
448
|
+
| 'combobox'
|
|
449
|
+
| 'listbox'
|
|
450
|
+
| 'option'
|
|
451
|
+
| 'checkbox'
|
|
452
|
+
| 'radio'
|
|
453
|
+
| 'radiogroup'
|
|
454
|
+
| 'switch'
|
|
455
|
+
| 'slider'
|
|
456
|
+
| 'spinbutton'
|
|
457
|
+
| 'progressbar'
|
|
458
|
+
| 'meter'
|
|
459
|
+
| 'image'
|
|
460
|
+
| 'heading'
|
|
461
|
+
| 'tab'
|
|
462
|
+
| 'tablist'
|
|
463
|
+
| 'tabpanel'
|
|
464
|
+
| 'menu'
|
|
465
|
+
| 'menubar'
|
|
466
|
+
| 'menuitem'
|
|
467
|
+
| 'menuitemcheckbox'
|
|
468
|
+
| 'menuitemradio'
|
|
469
|
+
| 'toolbar'
|
|
470
|
+
| 'tooltip'
|
|
471
|
+
| 'tree'
|
|
472
|
+
| 'treeitem'
|
|
473
|
+
| 'list'
|
|
474
|
+
| 'listitem'
|
|
475
|
+
| 'table'
|
|
476
|
+
| 'grid'
|
|
477
|
+
| 'row'
|
|
478
|
+
| 'rowgroup'
|
|
479
|
+
| 'rowheader'
|
|
480
|
+
| 'cell'
|
|
481
|
+
| 'gridcell'
|
|
482
|
+
| 'columnheader'
|
|
483
|
+
| 'separator'
|
|
484
|
+
| 'group'
|
|
485
|
+
| 'article'
|
|
486
|
+
| 'figure'
|
|
487
|
+
| 'form'
|
|
488
|
+
| 'status'
|
|
489
|
+
| 'alert'
|
|
490
|
+
| 'dialog'
|
|
491
|
+
| 'alertdialog'
|
|
492
|
+
| 'main'
|
|
493
|
+
| 'navigation'
|
|
494
|
+
| 'banner'
|
|
495
|
+
| 'contentinfo'
|
|
496
|
+
| 'complementary'
|
|
497
|
+
| 'region';
|
|
498
|
+
|
|
499
|
+
/**
|
|
500
|
+
* Spellings `getByRole` accepts beside the vocabulary and rewrites to a `Role`
|
|
501
|
+
* before the query is built: `img` is the ARIA name of `image`. An alias never
|
|
502
|
+
* reaches an engine, the replay cache, or a report; a `Role` is what they see.
|
|
503
|
+
*/
|
|
504
|
+
export type RoleAlias = 'img';
|
|
505
|
+
|
|
506
|
+
/** A string matches exactly; a RegExp uses its own source and flags. */
|
|
507
|
+
export type TextMatch = string | RegExp;
|
|
508
|
+
|
|
509
|
+
export interface TextMatchOptions {
|
|
510
|
+
/** Exact match, the default. `false` is case-insensitive substring matching; a RegExp ignores it. */
|
|
511
|
+
exact?: boolean;
|
|
512
|
+
/**
|
|
513
|
+
* When true, nodes the platform reports as hidden are excluded before the
|
|
514
|
+
* exactly-one rule is applied, so a visible node with a hidden twin (a
|
|
515
|
+
* prerendered copy kept after navigation, a closed drawer) still resolves.
|
|
516
|
+
* Omitted or false keeps every match, hidden ones included.
|
|
517
|
+
*/
|
|
518
|
+
visible?: boolean;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
/**
|
|
522
|
+
* Role query options. A role query never matches a node hidden from the
|
|
523
|
+
* accessibility tree, on every engine; `visible` (inherited) is the one knob
|
|
524
|
+
* that narrows the other query kinds the same way.
|
|
525
|
+
*/
|
|
526
|
+
export interface RoleOptions extends TextMatchOptions {
|
|
527
|
+
/** Accessible name filter. */
|
|
528
|
+
name?: TextMatch;
|
|
529
|
+
/** Requires the checked state. */
|
|
530
|
+
checked?: boolean;
|
|
531
|
+
/** Requires the disabled state. */
|
|
532
|
+
disabled?: boolean;
|
|
533
|
+
/** Requires the selected state. */
|
|
534
|
+
selected?: boolean;
|
|
535
|
+
/** Requires the expanded state. */
|
|
536
|
+
expanded?: boolean;
|
|
537
|
+
/** Requires the pressed state of a toggle button. */
|
|
538
|
+
pressed?: boolean;
|
|
539
|
+
/** Requires a heading level, 1 through 6. */
|
|
540
|
+
level?: number;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
export interface ActionOptions {
|
|
544
|
+
/** Deadline in milliseconds; defaults to `config.actionTimeout`. */
|
|
545
|
+
timeout?: number;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
/** `longPress` options: the hold time in milliseconds, 100 through 10000, default the engine's own. */
|
|
549
|
+
export interface LongPressOptions extends ActionOptions {
|
|
550
|
+
/** Hold time in milliseconds, 100 through 10000; unset, the engine's default. */
|
|
551
|
+
duration?: number;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
/** `pressSequentially` options: an optional pause between characters. */
|
|
555
|
+
export interface PressSequentiallyOptions extends ActionOptions {
|
|
556
|
+
/** Milliseconds to wait between characters; omitted types the whole text in one call. */
|
|
557
|
+
delay?: number;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
export interface SwipeOptions {
|
|
561
|
+
/** Swipe direction. */
|
|
562
|
+
direction: ScrollDirection;
|
|
563
|
+
/** Fling strength; default `none`. The three values are defined under screen.swipe. */
|
|
564
|
+
momentum?: Momentum;
|
|
565
|
+
from?: never;
|
|
566
|
+
to?: never;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
/** A swipe along a path between two viewport points: a touch swipe on a device, a pointer drag on a document platform. */
|
|
570
|
+
export interface SwipePathOptions {
|
|
571
|
+
/** Where the finger goes down. */
|
|
572
|
+
from: Point;
|
|
573
|
+
/** Where it lifts. */
|
|
574
|
+
to: Point;
|
|
575
|
+
direction?: never;
|
|
576
|
+
momentum?: never;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
/**
|
|
580
|
+
* A point in CSS pixels. For `screen.tapAt` and `screen.swipe` the origin is
|
|
581
|
+
* the viewport's top-left corner, the space `boundingBox()` reports in; for
|
|
582
|
+
* `tap({ position })` it is the node's own top-left corner.
|
|
583
|
+
*/
|
|
584
|
+
export type Point = ViewportPoint; // { x: number; y: number }
|
|
585
|
+
|
|
586
|
+
/** `tap`, `click`, `doubleTap`, and `secondaryTap` options: the action timeout and the keys held for the click. */
|
|
587
|
+
export interface ClickOptions extends ActionOptions {
|
|
588
|
+
/**
|
|
589
|
+
* Keys held while the pointer clicks, as a Shift-click extends a selection:
|
|
590
|
+
* `Shift`, `Control`, `Alt`, `Meta`, or `ControlOrMeta` (Control on Windows
|
|
591
|
+
* and Linux, Meta on macOS). Needs an engine that declares `tapModifiers`,
|
|
592
|
+
* `UNSUPPORTED_CAPABILITY` otherwise.
|
|
593
|
+
*/
|
|
594
|
+
modifiers?: readonly KeyModifier[];
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
/** A modifier key, for `modifiers` and the `press` key grammar. */
|
|
598
|
+
export type KeyModifier = 'Shift' | 'Control' | 'Alt' | 'Meta' | 'ControlOrMeta';
|
|
599
|
+
|
|
600
|
+
/** `tap` and `click` options. */
|
|
601
|
+
export interface TapOptions extends ClickOptions {
|
|
602
|
+
/**
|
|
603
|
+
* Where to tap, relative to the node's top-left corner. Without it the
|
|
604
|
+
* platform picks a point of the node, usually its center, behind its own
|
|
605
|
+
* actionability checks; with it the pointer is dispatched at that point
|
|
606
|
+
* once the node is in view and has a box, which needs the engine's `tap`
|
|
607
|
+
* pointer action.
|
|
608
|
+
*/
|
|
609
|
+
position?: Point;
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
/** One option of a select: its label (a bare string too), its `value` attribute, or its zero-based index. */
|
|
613
|
+
export type SelectOption =
|
|
614
|
+
| string
|
|
615
|
+
| { label: string; value?: never; index?: never }
|
|
616
|
+
| { value: string; label?: never; index?: never }
|
|
617
|
+
| { index: number; label?: never; value?: never };
|
|
618
|
+
|
|
619
|
+
/** A swipe or scroll direction. */
|
|
620
|
+
export type ScrollDirection = 'up' | 'down' | 'left' | 'right';
|
|
621
|
+
|
|
622
|
+
/** Fling strength of a swipe; defined under screen.swipe. */
|
|
623
|
+
export type Momentum = 'none' | 'slow' | 'fast';
|
|
624
|
+
```
|
|
625
|
+
|
|
626
|
+
A role query never matches a node hidden from the accessibility tree.
|
|
627
|
+
`visible` applies the same rule to the other query kinds. See
|
|
628
|
+
[Hidden duplicates](#hidden-duplicates).
|
|
629
|
+
|
|
630
|
+
`img` is an alias, not a role: `getByRole('img', { name })` builds the same
|
|
631
|
+
query as `getByRole('image', { name })`, so a test ported from Playwright keeps
|
|
632
|
+
its spelling while engines, the replay cache, and reports see `image`. On the
|
|
633
|
+
web, `image` covers `<img>` with alt text and any element with `role="img"`;
|
|
634
|
+
an `<img alt="">` is decoration and matches nothing.
|
|
635
|
+
|
|
636
|
+
The web engine derives the roles from HTML the way HTML-AAM does: `<progress>`
|
|
637
|
+
is `progressbar`, `<meter>` is `meter`, `<hr>` is `separator`, `<fieldset>`
|
|
638
|
+
and `<details>` are `group`, `<input type="number">` is `spinbutton`,
|
|
639
|
+
`<input type="file">` is `button`, as Chrome and Playwright read it,
|
|
640
|
+
`<select size="3">` is a `listbox` like a `multiple` one, an `<input list>`
|
|
641
|
+
naming a `<datalist>` is a `combobox`, a `<td>` of a `role="grid"` table is a
|
|
642
|
+
`gridcell`,
|
|
643
|
+
`<thead>` and `<tbody>` are `rowgroup`, `<th scope="row">` is `rowheader`,
|
|
644
|
+
`<figure>` is `figure`, `<article>` is `article`, `<header>` and `<footer>` are
|
|
645
|
+
`banner` and `contentinfo` only outside sectioning content or an element with
|
|
646
|
+
a sectioning role (`article`, `complementary`, `main`, `navigation`, `region`),
|
|
647
|
+
and `<form>` and `<section>`
|
|
648
|
+
are `form` and `region` only when they carry an `aria-label`,
|
|
649
|
+
`aria-labelledby`, or `title`. A `<legend>`, `<figcaption>`, or `<caption>`
|
|
650
|
+
names its fieldset, figure, or table, so `toHaveAccessibleName` reads what
|
|
651
|
+
`getByRole('group', { name })` matched. A role accname names from content (a
|
|
652
|
+
button, link, heading, tab, menu item, option, checkbox, radio, switch, cell,
|
|
653
|
+
row or column header, row, tree item, tooltip) is named by its descendants, a
|
|
654
|
+
custom element's shadow tree included, so `getByRole('checkbox', { name })`
|
|
655
|
+
and `toHaveAccessibleName(name)` agree on a `<div role="checkbox">`. An
|
|
656
|
+
`<input type="submit">` or `type="reset"` with no `value` reads `Submit` or
|
|
657
|
+
`Reset`. `<menu>` is a `list`; only `role="menu"` is a
|
|
658
|
+
`menu`. A device engine maps the platform types it has (a tab bar or segmented
|
|
659
|
+
control is a `tablist`, a progress indicator a `progressbar`, a stepper a
|
|
660
|
+
`spinbutton`) and keeps any other type verbatim, so a role with no native
|
|
661
|
+
widget, such as `tooltip` or `figure`, matches nothing on a phone.
|
|
662
|
+
|
|
663
|
+
A `contenteditable` editing host, the root a TipTap, ProseMirror, Lexical, or
|
|
664
|
+
Slate editor renders into, is a `textbox` whose value is its text as rendered
|
|
665
|
+
(and, while it has focus, whose `selection` is the text selected inside it).
|
|
666
|
+
The value keeps spaces and newlines, so `toHaveValue` and `inputValue()` read
|
|
667
|
+
a `white-space: pre-wrap` editor like a textarea. An empty editor, holding only
|
|
668
|
+
the `<p><br></p>` or the zero-width no-break space its editor pads an empty
|
|
669
|
+
line with, reads as `''`, and that padding is never part of a value. It is named by its
|
|
670
|
+
`aria-label`, `aria-placeholder`, or the `data-placeholder` its editor paints;
|
|
671
|
+
the blocks inside it are content, not textboxes. The tag does not matter (an
|
|
672
|
+
`<article contenteditable>` is the editor, not an article), while a native
|
|
673
|
+
control or a drawn surface made editable keeps its own role. `getByRole` resolves through
|
|
674
|
+
Playwright's role selector, which has no such rule, so a test addresses an
|
|
675
|
+
editor by role only when the host carries `role="textbox"`; `getByLabel` and
|
|
676
|
+
`getByTestId` reach it either way. A name taken from a block's
|
|
677
|
+
`data-placeholder` (TipTap, ProseMirror) goes once the editor has content, as
|
|
678
|
+
the painted hint does, so the host reads as an unnamed `textbox` from then on;
|
|
679
|
+
a name from the host's own `aria-label`, `aria-labelledby`, `aria-placeholder`,
|
|
680
|
+
or `data-placeholder` (Quill) stays. Anchor a test on a label or a test id, not
|
|
681
|
+
on the placeholder.
|
|
682
|
+
|
|
683
|
+
|
|
684
|
+
For `selectOption`, pass a label string or exactly one of `label`, `value`,
|
|
685
|
+
or `index`. `value` matches the option's value attribute, which is what the
|
|
686
|
+
form submits. One option per call: an array, an object with more than one of
|
|
687
|
+
those keys, or a negative or fractional `index` fails with `INVALID_ARGUMENT`
|
|
688
|
+
before the engine acts.
|