craftdriver 1.0.3 → 1.0.4

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 (50) hide show
  1. package/CHANGELOG.md +12 -3
  2. package/README.md +80 -160
  3. package/dist/lib/driver.d.ts.map +1 -1
  4. package/dist/lib/driver.js +2 -0
  5. package/dist/lib/driver.js.map +1 -1
  6. package/dist/lib/driverManager.d.ts.map +1 -1
  7. package/dist/lib/driverManager.js +67 -24
  8. package/dist/lib/driverManager.js.map +1 -1
  9. package/dist/lib/http.d.ts +1 -1
  10. package/dist/lib/http.d.ts.map +1 -1
  11. package/dist/lib/http.js +35 -7
  12. package/dist/lib/http.js.map +1 -1
  13. package/dist/lib/timing.d.ts +13 -0
  14. package/dist/lib/timing.d.ts.map +1 -1
  15. package/dist/lib/timing.js +13 -0
  16. package/dist/lib/timing.js.map +1 -1
  17. package/dist/lib/types.d.ts +2 -0
  18. package/dist/lib/types.d.ts.map +1 -1
  19. package/docs/agents.md +92 -0
  20. package/docs/api-reference.md +108 -46
  21. package/docs/browser-api.md +1 -1
  22. package/docs/browser-context.md +4 -7
  23. package/docs/browser-logs.md +158 -0
  24. package/docs/cli.md +26 -26
  25. package/docs/element-api.md +37 -6
  26. package/docs/emulation.md +6 -2
  27. package/docs/index.md +64 -0
  28. package/docs/keyboard-mouse.md +28 -10
  29. package/docs/mobile-emulation.md +5 -2
  30. package/docs/network.md +203 -0
  31. package/docs/public/craftdriver-mark.svg +15 -0
  32. package/docs/public/social-card.svg +27 -0
  33. package/docs/recipes/accessibility-gate.md +53 -0
  34. package/docs/recipes/console-error-gate.md +57 -0
  35. package/docs/recipes/file-upload-download.md +58 -0
  36. package/docs/recipes/login-once-reuse-session.md +73 -0
  37. package/docs/recipes/mobile-flow-with-network-and-logs.md +57 -0
  38. package/docs/recipes/mock-api-and-assert-network.md +60 -0
  39. package/docs/recipes/multi-user-contexts.md +58 -0
  40. package/docs/recipes/trace-failing-test.md +64 -0
  41. package/docs/recipes/virtual-clock-time-sensitive-ui.md +60 -0
  42. package/docs/recipes/vitest-browser-lifecycle.md +56 -0
  43. package/docs/recipes.md +33 -0
  44. package/docs/selectors.md +22 -1
  45. package/docs/session-management.md +18 -1
  46. package/docs/standards.md +40 -0
  47. package/docs/why-craftdriver.md +28 -0
  48. package/docs/zero-config-drivers.md +52 -0
  49. package/package.json +19 -5
  50. package/docs/bidi-features.md +0 -470
@@ -0,0 +1,158 @@
1
+ # Console Logs And JavaScript Errors
2
+
3
+ Use `browser.logs` to capture what the page writes to the console and what
4
+ JavaScript errors the browser reports. This is useful for failing tests on
5
+ unexpected client-side errors, debugging flaky flows, or asserting that an app
6
+ emitted a known log message.
7
+
8
+ > **Requires the default browser event connection.** Console and JavaScript
9
+ > error capture requires `enableBiDi: true` (the default) and a browser that
10
+ > supports WebDriver BiDi. If you launch with `enableBiDi: false`, these
11
+ > methods throw a clear `requires BiDi` error.
12
+
13
+ ## Capture Timing
14
+
15
+ Capture is lazy by default. CraftDriver subscribes to log events the first time
16
+ you touch `browser.logs.onLog()`, `.onConsole()`, `.onError()`, `.on()`,
17
+ `.waitForConsole()`, or `.waitForError()`.
18
+
19
+ Messages emitted before that first touch are not captured, so this can return an
20
+ empty array:
21
+
22
+ ```typescript
23
+ await browser.navigateTo('https://example.com');
24
+ const messages = browser.logs.getMessages();
25
+ ```
26
+
27
+ Start capture before the action that logs:
28
+
29
+ ```typescript
30
+ const warning = browser.logs.waitForConsole((msg) => msg.level === 'warn');
31
+
32
+ await browser.getByRole('button', { name: 'Show warning' }).click();
33
+ expect((await warning).text).toContain('Heads up');
34
+ ```
35
+
36
+ Or start capture immediately at launch:
37
+
38
+ ```typescript
39
+ const browser = await Browser.launch({
40
+ browserName: 'chrome',
41
+ captureLogs: true,
42
+ });
43
+ ```
44
+
45
+ ## Read Collected Messages
46
+
47
+ ```typescript
48
+ const messages = browser.logs.getMessages();
49
+
50
+ for (const msg of messages) {
51
+ console.log(`[${msg.level}] ${msg.text}`);
52
+ }
53
+ ```
54
+
55
+ Each console message has:
56
+
57
+ | Property | Description |
58
+ | -------- | ----------- |
59
+ | `type` | Always `'console'` |
60
+ | `level` | `'debug'`, `'info'`, `'warn'`, or `'error'` |
61
+ | `text` | Message text |
62
+ | `method` | Console method, such as `'log'`, `'warn'`, or `'error'` |
63
+ | `args` | Arguments passed to `console.*` |
64
+ | `timestamp` | When the message was logged |
65
+ | `stackTrace` | Optional stack frames |
66
+
67
+ Filter by level:
68
+
69
+ ```typescript
70
+ const warnings = browser.logs.getLogsByLevel('warn');
71
+ const errors = browser.logs.getLogsByLevel('error');
72
+ ```
73
+
74
+ ## Read JavaScript Errors
75
+
76
+ ```typescript
77
+ const errors = browser.logs.getErrors();
78
+
79
+ for (const error of errors) {
80
+ console.log(`Error: ${error.text}`);
81
+ }
82
+ ```
83
+
84
+ Each JavaScript error has:
85
+
86
+ | Property | Description |
87
+ | -------- | ----------- |
88
+ | `type` | Always `'javascript'` |
89
+ | `level` | Always `'error'` |
90
+ | `text` | Error message |
91
+ | `timestamp` | When the error occurred |
92
+ | `stackTrace` | Optional stack frames with function, URL, line, and column |
93
+
94
+ ## Wait For A Log Or Error
95
+
96
+ ```typescript
97
+ const consoleMessage = browser.logs.waitForConsole((msg) => {
98
+ return msg.level === 'info' && msg.text.includes('Saved');
99
+ });
100
+
101
+ await browser.getByRole('button', { name: 'Save' }).click();
102
+ expect((await consoleMessage).text).toContain('Saved');
103
+ ```
104
+
105
+ ```typescript
106
+ const pageError = browser.logs.waitForError((error) => {
107
+ return error.text.includes('Cannot read properties');
108
+ });
109
+
110
+ await browser.getByRole('button', { name: 'Trigger error' }).click();
111
+ expect((await pageError).text).toContain('Cannot read properties');
112
+ ```
113
+
114
+ ## Subscribe In Real Time
115
+
116
+ ```typescript
117
+ const unsubscribeConsole = browser.logs.onConsole((msg) => {
118
+ if (msg.level === 'error') {
119
+ console.log('Console error:', msg.text);
120
+ }
121
+ });
122
+
123
+ const unsubscribeErrors = browser.logs.onError((error) => {
124
+ console.log('JavaScript error:', error.text);
125
+ });
126
+
127
+ // Later:
128
+ unsubscribeConsole();
129
+ unsubscribeErrors();
130
+ ```
131
+
132
+ ## Fail On Unexpected Errors
133
+
134
+ ```typescript
135
+ await browser.navigateTo('https://example.com');
136
+ await browser.getByRole('button', { name: 'Run flow' }).click();
137
+
138
+ browser.logs.assertNoErrors();
139
+ ```
140
+
141
+ You can also inspect errors yourself:
142
+
143
+ ```typescript
144
+ const errors = browser.logs.getErrors();
145
+ expect(errors).toHaveLength(0);
146
+ ```
147
+
148
+ ## Other Helpers
149
+
150
+ | Method | Description |
151
+ | ------ | ----------- |
152
+ | `onLog(handler)` | Subscribe to all log messages |
153
+ | `on(level, handler)` | Subscribe to one level: `debug`, `info`, `warn`, or `error` |
154
+ | `getLogs()` | Return all captured console and JavaScript error messages |
155
+ | `clearLogs()` | Clear all collected console messages and errors |
156
+ | `waitForConsole(predicate, timeout?)` | Wait for a matching console message |
157
+ | `waitForError(predicate?, timeout?)` | Wait for a JavaScript error |
158
+ | `assertNoErrors()` | Throw if any JavaScript errors were captured |
package/docs/cli.md CHANGED
@@ -83,21 +83,21 @@ Run `craftdriver --help` for the full list.
83
83
 
84
84
  CSS is the default. Switch kind with a `prefix=value` form:
85
85
 
86
- | Prefix | Maps to | Example |
87
- | --------------- | ---------------------------------------- | ------------------------------------ |
88
- | _none_, `css=` | `By.css` | `'.product-list li'` |
89
- | `xpath=` | `By.xpath` | `'xpath=//button[1]'` |
90
- | `role=` | `By.role` (+ `[name=...]` for the name) | `'role=button[name=Submit]'` |
91
- | `text=` | `By.text` (exact) | `'text=Sign In'` |
92
- | `text*=` | `By.partialText` | `'text*=Sign'` |
93
- | `label=` | `By.labelText` | `'label=Email'` |
94
- | `placeholder=` | `By.placeholder` | `'placeholder=name@example.com'` |
95
- | `alt=` | `By.altText` | `'alt=Logo'` |
96
- | `title=` | `By.title` | `'title=Help'` |
97
- | `testid=` | `By.testId` | `'testid=login-btn'` |
98
- | `id=` | `By.id` | `'id=submit'` |
99
- | `name=` | `By.name` | `'name=email'` |
100
- | `ref=` | snapshot ref (`craftdriver snapshot`) | `'ref=e5'` |
86
+ | Prefix | Maps to | Example |
87
+ | -------------- | --------------------------------------- | -------------------------------- |
88
+ | _none_, `css=` | `By.css` | `'.product-list li'` |
89
+ | `xpath=` | `By.xpath` | `'xpath=//button[1]'` |
90
+ | `role=` | `By.role` (+ `[name=...]` for the name) | `'role=button[name=Submit]'` |
91
+ | `text=` | `By.text` (exact) | `'text=Sign In'` |
92
+ | `text*=` | `By.partialText` | `'text*=Sign'` |
93
+ | `label=` | `By.labelText` | `'label=Email'` |
94
+ | `placeholder=` | `By.placeholder` | `'placeholder=name@example.com'` |
95
+ | `alt=` | `By.altText` | `'alt=Logo'` |
96
+ | `title=` | `By.title` | `'title=Help'` |
97
+ | `testid=` | `By.testId` | `'testid=login-btn'` |
98
+ | `id=` | `By.id` | `'id=submit'` |
99
+ | `name=` | `By.name` | `'name=email'` |
100
+ | `ref=` | snapshot ref (`craftdriver snapshot`) | `'ref=e5'` |
101
101
 
102
102
  Anything else is treated as a CSS selector, so attribute selectors with
103
103
  `=` inside (e.g. `'button[type=submit]'`) work as expected.
@@ -148,11 +148,11 @@ code: NO_MATCH
148
148
 
149
149
  ## Exit codes
150
150
 
151
- | Code | Meaning |
152
- | ---- | ----------------------------------------------------------------------- |
153
- | `0` | success (or `exists` matched at least one element) |
154
- | `1` | assertion / timeout / `NO_MATCH` / `exists` matched zero elements |
155
- | `2` | usage error (missing argument, unknown command) |
151
+ | Code | Meaning |
152
+ | ---- | ----------------------------------------------------------------- |
153
+ | `0` | success (or `exists` matched at least one element) |
154
+ | `1` | assertion / timeout / `NO_MATCH` / `exists` matched zero elements |
155
+ | `2` | usage error (missing argument, unknown command) |
156
156
 
157
157
  ## Fail-fast defaults
158
158
 
@@ -221,12 +221,12 @@ For agents that load skills explicitly (Claude Code's Skills system,
221
221
  Copilot agent customization, custom orchestrators), the npm tarball
222
222
  ships a tiered skill pack under `skills/craftdriver/`:
223
223
 
224
- | File | Purpose |
225
- | ------------------------------------------------------------- | ------------------------------------------------------------------------ |
226
- | [`SKILL.md`](../skills/craftdriver/SKILL.md) | Always-on, ≤ 500 tokens. Selector order, error-code-first, auto-wait. |
227
- | [`cheatsheet.md`](../skills/craftdriver/cheatsheet.md) | Command-by-command reference for writing tests. |
228
- | [`patterns.md`](../skills/craftdriver/patterns.md) | Worked recipes (login, upload, network-wait, a11y, tracing, clock). |
229
- | [`cli.md`](../skills/craftdriver/cli.md) | Agent-facing CLI reference. |
224
+ | File | Purpose |
225
+ | ----------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------- |
226
+ | [`SKILL.md`](https://github.com/dtopuzov/craftdriver/blob/main/skills/craftdriver/SKILL.md) | Always-on, ≤ 500 tokens. Selector order, error-code-first, auto-wait. |
227
+ | [`cheatsheet.md`](https://github.com/dtopuzov/craftdriver/blob/main/skills/craftdriver/cheatsheet.md) | Command-by-command reference for writing tests. |
228
+ | [`patterns.md`](https://github.com/dtopuzov/craftdriver/blob/main/skills/craftdriver/patterns.md) | Worked recipes (login, upload, network-wait, a11y, tracing, clock). |
229
+ | [`cli.md`](https://github.com/dtopuzov/craftdriver/blob/main/skills/craftdriver/cli.md) | Agent-facing CLI reference. |
230
230
 
231
231
  Point your agent at `node_modules/craftdriver/skills/craftdriver/SKILL.md`
232
232
  or copy it into your project. The other files are referenced from
@@ -9,8 +9,10 @@ The `ElementHandle` provides methods for interacting with individual DOM element
9
9
  const element = browser.find('#my-element');
10
10
  const button = browser.find(By.text('Submit'));
11
11
 
12
- // Chained operations
13
- await browser.find('#input').fill('text').press('Enter');
12
+ // Element-scoped operations
13
+ const input = browser.find('#input');
14
+ await input.fill('text');
15
+ await input.press('Enter');
14
16
  ```
15
17
 
16
18
  > **Tip:** Use `browser.locator(selector)` instead of `find()` when you need
@@ -99,6 +101,14 @@ Get the current value of an input element.
99
101
  const inputValue = await browser.find('#email').value();
100
102
  ```
101
103
 
104
+ ### tagName()
105
+
106
+ Get the element tag name.
107
+
108
+ ```typescript
109
+ const tag = await browser.find('#submit').tagName(); // "button"
110
+ ```
111
+
102
112
  ### getAttribute(name)
103
113
 
104
114
  Get an attribute value from the element.
@@ -166,14 +176,35 @@ await browser.find('#message').expect().toHaveText('Success');
166
176
  await browser.find('#input').expect().toHaveValue('test@example.com');
167
177
  ```
168
178
 
169
- ## Method Chaining
179
+ ### setInputFiles(filePaths, options?)
180
+
181
+ Set files on an `<input type="file">`.
182
+
183
+ ```typescript
184
+ await browser.find('#avatar').setInputFiles('/absolute/path/avatar.png');
185
+ await browser.find('#photos').setInputFiles([
186
+ '/absolute/path/one.jpg',
187
+ '/absolute/path/two.jpg',
188
+ ]);
189
+ ```
190
+
191
+ ### evaluate(fn, ...args)
170
192
 
171
- ElementHandle methods that don't return values can be chained:
193
+ Run JavaScript with this element as the first argument.
172
194
 
173
195
  ```typescript
174
- await browser.find('#username').fill('testuser').press('Tab');
196
+ const tag = await browser.find('#submit').evaluate(el => el.tagName.toLowerCase());
197
+ const active = await browser
198
+ .find('#submit')
199
+ .evaluate((el, cls) => el.classList.contains(cls), 'active');
200
+ ```
175
201
 
176
- await browser.find('#password').fill('secret123').press('Enter');
202
+ ### a11y
203
+
204
+ Run an accessibility audit scoped to this element and its descendants.
205
+
206
+ ```typescript
207
+ const result = await browser.find('#checkout').a11y.audit();
177
208
  ```
178
209
 
179
210
  ## Examples
package/docs/emulation.md CHANGED
@@ -65,7 +65,9 @@ JavaScript-only patch.
65
65
  await browser.emulate({ colorScheme: 'dark' });
66
66
  await browser.navigateTo('https://app.example.com');
67
67
 
68
- const bg = await browser.find('body').getCssValue('background-color');
68
+ const bg = await browser.evaluate(() =>
69
+ getComputedStyle(document.body).backgroundColor
70
+ );
69
71
  expect(bg).toBe('rgb(17, 17, 17)');
70
72
  ```
71
73
 
@@ -108,7 +110,9 @@ Confirm animations are actually suppressed for users who request it.
108
110
  await browser.emulate({ reducedMotion: 'reduce' });
109
111
  await browser.navigateTo('https://app.example.com');
110
112
 
111
- const anim = await browser.find('.hero').getCssValue('animation-name');
113
+ const anim = await browser.evaluate(() =>
114
+ getComputedStyle(document.querySelector('.hero')!).animationName
115
+ );
112
116
  expect(anim).toBe('none');
113
117
  ```
114
118
 
package/docs/index.md ADDED
@@ -0,0 +1,64 @@
1
+ ---
2
+ layout: home
3
+ title: CraftDriver
4
+ titleTemplate: false
5
+ hero:
6
+ name: CraftDriver
7
+ text: Crafted browser automation for Node.js.
8
+ tagline: Playwright-style ergonomics. WebDriver standards. AI friendly.
9
+ image:
10
+ src: /craftdriver-mark.svg
11
+ alt: CraftDriver
12
+ actions:
13
+ - theme: brand
14
+ text: Get Started
15
+ link: /getting-started
16
+ - theme: alt
17
+ text: API Reference
18
+ link: /api-reference
19
+ - theme: alt
20
+ text: AI Agent Guide
21
+ link: /agents
22
+ features:
23
+ - title: Control network traffic
24
+ details: Mock APIs, block noisy requests, intercept calls, and wait for real requests or responses.
25
+ - title: Reuse browser state
26
+ details: Save cookies and localStorage, launch already signed in, and isolate users with browser contexts.
27
+ - title: Test time and quality
28
+ details: Use virtual time, axe-core accessibility checks, traces, screenshots, console logs, and JS error capture.
29
+ - title: Agent-friendly
30
+ details: CLI, MCP server, and assistant rules are there when you want coding agents to drive the browser too.
31
+ ---
32
+
33
+ ## One Install
34
+
35
+ ```bash
36
+ npm install craftdriver --save-dev
37
+ ```
38
+
39
+ ```ts
40
+ import { Browser } from 'craftdriver';
41
+
42
+ const browser = await Browser.launch({ browserName: 'chrome' });
43
+
44
+ await browser.navigateTo('https://example.com/login');
45
+ await browser.getByLabel('Username').fill('alice');
46
+ await browser.getByLabel('Password').fill('hunter2');
47
+ await browser.getByRole('button', { name: 'Sign in' }).click();
48
+ await browser.expect('#result').toHaveText('Welcome alice');
49
+
50
+ await browser.quit();
51
+ ```
52
+
53
+ ## Choose Your Path
54
+
55
+ | You want to... | Start |
56
+ | --------------------------------- | --------------------------------------- |
57
+ | Write browser automation | [Getting Started](./getting-started.md) |
58
+ | Give an AI coding agent a browser | [AI Agent Guide](./agents.md) |
59
+
60
+ ## Good Stuff To Read Next
61
+
62
+ - [Zero-Config Drivers](./zero-config-drivers.md) explains driver resolution and caching.
63
+ - [WebDriver Standards](./standards.md) explains how Classic and BiDi work together.
64
+ - [Recipes](./recipes.md) collects KB-style patterns for common real-world workflows.
@@ -48,6 +48,15 @@ Release a held key.
48
48
  await browser.keyboard.up('Shift');
49
49
  ```
50
50
 
51
+ ### chord(...keys)
52
+
53
+ Press a key combination.
54
+
55
+ ```typescript
56
+ await browser.keyboard.chord('Control', 'a');
57
+ await browser.keyboard.chord('Shift', 'Tab');
58
+ ```
59
+
51
60
  ### Key Combinations
52
61
 
53
62
  Combine `down()` and `up()` for modifier key combinations:
@@ -129,23 +138,25 @@ await browser.mouse.move({ x: 150, y: 300 });
129
138
  await browser.mouse.move('#target');
130
139
  ```
131
140
 
132
- ### down()
141
+ ### down(button?)
133
142
 
134
143
  Press the mouse button.
135
144
 
136
145
  ```typescript
137
146
  await browser.mouse.down();
147
+ await browser.mouse.down('right');
138
148
  ```
139
149
 
140
- ### up()
150
+ ### up(button?)
141
151
 
142
152
  Release the mouse button.
143
153
 
144
154
  ```typescript
145
155
  await browser.mouse.up();
156
+ await browser.mouse.up('right');
146
157
  ```
147
158
 
148
- ### wheel(deltaX, deltaY)
159
+ ### wheel(deltaX, deltaY, target?)
149
160
 
150
161
  Scroll the page.
151
162
 
@@ -158,6 +169,9 @@ await browser.mouse.wheel(0, -100);
158
169
 
159
170
  // Scroll right
160
171
  await browser.mouse.wheel(100, 0);
172
+
173
+ // Scroll while targeting an element
174
+ await browser.mouse.wheel(0, 300, '#scroll-panel');
161
175
  ```
162
176
 
163
177
  ### dragAndDrop(from, to)
@@ -194,14 +208,14 @@ await browser.mouse.dragAndDrop({ x: startX, y: startY }, { x: endX, y: endY });
194
208
 
195
209
  ```typescript
196
210
  // Move to start position
197
- await browser.mouse.move(100, 100);
211
+ await browser.mouse.move({ x: 100, y: 100 });
198
212
  await browser.mouse.down();
199
213
 
200
214
  // Draw a line
201
- await browser.mouse.move(200, 100);
202
- await browser.mouse.move(200, 200);
203
- await browser.mouse.move(100, 200);
204
- await browser.mouse.move(100, 100);
215
+ await browser.mouse.move({ x: 200, y: 100 });
216
+ await browser.mouse.move({ x: 200, y: 200 });
217
+ await browser.mouse.move({ x: 100, y: 200 });
218
+ await browser.mouse.move({ x: 100, y: 100 });
205
219
 
206
220
  await browser.mouse.up();
207
221
  ```
@@ -227,10 +241,14 @@ For element-scoped key presses, use the `press()` method on ElementHandle:
227
241
 
228
242
  ```typescript
229
243
  // Press Enter in a specific input
230
- await browser.find('#search').fill('query').press('Enter');
244
+ const search = browser.find('#search');
245
+ await search.fill('query');
246
+ await search.press('Enter');
231
247
 
232
248
  // Tab out of a field
233
- await browser.find('#username').fill('user').press('Tab');
249
+ const username = browser.find('#username');
250
+ await username.fill('user');
251
+ await username.press('Tab');
234
252
  ```
235
253
 
236
254
  This is often more convenient than using the global keyboard API.
@@ -163,9 +163,9 @@ await browser.click('#mobile-menu-button');
163
163
  await browser.expect('#mobile-menu').toBeVisible();
164
164
  ```
165
165
 
166
- ### Combine with BiDi Features
166
+ ### Combine With Network Mocking And Logs
167
167
 
168
- Mobile emulation works alongside BiDi features:
168
+ Mobile emulation works alongside network mocking and browser log capture:
169
169
 
170
170
  ```typescript
171
171
  const browser = await Browser.launch({
@@ -186,6 +186,9 @@ const errors = browser.logs.getErrors();
186
186
  expect(errors).toHaveLength(0);
187
187
  ```
188
188
 
189
+ Learn more in [Network Mocking And Request Waiting](./network.md) and
190
+ [Console Logs And JavaScript Errors](./browser-logs.md).
191
+
189
192
  ## Limitations
190
193
 
191
194
  - **Chrome/Chromium only**: Mobile emulation uses Chrome-specific capabilities