craftdriver 1.0.2 → 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 (64) hide show
  1. package/CHANGELOG.md +25 -3
  2. package/README.md +80 -160
  3. package/dist/lib/browser.d.ts.map +1 -1
  4. package/dist/lib/browser.js +3 -4
  5. package/dist/lib/browser.js.map +1 -1
  6. package/dist/lib/clearFastPath.d.ts +7 -0
  7. package/dist/lib/clearFastPath.d.ts.map +1 -0
  8. package/dist/lib/clearFastPath.js +37 -0
  9. package/dist/lib/clearFastPath.js.map +1 -0
  10. package/dist/lib/driver.d.ts.map +1 -1
  11. package/dist/lib/driver.js +19 -1
  12. package/dist/lib/driver.js.map +1 -1
  13. package/dist/lib/driverManager.d.ts.map +1 -1
  14. package/dist/lib/driverManager.js +67 -24
  15. package/dist/lib/driverManager.js.map +1 -1
  16. package/dist/lib/elementHandle.d.ts.map +1 -1
  17. package/dist/lib/elementHandle.js +44 -35
  18. package/dist/lib/elementHandle.js.map +1 -1
  19. package/dist/lib/errors.d.ts +1 -0
  20. package/dist/lib/errors.d.ts.map +1 -1
  21. package/dist/lib/errors.js +26 -0
  22. package/dist/lib/errors.js.map +1 -1
  23. package/dist/lib/http.d.ts +1 -1
  24. package/dist/lib/http.d.ts.map +1 -1
  25. package/dist/lib/http.js +35 -7
  26. package/dist/lib/http.js.map +1 -1
  27. package/dist/lib/timing.d.ts +13 -0
  28. package/dist/lib/timing.d.ts.map +1 -1
  29. package/dist/lib/timing.js +13 -0
  30. package/dist/lib/timing.js.map +1 -1
  31. package/dist/lib/types.d.ts +2 -0
  32. package/dist/lib/types.d.ts.map +1 -1
  33. package/docs/agents.md +92 -0
  34. package/docs/api-reference.md +108 -46
  35. package/docs/browser-api.md +1 -1
  36. package/docs/browser-context.md +4 -7
  37. package/docs/browser-logs.md +158 -0
  38. package/docs/cli.md +26 -26
  39. package/docs/element-api.md +37 -6
  40. package/docs/emulation.md +6 -2
  41. package/docs/index.md +64 -0
  42. package/docs/keyboard-mouse.md +28 -10
  43. package/docs/mobile-emulation.md +5 -2
  44. package/docs/network.md +203 -0
  45. package/docs/public/craftdriver-mark.svg +15 -0
  46. package/docs/public/social-card.svg +27 -0
  47. package/docs/recipes/accessibility-gate.md +53 -0
  48. package/docs/recipes/console-error-gate.md +57 -0
  49. package/docs/recipes/file-upload-download.md +58 -0
  50. package/docs/recipes/login-once-reuse-session.md +73 -0
  51. package/docs/recipes/mobile-flow-with-network-and-logs.md +57 -0
  52. package/docs/recipes/mock-api-and-assert-network.md +60 -0
  53. package/docs/recipes/multi-user-contexts.md +58 -0
  54. package/docs/recipes/trace-failing-test.md +64 -0
  55. package/docs/recipes/virtual-clock-time-sensitive-ui.md +60 -0
  56. package/docs/recipes/vitest-browser-lifecycle.md +56 -0
  57. package/docs/recipes.md +33 -0
  58. package/docs/selectors.md +22 -1
  59. package/docs/session-management.md +18 -1
  60. package/docs/standards.md +40 -0
  61. package/docs/why-craftdriver.md +28 -0
  62. package/docs/zero-config-drivers.md +52 -0
  63. package/package.json +19 -5
  64. package/docs/bidi-features.md +0 -470
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>
@@ -0,0 +1,53 @@
1
+ # Run Accessibility Gates
2
+
3
+ Use this pattern when CI should reject serious accessibility regressions, while
4
+ still allowing deliberate rule exceptions in one place.
5
+
6
+ ```ts
7
+ import { afterAll, beforeAll, beforeEach, describe, it } from 'vitest';
8
+ import { Browser } from 'craftdriver';
9
+
10
+ const A11Y_OPTIONS = {
11
+ minImpact: 'serious' as const,
12
+ disableRules: [
13
+ // Example: keep temporary exceptions explicit and reviewed.
14
+ 'color-contrast',
15
+ ],
16
+ };
17
+
18
+ describe('accessibility gate', () => {
19
+ let browser: Browser;
20
+
21
+ beforeAll(async () => {
22
+ browser = await Browser.launch({ browserName: 'chrome' });
23
+ });
24
+
25
+ beforeEach(async () => {
26
+ await browser.navigateTo('http://localhost:3000/settings');
27
+ });
28
+
29
+ afterAll(async () => {
30
+ await browser.quit();
31
+ });
32
+
33
+ it('passes page-level accessibility checks', async () => {
34
+ await browser.a11y.check(A11Y_OPTIONS);
35
+ });
36
+
37
+ it('passes modal accessibility checks', async () => {
38
+ await browser.getByRole('button', { name: 'Edit profile' }).click();
39
+ await browser.locator('#profile-modal').a11y.check(A11Y_OPTIONS);
40
+ });
41
+ });
42
+ ```
43
+
44
+ ## Notes
45
+
46
+ - Use `check()` when violations should fail the test.
47
+ - Use `audit()` when you want to write a report or inspect violations manually.
48
+ - Prefer scoped checks for dynamic UI such as dialogs, menus, and checkout panels.
49
+
50
+ ## Learn More
51
+
52
+ - [Accessibility](../accessibility.md)
53
+ - [Selectors](../selectors.md)
@@ -0,0 +1,57 @@
1
+ # Fail On Console And JavaScript Errors
2
+
3
+ Use this pattern when a flow can look correct in the DOM while still logging
4
+ client-side errors. Start log capture early, run the flow, then fail on
5
+ unexpected JavaScript errors.
6
+
7
+ ```ts
8
+ import { afterAll, beforeAll, beforeEach, describe, it } from 'vitest';
9
+ import { Browser } from 'craftdriver';
10
+
11
+ describe('critical browser flows', () => {
12
+ let browser: Browser;
13
+
14
+ beforeAll(async () => {
15
+ browser = await Browser.launch({
16
+ browserName: 'chrome',
17
+ captureLogs: true,
18
+ });
19
+ });
20
+
21
+ beforeEach(async () => {
22
+ browser.logs.clearLogs();
23
+ });
24
+
25
+ afterAll(async () => {
26
+ await browser.quit();
27
+ });
28
+
29
+ it('saves settings without browser errors', async () => {
30
+ await browser.navigateTo('http://localhost:3000/settings');
31
+ await browser.getByLabel('Display name').fill('Alice');
32
+ await browser.getByRole('button', { name: 'Save' }).click();
33
+
34
+ await browser.expect('#toast').toContainText('Saved');
35
+ browser.logs.assertNoErrors();
36
+ });
37
+ });
38
+ ```
39
+
40
+ ## Wait For A Known Log
41
+
42
+ When the log itself is part of the expected behavior, arm the wait before the
43
+ action that emits it:
44
+
45
+ ```ts
46
+ const saved = browser.logs.waitForConsole((message) => {
47
+ return message.level === 'info' && message.text.includes('settings:saved');
48
+ });
49
+
50
+ await browser.getByRole('button', { name: 'Save' }).click();
51
+ await saved;
52
+ ```
53
+
54
+ ## Learn More
55
+
56
+ - [Console Logs And JavaScript Errors](../browser-logs.md)
57
+ - [Tracing](../tracing.md)
@@ -0,0 +1,58 @@
1
+ # Test File Uploads And Downloads
2
+
3
+ Use this pattern when a workflow imports data, uploads an attachment, exports a
4
+ report, or verifies a generated file.
5
+
6
+ ```ts
7
+ import { afterAll, beforeAll, describe, expect, it } from 'vitest';
8
+ import { existsSync, mkdirSync, readFileSync, rmSync } from 'node:fs';
9
+ import { resolve, join } from 'node:path';
10
+ import { Browser } from 'craftdriver';
11
+
12
+ describe('reports', () => {
13
+ let browser: Browser;
14
+ const downloadsDir = resolve('.tmp/downloads');
15
+ const fixture = resolve('tests/fixtures/sample.txt');
16
+
17
+ beforeAll(async () => {
18
+ mkdirSync(downloadsDir, { recursive: true });
19
+ browser = await Browser.launch({
20
+ browserName: 'chrome',
21
+ downloadsDir,
22
+ });
23
+ });
24
+
25
+ afterAll(async () => {
26
+ await browser.quit();
27
+ rmSync(downloadsDir, { recursive: true, force: true });
28
+ });
29
+
30
+ it('uploads a source file and downloads a report', async () => {
31
+ await browser.navigateTo('http://localhost:3000/reports');
32
+
33
+ await browser.find('#source-file').setInputFiles(fixture);
34
+ await browser.expect('#upload-status').toContainText('sample.txt');
35
+
36
+ const download = await browser.waitForDownload(() => {
37
+ return browser.getByRole('button', { name: 'Export report' }).click();
38
+ });
39
+
40
+ const target = join(downloadsDir, download.suggestedFilename);
41
+ await download.saveAs(target);
42
+
43
+ expect(existsSync(target)).toBe(true);
44
+ expect(readFileSync(target, 'utf8')).toContain('Report');
45
+ });
46
+ });
47
+ ```
48
+
49
+ ## Notes
50
+
51
+ - Configure `downloadsDir` at launch so files land somewhere predictable.
52
+ - Wrap the click that triggers the download in `waitForDownload()`.
53
+ - Use `setInputFiles()` on the actual `<input type="file">` element.
54
+
55
+ ## Learn More
56
+
57
+ - [Element API](../element-api.md)
58
+ - [Browser API](../browser-api.md)
@@ -0,0 +1,73 @@
1
+ # Log In Once And Reuse The Session
2
+
3
+ Use this pattern when login is slow, rate-limited, or not the thing most tests
4
+ are trying to prove. Log in once, save cookies and localStorage, then launch
5
+ future tests already signed in.
6
+
7
+ ## Generate Auth State
8
+
9
+ Run this as a setup step before the tests that need an authenticated user.
10
+
11
+ ```ts
12
+ import { mkdir } from 'node:fs/promises';
13
+ import { Browser } from 'craftdriver';
14
+
15
+ const authState = '.auth/alice.json';
16
+
17
+ await mkdir('.auth', { recursive: true });
18
+
19
+ const browser = await Browser.launch({ browserName: 'chrome' });
20
+
21
+ try {
22
+ await browser.navigateTo('http://localhost:3000/login');
23
+ await browser.getByLabel('Email').fill('alice@example.com');
24
+ await browser.getByLabel('Password').fill(process.env.TEST_PASSWORD!);
25
+ await browser.getByRole('button', { name: 'Sign in' }).click();
26
+ await browser.expect('#account').toContainText('Alice');
27
+
28
+ await browser.saveState(authState);
29
+ } finally {
30
+ await browser.quit();
31
+ }
32
+ ```
33
+
34
+ ## Use Auth State In Tests
35
+
36
+ ```ts
37
+ import { afterAll, beforeAll, beforeEach, describe, it } from 'vitest';
38
+ import { Browser } from 'craftdriver';
39
+
40
+ describe('authenticated dashboard', () => {
41
+ let browser: Browser;
42
+
43
+ beforeAll(async () => {
44
+ browser = await Browser.launch({
45
+ browserName: 'chrome',
46
+ storageState: '.auth/alice.json',
47
+ });
48
+ });
49
+
50
+ beforeEach(async () => {
51
+ await browser.navigateTo('http://localhost:3000/dashboard');
52
+ });
53
+
54
+ afterAll(async () => {
55
+ await browser.quit();
56
+ });
57
+
58
+ it('shows account data', async () => {
59
+ await browser.expect('#account').toContainText('Alice');
60
+ });
61
+ });
62
+ ```
63
+
64
+ ## Notes
65
+
66
+ - Keep generated auth files out of source control if they contain real secrets.
67
+ - Regenerate auth state when the app changes login/session behavior.
68
+ - Use separate files such as `.auth/admin.json` and `.auth/customer.json` for different roles.
69
+
70
+ ## Learn More
71
+
72
+ - [Session Management](../session-management.md)
73
+ - [Browser Contexts](../browser-context.md)