craftdriver 1.1.0 → 1.2.0

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 (68) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +1 -1
  3. package/dist/cli/snapshot.js +31 -4
  4. package/dist/cli/snapshot.js.map +1 -1
  5. package/dist/lib/bidi/index.d.ts +4 -5
  6. package/dist/lib/bidi/index.d.ts.map +1 -1
  7. package/dist/lib/bidi/index.js +18 -8
  8. package/dist/lib/bidi/index.js.map +1 -1
  9. package/dist/lib/bidi/logs.d.ts +12 -1
  10. package/dist/lib/bidi/logs.d.ts.map +1 -1
  11. package/dist/lib/bidi/logs.js +20 -8
  12. package/dist/lib/bidi/logs.js.map +1 -1
  13. package/dist/lib/browser.d.ts +6 -9
  14. package/dist/lib/browser.d.ts.map +1 -1
  15. package/dist/lib/browser.js +6 -4
  16. package/dist/lib/browser.js.map +1 -1
  17. package/dist/lib/by.d.ts +18 -0
  18. package/dist/lib/by.d.ts.map +1 -1
  19. package/dist/lib/by.js +141 -17
  20. package/dist/lib/by.js.map +1 -1
  21. package/dist/lib/locator.d.ts +28 -0
  22. package/dist/lib/locator.d.ts.map +1 -1
  23. package/dist/lib/locator.js +28 -0
  24. package/dist/lib/locator.js.map +1 -1
  25. package/docs/browser-api.md +4 -2
  26. package/docs/browser-logs.md +8 -23
  27. package/docs/public/examples/a11y.html +44 -0
  28. package/docs/public/examples/clock.html +83 -0
  29. package/docs/public/examples/console-errors.html +344 -0
  30. package/docs/public/examples/dialogs.html +42 -0
  31. package/docs/public/examples/download.html +31 -0
  32. package/docs/public/examples/dynamic.html +131 -0
  33. package/docs/public/examples/emulate.html +127 -0
  34. package/docs/public/examples/evaluate.html +28 -0
  35. package/docs/public/examples/hover-select.html +295 -0
  36. package/docs/public/examples/iframe-child.html +51 -0
  37. package/docs/public/examples/iframes.html +35 -0
  38. package/docs/public/examples/keyboard.html +195 -0
  39. package/docs/public/examples/locator.html +131 -0
  40. package/docs/public/examples/login.html +116 -0
  41. package/docs/public/examples/mobile.html +270 -0
  42. package/docs/public/examples/mouse.html +505 -0
  43. package/docs/public/examples/network.html +293 -0
  44. package/docs/public/examples/popup-target.html +26 -0
  45. package/docs/public/examples/popup.html +50 -0
  46. package/docs/public/examples/screenshot.html +99 -0
  47. package/docs/public/examples/selectors.html +150 -0
  48. package/docs/public/examples/session.html +514 -0
  49. package/docs/public/examples/upload.html +36 -0
  50. package/docs/recipes/accessibility-gate.md +24 -40
  51. package/docs/recipes/console-error-gate.md +27 -39
  52. package/docs/recipes/file-upload-download.md +24 -44
  53. package/docs/recipes/find-elements.md +142 -0
  54. package/docs/recipes/login-once-reuse-session.md +22 -45
  55. package/docs/recipes/mobile-flow-with-network-and-logs.md +22 -41
  56. package/docs/recipes/mock-api-and-assert-network.md +22 -35
  57. package/docs/recipes/multi-user-contexts.md +32 -43
  58. package/docs/recipes/page-objects.md +52 -0
  59. package/docs/recipes/trace-failing-test.md +31 -44
  60. package/docs/recipes/virtual-clock-time-sensitive-ui.md +24 -40
  61. package/docs/recipes/vitest-browser-lifecycle.md +15 -15
  62. package/docs/recipes.md +20 -1
  63. package/docs/selectors.md +291 -229
  64. package/docs/tracing.md +2 -2
  65. package/package.json +5 -1
  66. package/skills/craftdriver/SKILL.md +3 -2
  67. package/skills/craftdriver/cheatsheet.md +2 -1
  68. package/skills/craftdriver/patterns.md +2 -5
package/docs/selectors.md CHANGED
@@ -1,317 +1,394 @@
1
1
  # Selectors & Locators
2
2
 
3
- CraftDriver provides multiple ways to locate elements on the page.
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
- ## CSS Selectors
80
+ ### getByRole(role, options?)
6
81
 
7
- The most common way to find elements is using CSS selectors as strings:
82
+ Find an element by its **ARIA role** and, optionally, CraftDriver's practical
83
+ accessible-name approximation.
8
84
 
9
- > String selectors are always parsed as CSS selectors. Craftdriver does not
10
- > support Playwright-style selector strings such as `text=Save` or `role=button`.
11
- > For semantic queries, use `By.*` or `browser.getBy*()`.
85
+ ```html
86
+ <button>Sign in</button>
87
+ ```
12
88
 
13
89
  ```typescript
14
- // By ID
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
- ## By Locators
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
- The `By` helper provides semantic locator strategies:
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
- import { By } from 'craftdriver';
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
- ### By.css(selector)
37
-
38
- Locate by CSS selector (equivalent to passing a string).
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
- ```typescript
41
- browser.find(By.css('#my-button'));
42
- ```
121
+ Options:
43
122
 
44
- ### By.id(id)
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
- Locate by element ID.
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
- browser.find(By.id('username'));
50
- // Equivalent to: browser.find('#username')
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
- ### By.className(name)
143
+ ### getByText(text, options?)
54
144
 
55
- Locate by CSS class name.
145
+ Find the innermost element whose visible text matches.
56
146
 
57
- ```typescript
58
- browser.find(By.className('btn-primary'));
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.find(By.name('email'));
69
- // Finds: <input name="email">
152
+ browser.getByText('Welcome back, Alice!'); // exact (default)
153
+ browser.getByText('Welcome back', { exact: false }); // substring
70
154
  ```
71
155
 
72
- ### By.tagName(tag)
73
-
74
- Locate by HTML tag name.
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.tagName('h1'));
78
- // Finds the first <h1> element
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
- ### By.attr(name, value)
165
+ ### getByLabel(text, options?)
82
166
 
83
- Locate by an arbitrary attribute and value.
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
- ```typescript
86
- browser.find(By.attr('role', 'dialog'));
87
- // Equivalent to: browser.find('[role="dialog"]')
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.find(By.dataAttr('state', 'open'));
97
- // Equivalent to: browser.find('[data-state="open"]')
176
+ browser.getByLabel('Password');
177
+ browser.getByLabel('Pass', { exact: false });
98
178
  ```
99
179
 
100
- ### By.aria(name, value)
180
+ ### getByPlaceholder(text, options?)
101
181
 
102
- Locate by an `aria-*` attribute. Pass the suffix only — `aria-` is added
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
- ```typescript
108
- browser.find(By.aria('expanded', 'true'));
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.find(By.testId('submit-button'));
118
- // Equivalent to: browser.find('[data-testid="submit-button"]')
189
+ browser.getByPlaceholder('Search products…');
119
190
  ```
120
191
 
121
- ### By.text(text, options?)
122
-
123
- Locate by visible text content. Defaults: `exact: true`,
124
- `caseSensitive: true`, `trim: true`.
192
+ ### getByAltText(text, options?)
125
193
 
126
- ```typescript
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
- // Substring match — equivalent to By.partialText('Submit')
131
- browser.find(By.text('Submit', { exact: false }));
196
+ ```html
197
+ <img src="/logo.svg" alt="Company logo" />
198
+ ```
132
199
 
133
- // Case-insensitive
134
- browser.find(By.text('submit order', { caseSensitive: false }));
200
+ ```typescript
201
+ browser.getByAltText('Company logo');
135
202
  ```
136
203
 
137
- > **`By.text` and `getByText` are the same vocabulary.**
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
- ### By.partialText(substring, options?)
206
+ Find an element by its `title` attribute (the native browser tooltip).
144
207
 
145
- Locate the innermost element whose visible text contains `substring`.
146
- Equivalent to `By.text(substring, { exact: false })`. Defaults:
147
- `caseSensitive: true`, `trim: true`.
208
+ ```html
209
+ <button title="Delete row">🗑</button>
210
+ ```
148
211
 
149
212
  ```typescript
150
- browser.find(By.partialText('Submit'));
151
- browser.find(By.partialText('error', { caseSensitive: false }));
213
+ browser.getByTitle('Delete row');
152
214
  ```
153
215
 
154
- ### By.altText(text, options?)
216
+ ### getByTestId(testId)
155
217
 
156
- Locate `<img>`, `<area>`, or `<input type="image">` by `alt` text.
157
- Defaults: `exact: true`.
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.find(By.altText('Company logo'));
161
- browser.find(By.altText('logo', { exact: false }));
227
+ browser.getByTestId('checkout');
162
228
  ```
163
229
 
164
- ### By.title(text, options?)
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
- Locate by the `title` attribute (browser tooltip text). Defaults:
167
- `exact: true`.
234
+ ## Attribute and structural locators
168
235
 
169
- ```typescript
170
- browser.find(By.title('Open in new tab'));
171
- browser.find(By.title('Open', { exact: false }));
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.xpath(expression)
240
+ ### By.id(id)
175
241
 
176
- Locate using XPath expression.
242
+ ```html
243
+ <input id="email" />
244
+ ```
177
245
 
178
246
  ```typescript
179
- browser.find(By.xpath('//button[@data-testid="submit"]'));
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
- ## Playwright-Style Locators
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 ARIA role.
252
+ Locate by any CSS selector — identical to passing the selector as a string.
190
253
 
191
254
  ```typescript
192
- browser.getByRole('button', { name: 'Submit' });
193
- browser.getByRole('textbox', { name: 'Email' });
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
- Options:
259
+ ### By.className(name)
199
260
 
200
- | Option | Type | Default | Description |
201
- | --------------- | --------- | ------- | ----------------------------------------------------- |
202
- | `name` | `string` | — | Accessible name (`aria-label` or visible text). |
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
- // Default: skip elements marked hidden / aria-hidden
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
- ### getByText(text, options?)
269
+ ### By.name(name)
215
270
 
216
- Locate by visible text.
271
+ Locate by the HTML `name` attribute.
217
272
 
218
- ```typescript
219
- // Exact match (default)
220
- browser.getByText('Welcome to our site');
273
+ ```html
274
+ <input name="email" />
275
+ ```
221
276
 
222
- // Partial match
223
- browser.getByText('Welcome', { exact: false });
277
+ ```typescript
278
+ browser.find(By.name('email'));
224
279
  ```
225
280
 
226
- > **Heads-up for Playwright users.** Craftdriver's `getByText` defaults
227
- > to **exact** matching, while Playwright's defaults to substring. Pass
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
- ### getByLabel(text, options?)
284
+ ### By.tagName(tag)
231
285
 
232
- Locate form controls by their associated label.
286
+ ```html
287
+ <h1>Page title</h1>
288
+ ```
233
289
 
234
290
  ```typescript
235
- browser.getByLabel('Email address');
236
- browser.getByLabel('Password');
291
+ browser.find(By.tagName('h1')); // the first <h1>
237
292
  ```
238
293
 
239
- ### getByPlaceholder(text, options?)
294
+ ### By.attr(name, value)
295
+
296
+ Locate by any attribute and value.
240
297
 
241
- Locate inputs by placeholder text.
298
+ ```html
299
+ <div role="dialog">…</div>
300
+ ```
242
301
 
243
302
  ```typescript
244
- browser.getByPlaceholder('Enter your email');
245
- browser.getByPlaceholder('Search...');
303
+ browser.find(By.attr('role', 'dialog')); // same as '[role="dialog"]'
246
304
  ```
247
305
 
248
- ### getByTestId(testId)
306
+ ### By.dataAttr(name, value)
249
307
 
250
- Locate by `data-testid` attribute.
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.getByTestId('submit-button');
254
- browser.getByTestId('user-profile');
315
+ browser.find(By.dataAttr('state', 'open')); // same as '[data-state="open"]'
255
316
  ```
256
317
 
257
- ## Selector Best Practices
318
+ ### By.aria(name, value)
258
319
 
259
- ### Prefer Stable Selectors
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
- ```typescript
262
- // ✅ Good - specific and stable
263
- browser.find('#login-button');
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
- // ❌ Avoid - fragile, depends on structure
268
- browser.find('div > div > button:nth-child(3)');
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
- ### Use Test IDs for Complex UIs
332
+ ### By.linkText(text) · By.partialLinkText(text)
273
333
 
274
- Add `data-testid` attributes to elements that are hard to select:
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
- <button class="btn btn-primary mx-2" data-testid="checkout-button">Proceed to Checkout</button>
340
+ <a href="/docs">Documentation</a>
278
341
  ```
279
342
 
280
343
  ```typescript
281
- await browser.find('[data-testid="checkout-button"]').click();
282
- // or
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
- ### Semantic Locators for Accessibility
348
+ ### By.xpath(expression)
287
349
 
288
- Using role-based locators ensures your UI is accessible:
350
+ The escape hatch for queries the other strategies can't express.
289
351
 
290
352
  ```typescript
291
- // This only works if the button is properly labeled
292
- browser.getByRole('button', { name: 'Add to cart' });
353
+ browser.find(By.xpath('//button[@data-testid="submit"]'));
354
+ browser.find(By.xpath('//div[contains(@class, "error")]'));
293
355
  ```
294
356
 
295
- ## Examples
357
+ ## Choosing a locator
296
358
 
297
- ### Finding Multiple Elements
359
+ Prefer locators that describe intent; avoid ones that pin to incidental
360
+ structure.
298
361
 
299
362
  ```typescript
300
- // Click the first matching element
301
- await browser.find('.product-card .add-to-cart').click();
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
- // For multiple elements, use specific selectors
304
- await browser.find('.product-card:nth-child(1) .add-to-cart').click();
305
- await browser.find('.product-card:nth-child(2) .add-to-cart').click();
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 that
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 case,
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 | Description |
349
- | ------ | ----------- |
350
- | `click(options?)` | Click the first matching visible element |
351
- | `fill(text, options?)` | Fill the first matching visible element |
352
- | `text(options?)` | Get visible text |
353
- | `textContent(options?)` | Alias for `text()` |
354
- | `isVisible(options?)` | Return `true` if a matching element is visible |
355
- | `count()` | Count current matches without auto-wait |
356
- | `all()` | Return snapshot `ElementHandle`s for current matches |
357
- | `waitFor({ state, timeout? })` | Wait for `attached`, `detached`, `visible`, or `hidden` |
358
- | `expect()` | Element assertions scoped to this locator |
359
- | `a11y` | Accessibility audit scoped to this locator |
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
- ```