craftdriver 0.1.0 → 0.2.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.
- package/CHANGELOG.md +104 -0
- package/README.md +157 -45
- package/bin/craftdriver.mjs +26 -0
- package/dist/cli/client.d.ts +17 -0
- package/dist/cli/client.d.ts.map +1 -0
- package/dist/cli/client.js +88 -0
- package/dist/cli/client.js.map +1 -0
- package/dist/cli/daemon.d.ts +15 -0
- package/dist/cli/daemon.d.ts.map +1 -0
- package/dist/cli/daemon.js +145 -0
- package/dist/cli/daemon.js.map +1 -0
- package/dist/cli/defaults.d.ts +10 -0
- package/dist/cli/defaults.d.ts.map +1 -0
- package/dist/cli/defaults.js +42 -0
- package/dist/cli/defaults.js.map +1 -0
- package/dist/cli/dispatcher.d.ts +28 -0
- package/dist/cli/dispatcher.d.ts.map +1 -0
- package/dist/cli/dispatcher.js +319 -0
- package/dist/cli/dispatcher.js.map +1 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +404 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/init.d.ts +24 -0
- package/dist/cli/init.d.ts.map +1 -0
- package/dist/cli/init.js +192 -0
- package/dist/cli/init.js.map +1 -0
- package/dist/cli/mcp/artifacts.d.ts +43 -0
- package/dist/cli/mcp/artifacts.d.ts.map +1 -0
- package/dist/cli/mcp/artifacts.js +104 -0
- package/dist/cli/mcp/artifacts.js.map +1 -0
- package/dist/cli/mcp/server.d.ts +34 -0
- package/dist/cli/mcp/server.d.ts.map +1 -0
- package/dist/cli/mcp/server.js +214 -0
- package/dist/cli/mcp/server.js.map +1 -0
- package/dist/cli/mcp/tools.d.ts +34 -0
- package/dist/cli/mcp/tools.d.ts.map +1 -0
- package/dist/cli/mcp/tools.js +236 -0
- package/dist/cli/mcp/tools.js.map +1 -0
- package/dist/cli/parseArgs.d.ts +37 -0
- package/dist/cli/parseArgs.d.ts.map +1 -0
- package/dist/cli/parseArgs.js +215 -0
- package/dist/cli/parseArgs.js.map +1 -0
- package/dist/cli/protocol.d.ts +34 -0
- package/dist/cli/protocol.d.ts.map +1 -0
- package/dist/cli/protocol.js +2 -0
- package/dist/cli/protocol.js.map +1 -0
- package/dist/cli/selector.d.ts +29 -0
- package/dist/cli/selector.d.ts.map +1 -0
- package/dist/cli/selector.js +100 -0
- package/dist/cli/selector.js.map +1 -0
- package/dist/cli/snapshot.d.ts +48 -0
- package/dist/cli/snapshot.d.ts.map +1 -0
- package/dist/cli/snapshot.js +179 -0
- package/dist/cli/snapshot.js.map +1 -0
- package/dist/index.d.ts +14 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +15 -1
- package/dist/index.js.map +1 -1
- package/dist/lib/a11y.d.ts +81 -0
- package/dist/lib/a11y.d.ts.map +1 -0
- package/dist/lib/a11y.js +171 -0
- package/dist/lib/a11y.js.map +1 -0
- package/dist/lib/bidi/index.d.ts +26 -1
- package/dist/lib/bidi/index.d.ts.map +1 -1
- package/dist/lib/bidi/index.js +49 -0
- package/dist/lib/bidi/index.js.map +1 -1
- package/dist/lib/bidi/logs.d.ts +2 -2
- package/dist/lib/bidi/logs.d.ts.map +1 -1
- package/dist/lib/bidi/logs.js +2 -2
- package/dist/lib/bidi/logs.js.map +1 -1
- package/dist/lib/bidi/network.d.ts +74 -1
- package/dist/lib/bidi/network.d.ts.map +1 -1
- package/dist/lib/bidi/network.js +222 -4
- package/dist/lib/bidi/network.js.map +1 -1
- package/dist/lib/bidi/storage.d.ts +5 -3
- package/dist/lib/bidi/storage.d.ts.map +1 -1
- package/dist/lib/bidi/storage.js +14 -7
- package/dist/lib/bidi/storage.js.map +1 -1
- package/dist/lib/bidi/types.d.ts +27 -6
- package/dist/lib/bidi/types.d.ts.map +1 -1
- package/dist/lib/browser.d.ts +637 -16
- package/dist/lib/browser.d.ts.map +1 -1
- package/dist/lib/browser.js +1428 -102
- package/dist/lib/browser.js.map +1 -1
- package/dist/lib/browserContext.d.ts +486 -0
- package/dist/lib/browserContext.d.ts.map +1 -0
- package/dist/lib/browserContext.js +937 -0
- package/dist/lib/browserContext.js.map +1 -0
- package/dist/lib/builder.d.ts +3 -0
- package/dist/lib/builder.d.ts.map +1 -1
- package/dist/lib/builder.js +31 -4
- package/dist/lib/builder.js.map +1 -1
- package/dist/lib/by.d.ts +18 -2
- package/dist/lib/by.d.ts.map +1 -1
- package/dist/lib/by.js +20 -2
- package/dist/lib/by.js.map +1 -1
- package/dist/lib/chrome.d.ts +13 -1
- package/dist/lib/chrome.d.ts.map +1 -1
- package/dist/lib/chrome.js +13 -17
- package/dist/lib/chrome.js.map +1 -1
- package/dist/lib/clock.d.ts +115 -0
- package/dist/lib/clock.d.ts.map +1 -0
- package/dist/lib/clock.js +407 -0
- package/dist/lib/clock.js.map +1 -0
- package/dist/lib/driver.d.ts +43 -0
- package/dist/lib/driver.d.ts.map +1 -1
- package/dist/lib/driver.js +120 -0
- package/dist/lib/driver.js.map +1 -1
- package/dist/lib/driverManager.d.ts +25 -0
- package/dist/lib/driverManager.d.ts.map +1 -0
- package/dist/lib/driverManager.js +399 -0
- package/dist/lib/driverManager.js.map +1 -0
- package/dist/lib/elementHandle.d.ts +67 -3
- package/dist/lib/elementHandle.d.ts.map +1 -1
- package/dist/lib/elementHandle.js +209 -67
- package/dist/lib/elementHandle.js.map +1 -1
- package/dist/lib/errors.d.ts +71 -0
- package/dist/lib/errors.d.ts.map +1 -0
- package/dist/lib/errors.js +73 -0
- package/dist/lib/errors.js.map +1 -0
- package/dist/lib/expect.d.ts +6 -1
- package/dist/lib/expect.d.ts.map +1 -1
- package/dist/lib/expect.js +55 -37
- package/dist/lib/expect.js.map +1 -1
- package/dist/lib/firefox.d.ts +18 -0
- package/dist/lib/firefox.d.ts.map +1 -0
- package/dist/lib/firefox.js +33 -0
- package/dist/lib/firefox.js.map +1 -0
- package/dist/lib/frame.d.ts +57 -0
- package/dist/lib/frame.d.ts.map +1 -0
- package/dist/lib/frame.js +277 -0
- package/dist/lib/frame.js.map +1 -0
- package/dist/lib/keyboard.d.ts +1 -1
- package/dist/lib/keyboard.d.ts.map +1 -1
- package/dist/lib/keyboard.js +1 -1
- package/dist/lib/keyboard.js.map +1 -1
- package/dist/lib/locator.d.ts +71 -2
- package/dist/lib/locator.d.ts.map +1 -1
- package/dist/lib/locator.js +289 -31
- package/dist/lib/locator.js.map +1 -1
- package/dist/lib/mouse.d.ts +1 -1
- package/dist/lib/mouse.d.ts.map +1 -1
- package/dist/lib/mouse.js +1 -1
- package/dist/lib/mouse.js.map +1 -1
- package/dist/lib/page.d.ts +107 -0
- package/dist/lib/page.d.ts.map +1 -0
- package/dist/lib/page.js +388 -0
- package/dist/lib/page.js.map +1 -0
- package/dist/lib/tracing.d.ts +128 -0
- package/dist/lib/tracing.d.ts.map +1 -0
- package/dist/lib/tracing.js +272 -0
- package/dist/lib/tracing.js.map +1 -0
- package/dist/lib/wait.d.ts.map +1 -1
- package/dist/lib/wait.js +14 -1
- package/dist/lib/wait.js.map +1 -1
- package/dist/lib/webelement.d.ts +2 -0
- package/dist/lib/webelement.d.ts.map +1 -1
- package/dist/lib/webelement.js +18 -0
- package/dist/lib/webelement.js.map +1 -1
- package/docs/accessibility.md +177 -0
- package/docs/api-reference.md +71 -0
- package/docs/assertions.md +182 -0
- package/docs/bidi-features.md +462 -0
- package/docs/browser-api.md +673 -0
- package/docs/browser-context.md +522 -0
- package/docs/cli.md +233 -0
- package/docs/clock.md +245 -0
- package/docs/dialogs.md +143 -0
- package/docs/driver-configuration.md +64 -0
- package/docs/element-api.md +219 -0
- package/docs/emulation.md +155 -0
- package/docs/error-codes.md +67 -0
- package/docs/getting-started.md +168 -0
- package/docs/keyboard-mouse.md +250 -0
- package/docs/mcp.md +250 -0
- package/docs/mobile-emulation.md +194 -0
- package/docs/screenshots.md +146 -0
- package/docs/selectors.md +382 -0
- package/docs/session-management.md +229 -0
- package/docs/tracing.md +282 -0
- package/package.json +23 -6
- package/skills/craftdriver/SKILL.md +84 -0
- package/skills/craftdriver/cheatsheet.md +194 -0
- package/skills/craftdriver/cli.md +104 -0
- package/skills/craftdriver/patterns.md +139 -0
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
# Session Management
|
|
2
|
+
|
|
3
|
+
Craftdriver supports Playwright-style session persistence, allowing you to save and restore browser state including cookies and localStorage. This is perfect for:
|
|
4
|
+
|
|
5
|
+
- **Skipping login in tests** - Log in once, reuse session across test runs
|
|
6
|
+
- **Sharing auth state** - Generate auth state in setup, use in parallel tests
|
|
7
|
+
- **Debugging** - Capture session state at any point
|
|
8
|
+
|
|
9
|
+
## Saving Session State
|
|
10
|
+
|
|
11
|
+
```typescript
|
|
12
|
+
// Save all cookies and localStorage to a file
|
|
13
|
+
await browser.saveState('./session.json');
|
|
14
|
+
|
|
15
|
+
// Save with options
|
|
16
|
+
await browser.saveState('./session.json', {
|
|
17
|
+
includeCookies: true, // default: true
|
|
18
|
+
includeLocalStorage: true, // default: true
|
|
19
|
+
includeSessionStorage: false, // default: false
|
|
20
|
+
});
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
The saved file contains:
|
|
24
|
+
|
|
25
|
+
```json
|
|
26
|
+
{
|
|
27
|
+
"cookies": [
|
|
28
|
+
{
|
|
29
|
+
"name": "session",
|
|
30
|
+
"value": "abc123",
|
|
31
|
+
"domain": "example.com",
|
|
32
|
+
"path": "/",
|
|
33
|
+
"secure": true,
|
|
34
|
+
"httpOnly": true,
|
|
35
|
+
"sameSite": "lax"
|
|
36
|
+
}
|
|
37
|
+
],
|
|
38
|
+
"localStorage": {
|
|
39
|
+
"https://example.com": {
|
|
40
|
+
"theme": "dark",
|
|
41
|
+
"userId": "12345"
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Loading Session State
|
|
48
|
+
|
|
49
|
+
```typescript
|
|
50
|
+
// Load state into current browser
|
|
51
|
+
await browser.loadState('./session.json');
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Launching with Pre-loaded State
|
|
55
|
+
|
|
56
|
+
The most common pattern - launch a browser with existing session:
|
|
57
|
+
|
|
58
|
+
```typescript
|
|
59
|
+
const browser = await Browser.launch({
|
|
60
|
+
browserName: 'chrome',
|
|
61
|
+
storageState: './session.json',
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// Navigate directly to authenticated page
|
|
65
|
+
await browser.navigateTo('https://example.com/dashboard');
|
|
66
|
+
// Already logged in!
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Cookie Management API
|
|
70
|
+
|
|
71
|
+
Direct cookie manipulation is also available:
|
|
72
|
+
|
|
73
|
+
```typescript
|
|
74
|
+
// Add a cookie
|
|
75
|
+
await browser.storage.addCookie({
|
|
76
|
+
name: 'session',
|
|
77
|
+
value: 'abc123',
|
|
78
|
+
domain: 'localhost',
|
|
79
|
+
path: '/',
|
|
80
|
+
secure: false,
|
|
81
|
+
httpOnly: true,
|
|
82
|
+
sameSite: 'Lax',
|
|
83
|
+
expiry: new Date('2025-12-31'), // or Unix timestamp
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
// Get all cookies
|
|
87
|
+
const cookies = await browser.storage.getCookies();
|
|
88
|
+
|
|
89
|
+
// Get cookies for specific domain
|
|
90
|
+
const domainCookies = await browser.storage.getCookies({ domain: 'example.com' });
|
|
91
|
+
|
|
92
|
+
// Clear all cookies
|
|
93
|
+
await browser.storage.clearCookies();
|
|
94
|
+
|
|
95
|
+
// Clear cookies by filter
|
|
96
|
+
await browser.storage.clearCookies({ name: 'session' });
|
|
97
|
+
await browser.storage.clearCookies({ domain: 'example.com' });
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## Example: Login Once, Reuse Session
|
|
101
|
+
|
|
102
|
+
### Step 1: Generate Auth State (run once)
|
|
103
|
+
|
|
104
|
+
```typescript
|
|
105
|
+
// scripts/generate-auth.ts
|
|
106
|
+
import { Browser } from 'craftdriver';
|
|
107
|
+
|
|
108
|
+
async function generateAuth() {
|
|
109
|
+
const browser = await Browser.launch({ browserName: 'chrome' });
|
|
110
|
+
|
|
111
|
+
await browser.navigateTo('https://myapp.com/login');
|
|
112
|
+
await browser.fill('#username', process.env.TEST_USER!);
|
|
113
|
+
await browser.fill('#password', process.env.TEST_PASS!);
|
|
114
|
+
await browser.click('#login-btn');
|
|
115
|
+
|
|
116
|
+
// Wait for login to complete
|
|
117
|
+
await browser.expect('#dashboard').toBeVisible();
|
|
118
|
+
|
|
119
|
+
// Save the authenticated state
|
|
120
|
+
await browser.saveState('./auth.json');
|
|
121
|
+
|
|
122
|
+
await browser.quit();
|
|
123
|
+
console.log('Auth state saved to auth.json');
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
generateAuth();
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### Step 2: Use Auth State in Tests
|
|
130
|
+
|
|
131
|
+
```typescript
|
|
132
|
+
// tests/dashboard.test.ts
|
|
133
|
+
import { describe, it, beforeEach, afterEach } from 'vitest';
|
|
134
|
+
import { Browser } from 'craftdriver';
|
|
135
|
+
|
|
136
|
+
describe('Dashboard', () => {
|
|
137
|
+
let browser: Browser;
|
|
138
|
+
|
|
139
|
+
beforeEach(async () => {
|
|
140
|
+
// Launch with saved auth - already logged in!
|
|
141
|
+
browser = await Browser.launch({
|
|
142
|
+
browserName: 'chrome',
|
|
143
|
+
storageState: './auth.json',
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
afterEach(async () => {
|
|
148
|
+
await browser.quit();
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('shows user profile', async () => {
|
|
152
|
+
// Go directly to authenticated page
|
|
153
|
+
await browser.navigateTo('https://myapp.com/profile');
|
|
154
|
+
|
|
155
|
+
// No login needed - session cookie is already set
|
|
156
|
+
await browser.expect('#username').toHaveText('testuser');
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it('can update settings', async () => {
|
|
160
|
+
await browser.navigateTo('https://myapp.com/settings');
|
|
161
|
+
await browser.click('#dark-mode-toggle');
|
|
162
|
+
await browser.expect('#theme').toHaveText('dark');
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
## Complete E2E Example
|
|
168
|
+
|
|
169
|
+
```typescript
|
|
170
|
+
import { Browser } from 'craftdriver';
|
|
171
|
+
|
|
172
|
+
async function testLoginPersistence() {
|
|
173
|
+
// First browser: Log in and save state
|
|
174
|
+
let browser = await Browser.launch({ browserName: 'chrome' });
|
|
175
|
+
|
|
176
|
+
await browser.navigateTo('http://localhost:3000/login');
|
|
177
|
+
await browser.fill('#username', 'testuser');
|
|
178
|
+
await browser.fill('#password', 'secret123');
|
|
179
|
+
await browser.click('#submit');
|
|
180
|
+
await browser.expect('#welcome').toBeVisible();
|
|
181
|
+
|
|
182
|
+
await browser.saveState('./session.json');
|
|
183
|
+
await browser.quit();
|
|
184
|
+
|
|
185
|
+
// Second browser: Load state and verify logged in
|
|
186
|
+
browser = await Browser.launch({
|
|
187
|
+
browserName: 'chrome',
|
|
188
|
+
storageState: './session.json',
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
await browser.navigateTo('http://localhost:3000/login');
|
|
192
|
+
|
|
193
|
+
// Should already be logged in!
|
|
194
|
+
await browser.expect('#welcome').toContainText('testuser');
|
|
195
|
+
|
|
196
|
+
await browser.quit();
|
|
197
|
+
}
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
## API Reference
|
|
201
|
+
|
|
202
|
+
### Browser Methods
|
|
203
|
+
|
|
204
|
+
| Method | Description |
|
|
205
|
+
| --------------------------- | ---------------------------------- |
|
|
206
|
+
| `saveState(path, options?)` | Save cookies and storage to file |
|
|
207
|
+
| `loadState(path)` | Load cookies and storage from file |
|
|
208
|
+
|
|
209
|
+
### StorageStateOptions
|
|
210
|
+
|
|
211
|
+
| Option | Type | Default | Description |
|
|
212
|
+
| ----------------------- | ---------- | ------- | ------------------------------ |
|
|
213
|
+
| `includeCookies` | `boolean` | `true` | Include cookies in saved state |
|
|
214
|
+
| `includeLocalStorage` | `boolean` | `true` | Include localStorage |
|
|
215
|
+
| `includeSessionStorage` | `boolean` | `false` | Include sessionStorage |
|
|
216
|
+
| `origins` | `string[]` | all | Limit to specific origins |
|
|
217
|
+
|
|
218
|
+
### SessionStateManager (browser.storage)
|
|
219
|
+
|
|
220
|
+
| Method | Description |
|
|
221
|
+
| --------------------------- | ---------------------------------- |
|
|
222
|
+
| `addCookie(cookie)` | Add a single cookie |
|
|
223
|
+
| `getCookies(filter?)` | Get cookies, optionally filtered |
|
|
224
|
+
| `setCookies(cookies)` | Set multiple cookies |
|
|
225
|
+
| `clearCookies(filter?)` | Clear cookies, optionally filtered |
|
|
226
|
+
| `getState(options?)` | Get current state as object |
|
|
227
|
+
| `setState(state)` | Set state from object |
|
|
228
|
+
| `saveState(path, options?)` | Save state to file |
|
|
229
|
+
| `loadState(path)` | Load state from file |
|
package/docs/tracing.md
ADDED
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
# Tracing
|
|
2
|
+
|
|
3
|
+
Capture an append-only recording of a browser session for **bug
|
|
4
|
+
investigation**. Every event is written synchronously to a line-delimited
|
|
5
|
+
JSON file as it happens, so a thrown `expect`, a failed locator, or a
|
|
6
|
+
process crash cannot lose the evidence that led up to it.
|
|
7
|
+
|
|
8
|
+
> **BiDi-only.** Tracing piggybacks on the same BiDi subscriptions used
|
|
9
|
+
> by `browser.logs` and `browser.network`. Calling `startTrace()` without
|
|
10
|
+
> `enableBiDi: true` throws.
|
|
11
|
+
|
|
12
|
+
## Quick start
|
|
13
|
+
|
|
14
|
+
```ts
|
|
15
|
+
import { chromium } from 'craftdriver';
|
|
16
|
+
|
|
17
|
+
const browser = await chromium.launch({ enableBiDi: true });
|
|
18
|
+
|
|
19
|
+
await browser.startTrace({ outDir: './artefacts/login' });
|
|
20
|
+
try {
|
|
21
|
+
await browser.navigateTo('http://127.0.0.1:8080/login.html');
|
|
22
|
+
await browser.fill('#user', 'alice');
|
|
23
|
+
await browser.click('#submit');
|
|
24
|
+
} finally {
|
|
25
|
+
await browser.stopTrace();
|
|
26
|
+
}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
After the run you get:
|
|
30
|
+
|
|
31
|
+
```
|
|
32
|
+
artefacts/login/
|
|
33
|
+
├── trace.ndjson ← one JSON event per line
|
|
34
|
+
└── screenshots/
|
|
35
|
+
├── 0001.png
|
|
36
|
+
└── 0002.png
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
`try/finally` is the recommended shape, but it's a courtesy — see the
|
|
40
|
+
next section.
|
|
41
|
+
|
|
42
|
+
## What happens when a test throws
|
|
43
|
+
|
|
44
|
+
Every recorded event hits disk **before** the next line of your test
|
|
45
|
+
runs (synchronous `writeSync` on an open file descriptor). So if your
|
|
46
|
+
test throws halfway through — `expect.toBeVisible()` times out, a click
|
|
47
|
+
misses, an a11y check fails — the partial `trace.ndjson` already
|
|
48
|
+
contains every action, console message, network event, navigation, and
|
|
49
|
+
screenshot reference that led up to the failure.
|
|
50
|
+
|
|
51
|
+
NDJSON has no closing bracket and no header. Partial files are valid
|
|
52
|
+
NDJSON: any reader simply ignores a truncated final line. The only
|
|
53
|
+
thing missing when `stopTrace()` doesn't run is a trailing
|
|
54
|
+
`{"type":"meta","endedAt":"…"}` line — readers infer the end time from
|
|
55
|
+
the last event's `t` field.
|
|
56
|
+
|
|
57
|
+
The same is true for screenshots: each PNG is a self-contained file
|
|
58
|
+
written as the capture resolves, so the ones taken before the failure
|
|
59
|
+
are intact on disk.
|
|
60
|
+
|
|
61
|
+
You don't need `try/finally` for correctness — only to write the closing
|
|
62
|
+
meta marker and close the file handle cleanly. `browser.quit()` will
|
|
63
|
+
close the handle for you on its way out.
|
|
64
|
+
|
|
65
|
+
## What gets recorded
|
|
66
|
+
|
|
67
|
+
Each line in `trace.ndjson` is one event. All events carry `t` (ms
|
|
68
|
+
since `startTrace`) and a `type`:
|
|
69
|
+
|
|
70
|
+
| `type` | Fields |
|
|
71
|
+
| -------------- | ----------------------------------------------------------- |
|
|
72
|
+
| `meta` | `startedAt` / `endedAt`, `opts` on the start line |
|
|
73
|
+
| `action` | `name`, `args?`, `selector?` |
|
|
74
|
+
| `console` | `level`, `text` |
|
|
75
|
+
| `error` | `text` (uncaught page errors) |
|
|
76
|
+
| `request` | `url`, `method`, `requestId?` |
|
|
77
|
+
| `response` | `url`, `status`, `mimeType?`, `fromCache?`, `requestId?` |
|
|
78
|
+
| `navigation` | `url`, `context?` |
|
|
79
|
+
| `screenshot` | `file` (relative path), `reason` (`'action'` \| `'error'`), `actionIndex?` |
|
|
80
|
+
|
|
81
|
+
Actions currently logged: `navigateTo`, `goBack`, `goForward`, `reload`,
|
|
82
|
+
`setContent`, `click`, `fill`, `clear`, `acceptDialog`, `dismissDialog`.
|
|
83
|
+
|
|
84
|
+
## Screenshots: evidence, not video
|
|
85
|
+
|
|
86
|
+
Screenshots are tied to **meaningful moments**, not a timer:
|
|
87
|
+
|
|
88
|
+
* Before every logged action — answers *"what did the page look like
|
|
89
|
+
when I clicked?"*
|
|
90
|
+
* On every page error — answers *"what was on screen when it broke?"*
|
|
91
|
+
|
|
92
|
+
A 30-second test with 5 clicks produces 5 PNGs, not 300. Turn it off
|
|
93
|
+
when you only want the JSON log:
|
|
94
|
+
|
|
95
|
+
```ts
|
|
96
|
+
await browser.startTrace({ outDir: './t', screenshots: 'off' });
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
`screenshots: 'auto'` (default), `true`, and omitting the option all
|
|
100
|
+
mean the same thing.
|
|
101
|
+
|
|
102
|
+
## Selectively disable pillars
|
|
103
|
+
|
|
104
|
+
Every pillar is on by default. Switch off what you don't need:
|
|
105
|
+
|
|
106
|
+
```ts
|
|
107
|
+
await browser.startTrace({
|
|
108
|
+
outDir: './t',
|
|
109
|
+
actions: true,
|
|
110
|
+
screenshots: 'auto',
|
|
111
|
+
network: false, // skip request/response events
|
|
112
|
+
console: false, // skip console + error events
|
|
113
|
+
});
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
`actions: false` keeps the timeline but stops adding action events (and
|
|
117
|
+
the screenshots that ride along with them).
|
|
118
|
+
|
|
119
|
+
## Reading a trace
|
|
120
|
+
|
|
121
|
+
NDJSON is trivial to grep, even mid-run:
|
|
122
|
+
|
|
123
|
+
```sh
|
|
124
|
+
# All errors and the action that preceded them
|
|
125
|
+
jq -c 'select(.type=="error" or .type=="action")' trace.ndjson
|
|
126
|
+
|
|
127
|
+
# Failed network responses
|
|
128
|
+
jq -c 'select(.type=="response" and .status>=400)' trace.ndjson
|
|
129
|
+
|
|
130
|
+
# Find the screenshot for action #7
|
|
131
|
+
jq -c 'select(.actionIndex==7)' trace.ndjson
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
```ts
|
|
135
|
+
// Programmatic:
|
|
136
|
+
import { readFileSync } from 'node:fs';
|
|
137
|
+
const events = readFileSync('trace.ndjson', 'utf8')
|
|
138
|
+
.split('\n').filter(Boolean).map((l) => JSON.parse(l));
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
## Using with Vitest
|
|
142
|
+
|
|
143
|
+
Vitest doesn't have Playwright-style fixtures, but a small helper using
|
|
144
|
+
its `beforeEach` / `afterEach` hooks gets you **automatic per-test
|
|
145
|
+
tracing with keep-on-failure**. One line per `describe()`, no per-test
|
|
146
|
+
boilerplate.
|
|
147
|
+
|
|
148
|
+
Drop this into your test folder (e.g. `tests/auto-trace.ts`):
|
|
149
|
+
|
|
150
|
+
```ts
|
|
151
|
+
import { beforeEach, afterEach } from 'vitest';
|
|
152
|
+
import { rmSync, writeFileSync } from 'node:fs';
|
|
153
|
+
import { join } from 'node:path';
|
|
154
|
+
import type { Browser, TraceScreenshotMode } from 'craftdriver';
|
|
155
|
+
|
|
156
|
+
const ROOT = process.env.CRAFTDRIVER_TRACE_DIR ?? './traces';
|
|
157
|
+
const MODE = (process.env.CRAFTDRIVER_TRACE ?? 'on-failure') as
|
|
158
|
+
'always' | 'on-failure' | 'off';
|
|
159
|
+
const SHOTS = (process.env.CRAFTDRIVER_TRACE_SCREENSHOTS ?? 'auto') as
|
|
160
|
+
TraceScreenshotMode;
|
|
161
|
+
|
|
162
|
+
export function autoTrace(getBrowser: () => Browser): void {
|
|
163
|
+
if (MODE === 'off') return;
|
|
164
|
+
let currentDir = '';
|
|
165
|
+
|
|
166
|
+
beforeEach(async ({ task }) => {
|
|
167
|
+
currentDir = join(ROOT, safeName(task));
|
|
168
|
+
await getBrowser().startTrace({ outDir: currentDir, screenshots: SHOTS });
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
afterEach(async ({ task }) => {
|
|
172
|
+
const failed = task.result?.state === 'fail';
|
|
173
|
+
// Snap the page as it looked at the failed assertion (not just
|
|
174
|
+
// before the last click).
|
|
175
|
+
if (failed) {
|
|
176
|
+
try {
|
|
177
|
+
const buf = await getBrowser().screenshot();
|
|
178
|
+
writeFileSync(join(currentDir, 'final.png'), buf);
|
|
179
|
+
} catch { /* browser may already be dead */ }
|
|
180
|
+
}
|
|
181
|
+
try { await getBrowser().stopTrace(); } catch { return; }
|
|
182
|
+
if (MODE === 'on-failure' && !failed) {
|
|
183
|
+
rmSync(currentDir, { recursive: true, force: true });
|
|
184
|
+
} else if (failed) {
|
|
185
|
+
console.error(` 📁 trace: ${currentDir}`);
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function safeName(t: { name: string; suite?: { name: string } | null }): string {
|
|
191
|
+
const parts: string[] = [];
|
|
192
|
+
let s: typeof t | null | undefined = t;
|
|
193
|
+
while (s && s.name) { parts.unshift(s.name); s = s.suite; }
|
|
194
|
+
return parts.join('/').replace(/[^a-z0-9/]+/gi, '-').toLowerCase();
|
|
195
|
+
}
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
Use it inside any `describe()`:
|
|
199
|
+
|
|
200
|
+
```ts
|
|
201
|
+
import { autoTrace } from './auto-trace';
|
|
202
|
+
|
|
203
|
+
describe('Login', () => {
|
|
204
|
+
let browser: Browser;
|
|
205
|
+
beforeAll(async () => { browser = await Browser.launch({ enableBiDi: true }); });
|
|
206
|
+
afterAll(async () => { await browser.quit(); });
|
|
207
|
+
|
|
208
|
+
autoTrace(() => browser); // ← that's it
|
|
209
|
+
|
|
210
|
+
it('signs in', async () => {
|
|
211
|
+
await browser.navigateTo('http://127.0.0.1:8080/login.html');
|
|
212
|
+
await browser.fill('#user', 'alice');
|
|
213
|
+
await browser.click('#submit');
|
|
214
|
+
// If this expect throws, ./traces/login/signs-in/trace.ndjson is on disk.
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
Knobs are all environment variables — switch behaviour without touching code:
|
|
220
|
+
|
|
221
|
+
| Env var | Values | Default | Effect |
|
|
222
|
+
| --- | --- | --- | --- |
|
|
223
|
+
| `CRAFTDRIVER_TRACE` | `off` \| `on-failure` \| `always` | `on-failure` | What to keep. `on-failure` deletes traces for passing tests. |
|
|
224
|
+
| `CRAFTDRIVER_TRACE_DIR` | path | `./traces` | Root output directory. |
|
|
225
|
+
| `CRAFTDRIVER_TRACE_SCREENSHOTS` | `auto` \| `off` | `auto` | Per the `screenshots` option. `off` skips the BiDi capture per action — much faster when you only need the JSON timeline. |
|
|
226
|
+
|
|
227
|
+
Typical workflows:
|
|
228
|
+
|
|
229
|
+
```sh
|
|
230
|
+
npm test # green run leaves no clutter; failures keep their trace
|
|
231
|
+
CRAFTDRIVER_TRACE_SCREENSHOTS=off npm test # cheap mode for big suites
|
|
232
|
+
CRAFTDRIVER_TRACE=always npm test # debugging the tracer itself
|
|
233
|
+
CRAFTDRIVER_TRACE=off npm test # tracing disabled
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
Why a helper and not built-in? Vitest owns the test lifecycle, not
|
|
237
|
+
craftdriver — and the runner-specific glue (pass/fail detection, hook
|
|
238
|
+
order, output paths) belongs on your side. The helper is ~30 lines you
|
|
239
|
+
can read, copy, and adjust to your team's conventions.
|
|
240
|
+
|
|
241
|
+
## API
|
|
242
|
+
|
|
243
|
+
### `browser.startTrace(opts)`
|
|
244
|
+
|
|
245
|
+
```ts
|
|
246
|
+
interface TraceStartOptions {
|
|
247
|
+
outDir: string; // required
|
|
248
|
+
actions?: boolean; // default true
|
|
249
|
+
network?: boolean; // default true
|
|
250
|
+
console?: boolean; // default true
|
|
251
|
+
screenshots?: boolean | 'auto' | 'off'; // default 'auto'
|
|
252
|
+
}
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
Creates `outDir` if missing, opens `outDir/trace.ndjson` for writing,
|
|
256
|
+
and emits the start `meta` line. Throws if a trace is already running
|
|
257
|
+
or BiDi is not enabled.
|
|
258
|
+
|
|
259
|
+
### `browser.stopTrace()`
|
|
260
|
+
|
|
261
|
+
Drains in-flight screenshot captures, writes the closing `meta` line,
|
|
262
|
+
and closes the file. Returns `Promise<void>`. Throws if no trace is
|
|
263
|
+
running.
|
|
264
|
+
|
|
265
|
+
### Cleanup on `browser.quit()`
|
|
266
|
+
|
|
267
|
+
If a trace is still running when you quit the browser, the file is
|
|
268
|
+
closed without a closing `meta` line — same shape as the test-throws
|
|
269
|
+
case. No file is deleted.
|
|
270
|
+
|
|
271
|
+
## What this is *not*
|
|
272
|
+
|
|
273
|
+
By design, the tracer does not:
|
|
274
|
+
|
|
275
|
+
* produce a self-contained `.zip` or hosted viewer,
|
|
276
|
+
* record video / fixed-interval screencast,
|
|
277
|
+
* emit HAR (the NDJSON already holds request/response events),
|
|
278
|
+
* capture DOM snapshots or sourcemaps.
|
|
279
|
+
|
|
280
|
+
Those features make sense at much larger scale (Playwright Trace
|
|
281
|
+
Viewer). For a single failing test, a tail-able NDJSON log plus a
|
|
282
|
+
handful of PNGs beats a zip you can't open.
|
package/package.json
CHANGED
|
@@ -1,22 +1,27 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "craftdriver",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Modern WebDriver automation library for NodeJS",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
7
7
|
"type": "module",
|
|
8
|
+
"bin": {
|
|
9
|
+
"craftdriver": "bin/craftdriver.mjs"
|
|
10
|
+
},
|
|
8
11
|
"scripts": {
|
|
9
12
|
"prebuild": "rimraf dist",
|
|
10
13
|
"build": "tsc -p tsconfig.json",
|
|
11
14
|
"lint": "eslint . --ext .ts",
|
|
12
15
|
"format": "prettier --write .",
|
|
13
|
-
"test": "vitest run",
|
|
14
|
-
"test:firefox": "BROWSER_NAME=firefox vitest run",
|
|
16
|
+
"test": "HEADLESS=true vitest run",
|
|
17
|
+
"test:firefox": "BROWSER_NAME=firefox HEADLESS=true vitest run --maxWorkers=2",
|
|
15
18
|
"test:chrome": "BROWSER_NAME=chrome vitest run",
|
|
16
19
|
"test:chromium": "BROWSER_NAME=chromium vitest run",
|
|
17
20
|
"clean": "rimraf dist",
|
|
18
21
|
"serve": "http-server ./examples -a 127.0.0.1 -p 8080 -c-1 --cors",
|
|
19
22
|
"examples:start": "npm run serve",
|
|
23
|
+
"docs:api": "node scripts/gen-api-reference.mjs",
|
|
24
|
+
"docs:api:check": "node scripts/gen-api-reference.mjs --check",
|
|
20
25
|
"prepublishOnly": "npm run build && npm run lint"
|
|
21
26
|
},
|
|
22
27
|
"keywords": [
|
|
@@ -28,7 +33,10 @@
|
|
|
28
33
|
"automation",
|
|
29
34
|
"browser-automation",
|
|
30
35
|
"testing",
|
|
31
|
-
"selenium"
|
|
36
|
+
"selenium",
|
|
37
|
+
"cli",
|
|
38
|
+
"agent",
|
|
39
|
+
"ai"
|
|
32
40
|
],
|
|
33
41
|
"author": "Dimitar Topuzov",
|
|
34
42
|
"license": "MIT",
|
|
@@ -45,7 +53,11 @@
|
|
|
45
53
|
},
|
|
46
54
|
"files": [
|
|
47
55
|
"dist",
|
|
56
|
+
"bin",
|
|
57
|
+
"docs",
|
|
58
|
+
"skills",
|
|
48
59
|
"README.md",
|
|
60
|
+
"CHANGELOG.md",
|
|
49
61
|
"LICENSE"
|
|
50
62
|
],
|
|
51
63
|
"sideEffects": false,
|
|
@@ -53,6 +65,7 @@
|
|
|
53
65
|
"@semantic-release/changelog": "^6.0.3",
|
|
54
66
|
"@semantic-release/git": "^10.0.1",
|
|
55
67
|
"@types/node": "^20.0.0",
|
|
68
|
+
"@types/ws": "^8.18.1",
|
|
56
69
|
"@typescript-eslint/eslint-plugin": "^8.1.0",
|
|
57
70
|
"@typescript-eslint/parser": "^8.1.0",
|
|
58
71
|
"eslint": "^9.8.0",
|
|
@@ -68,7 +81,11 @@
|
|
|
68
81
|
"vitest": "^4.0.18"
|
|
69
82
|
},
|
|
70
83
|
"dependencies": {
|
|
71
|
-
"
|
|
72
|
-
"ws": "^8.
|
|
84
|
+
"axe-core": "^4.11.4",
|
|
85
|
+
"ws": "^8.21.0"
|
|
86
|
+
},
|
|
87
|
+
"overrides": {
|
|
88
|
+
"brace-expansion": "5.0.6",
|
|
89
|
+
"ip-address": "10.1.1"
|
|
73
90
|
}
|
|
74
91
|
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# craftdriver — SKILL
|
|
2
|
+
|
|
3
|
+
Modern BiDi-first WebDriver library for Node.js. Playwright ergonomics,
|
|
4
|
+
WebDriver standards-compliance, no AI in the hot path.
|
|
5
|
+
|
|
6
|
+
## Core loop
|
|
7
|
+
|
|
8
|
+
```
|
|
9
|
+
navigate → find → action → assert
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
Every action and every `expect(locator).to…()` **auto-waits** up to the
|
|
13
|
+
default timeout (30 s in-library, 5 s on agent surfaces). Never add
|
|
14
|
+
`sleep()` / `setTimeout()` — it's a footgun.
|
|
15
|
+
|
|
16
|
+
## Selector preference order
|
|
17
|
+
|
|
18
|
+
```
|
|
19
|
+
By.testId > By.role({name}) > By.labelText >
|
|
20
|
+
By.text({exact:true}) > By.css > By.xpath
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
`testId` and `role` are stable across DOM refactors; CSS and XPath
|
|
24
|
+
break the moment markup shifts.
|
|
25
|
+
|
|
26
|
+
## Five rules
|
|
27
|
+
|
|
28
|
+
1. Use `expect(locator).to…()` for assertions. Never hand-roll
|
|
29
|
+
`while (...) { ... sleep(100) }` retry loops — they auto-wait.
|
|
30
|
+
2. Read errors by `code`, not by prose. Every public throw is a
|
|
31
|
+
`CraftdriverError` with a stable `code`. See
|
|
32
|
+
[docs/error-codes.md](../../docs/error-codes.md).
|
|
33
|
+
3. `instanceof CraftdriverError` is true on every public throw;
|
|
34
|
+
`instanceof Error` is also true.
|
|
35
|
+
4. BiDi features (`network`, `logs`, tracing, init scripts, true load
|
|
36
|
+
events) require `Browser.launch({ enableBiDi: true })`. The error
|
|
37
|
+
code on the wrong transport is `UNSUPPORTED`.
|
|
38
|
+
5. Tests fetch from the example server. Start it in a separate
|
|
39
|
+
terminal: `npm run examples:start` before `npm test`.
|
|
40
|
+
|
|
41
|
+
## When you reach for more
|
|
42
|
+
|
|
43
|
+
- **Writing tests, looking for a method** → read
|
|
44
|
+
[skills/craftdriver/cheatsheet.md](cheatsheet.md). Also see
|
|
45
|
+
[docs/api-reference.md](../../docs/api-reference.md) — every public
|
|
46
|
+
export, one row each.
|
|
47
|
+
- **Login, upload, wait-for-network, etc.** → read
|
|
48
|
+
[skills/craftdriver/patterns.md](patterns.md).
|
|
49
|
+
- **Driving the browser from a shell or agent loop** → read
|
|
50
|
+
[skills/craftdriver/cli.md](cli.md). Same Browser API, exposed as a
|
|
51
|
+
`craftdriver` binary with daemon + ephemeral modes.
|
|
52
|
+
- **Driving from an MCP-aware AI host** (Claude Desktop / Code, Cursor,
|
|
53
|
+
Windsurf, Zed, Goose, Gemini CLI) → `craftdriver mcp` is a stdio
|
|
54
|
+
JSON-RPC server with 14 schema-typed tools. Mutating tools return a
|
|
55
|
+
compact a11y snapshot **diffed from the previous turn** — you see
|
|
56
|
+
what changed without a follow-up read. See
|
|
57
|
+
[docs/mcp.md](../../docs/mcp.md).
|
|
58
|
+
- **An error code you don't recognise** → read
|
|
59
|
+
[docs/error-codes.md](../../docs/error-codes.md).
|
|
60
|
+
|
|
61
|
+
## Probing rule
|
|
62
|
+
|
|
63
|
+
When unsure a selector exists, check before acting. Today: call
|
|
64
|
+
`await locator.count()` (zero-wait, returns the current match count).
|
|
65
|
+
Acting on a hallucinated selector wastes the whole auto-wait budget.
|
|
66
|
+
|
|
67
|
+
For the **CLI and MCP**, the cheapest way to drive a page is to take a
|
|
68
|
+
snapshot first and use refs:
|
|
69
|
+
|
|
70
|
+
```
|
|
71
|
+
$ craftdriver snapshot
|
|
72
|
+
e4: textbox "Username" #username
|
|
73
|
+
e7: button "Sign in" #submit
|
|
74
|
+
$ craftdriver fill ref=e4 alice
|
|
75
|
+
$ craftdriver click ref=e7
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
`ref=eN` resolves to a CSS attribute selector, auto-waits like any
|
|
79
|
+
other selector, and re-allocates on every snapshot. No hallucination.
|
|
80
|
+
|
|
81
|
+
## Imports
|
|
82
|
+
|
|
83
|
+
Always `import { ... } from 'craftdriver'`. Never reach into
|
|
84
|
+
`craftdriver/src/lib/...` — internals are unstable.
|