craftdriver 1.0.3 → 1.1.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 (63) hide show
  1. package/CHANGELOG.md +25 -3
  2. package/README.md +80 -160
  3. package/dist/lib/browser.d.ts +22 -0
  4. package/dist/lib/browser.d.ts.map +1 -1
  5. package/dist/lib/browser.js +14 -1
  6. package/dist/lib/browser.js.map +1 -1
  7. package/dist/lib/chrome.d.ts +15 -3
  8. package/dist/lib/chrome.d.ts.map +1 -1
  9. package/dist/lib/chrome.js +4 -2
  10. package/dist/lib/chrome.js.map +1 -1
  11. package/dist/lib/driver.d.ts.map +1 -1
  12. package/dist/lib/driver.js +2 -0
  13. package/dist/lib/driver.js.map +1 -1
  14. package/dist/lib/driverManager.d.ts +31 -0
  15. package/dist/lib/driverManager.d.ts.map +1 -1
  16. package/dist/lib/driverManager.js +165 -31
  17. package/dist/lib/driverManager.js.map +1 -1
  18. package/dist/lib/firefox.d.ts +4 -3
  19. package/dist/lib/firefox.d.ts.map +1 -1
  20. package/dist/lib/firefox.js.map +1 -1
  21. package/dist/lib/http.d.ts +1 -1
  22. package/dist/lib/http.d.ts.map +1 -1
  23. package/dist/lib/http.js +35 -7
  24. package/dist/lib/http.js.map +1 -1
  25. package/dist/lib/timing.d.ts +13 -0
  26. package/dist/lib/timing.d.ts.map +1 -1
  27. package/dist/lib/timing.js +13 -0
  28. package/dist/lib/timing.js.map +1 -1
  29. package/dist/lib/types.d.ts +2 -0
  30. package/dist/lib/types.d.ts.map +1 -1
  31. package/docs/agents.md +92 -0
  32. package/docs/api-reference.md +108 -46
  33. package/docs/browser-api.md +2 -1
  34. package/docs/browser-context.md +4 -7
  35. package/docs/browser-logs.md +158 -0
  36. package/docs/cli.md +26 -26
  37. package/docs/driver-configuration.md +103 -4
  38. package/docs/element-api.md +37 -6
  39. package/docs/emulation.md +6 -2
  40. package/docs/index.md +64 -0
  41. package/docs/keyboard-mouse.md +28 -10
  42. package/docs/mobile-emulation.md +5 -2
  43. package/docs/network.md +203 -0
  44. package/docs/public/craftdriver-mark.svg +15 -0
  45. package/docs/public/social-card.svg +27 -0
  46. package/docs/recipes/accessibility-gate.md +53 -0
  47. package/docs/recipes/console-error-gate.md +57 -0
  48. package/docs/recipes/file-upload-download.md +58 -0
  49. package/docs/recipes/login-once-reuse-session.md +73 -0
  50. package/docs/recipes/mobile-flow-with-network-and-logs.md +57 -0
  51. package/docs/recipes/mock-api-and-assert-network.md +60 -0
  52. package/docs/recipes/multi-user-contexts.md +58 -0
  53. package/docs/recipes/trace-failing-test.md +64 -0
  54. package/docs/recipes/virtual-clock-time-sensitive-ui.md +60 -0
  55. package/docs/recipes/vitest-browser-lifecycle.md +56 -0
  56. package/docs/recipes.md +33 -0
  57. package/docs/selectors.md +22 -1
  58. package/docs/session-management.md +18 -1
  59. package/docs/standards.md +40 -0
  60. package/docs/why-craftdriver.md +28 -0
  61. package/docs/zero-config-drivers.md +64 -0
  62. package/package.json +19 -5
  63. package/docs/bidi-features.md +0 -470
@@ -8,10 +8,11 @@ Craftdriver resolves the WebDriver binary through a chain — first match wins:
8
8
  | 2 | `CRAFTDRIVER_CHROMEDRIVER_PATH` / `CRAFTDRIVER_GECKODRIVER_PATH` env var |
9
9
  | 3 | `CRAFTDRIVER_DRIVER_PATH` env var (generic fallback for either browser) |
10
10
  | 4 | Legacy/Selenium-compatible env vars: `CHROMEDRIVER_PATH`, `SE_CHROMEDRIVER` (chromedriver) or `GECKODRIVER_PATH`, `GECKODRIVER_FILEPATH`, `SE_GECKODRIVER` (geckodriver) |
11
- | 5 | **Cached auto-resolution** the path a previous auto-resolve settled on, reused within the `CRAFTDRIVER_DRIVER_TTL` window |
12
- | 6 | `chromedriver` / `geckodriver` in `node_modules/.bin` |
13
- | 7 | `chromedriver` / `geckodriver` on `PATH` |
14
- | 8 | **Auto-download from Chrome for Testing / GitHub** the zero-config default |
11
+ | 5 | Known CI-provided driver directories (e.g. GitHub Actions' `CHROMEWEBDRIVER` / `GECKOWEBDRIVER`) see [CI platform detection](#ci-platform-detection) |
12
+ | 6 | **Cached auto-resolution** the path a previous auto-resolve settled on, reused within the `CRAFTDRIVER_DRIVER_TTL` window |
13
+ | 7 | `chromedriver` / `geckodriver` in `node_modules/.bin` |
14
+ | 8 | `chromedriver` / `geckodriver` on `PATH` |
15
+ | 9 | **Auto-download from Chrome for Testing / GitHub** ← the zero-config default |
15
16
 
16
17
  Downloaded drivers are cached in `~/.cache/craftdriver`, and so is the
17
18
  *resolution itself* — which driver path to use. Within the
@@ -27,6 +28,87 @@ re-downloaded if needed. Only the driver binary is ever downloaded, never the
27
28
  browser itself. Explicit configuration (steps 1–4) always takes precedence over
28
29
  the cache.
29
30
 
31
+ ## CI platform detection
32
+
33
+ On **GitHub Actions**, craftdriver recognizes the runner's own pre-installed,
34
+ version-matched driver automatically — no configuration needed. GitHub's
35
+ `ubuntu-latest`, `macos-latest`, and `windows-latest` images all publish a
36
+ driver directory via `CHROMEWEBDRIVER` / `GECKOWEBDRIVER` env vars (see
37
+ [actions/runner-images](https://github.com/actions/runner-images)); craftdriver
38
+ checks those directories as step 5 above, before falling back to its own
39
+ cache/PATH-probe/download machinery.
40
+
41
+ This is best-effort: if a future runner image drops or renames these vars,
42
+ craftdriver falls through safely to the next resolution step rather than
43
+ erroring. No other CI platform is auto-detected yet — set
44
+ `CRAFTDRIVER_CHROMEDRIVER_PATH` / `CRAFTDRIVER_GECKODRIVER_PATH` explicitly on
45
+ other providers (or to pin a non-default driver even on GitHub Actions;
46
+ explicit config always wins, per the chain above).
47
+
48
+ ## Browser Binary Configuration
49
+
50
+ Everything above resolves the **driver** (chromedriver/geckodriver). A
51
+ separate, independent chain resolves the **browser** binary itself — useful
52
+ for a non-default Chrome/Chromium install or a portable Firefox build.
53
+
54
+ The `browserPath` option itself is opt-in: craftdriver never sets it for you,
55
+ and omitting it changes nothing. Steps 4–5 below are **not** craftdriver-opt-in
56
+ the same way, though — `CHROME_BIN`, `FIREFOX_BIN`, `SE_CHROME_PATH`, and
57
+ `SE_FIREFOX_PATH` are conventions other tools (Karma, Selenium Manager) already
58
+ set in CI images and local environments for their own purposes. If one of
59
+ those happens to already be exported in your environment, craftdriver will
60
+ start forwarding it as of this version — a behavior change you didn't ask
61
+ craftdriver for, even though you didn't touch any craftdriver config either.
62
+ If you don't want that, set `CRAFTDRIVER_OFFLINE`-style pinning instead, or
63
+ simply don't rely on those four var names for anything craftdriver-adjacent.
64
+
65
+ | Step | Source |
66
+ |---|---|
67
+ | 1 | `browserPath` option in `Browser.launch()` |
68
+ | 2 | `CRAFTDRIVER_CHROME_PATH` / `CRAFTDRIVER_FIREFOX_PATH` env var |
69
+ | 3 | `CRAFTDRIVER_BROWSER_PATH` env var (generic fallback for either browser) |
70
+ | 4 | `CHROME_BIN` / `FIREFOX_BIN` env var (common CI/tooling convention, e.g. Karma) |
71
+ | 5 | `SE_CHROME_PATH` / `SE_FIREFOX_PATH` env var (Selenium Manager convention) |
72
+
73
+ Each step is validated (the file must exist) before use; an invalid path falls
74
+ through to the next step rather than being forwarded to the driver as-is (and
75
+ logs a `[craftdriver]` note to stderr when it does, so a typo doesn't silently
76
+ launch the wrong browser).
77
+
78
+ `CRAFTDRIVER_BROWSER_PATH` (step 3) is forwarded verbatim regardless of
79
+ whether the browser being launched is Chrome or Firefox — it's a single path,
80
+ so it can only ever be correct for one of them. Don't set it in a process or
81
+ CI job that launches both browsers; use the browser-specific `CRAFTDRIVER_CHROME_PATH`
82
+ / `CRAFTDRIVER_FIREFOX_PATH` instead.
83
+
84
+ ```typescript
85
+ // Launch a custom Chromium build
86
+ const browser = await Browser.launch({
87
+ browserName: 'chrome',
88
+ browserPath: '/opt/chromium-custom/chrome',
89
+ });
90
+ ```
91
+
92
+ ```bash
93
+ # Equivalent via env var — no code change needed
94
+ CRAFTDRIVER_CHROME_PATH=/opt/chromium-custom/chrome npm test
95
+ ```
96
+
97
+ If chromedriver itself also has to be auto-downloaded (no driver pinned
98
+ anywhere in the chain above), its version is detected from *this* binary
99
+ instead of probing for system Chrome — a custom Chromium build's version
100
+ routinely doesn't match system Chrome, so using the right binary for version
101
+ detection avoids downloading a mismatched chromedriver. Firefox has no
102
+ equivalent concern: geckodriver's own download isn't gated on the Firefox
103
+ version.
104
+
105
+ Version detection runs the binary with `--version` and parses its output, the
106
+ same way craftdriver already probes system Chrome/Firefox — so `browserPath`
107
+ is supported for Chrome/Chromium and Firefox builds only. Anything that
108
+ doesn't answer `--version` the way those browsers do (Electron apps and other
109
+ Chromium embedders) is out of scope for now; point `CRAFTDRIVER_CHROMEDRIVER_PATH`
110
+ at a pinned driver instead of relying on auto-detection in that case.
111
+
30
112
  ## Environment variables
31
113
 
32
114
  | Variable | Description | Default |
@@ -34,6 +116,9 @@ the cache.
34
116
  | `CRAFTDRIVER_CHROMEDRIVER_PATH` | Absolute path to a chromedriver binary | — |
35
117
  | `CRAFTDRIVER_GECKODRIVER_PATH` | Absolute path to a geckodriver binary | — |
36
118
  | `CRAFTDRIVER_DRIVER_PATH` | Generic fallback path (either browser) | — |
119
+ | `CRAFTDRIVER_CHROME_PATH` | Absolute path to a Chrome/Chromium binary — see [Browser Binary Configuration](#browser-binary-configuration) | — |
120
+ | `CRAFTDRIVER_FIREFOX_PATH` | Absolute path to a Firefox binary | — |
121
+ | `CRAFTDRIVER_BROWSER_PATH` | Generic browser-binary fallback path (either browser) | — |
37
122
  | `CRAFTDRIVER_CACHE_DIR` | Directory for cached driver downloads | `~/.cache/craftdriver` |
38
123
  | `CRAFTDRIVER_OFFLINE` | Set to `1` to disable all network calls | — |
39
124
  | `CRAFTDRIVER_DRIVER_TTL` | Driver-resolution cache lifetime, in seconds (both browsers). `0` disables the cache | `86400` (24 h) |
@@ -115,6 +200,20 @@ but the direction holds):
115
200
 
116
201
  Run it yourself with `npm run bench -- launch-critical-path`.
117
202
 
203
+ A separate micro-benchmark (`tests/perf/driver-resolution.perf.ts`) isolates
204
+ just the CI-provided driver directory step from the rest of the resolution
205
+ chain — `resolveChromeDriver()` alone, cache isolated so both paths are
206
+ measured cold:
207
+
208
+ | Scenario | Time |
209
+ |---|---|
210
+ | `CHROMEWEBDRIVER` set (new) | ~0ms (an `fs.existsSync` check) |
211
+ | PATH-probe + Chrome-version-detect fallback (old) | ~360ms |
212
+
213
+ That's the cost every cold `Browser.launch()` used to pay once per process on
214
+ a GitHub Actions runner before this step existed — now skipped entirely. Run
215
+ it yourself with `npm run bench -- driver-resolution`.
216
+
118
217
  ### Browser startup flags (advanced, opt-in)
119
218
 
120
219
  craftdriver launches the browser with **no performance flags of its own** — it
@@ -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
@@ -0,0 +1,203 @@
1
+ # Network Mocking And Request Waiting
2
+
3
+ Use CraftDriver network tools when a test needs to control or observe HTTP traffic:
4
+
5
+ - mock an API response without changing the backend
6
+ - block analytics, ads, or flaky third-party services
7
+ - add headers such as auth tokens or test markers
8
+ - wait for the request or response triggered by a click
9
+ - assert that the app sent the right method, URL, or headers
10
+
11
+ > **Requires the default browser event connection.** Network interception and
12
+ > request/response waiting require `enableBiDi: true` (the default) and a
13
+ > browser that supports WebDriver BiDi. If you launch with
14
+ > `enableBiDi: false`, these methods throw a clear `requires BiDi` error.
15
+
16
+ ## Browser Support
17
+
18
+ CraftDriver supports these network APIs on Chrome, Chromium, and Firefox.
19
+
20
+ ## Quick Mock
21
+
22
+ ```typescript
23
+ await browser.network.mock('**/api/users', {
24
+ status: 200,
25
+ body: {
26
+ users: [{ id: 1, name: 'Test User' }],
27
+ },
28
+ });
29
+
30
+ await browser.navigateTo('https://example.com/dashboard');
31
+ await browser.expect('#users').toContainText('Test User');
32
+ ```
33
+
34
+ The pattern uses URL globs. `**/api/users` matches that path on any host.
35
+
36
+ ## Common Tasks
37
+
38
+ | Goal | API |
39
+ | ---- | --- |
40
+ | Return a fake response | `browser.network.mock(pattern, response)` |
41
+ | Block matching requests | `browser.network.block(pattern)` |
42
+ | Dynamically inspect and respond | `browser.network.intercept(pattern, handler)` |
43
+ | Remove one route | `browser.network.removeIntercept(id)` |
44
+ | Remove every route | `browser.network.removeAllIntercepts()` |
45
+ | Add headers to outgoing requests | `browser.network.setExtraHeaders(headers)` |
46
+ | Bypass or restore browser cache | `browser.network.setCacheBehavior(behavior)` |
47
+ | Wait until traffic settles | `browser.network.waitForNetworkIdle(opts?)` |
48
+ | Wait for one request | `browser.waitForRequest(patternOrPredicate, opts?)` |
49
+ | Wait for one response | `browser.waitForResponse(patternOrPredicate, opts?)` |
50
+
51
+ ## Mock Responses
52
+
53
+ ### Static response
54
+
55
+ ```typescript
56
+ await browser.network.mock('**/api/login', {
57
+ status: 401,
58
+ body: { error: 'Invalid credentials' },
59
+ });
60
+
61
+ await browser.getByLabel('Username').fill('baduser');
62
+ await browser.getByLabel('Password').fill('wrongpass');
63
+ await browser.getByRole('button', { name: 'Sign in' }).click();
64
+
65
+ await browser.expect('#error').toHaveText('Invalid credentials');
66
+ ```
67
+
68
+ ### Dynamic response
69
+
70
+ ```typescript
71
+ await browser.network.mock('**/api/items/*', (request) => {
72
+ const id = request.url.split('/').pop();
73
+
74
+ return {
75
+ status: 200,
76
+ body: { id, name: `Item ${id}`, price: 9.99 },
77
+ };
78
+ });
79
+ ```
80
+
81
+ ## Block Requests
82
+
83
+ ```typescript
84
+ await browser.network.block('**/analytics/**');
85
+ await browser.network.block('**/tracking/**');
86
+ ```
87
+
88
+ This is useful when third-party services make tests slower or less predictable.
89
+
90
+ ## Add Headers
91
+
92
+ ```typescript
93
+ await browser.network.setExtraHeaders({
94
+ 'X-Test-Mode': 'true',
95
+ Authorization: 'Bearer test-token',
96
+ });
97
+ ```
98
+
99
+ Extra headers are applied to matching browser requests after they are configured.
100
+
101
+ ## Control Cache
102
+
103
+ ```typescript
104
+ await browser.network.setCacheBehavior('bypass');
105
+
106
+ // Later, restore normal browser caching.
107
+ await browser.network.setCacheBehavior('default');
108
+ ```
109
+
110
+ ## Intercept Requests
111
+
112
+ Use `intercept()` when the response depends on request details or asynchronous
113
+ test setup.
114
+
115
+ ```typescript
116
+ const seen: string[] = [];
117
+
118
+ const interceptId = await browser.network.intercept('**/api/**', async (request) => {
119
+ seen.push(`${request.method} ${request.url}`);
120
+
121
+ return {
122
+ status: 200,
123
+ body: { intercepted: true, originalUrl: request.url },
124
+ };
125
+ });
126
+
127
+ // Later, remove only this intercept.
128
+ await browser.network.removeIntercept(interceptId);
129
+ ```
130
+
131
+ You can also delay a response to test loading UI:
132
+
133
+ ```typescript
134
+ await browser.network.intercept('**/api/**', async () => {
135
+ await new Promise((resolve) => setTimeout(resolve, 3000));
136
+ return { status: 200, body: { data: 'delayed response' } };
137
+ });
138
+
139
+ await browser.getByRole('button', { name: 'Load data' }).click();
140
+ await browser.expect('#loading-spinner').toBeVisible();
141
+ ```
142
+
143
+ ## Wait For Real Traffic
144
+
145
+ `browser.waitForRequest()` and `browser.waitForResponse()` observe real network
146
+ traffic without mocking it. Register the wait before the action that triggers the
147
+ request. `Promise.all` is the safest pattern:
148
+
149
+ ```typescript
150
+ const [response] = await Promise.all([
151
+ browser.waitForResponse('**/api/users'),
152
+ browser.getByRole('button', { name: 'Load users' }).click(),
153
+ ]);
154
+
155
+ expect(response.status).toBe(200);
156
+ ```
157
+
158
+ Both methods accept a URL glob or a predicate:
159
+
160
+ ```typescript
161
+ const [response] = await Promise.all([
162
+ browser.waitForResponse((res) => {
163
+ return res.url.includes('/api/users') && res.status === 200;
164
+ }),
165
+ browser.getByRole('button', { name: 'Load users' }).click(),
166
+ ]);
167
+ ```
168
+
169
+ ### Response shape
170
+
171
+ | Property | Type | Description |
172
+ | -------- | ---- | ----------- |
173
+ | `url` | `string` | Full request URL |
174
+ | `status` | `number` | HTTP status code |
175
+ | `statusText` | `string` | Status text such as `"OK"` |
176
+ | `headers` | `Record<string, string>` | Response headers |
177
+ | `mimeType` | `string` | Response MIME type |
178
+ | `fromCache` | `boolean` | Whether the browser served it from cache |
179
+ | `request` | `{ id, url, method, headers }` | Matching request info |
180
+
181
+ ### Request shape
182
+
183
+ | Property | Type | Description |
184
+ | -------- | ---- | ----------- |
185
+ | `id` | `string` | Browser request id |
186
+ | `url` | `string` | Full URL |
187
+ | `method` | `string` | Request method, such as `"GET"` or `"POST"` |
188
+ | `headers` | `Record<string, string>` | Request headers |
189
+
190
+ ## Timeouts
191
+
192
+ Both wait methods accept `{ timeout?: number }`. The default is the browser
193
+ navigation timeout, 30 seconds.
194
+
195
+ ```typescript
196
+ await browser.waitForResponse('**/api/users', { timeout: 10_000 });
197
+ ```
198
+
199
+ On timeout, CraftDriver throws a clear error:
200
+
201
+ ```text
202
+ waitForResponse("**/api/users") timed out after 30000ms
203
+ ```
@@ -0,0 +1,15 @@
1
+ <svg width="160" height="160" viewBox="0 0 160 160" fill="none" xmlns="http://www.w3.org/2000/svg" role="img" aria-labelledby="title desc">
2
+ <title id="title">CraftDriver mark</title>
3
+ <desc id="desc">A browser window with a command prompt and connection nodes.</desc>
4
+ <rect x="12" y="24" width="136" height="112" rx="18" fill="#111827"/>
5
+ <rect x="24" y="42" width="112" height="78" rx="10" fill="#f8fafc"/>
6
+ <circle cx="34" cy="34" r="5" fill="#f97316"/>
7
+ <circle cx="50" cy="34" r="5" fill="#f59e0b"/>
8
+ <circle cx="66" cy="34" r="5" fill="#22c55e"/>
9
+ <path d="M44 78L60 64L44 50" stroke="#b45309" stroke-width="8" stroke-linecap="round" stroke-linejoin="round"/>
10
+ <path d="M70 82H104" stroke="#2563eb" stroke-width="8" stroke-linecap="round"/>
11
+ <path d="M91 60H112C121.941 60 130 68.0589 130 78V78C130 87.9411 121.941 96 112 96H91" stroke="#94a3b8" stroke-width="6" stroke-linecap="round"/>
12
+ <circle cx="88" cy="60" r="8" fill="#2563eb"/>
13
+ <circle cx="88" cy="96" r="8" fill="#f59e0b"/>
14
+ <circle cx="118" cy="78" r="8" fill="#22c55e"/>
15
+ </svg>
@@ -0,0 +1,27 @@
1
+ <svg width="1200" height="630" viewBox="0 0 1200 630" fill="none" xmlns="http://www.w3.org/2000/svg" role="img" aria-labelledby="title desc">
2
+ <title id="title">CraftDriver social card</title>
3
+ <desc id="desc">Standards-based browser automation for Node.js, tests, scripts, and AI agents.</desc>
4
+ <rect width="1200" height="630" fill="#0f172a"/>
5
+ <rect x="72" y="72" width="1056" height="486" rx="32" fill="#111827" stroke="#334155" stroke-width="2"/>
6
+ <rect x="116" y="122" width="396" height="276" rx="24" fill="#f8fafc"/>
7
+ <rect x="116" y="122" width="396" height="54" rx="24" fill="#1f2937"/>
8
+ <circle cx="154" cy="149" r="9" fill="#f97316"/>
9
+ <circle cx="184" cy="149" r="9" fill="#f59e0b"/>
10
+ <circle cx="214" cy="149" r="9" fill="#22c55e"/>
11
+ <path d="M184 274L230 232L184 190" stroke="#b45309" stroke-width="18" stroke-linecap="round" stroke-linejoin="round"/>
12
+ <path d="M264 286H382" stroke="#2563eb" stroke-width="18" stroke-linecap="round"/>
13
+ <path d="M349 210H414C446.585 210 473 236.415 473 269V269C473 301.585 446.585 328 414 328H349" stroke="#94a3b8" stroke-width="12" stroke-linecap="round"/>
14
+ <circle cx="346" cy="210" r="17" fill="#2563eb"/>
15
+ <circle cx="346" cy="328" r="17" fill="#f59e0b"/>
16
+ <circle cx="432" cy="269" r="17" fill="#22c55e"/>
17
+ <text x="574" y="212" fill="#f8fafc" font-family="Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, Segoe UI, sans-serif" font-size="82" font-weight="800">CraftDriver</text>
18
+ <text x="578" y="281" fill="#f59e0b" font-family="Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, Segoe UI, sans-serif" font-size="34" font-weight="700">WebDriver automation for humans and AI agents</text>
19
+ <text x="578" y="356" fill="#cbd5e1" font-family="Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, Segoe UI, sans-serif" font-size="30">Playwright-style ergonomics. WebDriver standards. AI friendly.</text>
20
+ <text x="578" y="408" fill="#cbd5e1" font-family="Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, Segoe UI, sans-serif" font-size="30">Library API, CLI, MCP server, and packaged agent skills.</text>
21
+ <rect x="578" y="462" width="156" height="48" rx="24" fill="#b45309"/>
22
+ <text x="614" y="495" fill="#fff7ed" font-family="Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, Segoe UI, sans-serif" font-size="22" font-weight="700">Node.js</text>
23
+ <rect x="754" y="462" width="178" height="48" rx="24" fill="#1d4ed8"/>
24
+ <text x="790" y="495" fill="#eff6ff" font-family="Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, Segoe UI, sans-serif" font-size="22" font-weight="700">WebDriver</text>
25
+ <rect x="952" y="462" width="102" height="48" rx="24" fill="#15803d"/>
26
+ <text x="984" y="495" fill="#f0fdf4" font-family="Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, Segoe UI, sans-serif" font-size="22" font-weight="700">MCP</text>
27
+ </svg>