craftdriver 0.1.0 → 0.2.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.
Files changed (186) hide show
  1. package/CHANGELOG.md +111 -0
  2. package/README.md +157 -45
  3. package/bin/craftdriver.mjs +26 -0
  4. package/dist/cli/client.d.ts +17 -0
  5. package/dist/cli/client.d.ts.map +1 -0
  6. package/dist/cli/client.js +88 -0
  7. package/dist/cli/client.js.map +1 -0
  8. package/dist/cli/daemon.d.ts +15 -0
  9. package/dist/cli/daemon.d.ts.map +1 -0
  10. package/dist/cli/daemon.js +145 -0
  11. package/dist/cli/daemon.js.map +1 -0
  12. package/dist/cli/defaults.d.ts +10 -0
  13. package/dist/cli/defaults.d.ts.map +1 -0
  14. package/dist/cli/defaults.js +42 -0
  15. package/dist/cli/defaults.js.map +1 -0
  16. package/dist/cli/dispatcher.d.ts +28 -0
  17. package/dist/cli/dispatcher.d.ts.map +1 -0
  18. package/dist/cli/dispatcher.js +319 -0
  19. package/dist/cli/dispatcher.js.map +1 -0
  20. package/dist/cli/index.d.ts +2 -0
  21. package/dist/cli/index.d.ts.map +1 -0
  22. package/dist/cli/index.js +404 -0
  23. package/dist/cli/index.js.map +1 -0
  24. package/dist/cli/init.d.ts +24 -0
  25. package/dist/cli/init.d.ts.map +1 -0
  26. package/dist/cli/init.js +192 -0
  27. package/dist/cli/init.js.map +1 -0
  28. package/dist/cli/mcp/artifacts.d.ts +43 -0
  29. package/dist/cli/mcp/artifacts.d.ts.map +1 -0
  30. package/dist/cli/mcp/artifacts.js +104 -0
  31. package/dist/cli/mcp/artifacts.js.map +1 -0
  32. package/dist/cli/mcp/server.d.ts +34 -0
  33. package/dist/cli/mcp/server.d.ts.map +1 -0
  34. package/dist/cli/mcp/server.js +214 -0
  35. package/dist/cli/mcp/server.js.map +1 -0
  36. package/dist/cli/mcp/tools.d.ts +34 -0
  37. package/dist/cli/mcp/tools.d.ts.map +1 -0
  38. package/dist/cli/mcp/tools.js +236 -0
  39. package/dist/cli/mcp/tools.js.map +1 -0
  40. package/dist/cli/parseArgs.d.ts +37 -0
  41. package/dist/cli/parseArgs.d.ts.map +1 -0
  42. package/dist/cli/parseArgs.js +215 -0
  43. package/dist/cli/parseArgs.js.map +1 -0
  44. package/dist/cli/protocol.d.ts +34 -0
  45. package/dist/cli/protocol.d.ts.map +1 -0
  46. package/dist/cli/protocol.js +2 -0
  47. package/dist/cli/protocol.js.map +1 -0
  48. package/dist/cli/selector.d.ts +29 -0
  49. package/dist/cli/selector.d.ts.map +1 -0
  50. package/dist/cli/selector.js +100 -0
  51. package/dist/cli/selector.js.map +1 -0
  52. package/dist/cli/snapshot.d.ts +48 -0
  53. package/dist/cli/snapshot.d.ts.map +1 -0
  54. package/dist/cli/snapshot.js +179 -0
  55. package/dist/cli/snapshot.js.map +1 -0
  56. package/dist/index.d.ts +14 -2
  57. package/dist/index.d.ts.map +1 -1
  58. package/dist/index.js +15 -1
  59. package/dist/index.js.map +1 -1
  60. package/dist/lib/a11y.d.ts +81 -0
  61. package/dist/lib/a11y.d.ts.map +1 -0
  62. package/dist/lib/a11y.js +171 -0
  63. package/dist/lib/a11y.js.map +1 -0
  64. package/dist/lib/bidi/index.d.ts +26 -1
  65. package/dist/lib/bidi/index.d.ts.map +1 -1
  66. package/dist/lib/bidi/index.js +49 -0
  67. package/dist/lib/bidi/index.js.map +1 -1
  68. package/dist/lib/bidi/logs.d.ts +2 -2
  69. package/dist/lib/bidi/logs.d.ts.map +1 -1
  70. package/dist/lib/bidi/logs.js +2 -2
  71. package/dist/lib/bidi/logs.js.map +1 -1
  72. package/dist/lib/bidi/network.d.ts +74 -1
  73. package/dist/lib/bidi/network.d.ts.map +1 -1
  74. package/dist/lib/bidi/network.js +222 -4
  75. package/dist/lib/bidi/network.js.map +1 -1
  76. package/dist/lib/bidi/storage.d.ts +5 -3
  77. package/dist/lib/bidi/storage.d.ts.map +1 -1
  78. package/dist/lib/bidi/storage.js +14 -7
  79. package/dist/lib/bidi/storage.js.map +1 -1
  80. package/dist/lib/bidi/types.d.ts +27 -6
  81. package/dist/lib/bidi/types.d.ts.map +1 -1
  82. package/dist/lib/browser.d.ts +637 -16
  83. package/dist/lib/browser.d.ts.map +1 -1
  84. package/dist/lib/browser.js +1428 -102
  85. package/dist/lib/browser.js.map +1 -1
  86. package/dist/lib/browserContext.d.ts +486 -0
  87. package/dist/lib/browserContext.d.ts.map +1 -0
  88. package/dist/lib/browserContext.js +937 -0
  89. package/dist/lib/browserContext.js.map +1 -0
  90. package/dist/lib/builder.d.ts +3 -0
  91. package/dist/lib/builder.d.ts.map +1 -1
  92. package/dist/lib/builder.js +31 -4
  93. package/dist/lib/builder.js.map +1 -1
  94. package/dist/lib/by.d.ts +18 -2
  95. package/dist/lib/by.d.ts.map +1 -1
  96. package/dist/lib/by.js +20 -2
  97. package/dist/lib/by.js.map +1 -1
  98. package/dist/lib/chrome.d.ts +13 -1
  99. package/dist/lib/chrome.d.ts.map +1 -1
  100. package/dist/lib/chrome.js +13 -17
  101. package/dist/lib/chrome.js.map +1 -1
  102. package/dist/lib/clock.d.ts +115 -0
  103. package/dist/lib/clock.d.ts.map +1 -0
  104. package/dist/lib/clock.js +407 -0
  105. package/dist/lib/clock.js.map +1 -0
  106. package/dist/lib/driver.d.ts +43 -0
  107. package/dist/lib/driver.d.ts.map +1 -1
  108. package/dist/lib/driver.js +120 -0
  109. package/dist/lib/driver.js.map +1 -1
  110. package/dist/lib/driverManager.d.ts +25 -0
  111. package/dist/lib/driverManager.d.ts.map +1 -0
  112. package/dist/lib/driverManager.js +399 -0
  113. package/dist/lib/driverManager.js.map +1 -0
  114. package/dist/lib/elementHandle.d.ts +67 -3
  115. package/dist/lib/elementHandle.d.ts.map +1 -1
  116. package/dist/lib/elementHandle.js +209 -67
  117. package/dist/lib/elementHandle.js.map +1 -1
  118. package/dist/lib/errors.d.ts +71 -0
  119. package/dist/lib/errors.d.ts.map +1 -0
  120. package/dist/lib/errors.js +73 -0
  121. package/dist/lib/errors.js.map +1 -0
  122. package/dist/lib/expect.d.ts +6 -1
  123. package/dist/lib/expect.d.ts.map +1 -1
  124. package/dist/lib/expect.js +55 -37
  125. package/dist/lib/expect.js.map +1 -1
  126. package/dist/lib/firefox.d.ts +18 -0
  127. package/dist/lib/firefox.d.ts.map +1 -0
  128. package/dist/lib/firefox.js +33 -0
  129. package/dist/lib/firefox.js.map +1 -0
  130. package/dist/lib/frame.d.ts +57 -0
  131. package/dist/lib/frame.d.ts.map +1 -0
  132. package/dist/lib/frame.js +277 -0
  133. package/dist/lib/frame.js.map +1 -0
  134. package/dist/lib/keyboard.d.ts +1 -1
  135. package/dist/lib/keyboard.d.ts.map +1 -1
  136. package/dist/lib/keyboard.js +1 -1
  137. package/dist/lib/keyboard.js.map +1 -1
  138. package/dist/lib/locator.d.ts +71 -2
  139. package/dist/lib/locator.d.ts.map +1 -1
  140. package/dist/lib/locator.js +289 -31
  141. package/dist/lib/locator.js.map +1 -1
  142. package/dist/lib/mouse.d.ts +1 -1
  143. package/dist/lib/mouse.d.ts.map +1 -1
  144. package/dist/lib/mouse.js +1 -1
  145. package/dist/lib/mouse.js.map +1 -1
  146. package/dist/lib/page.d.ts +107 -0
  147. package/dist/lib/page.d.ts.map +1 -0
  148. package/dist/lib/page.js +388 -0
  149. package/dist/lib/page.js.map +1 -0
  150. package/dist/lib/tracing.d.ts +128 -0
  151. package/dist/lib/tracing.d.ts.map +1 -0
  152. package/dist/lib/tracing.js +272 -0
  153. package/dist/lib/tracing.js.map +1 -0
  154. package/dist/lib/wait.d.ts.map +1 -1
  155. package/dist/lib/wait.js +14 -1
  156. package/dist/lib/wait.js.map +1 -1
  157. package/dist/lib/webelement.d.ts +2 -0
  158. package/dist/lib/webelement.d.ts.map +1 -1
  159. package/dist/lib/webelement.js +18 -0
  160. package/dist/lib/webelement.js.map +1 -1
  161. package/docs/accessibility.md +177 -0
  162. package/docs/api-reference.md +71 -0
  163. package/docs/assertions.md +182 -0
  164. package/docs/bidi-features.md +462 -0
  165. package/docs/browser-api.md +673 -0
  166. package/docs/browser-context.md +522 -0
  167. package/docs/cli.md +233 -0
  168. package/docs/clock.md +245 -0
  169. package/docs/dialogs.md +143 -0
  170. package/docs/driver-configuration.md +64 -0
  171. package/docs/element-api.md +219 -0
  172. package/docs/emulation.md +155 -0
  173. package/docs/error-codes.md +67 -0
  174. package/docs/getting-started.md +168 -0
  175. package/docs/keyboard-mouse.md +250 -0
  176. package/docs/mcp.md +250 -0
  177. package/docs/mobile-emulation.md +194 -0
  178. package/docs/screenshots.md +146 -0
  179. package/docs/selectors.md +382 -0
  180. package/docs/session-management.md +229 -0
  181. package/docs/tracing.md +282 -0
  182. package/package.json +26 -8
  183. package/skills/craftdriver/SKILL.md +84 -0
  184. package/skills/craftdriver/cheatsheet.md +194 -0
  185. package/skills/craftdriver/cli.md +104 -0
  186. package/skills/craftdriver/patterns.md +139 -0
@@ -0,0 +1,146 @@
1
+ # Screenshots
2
+
3
+ craftdriver exposes a single options-bag `screenshot()` method on both
4
+ `Browser` and `ElementHandle`. PNG bytes are always returned as a
5
+ `Buffer`; pass `path` to also write the file in one step.
6
+
7
+ ## Page screenshots
8
+
9
+ ### `browser.screenshot(opts?)`
10
+
11
+ | Option | Type | Default | Notes |
12
+ | ---------- | ------------------- | ------------ | ------------------------------------------------ |
13
+ | `path` | `string` | — | Write the PNG to this file path. |
14
+ | `selector` | `string \| By` | full viewport| If set, capture only the matching element. |
15
+ | `fullPage` | `boolean` | `false` | Capture the entire scrollable document. Requires BiDi. |
16
+ | `timeout` | `number` (ms) | default | Wait timeout when locating `selector`. |
17
+
18
+ `fullPage` and `selector` are mutually exclusive.
19
+
20
+ ```typescript
21
+ // Viewport only (default), buffer
22
+ const buffer = await browser.screenshot();
23
+
24
+ // Viewport, written to disk
25
+ await browser.screenshot({ path: 'screenshots/homepage.png' });
26
+
27
+ // Whole scrollable document (BiDi `browsingContext.captureScreenshot`
28
+ // with `origin: "document"`).
29
+ await browser.screenshot({ fullPage: true, path: 'screenshots/full.png' });
30
+
31
+ // One element by CSS selector
32
+ const chart = await browser.screenshot({ selector: '#sales-chart' });
33
+
34
+ // One element, written to disk
35
+ await browser.screenshot({
36
+ selector: '#sales-chart',
37
+ path: 'screenshots/chart.png',
38
+ });
39
+ ```
40
+
41
+ #### Full-page vs viewport
42
+
43
+ Viewport capture (`fullPage: false`, the default) uses the W3C Classic
44
+ `Take Screenshot` endpoint and is bounded by the visible viewport.
45
+ Full-page capture uses BiDi `browsingContext.captureScreenshot` with
46
+ `origin: "document"` and produces a PNG sized to the full scrollable
47
+ content — useful for visual diffs of long pages and for archiving the
48
+ complete rendered output. Full-page capture requires `enableBiDi: true`
49
+ (the default); on Classic-only sessions it throws.
50
+
51
+ ## Element screenshots
52
+
53
+ ### `element.screenshot(opts?)`
54
+
55
+ | Option | Type | Default | Notes |
56
+ | --------- | ----------- | ------- | ---------------------------------- |
57
+ | `path` | `string` | — | Write the PNG to this file path. |
58
+ | `timeout` | `number` | default | Element resolution timeout. |
59
+
60
+ ```typescript
61
+ const buffer = await browser.find('#product-image').screenshot();
62
+ await browser.find('#logo').screenshot({ path: 'logo.png' });
63
+ ```
64
+
65
+ ## Use cases
66
+
67
+ ### Test failure documentation
68
+
69
+ ```typescript
70
+ afterEach(async (context) => {
71
+ if (context.task.result?.state === 'fail') {
72
+ const name = context.task.name.replace(/\s+/g, '-');
73
+ await browser.screenshot({ path: `screenshots/${name}.png` });
74
+ }
75
+ await browser.quit();
76
+ });
77
+ ```
78
+
79
+ ### Visual comparison baseline
80
+
81
+ ```typescript
82
+ await browser.navigateTo('https://example.com');
83
+ await browser.screenshot({ path: 'baseline/homepage.png' });
84
+
85
+ // Later, capture the candidate and diff with an image library:
86
+ await browser.navigateTo('https://example.com');
87
+ await browser.screenshot({ path: 'current/homepage.png' });
88
+ ```
89
+
90
+ ### Component screenshots
91
+
92
+ ```typescript
93
+ await browser.screenshot({ selector: 'header', path: 'components/header.png' });
94
+ await browser.screenshot({ selector: '.sidebar', path: 'components/sidebar.png' });
95
+ await browser.screenshot({ selector: 'footer', path: 'components/footer.png' });
96
+ ```
97
+
98
+ ### Before / after an action
99
+
100
+ ```typescript
101
+ await browser.screenshot({ selector: '#order-form', path: 'before-submit.png' });
102
+ await browser.find('#submit-order').click();
103
+ await browser.expect('#confirmation').toBeVisible();
104
+ await browser.screenshot({ selector: '#confirmation', path: 'after-submit.png' });
105
+ ```
106
+
107
+ ## Tips
108
+
109
+ ### Create the screenshot directory
110
+
111
+ `screenshot({ path })` writes through `fs.writeFile`; the parent
112
+ directory must already exist:
113
+
114
+ ```typescript
115
+ import { mkdir } from 'fs/promises';
116
+ await mkdir('screenshots', { recursive: true });
117
+ await browser.screenshot({ path: 'screenshots/test.png' });
118
+ ```
119
+
120
+ ### Timestamped filenames
121
+
122
+ ```typescript
123
+ const ts = new Date().toISOString().replace(/[:.]/g, '-');
124
+ await browser.screenshot({ path: `screenshots/page-${ts}.png` });
125
+ ```
126
+
127
+ ### Wait for stability
128
+
129
+ ```typescript
130
+ await browser.navigateTo('https://example.com');
131
+ await browser.expect('#main-content').toBeVisible();
132
+ await browser.screenshot({ path: 'stable-page.png' });
133
+ ```
134
+
135
+ ## Buffer usage
136
+
137
+ When you only need bytes (uploading, image diffing), omit `path`:
138
+
139
+ ```typescript
140
+ const buffer = await browser.screenshot();
141
+
142
+ import { writeFile } from 'fs/promises';
143
+ await writeFile('screenshot.png', buffer);
144
+
145
+ await uploadToS3(buffer, 'screenshots/latest.png');
146
+ ```
@@ -0,0 +1,382 @@
1
+ # Selectors & Locators
2
+
3
+ CraftDriver provides multiple ways to locate elements on the page.
4
+
5
+ ## CSS Selectors
6
+
7
+ The most common way to find elements is using CSS selectors as strings:
8
+
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*()`.
12
+
13
+ ```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');
26
+ ```
27
+
28
+ ## By Locators
29
+
30
+ The `By` helper provides semantic locator strategies:
31
+
32
+ ```typescript
33
+ import { By } from 'craftdriver';
34
+ ```
35
+
36
+ ### By.css(selector)
37
+
38
+ Locate by CSS selector (equivalent to passing a string).
39
+
40
+ ```typescript
41
+ browser.find(By.css('#my-button'));
42
+ ```
43
+
44
+ ### By.id(id)
45
+
46
+ Locate by element ID.
47
+
48
+ ```typescript
49
+ browser.find(By.id('username'));
50
+ // Equivalent to: browser.find('#username')
51
+ ```
52
+
53
+ ### By.className(name)
54
+
55
+ Locate by CSS class name.
56
+
57
+ ```typescript
58
+ browser.find(By.className('btn-primary'));
59
+ // Equivalent to: browser.find('.btn-primary')
60
+ ```
61
+
62
+ ### By.name(name)
63
+
64
+ Locate by the `name` attribute (matches the canonical Selenium
65
+ `By.name` locator).
66
+
67
+ ```typescript
68
+ browser.find(By.name('email'));
69
+ // Finds: <input name="email">
70
+ ```
71
+
72
+ ### By.tagName(tag)
73
+
74
+ Locate by HTML tag name.
75
+
76
+ ```typescript
77
+ browser.find(By.tagName('h1'));
78
+ // Finds the first <h1> element
79
+ ```
80
+
81
+ ### By.attr(name, value)
82
+
83
+ Locate by an arbitrary attribute and value.
84
+
85
+ ```typescript
86
+ browser.find(By.attr('role', 'dialog'));
87
+ // Equivalent to: browser.find('[role="dialog"]')
88
+ ```
89
+
90
+ ### By.dataAttr(name, value)
91
+
92
+ Locate by a `data-*` attribute. Pass the suffix only — `data-` is added
93
+ for you.
94
+
95
+ ```typescript
96
+ browser.find(By.dataAttr('state', 'open'));
97
+ // Equivalent to: browser.find('[data-state="open"]')
98
+ ```
99
+
100
+ ### By.aria(name, value)
101
+
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.
106
+
107
+ ```typescript
108
+ browser.find(By.aria('expanded', 'true'));
109
+ // Equivalent to: browser.find('[aria-expanded="true"]')
110
+ ```
111
+
112
+ ### By.testId(value)
113
+
114
+ Locate by `data-testid` attribute.
115
+
116
+ ```typescript
117
+ browser.find(By.testId('submit-button'));
118
+ // Equivalent to: browser.find('[data-testid="submit-button"]')
119
+ ```
120
+
121
+ ### By.text(text, options?)
122
+
123
+ Locate by visible text content. Defaults: `exact: true`,
124
+ `caseSensitive: true`, `trim: true`.
125
+
126
+ ```typescript
127
+ // Exact match (whitespace-normalised)
128
+ browser.find(By.text('Submit Order'));
129
+
130
+ // Substring match — equivalent to By.partialText('Submit')
131
+ browser.find(By.text('Submit', { exact: false }));
132
+
133
+ // Case-insensitive
134
+ browser.find(By.text('submit order', { caseSensitive: false }));
135
+ ```
136
+
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.
142
+
143
+ ### By.partialText(substring, options?)
144
+
145
+ Locate the innermost element whose visible text contains `substring`.
146
+ Equivalent to `By.text(substring, { exact: false })`. Defaults:
147
+ `caseSensitive: true`, `trim: true`.
148
+
149
+ ```typescript
150
+ browser.find(By.partialText('Submit'));
151
+ browser.find(By.partialText('error', { caseSensitive: false }));
152
+ ```
153
+
154
+ ### By.altText(text, options?)
155
+
156
+ Locate `<img>`, `<area>`, or `<input type="image">` by `alt` text.
157
+ Defaults: `exact: true`.
158
+
159
+ ```typescript
160
+ browser.find(By.altText('Company logo'));
161
+ browser.find(By.altText('logo', { exact: false }));
162
+ ```
163
+
164
+ ### By.title(text, options?)
165
+
166
+ Locate by the `title` attribute (browser tooltip text). Defaults:
167
+ `exact: true`.
168
+
169
+ ```typescript
170
+ browser.find(By.title('Open in new tab'));
171
+ browser.find(By.title('Open', { exact: false }));
172
+ ```
173
+
174
+ ### By.xpath(expression)
175
+
176
+ Locate using XPath expression.
177
+
178
+ ```typescript
179
+ browser.find(By.xpath('//button[@data-testid="submit"]'));
180
+ browser.find(By.xpath('//div[contains(@class, "error")]'));
181
+ ```
182
+
183
+ ## Playwright-Style Locators
184
+
185
+ CraftDriver also supports Playwright-style semantic locators directly on the browser:
186
+
187
+ ### getByRole(role, options?)
188
+
189
+ Locate by ARIA role.
190
+
191
+ ```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' });
196
+ ```
197
+
198
+ Options:
199
+
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"`. |
205
+
206
+ ```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 });
212
+ ```
213
+
214
+ ### getByText(text, options?)
215
+
216
+ Locate by visible text.
217
+
218
+ ```typescript
219
+ // Exact match (default)
220
+ browser.getByText('Welcome to our site');
221
+
222
+ // Partial match
223
+ browser.getByText('Welcome', { exact: false });
224
+ ```
225
+
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.
229
+
230
+ ### getByLabel(text, options?)
231
+
232
+ Locate form controls by their associated label.
233
+
234
+ ```typescript
235
+ browser.getByLabel('Email address');
236
+ browser.getByLabel('Password');
237
+ ```
238
+
239
+ ### getByPlaceholder(text, options?)
240
+
241
+ Locate inputs by placeholder text.
242
+
243
+ ```typescript
244
+ browser.getByPlaceholder('Enter your email');
245
+ browser.getByPlaceholder('Search...');
246
+ ```
247
+
248
+ ### getByTestId(testId)
249
+
250
+ Locate by `data-testid` attribute.
251
+
252
+ ```typescript
253
+ browser.getByTestId('submit-button');
254
+ browser.getByTestId('user-profile');
255
+ ```
256
+
257
+ ## Selector Best Practices
258
+
259
+ ### Prefer Stable Selectors
260
+
261
+ ```typescript
262
+ // ✅ Good - specific and stable
263
+ browser.find('#login-button');
264
+ browser.find('[data-testid="submit"]');
265
+ browser.getByRole('button', { name: 'Login' });
266
+
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
270
+ ```
271
+
272
+ ### Use Test IDs for Complex UIs
273
+
274
+ Add `data-testid` attributes to elements that are hard to select:
275
+
276
+ ```html
277
+ <button class="btn btn-primary mx-2" data-testid="checkout-button">Proceed to Checkout</button>
278
+ ```
279
+
280
+ ```typescript
281
+ await browser.find('[data-testid="checkout-button"]').click();
282
+ // or
283
+ await browser.getByTestId('checkout-button').click();
284
+ ```
285
+
286
+ ### Semantic Locators for Accessibility
287
+
288
+ Using role-based locators ensures your UI is accessible:
289
+
290
+ ```typescript
291
+ // This only works if the button is properly labeled
292
+ browser.getByRole('button', { name: 'Add to cart' });
293
+ ```
294
+
295
+ ## Examples
296
+
297
+ ### Finding Multiple Elements
298
+
299
+ ```typescript
300
+ // Click the first matching element
301
+ await browser.find('.product-card .add-to-cart').click();
302
+
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();
306
+ ```
307
+
308
+ ## Locators (lazy & chainable)
309
+
310
+ `browser.locator(selector)` returns a `Locator` — a lazy, re-resolving handle that
311
+ supports composition, filtering, and indexed access.
312
+
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.
315
+
316
+ ```typescript
317
+ import { Browser } from 'craftdriver';
318
+
319
+ // Count matching elements
320
+ const n = await browser.locator('.product').count();
321
+
322
+ // Pick by index (0-based)
323
+ await browser.locator('.buy-btn').first().click();
324
+ await browser.locator('.buy-btn').last().click();
325
+ await browser.locator('.buy-btn').nth(2).click();
326
+
327
+ // Filter by text content
328
+ await browser.locator('.product').filter({ hasText: 'Pro' }).locator('.buy-btn').click();
329
+
330
+ // Filter by presence of a child locator
331
+ const hasPromo = browser.locator('.badge');
332
+ const promoCards = browser.locator('.card').filter({ has: hasPromo });
333
+ await promoCards.first().click();
334
+
335
+ // Chain into a child element
336
+ await browser.locator('.card').nth(0).locator('button').click();
337
+
338
+ // Get all matching elements as snapshot handles
339
+ const handles = await browser.locator('.product').all();
340
+ const texts = await Promise.all(handles.map(h => h.text()));
341
+
342
+ // Simple array shortcut (no filtering)
343
+ const allButtons = await browser.findAll('.buy-btn');
344
+ ```
345
+
346
+ ### Assertions on locators
347
+
348
+ ```typescript
349
+ await browser.locator('#result').expect().toHaveText('Done');
350
+ await browser.locator('.error').expect().not.toBeVisible();
351
+ ```
352
+
353
+
354
+ ### Form Controls
355
+
356
+ ```typescript
357
+ // By label
358
+ await browser.getByLabel('Email').fill('test@example.com');
359
+ await browser.getByLabel('Password').fill('secret');
360
+
361
+ // By placeholder
362
+ await browser.getByPlaceholder('Search products...').fill('laptop');
363
+
364
+ // By role
365
+ await browser.getByRole('button', { name: 'Search' }).click();
366
+ ```
367
+
368
+ ### Navigation Links
369
+
370
+ ```typescript
371
+ await browser.getByRole('link', { name: 'Products' }).click();
372
+ await browser.getByText('Contact Us').click();
373
+ await browser.find('nav a[href="/about"]').click();
374
+ ```
375
+
376
+ ### Buttons by Text
377
+
378
+ ```typescript
379
+ await browser.getByRole('button', { name: 'Submit' }).click();
380
+ await browser.getByText('Cancel').click();
381
+ await browser.find('button:contains("Save")').click();
382
+ ```