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.
- package/CHANGELOG.md +25 -3
- package/README.md +80 -160
- package/dist/lib/browser.d.ts.map +1 -1
- package/dist/lib/browser.js +3 -4
- package/dist/lib/browser.js.map +1 -1
- package/dist/lib/clearFastPath.d.ts +7 -0
- package/dist/lib/clearFastPath.d.ts.map +1 -0
- package/dist/lib/clearFastPath.js +37 -0
- package/dist/lib/clearFastPath.js.map +1 -0
- package/dist/lib/driver.d.ts.map +1 -1
- package/dist/lib/driver.js +19 -1
- package/dist/lib/driver.js.map +1 -1
- package/dist/lib/driverManager.d.ts.map +1 -1
- package/dist/lib/driverManager.js +67 -24
- package/dist/lib/driverManager.js.map +1 -1
- package/dist/lib/elementHandle.d.ts.map +1 -1
- package/dist/lib/elementHandle.js +44 -35
- package/dist/lib/elementHandle.js.map +1 -1
- package/dist/lib/errors.d.ts +1 -0
- package/dist/lib/errors.d.ts.map +1 -1
- package/dist/lib/errors.js +26 -0
- package/dist/lib/errors.js.map +1 -1
- package/dist/lib/http.d.ts +1 -1
- package/dist/lib/http.d.ts.map +1 -1
- package/dist/lib/http.js +35 -7
- package/dist/lib/http.js.map +1 -1
- package/dist/lib/timing.d.ts +13 -0
- package/dist/lib/timing.d.ts.map +1 -1
- package/dist/lib/timing.js +13 -0
- package/dist/lib/timing.js.map +1 -1
- package/dist/lib/types.d.ts +2 -0
- package/dist/lib/types.d.ts.map +1 -1
- package/docs/agents.md +92 -0
- package/docs/api-reference.md +108 -46
- package/docs/browser-api.md +1 -1
- package/docs/browser-context.md +4 -7
- package/docs/browser-logs.md +158 -0
- package/docs/cli.md +26 -26
- package/docs/element-api.md +37 -6
- package/docs/emulation.md +6 -2
- package/docs/index.md +64 -0
- package/docs/keyboard-mouse.md +28 -10
- package/docs/mobile-emulation.md +5 -2
- package/docs/network.md +203 -0
- package/docs/public/craftdriver-mark.svg +15 -0
- package/docs/public/social-card.svg +27 -0
- package/docs/recipes/accessibility-gate.md +53 -0
- package/docs/recipes/console-error-gate.md +57 -0
- package/docs/recipes/file-upload-download.md +58 -0
- package/docs/recipes/login-once-reuse-session.md +73 -0
- package/docs/recipes/mobile-flow-with-network-and-logs.md +57 -0
- package/docs/recipes/mock-api-and-assert-network.md +60 -0
- package/docs/recipes/multi-user-contexts.md +58 -0
- package/docs/recipes/trace-failing-test.md +64 -0
- package/docs/recipes/virtual-clock-time-sensitive-ui.md +60 -0
- package/docs/recipes/vitest-browser-lifecycle.md +56 -0
- package/docs/recipes.md +33 -0
- package/docs/selectors.md +22 -1
- package/docs/session-management.md +18 -1
- package/docs/standards.md +40 -0
- package/docs/why-craftdriver.md +28 -0
- package/docs/zero-config-drivers.md +52 -0
- package/package.json +19 -5
- 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.
|
package/docs/keyboard-mouse.md
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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.
|
package/docs/mobile-emulation.md
CHANGED
|
@@ -163,9 +163,9 @@ await browser.click('#mobile-menu-button');
|
|
|
163
163
|
await browser.expect('#mobile-menu').toBeVisible();
|
|
164
164
|
```
|
|
165
165
|
|
|
166
|
-
### Combine
|
|
166
|
+
### Combine With Network Mocking And Logs
|
|
167
167
|
|
|
168
|
-
Mobile emulation works alongside
|
|
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
|
package/docs/network.md
ADDED
|
@@ -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)
|