craftdriver 1.1.0 → 1.1.1
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/CHANGELOG.md +7 -0
- package/README.md +1 -1
- package/dist/cli/snapshot.js +31 -4
- package/dist/cli/snapshot.js.map +1 -1
- package/dist/lib/browser.d.ts +6 -0
- package/dist/lib/browser.d.ts.map +1 -1
- package/dist/lib/browser.js +6 -0
- package/dist/lib/browser.js.map +1 -1
- package/dist/lib/by.d.ts +18 -0
- package/dist/lib/by.d.ts.map +1 -1
- package/dist/lib/by.js +141 -17
- package/dist/lib/by.js.map +1 -1
- package/dist/lib/locator.d.ts +28 -0
- package/dist/lib/locator.d.ts.map +1 -1
- package/dist/lib/locator.js +28 -0
- package/dist/lib/locator.js.map +1 -1
- package/docs/browser-api.md +4 -1
- package/docs/public/examples/a11y.html +44 -0
- package/docs/public/examples/clock.html +83 -0
- package/docs/public/examples/console-errors.html +344 -0
- package/docs/public/examples/dialogs.html +42 -0
- package/docs/public/examples/download.html +31 -0
- package/docs/public/examples/dynamic.html +131 -0
- package/docs/public/examples/emulate.html +127 -0
- package/docs/public/examples/evaluate.html +28 -0
- package/docs/public/examples/hover-select.html +295 -0
- package/docs/public/examples/iframe-child.html +51 -0
- package/docs/public/examples/iframes.html +35 -0
- package/docs/public/examples/keyboard.html +195 -0
- package/docs/public/examples/locator.html +131 -0
- package/docs/public/examples/login.html +116 -0
- package/docs/public/examples/mobile.html +270 -0
- package/docs/public/examples/mouse.html +505 -0
- package/docs/public/examples/network.html +293 -0
- package/docs/public/examples/popup-target.html +26 -0
- package/docs/public/examples/popup.html +50 -0
- package/docs/public/examples/screenshot.html +99 -0
- package/docs/public/examples/selectors.html +150 -0
- package/docs/public/examples/session.html +514 -0
- package/docs/public/examples/upload.html +36 -0
- package/docs/recipes/accessibility-gate.md +24 -40
- package/docs/recipes/console-error-gate.md +29 -38
- package/docs/recipes/file-upload-download.md +26 -43
- package/docs/recipes/find-elements.md +142 -0
- package/docs/recipes/login-once-reuse-session.md +24 -44
- package/docs/recipes/mobile-flow-with-network-and-logs.md +26 -41
- package/docs/recipes/mock-api-and-assert-network.md +22 -35
- package/docs/recipes/multi-user-contexts.md +32 -43
- package/docs/recipes/page-objects.md +52 -0
- package/docs/recipes/trace-failing-test.md +31 -44
- package/docs/recipes/virtual-clock-time-sensitive-ui.md +24 -40
- package/docs/recipes/vitest-browser-lifecycle.md +14 -11
- package/docs/recipes.md +20 -1
- package/docs/selectors.md +291 -229
- package/package.json +5 -1
package/docs/selectors.md
CHANGED
|
@@ -1,317 +1,394 @@
|
|
|
1
1
|
# Selectors & Locators
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Every interaction — a click, a fill, an assertion — starts by pointing at an
|
|
4
|
+
element. This page is the complete guide to **naming** the element you want.
|
|
5
|
+
What you do with it afterwards lives in the [Element API](./element-api.md) and
|
|
6
|
+
[Assertions](./assertions.md).
|
|
7
|
+
|
|
8
|
+
## Three ways to name an element
|
|
9
|
+
|
|
10
|
+
CraftDriver accepts three kinds of "where is it?", and they all resolve to the
|
|
11
|
+
same kind of element handle:
|
|
12
|
+
|
|
13
|
+
```typescript
|
|
14
|
+
import { Browser, By } from 'craftdriver';
|
|
15
|
+
|
|
16
|
+
// 1. A CSS string — quickest for structural matching
|
|
17
|
+
await browser.click('#submit');
|
|
18
|
+
|
|
19
|
+
// 2. A By.* locator — a strategy object for one specific way of matching
|
|
20
|
+
await browser.find(By.role('button', { name: 'Save' })).click();
|
|
21
|
+
|
|
22
|
+
// 3. A getBy* shortcut — the common semantic strategies, straight on the browser
|
|
23
|
+
await browser.getByRole('button', { name: 'Save' }).click();
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
A few rules tie these together:
|
|
27
|
+
|
|
28
|
+
- **A bare string is always a CSS selector.** There is no `text=` or `role=`
|
|
29
|
+
string shorthand — to match by text or role, use a `By.*` locator or a
|
|
30
|
+
`getBy*` method.
|
|
31
|
+
- **`getByRole(...)` is exactly `find(By.role(...))`.** The `getBy*` methods are
|
|
32
|
+
ergonomic shortcuts for the most common semantic `By.*` strategies. Use
|
|
33
|
+
whichever reads better; they resolve identically.
|
|
34
|
+
- **`find()` vs `locator()`.** `find()` returns a one-shot handle. `locator()`
|
|
35
|
+
returns a lazy, re-resolving handle you can filter, index (`.nth()`), and
|
|
36
|
+
scope — see [Locators (lazy & chainable)](#locators-lazy--chainable). Both
|
|
37
|
+
accept a string or a `By`.
|
|
38
|
+
|
|
39
|
+
> **Which one should I reach for?** Prefer locators that describe an element the
|
|
40
|
+
> way a person perceives it — its role, label, or visible text — plus test IDs
|
|
41
|
+
> for anything ambiguous. They keep working when the surrounding markup changes.
|
|
42
|
+
> Drop to CSS for structural matching, and to XPath only when nothing else fits.
|
|
43
|
+
|
|
44
|
+
## Quick reference
|
|
45
|
+
|
|
46
|
+
| To find an element by… | Use | It matches, e.g. |
|
|
47
|
+
| ------------------------- | ---------------------------------------- | --------------------------------- |
|
|
48
|
+
| Role + its label | `getByRole('button', { name: 'Save' })` | `<button>Save</button>` |
|
|
49
|
+
| Visible text | `getByText('Save')` | `<span>Save</span>` |
|
|
50
|
+
| A form field's label | `getByLabel('Email')` | `<label>Email</label><input>` |
|
|
51
|
+
| Placeholder text | `getByPlaceholder('Search')` | `<input placeholder="Search">` |
|
|
52
|
+
| Image `alt` text | `getByAltText('Logo')` | `<img alt="Logo">` |
|
|
53
|
+
| Tooltip (`title`) | `getByTitle('Delete')` | `<button title="Delete">` |
|
|
54
|
+
| Test id | `getByTestId('checkout')` | `<button data-testid="checkout">` |
|
|
55
|
+
| Id | `By.id('email')` · `'#email'` | `<input id="email">` |
|
|
56
|
+
| Class | `By.className('primary')` · `'.primary'` | `<button class="primary">` |
|
|
57
|
+
| `name` attribute | `By.name('email')` | `<input name="email">` |
|
|
58
|
+
| Tag | `By.tagName('h1')` | `<h1>…</h1>` |
|
|
59
|
+
| Any attribute | `By.attr('role', 'dialog')` | `<div role="dialog">` |
|
|
60
|
+
| `data-*` attribute | `By.dataAttr('state', 'open')` | `<div data-state="open">` |
|
|
61
|
+
| `aria-*` attribute | `By.aria('expanded', 'true')` | `<button aria-expanded="true">` |
|
|
62
|
+
| Link text | `By.linkText('Docs')` | `<a href="/docs">Docs</a>` |
|
|
63
|
+
| A CSS selector | `By.css(…)` · any string | anything CSS can express |
|
|
64
|
+
| An XPath expression | `By.xpath(…)` | anything XPath can express |
|
|
65
|
+
|
|
66
|
+
## Semantic locators
|
|
67
|
+
|
|
68
|
+
These describe an element by what it *is* to the user — its role, label, or
|
|
69
|
+
text — not by where it sits in the markup. They're the most readable and the
|
|
70
|
+
most resilient, so reach for them first. Each is a `getBy*` method on the
|
|
71
|
+
browser (and on any [locator](#locators-lazy--chainable)); the `By.*` form named
|
|
72
|
+
in each heading is the same strategy as an object you can pass to `find()`,
|
|
73
|
+
`locator()`, `click()`, or a filter.
|
|
74
|
+
|
|
75
|
+
> **Exact by default.** `getByRole`'s `name`, `getByText`, `getByLabel`,
|
|
76
|
+
> `getByPlaceholder`, `getByAltText`, and `getByTitle` all match the **whole**
|
|
77
|
+
> (whitespace-normalised) string by default. Pass `{ exact: false }` to match a
|
|
78
|
+
> substring instead.
|
|
4
79
|
|
|
5
|
-
|
|
80
|
+
### getByRole(role, options?)
|
|
6
81
|
|
|
7
|
-
|
|
82
|
+
Find an element by its **ARIA role** and, optionally, CraftDriver's practical
|
|
83
|
+
accessible-name approximation.
|
|
8
84
|
|
|
9
|
-
|
|
10
|
-
>
|
|
11
|
-
|
|
85
|
+
```html
|
|
86
|
+
<button>Sign in</button>
|
|
87
|
+
```
|
|
12
88
|
|
|
13
89
|
```typescript
|
|
14
|
-
|
|
15
|
-
await browser.click('#submit-button');
|
|
16
|
-
|
|
17
|
-
// By class
|
|
18
|
-
await browser.click('.btn-primary');
|
|
19
|
-
|
|
20
|
-
// By tag and attribute
|
|
21
|
-
await browser.click('input[type="email"]');
|
|
22
|
-
|
|
23
|
-
// Complex selectors
|
|
24
|
-
await browser.click('form.login #username');
|
|
25
|
-
await browser.click('ul.nav > li:first-child a');
|
|
90
|
+
browser.getByRole('button', { name: 'Sign in' });
|
|
26
91
|
```
|
|
27
92
|
|
|
28
|
-
|
|
93
|
+
- A **role** is the kind of control an element represents — `button`, `link`,
|
|
94
|
+
`checkbox`, `textbox`, `heading`, and so on. Native elements carry an
|
|
95
|
+
*implicit* role, so `getByRole('button')` matches a plain `<button>` and
|
|
96
|
+
`<input type="submit">`, not only elements with an explicit `role="button"`.
|
|
97
|
+
- The **name** is the short label a user would recognize. CraftDriver matches
|
|
98
|
+
the common sources: `aria-label`, simple `aria-labelledby`, visible text,
|
|
99
|
+
associated `<label>` text for form controls, `value`, `alt`, and `title`.
|
|
100
|
+
Pass it as `name` to pick one element out of several with the same role.
|
|
29
101
|
|
|
30
|
-
|
|
102
|
+
```html
|
|
103
|
+
<!-- the role is textbox; the name comes from the associated label -->
|
|
104
|
+
<label for="email">Email</label>
|
|
105
|
+
<input id="email" type="email" />
|
|
106
|
+
```
|
|
31
107
|
|
|
32
108
|
```typescript
|
|
33
|
-
|
|
109
|
+
browser.getByRole('textbox', { name: 'Email' });
|
|
110
|
+
browser.getByRole('checkbox', { name: 'Remember me' });
|
|
111
|
+
browser.getByRole('link', { name: 'Learn more' });
|
|
112
|
+
browser.getByRole('heading', { name: 'Welcome' });
|
|
34
113
|
```
|
|
35
114
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
115
|
+
> `getByRole` is an approximation of browser accessible-name computation, not a
|
|
116
|
+
> full ARIA engine. If the label itself is the thing you want to select by, use
|
|
117
|
+
> [`getByLabel()`](#getbylabel-text-options). To match
|
|
118
|
+
> `<input name="email">` by the HTML `name` attribute, use
|
|
119
|
+
> [`By.name()`](#by-name-name).
|
|
39
120
|
|
|
40
|
-
|
|
41
|
-
browser.find(By.css('#my-button'));
|
|
42
|
-
```
|
|
121
|
+
Options:
|
|
43
122
|
|
|
44
|
-
|
|
123
|
+
| Option | Type | Default | Description |
|
|
124
|
+
| --------------- | --------- | ------- | ------------------------------------------------------------------- |
|
|
125
|
+
| `name` | `string` | — | Name to match (`aria-label`, simple `aria-labelledby`, text, label, `value`, `alt`, `title`). |
|
|
126
|
+
| `exact` | `boolean` | `true` | When `false`, match the name as a substring. |
|
|
127
|
+
| `includeHidden` | `boolean` | `false` | Also match elements hidden from assistive tech (see below). |
|
|
45
128
|
|
|
46
|
-
|
|
129
|
+
By default an element is skipped when it — or any ancestor — carries `hidden` or
|
|
130
|
+
`aria-hidden="true"`, mirroring how those attributes remove a whole subtree from
|
|
131
|
+
the accessibility tree. Set `includeHidden: true` to match them anyway
|
|
132
|
+
(off-canvas menus, collapsed panels). CSS hiding such as `display:none` is
|
|
133
|
+
handled separately by the locator's visibility wait, not by this check.
|
|
47
134
|
|
|
48
135
|
```typescript
|
|
49
|
-
|
|
50
|
-
|
|
136
|
+
// Default: skip elements hidden from assistive tech (self or ancestor)
|
|
137
|
+
browser.getByRole('button', { name: 'Close' });
|
|
138
|
+
|
|
139
|
+
// Include hidden elements
|
|
140
|
+
browser.getByRole('button', { name: 'Close', includeHidden: true });
|
|
51
141
|
```
|
|
52
142
|
|
|
53
|
-
###
|
|
143
|
+
### getByText(text, options?)
|
|
54
144
|
|
|
55
|
-
|
|
145
|
+
Find the innermost element whose visible text matches.
|
|
56
146
|
|
|
57
|
-
```
|
|
58
|
-
|
|
59
|
-
// Equivalent to: browser.find('.btn-primary')
|
|
147
|
+
```html
|
|
148
|
+
<p>Welcome back, Alice!</p>
|
|
60
149
|
```
|
|
61
150
|
|
|
62
|
-
### By.name(name)
|
|
63
|
-
|
|
64
|
-
Locate by the `name` attribute (matches the canonical Selenium
|
|
65
|
-
`By.name` locator).
|
|
66
|
-
|
|
67
151
|
```typescript
|
|
68
|
-
browser.
|
|
69
|
-
|
|
152
|
+
browser.getByText('Welcome back, Alice!'); // exact (default)
|
|
153
|
+
browser.getByText('Welcome back', { exact: false }); // substring
|
|
70
154
|
```
|
|
71
155
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
156
|
+
Text is whitespace-normalised — runs of spaces and line breaks collapse to a
|
|
157
|
+
single space — before matching. For case-insensitive or non-normalised matching,
|
|
158
|
+
use the `By.text` form, which also accepts `caseSensitive` and `trim`:
|
|
75
159
|
|
|
76
160
|
```typescript
|
|
77
|
-
browser.find(By.
|
|
78
|
-
//
|
|
161
|
+
browser.find(By.text('submit', { caseSensitive: false }));
|
|
162
|
+
browser.find(By.partialText('Submit')); // lower-level alias for { exact: false }
|
|
79
163
|
```
|
|
80
164
|
|
|
81
|
-
###
|
|
165
|
+
### getByLabel(text, options?)
|
|
82
166
|
|
|
83
|
-
|
|
167
|
+
Find a form control by its associated `<label>` — whether the label wraps the
|
|
168
|
+
control or points to it with `for`.
|
|
84
169
|
|
|
85
|
-
```
|
|
86
|
-
|
|
87
|
-
|
|
170
|
+
```html
|
|
171
|
+
<label for="pw">Password</label>
|
|
172
|
+
<input id="pw" type="password" />
|
|
88
173
|
```
|
|
89
174
|
|
|
90
|
-
### By.dataAttr(name, value)
|
|
91
|
-
|
|
92
|
-
Locate by a `data-*` attribute. Pass the suffix only — `data-` is added
|
|
93
|
-
for you.
|
|
94
|
-
|
|
95
175
|
```typescript
|
|
96
|
-
browser.
|
|
97
|
-
|
|
176
|
+
browser.getByLabel('Password');
|
|
177
|
+
browser.getByLabel('Pass', { exact: false });
|
|
98
178
|
```
|
|
99
179
|
|
|
100
|
-
###
|
|
180
|
+
### getByPlaceholder(text, options?)
|
|
101
181
|
|
|
102
|
-
|
|
103
|
-
for you. Use `By.role()` / `getByRole()` for role + accessible-name
|
|
104
|
-
matching; use this helper when you need a specific ARIA state or
|
|
105
|
-
property.
|
|
182
|
+
Find an `<input>` or `<textarea>` by its placeholder.
|
|
106
183
|
|
|
107
|
-
```
|
|
108
|
-
|
|
109
|
-
// Equivalent to: browser.find('[aria-expanded="true"]')
|
|
184
|
+
```html
|
|
185
|
+
<input placeholder="Search products…" />
|
|
110
186
|
```
|
|
111
187
|
|
|
112
|
-
### By.testId(value)
|
|
113
|
-
|
|
114
|
-
Locate by `data-testid` attribute.
|
|
115
|
-
|
|
116
188
|
```typescript
|
|
117
|
-
browser.
|
|
118
|
-
// Equivalent to: browser.find('[data-testid="submit-button"]')
|
|
189
|
+
browser.getByPlaceholder('Search products…');
|
|
119
190
|
```
|
|
120
191
|
|
|
121
|
-
###
|
|
122
|
-
|
|
123
|
-
Locate by visible text content. Defaults: `exact: true`,
|
|
124
|
-
`caseSensitive: true`, `trim: true`.
|
|
192
|
+
### getByAltText(text, options?)
|
|
125
193
|
|
|
126
|
-
|
|
127
|
-
// Exact match (whitespace-normalised)
|
|
128
|
-
browser.find(By.text('Submit Order'));
|
|
194
|
+
Find an `<img>`, `<area>`, or `<input type="image">` by its `alt` text.
|
|
129
195
|
|
|
130
|
-
|
|
131
|
-
|
|
196
|
+
```html
|
|
197
|
+
<img src="/logo.svg" alt="Company logo" />
|
|
198
|
+
```
|
|
132
199
|
|
|
133
|
-
|
|
134
|
-
browser.
|
|
200
|
+
```typescript
|
|
201
|
+
browser.getByAltText('Company logo');
|
|
135
202
|
```
|
|
136
203
|
|
|
137
|
-
|
|
138
|
-
> `getByText(text, opts)` is just `By.text(text, opts)`. Pass
|
|
139
|
-
> `{ exact: false }` on either to get substring matching;
|
|
140
|
-
> `By.partialText` remains as the lower-level entry point if you prefer
|
|
141
|
-
> to be explicit.
|
|
204
|
+
### getByTitle(text, options?)
|
|
142
205
|
|
|
143
|
-
|
|
206
|
+
Find an element by its `title` attribute (the native browser tooltip).
|
|
144
207
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
208
|
+
```html
|
|
209
|
+
<button title="Delete row">🗑</button>
|
|
210
|
+
```
|
|
148
211
|
|
|
149
212
|
```typescript
|
|
150
|
-
browser.
|
|
151
|
-
browser.find(By.partialText('error', { caseSensitive: false }));
|
|
213
|
+
browser.getByTitle('Delete row');
|
|
152
214
|
```
|
|
153
215
|
|
|
154
|
-
###
|
|
216
|
+
### getByTestId(testId)
|
|
155
217
|
|
|
156
|
-
|
|
157
|
-
|
|
218
|
+
Find an element by its `data-testid`. Test ids are invisible to users and
|
|
219
|
+
untouched by design changes, so they're the most stable choice for elements with
|
|
220
|
+
no meaningful role or text.
|
|
221
|
+
|
|
222
|
+
```html
|
|
223
|
+
<button data-testid="checkout">Proceed to checkout</button>
|
|
224
|
+
```
|
|
158
225
|
|
|
159
226
|
```typescript
|
|
160
|
-
browser.
|
|
161
|
-
browser.find(By.altText('logo', { exact: false }));
|
|
227
|
+
browser.getByTestId('checkout');
|
|
162
228
|
```
|
|
163
229
|
|
|
164
|
-
|
|
230
|
+
> Each `getBy*` method has a `By.*` twin — `By.role`, `By.text`, `By.labelText`,
|
|
231
|
+
> `By.placeholder`, `By.altText`, `By.title`, `By.testId` — for when you're
|
|
232
|
+
> passing a locator into `find()`, `locator()`, `click()`, or a filter.
|
|
165
233
|
|
|
166
|
-
|
|
167
|
-
`exact: true`.
|
|
234
|
+
## Attribute and structural locators
|
|
168
235
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
```
|
|
236
|
+
When an element has no meaningful role or text, match it by id, class,
|
|
237
|
+
attribute, or position. These come as `By.*` locators (a plain string already
|
|
238
|
+
covers the CSS cases).
|
|
173
239
|
|
|
174
|
-
### By.
|
|
240
|
+
### By.id(id)
|
|
175
241
|
|
|
176
|
-
|
|
242
|
+
```html
|
|
243
|
+
<input id="email" />
|
|
244
|
+
```
|
|
177
245
|
|
|
178
246
|
```typescript
|
|
179
|
-
browser.find(By.
|
|
180
|
-
browser.find(By.xpath('//div[contains(@class, "error")]'));
|
|
247
|
+
browser.find(By.id('email')); // same as browser.find('#email')
|
|
181
248
|
```
|
|
182
249
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
CraftDriver also supports Playwright-style semantic locators directly on the browser:
|
|
186
|
-
|
|
187
|
-
### getByRole(role, options?)
|
|
250
|
+
### By.css(selector)
|
|
188
251
|
|
|
189
|
-
Locate by
|
|
252
|
+
Locate by any CSS selector — identical to passing the selector as a string.
|
|
190
253
|
|
|
191
254
|
```typescript
|
|
192
|
-
browser.
|
|
193
|
-
browser.
|
|
194
|
-
browser.getByRole('checkbox', { name: 'Remember me' });
|
|
195
|
-
browser.getByRole('link', { name: 'Learn more' });
|
|
255
|
+
browser.find(By.css('input[type="email"]'));
|
|
256
|
+
browser.find(By.css('form.login #username'));
|
|
196
257
|
```
|
|
197
258
|
|
|
198
|
-
|
|
259
|
+
### By.className(name)
|
|
199
260
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
| `exact` | `boolean` | `true` | If `false`, substring-match the accessible name. |
|
|
204
|
-
| `includeHidden` | `boolean` | `false` | Match elements with `hidden` or `aria-hidden="true"`. |
|
|
261
|
+
```html
|
|
262
|
+
<button class="primary">Save</button>
|
|
263
|
+
```
|
|
205
264
|
|
|
206
265
|
```typescript
|
|
207
|
-
//
|
|
208
|
-
browser.getByRole('button', { name: 'Close' });
|
|
209
|
-
|
|
210
|
-
// Include hidden elements (e.g., off-canvas menus, collapsed sections)
|
|
211
|
-
browser.getByRole('button', { name: 'Close', includeHidden: true });
|
|
266
|
+
browser.find(By.className('primary')); // same as browser.find('.primary')
|
|
212
267
|
```
|
|
213
268
|
|
|
214
|
-
###
|
|
269
|
+
### By.name(name)
|
|
215
270
|
|
|
216
|
-
Locate by
|
|
271
|
+
Locate by the HTML `name` attribute.
|
|
217
272
|
|
|
218
|
-
```
|
|
219
|
-
|
|
220
|
-
|
|
273
|
+
```html
|
|
274
|
+
<input name="email" />
|
|
275
|
+
```
|
|
221
276
|
|
|
222
|
-
|
|
223
|
-
browser.
|
|
277
|
+
```typescript
|
|
278
|
+
browser.find(By.name('email'));
|
|
224
279
|
```
|
|
225
280
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
> `{ exact: false }` for substring behaviour.
|
|
281
|
+
This is the `name` **attribute** — not the `name` option of
|
|
282
|
+
[`getByRole()`](#getbyrole-role-options), which matches the accessible name.
|
|
229
283
|
|
|
230
|
-
###
|
|
284
|
+
### By.tagName(tag)
|
|
231
285
|
|
|
232
|
-
|
|
286
|
+
```html
|
|
287
|
+
<h1>Page title</h1>
|
|
288
|
+
```
|
|
233
289
|
|
|
234
290
|
```typescript
|
|
235
|
-
browser.
|
|
236
|
-
browser.getByLabel('Password');
|
|
291
|
+
browser.find(By.tagName('h1')); // the first <h1>
|
|
237
292
|
```
|
|
238
293
|
|
|
239
|
-
###
|
|
294
|
+
### By.attr(name, value)
|
|
295
|
+
|
|
296
|
+
Locate by any attribute and value.
|
|
240
297
|
|
|
241
|
-
|
|
298
|
+
```html
|
|
299
|
+
<div role="dialog">…</div>
|
|
300
|
+
```
|
|
242
301
|
|
|
243
302
|
```typescript
|
|
244
|
-
browser.
|
|
245
|
-
browser.getByPlaceholder('Search...');
|
|
303
|
+
browser.find(By.attr('role', 'dialog')); // same as '[role="dialog"]'
|
|
246
304
|
```
|
|
247
305
|
|
|
248
|
-
###
|
|
306
|
+
### By.dataAttr(name, value)
|
|
249
307
|
|
|
250
|
-
Locate by `data
|
|
308
|
+
Locate by a `data-*` attribute — pass the suffix only, `data-` is added for you.
|
|
309
|
+
|
|
310
|
+
```html
|
|
311
|
+
<div data-state="open">…</div>
|
|
312
|
+
```
|
|
251
313
|
|
|
252
314
|
```typescript
|
|
253
|
-
browser.
|
|
254
|
-
browser.getByTestId('user-profile');
|
|
315
|
+
browser.find(By.dataAttr('state', 'open')); // same as '[data-state="open"]'
|
|
255
316
|
```
|
|
256
317
|
|
|
257
|
-
|
|
318
|
+
### By.aria(name, value)
|
|
258
319
|
|
|
259
|
-
|
|
320
|
+
Locate by an `aria-*` attribute — pass the suffix only, `aria-` is added for
|
|
321
|
+
you. Use this for a specific ARIA **state or property**; for role + name
|
|
322
|
+
matching use [`getByRole()`](#getbyrole-role-options).
|
|
260
323
|
|
|
261
|
-
```
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
browser.find('[data-testid="submit"]');
|
|
265
|
-
browser.getByRole('button', { name: 'Login' });
|
|
324
|
+
```html
|
|
325
|
+
<button aria-expanded="true">Menu</button>
|
|
326
|
+
```
|
|
266
327
|
|
|
267
|
-
|
|
268
|
-
browser.find('
|
|
269
|
-
browser.find('.btn.mt-4.px-6'); // CSS utility classes change
|
|
328
|
+
```typescript
|
|
329
|
+
browser.find(By.aria('expanded', 'true')); // same as '[aria-expanded="true"]'
|
|
270
330
|
```
|
|
271
331
|
|
|
272
|
-
###
|
|
332
|
+
### By.linkText(text) · By.partialLinkText(text)
|
|
273
333
|
|
|
274
|
-
|
|
334
|
+
Locate an `<a>` by its rendered text — exact for `linkText`, substring for
|
|
335
|
+
`partialLinkText`. These match only anchors, and compare the browser-rendered
|
|
336
|
+
text (so CSS `text-transform` is respected). For non-links or richer text
|
|
337
|
+
options, use [`getByText`](#getbytext-text-options) / `By.text`.
|
|
275
338
|
|
|
276
339
|
```html
|
|
277
|
-
<
|
|
340
|
+
<a href="/docs">Documentation</a>
|
|
278
341
|
```
|
|
279
342
|
|
|
280
343
|
```typescript
|
|
281
|
-
|
|
282
|
-
//
|
|
283
|
-
await browser.getByTestId('checkout-button').click();
|
|
344
|
+
browser.find(By.linkText('Documentation')); // exact anchor text
|
|
345
|
+
browser.find(By.partialLinkText('Read the')); // substring of anchor text
|
|
284
346
|
```
|
|
285
347
|
|
|
286
|
-
###
|
|
348
|
+
### By.xpath(expression)
|
|
287
349
|
|
|
288
|
-
|
|
350
|
+
The escape hatch for queries the other strategies can't express.
|
|
289
351
|
|
|
290
352
|
```typescript
|
|
291
|
-
//
|
|
292
|
-
browser.
|
|
353
|
+
browser.find(By.xpath('//button[@data-testid="submit"]'));
|
|
354
|
+
browser.find(By.xpath('//div[contains(@class, "error")]'));
|
|
293
355
|
```
|
|
294
356
|
|
|
295
|
-
##
|
|
357
|
+
## Choosing a locator
|
|
296
358
|
|
|
297
|
-
|
|
359
|
+
Prefer locators that describe intent; avoid ones that pin to incidental
|
|
360
|
+
structure.
|
|
298
361
|
|
|
299
362
|
```typescript
|
|
300
|
-
//
|
|
301
|
-
|
|
363
|
+
// ✅ Resilient — tracks what the element is
|
|
364
|
+
browser.getByRole('button', { name: 'Log in' });
|
|
365
|
+
browser.getByLabel('Email');
|
|
366
|
+
browser.getByTestId('checkout');
|
|
367
|
+
browser.find('#login-button');
|
|
302
368
|
|
|
303
|
-
//
|
|
304
|
-
|
|
305
|
-
|
|
369
|
+
// ❌ Fragile — breaks when markup or styling shifts
|
|
370
|
+
browser.find('div > div > button:nth-child(3)');
|
|
371
|
+
browser.find('.btn.mt-4.px-6'); // utility classes churn
|
|
306
372
|
```
|
|
307
373
|
|
|
374
|
+
A rough order of preference:
|
|
375
|
+
|
|
376
|
+
```
|
|
377
|
+
getByTestId > getByRole({ name }) > getByLabel >
|
|
378
|
+
getByText > By.id / By.css > By.xpath
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
The first few say *what the element is*; the last two pin to *how the page is
|
|
382
|
+
built* and break more easily. When an element is genuinely hard to select, add a
|
|
383
|
+
`data-testid` to it rather than reaching for a brittle CSS or XPath path.
|
|
384
|
+
|
|
308
385
|
## Locators (lazy & chainable)
|
|
309
386
|
|
|
310
|
-
`browser.locator(selector)` returns a `Locator` — a lazy, re-resolving handle
|
|
311
|
-
supports composition, filtering, and indexed access.
|
|
387
|
+
`browser.locator(selector)` returns a `Locator` — a lazy, re-resolving handle
|
|
388
|
+
that supports composition, filtering, and indexed access.
|
|
312
389
|
|
|
313
|
-
Use `find()` for a single one-shot element, `findAll()` for the simple array
|
|
314
|
-
and `locator()` when you need composition or want to pick from a list.
|
|
390
|
+
Use `find()` for a single one-shot element, `findAll()` for the simple array
|
|
391
|
+
case, and `locator()` when you need composition or want to pick from a list.
|
|
315
392
|
|
|
316
393
|
```typescript
|
|
317
394
|
import { Browser } from 'craftdriver';
|
|
@@ -337,26 +414,41 @@ await browser.locator('.card').nth(0).locator('button').click();
|
|
|
337
414
|
|
|
338
415
|
// Get all matching elements as snapshot handles
|
|
339
416
|
const handles = await browser.locator('.product').all();
|
|
340
|
-
const texts = await Promise.all(handles.map(h => h.text()));
|
|
417
|
+
const texts = await Promise.all(handles.map((h) => h.text()));
|
|
341
418
|
|
|
342
419
|
// Simple array shortcut (no filtering)
|
|
343
420
|
const allButtons = await browser.findAll('.buy-btn');
|
|
344
421
|
```
|
|
345
422
|
|
|
423
|
+
### Scoped semantic locators
|
|
424
|
+
|
|
425
|
+
`Locator` also exposes `getByRole` / `getByText` / `getByLabel` /
|
|
426
|
+
`getByPlaceholder` / `getByAltText` / `getByTitle` / `getByTestId`, each scoped
|
|
427
|
+
to that locator's match — the composable equivalent of the `browser.getBy*()`
|
|
428
|
+
methods above, for when you need to find something *within* a specific row,
|
|
429
|
+
card, or dialog rather than anywhere on the page.
|
|
430
|
+
|
|
431
|
+
```typescript
|
|
432
|
+
// Scope a role/text query to one product card, not the whole page
|
|
433
|
+
const card = browser.locator('.product-card').nth(2);
|
|
434
|
+
await card.getByRole('button', { name: 'Buy' }).click();
|
|
435
|
+
await card.getByText('In stock').expect().toBeVisible();
|
|
436
|
+
```
|
|
437
|
+
|
|
346
438
|
### Locator actions and state
|
|
347
439
|
|
|
348
|
-
| Method
|
|
349
|
-
|
|
|
350
|
-
| `click(options?)`
|
|
351
|
-
| `fill(text, options?)`
|
|
352
|
-
| `text(options?)`
|
|
353
|
-
| `textContent(options?)`
|
|
354
|
-
| `isVisible(options?)`
|
|
355
|
-
| `count()`
|
|
356
|
-
| `all()`
|
|
357
|
-
| `waitFor({ state, timeout? })`
|
|
358
|
-
| `expect()`
|
|
359
|
-
| `a11y`
|
|
440
|
+
| Method | Description |
|
|
441
|
+
| ------------------------------- | ----------------------------------------------------- |
|
|
442
|
+
| `click(options?)` | Click the first matching visible element |
|
|
443
|
+
| `fill(text, options?)` | Fill the first matching visible element |
|
|
444
|
+
| `text(options?)` | Get visible text |
|
|
445
|
+
| `textContent(options?)` | Alias for `text()` |
|
|
446
|
+
| `isVisible(options?)` | Return `true` if a matching element is visible |
|
|
447
|
+
| `count()` | Count current matches without auto-wait |
|
|
448
|
+
| `all()` | Return snapshot `ElementHandle`s for current matches |
|
|
449
|
+
| `waitFor({ state, timeout? })` | Wait for `attached`, `detached`, `visible`, `hidden` |
|
|
450
|
+
| `expect()` | Element assertions scoped to this locator |
|
|
451
|
+
| `a11y` | Accessibility audit scoped to this locator |
|
|
360
452
|
|
|
361
453
|
```typescript
|
|
362
454
|
await browser.locator('.toast').waitFor({ state: 'visible' });
|
|
@@ -371,33 +463,3 @@ const audit = await browser.locator('#checkout').a11y.audit();
|
|
|
371
463
|
await browser.locator('#result').expect().toHaveText('Done');
|
|
372
464
|
await browser.locator('.error').expect().not.toBeVisible();
|
|
373
465
|
```
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
### Form Controls
|
|
377
|
-
|
|
378
|
-
```typescript
|
|
379
|
-
// By label
|
|
380
|
-
await browser.getByLabel('Email').fill('test@example.com');
|
|
381
|
-
await browser.getByLabel('Password').fill('secret');
|
|
382
|
-
|
|
383
|
-
// By placeholder
|
|
384
|
-
await browser.getByPlaceholder('Search products...').fill('laptop');
|
|
385
|
-
|
|
386
|
-
// By role
|
|
387
|
-
await browser.getByRole('button', { name: 'Search' }).click();
|
|
388
|
-
```
|
|
389
|
-
|
|
390
|
-
### Navigation Links
|
|
391
|
-
|
|
392
|
-
```typescript
|
|
393
|
-
await browser.getByRole('link', { name: 'Products' }).click();
|
|
394
|
-
await browser.getByText('Contact Us').click();
|
|
395
|
-
await browser.find('nav a[href="/about"]').click();
|
|
396
|
-
```
|
|
397
|
-
|
|
398
|
-
### Buttons by Text
|
|
399
|
-
|
|
400
|
-
```typescript
|
|
401
|
-
await browser.getByRole('button', { name: 'Submit' }).click();
|
|
402
|
-
await browser.getByText('Cancel').click();
|
|
403
|
-
```
|