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.
Files changed (186) hide show
  1. package/CHANGELOG.md +104 -0
  2. package/README.md +157 -45
  3. package/bin/craftdriver.mjs +26 -0
  4. package/dist/cli/client.d.ts +17 -0
  5. package/dist/cli/client.d.ts.map +1 -0
  6. package/dist/cli/client.js +88 -0
  7. package/dist/cli/client.js.map +1 -0
  8. package/dist/cli/daemon.d.ts +15 -0
  9. package/dist/cli/daemon.d.ts.map +1 -0
  10. package/dist/cli/daemon.js +145 -0
  11. package/dist/cli/daemon.js.map +1 -0
  12. package/dist/cli/defaults.d.ts +10 -0
  13. package/dist/cli/defaults.d.ts.map +1 -0
  14. package/dist/cli/defaults.js +42 -0
  15. package/dist/cli/defaults.js.map +1 -0
  16. package/dist/cli/dispatcher.d.ts +28 -0
  17. package/dist/cli/dispatcher.d.ts.map +1 -0
  18. package/dist/cli/dispatcher.js +319 -0
  19. package/dist/cli/dispatcher.js.map +1 -0
  20. package/dist/cli/index.d.ts +2 -0
  21. package/dist/cli/index.d.ts.map +1 -0
  22. package/dist/cli/index.js +404 -0
  23. package/dist/cli/index.js.map +1 -0
  24. package/dist/cli/init.d.ts +24 -0
  25. package/dist/cli/init.d.ts.map +1 -0
  26. package/dist/cli/init.js +192 -0
  27. package/dist/cli/init.js.map +1 -0
  28. package/dist/cli/mcp/artifacts.d.ts +43 -0
  29. package/dist/cli/mcp/artifacts.d.ts.map +1 -0
  30. package/dist/cli/mcp/artifacts.js +104 -0
  31. package/dist/cli/mcp/artifacts.js.map +1 -0
  32. package/dist/cli/mcp/server.d.ts +34 -0
  33. package/dist/cli/mcp/server.d.ts.map +1 -0
  34. package/dist/cli/mcp/server.js +214 -0
  35. package/dist/cli/mcp/server.js.map +1 -0
  36. package/dist/cli/mcp/tools.d.ts +34 -0
  37. package/dist/cli/mcp/tools.d.ts.map +1 -0
  38. package/dist/cli/mcp/tools.js +236 -0
  39. package/dist/cli/mcp/tools.js.map +1 -0
  40. package/dist/cli/parseArgs.d.ts +37 -0
  41. package/dist/cli/parseArgs.d.ts.map +1 -0
  42. package/dist/cli/parseArgs.js +215 -0
  43. package/dist/cli/parseArgs.js.map +1 -0
  44. package/dist/cli/protocol.d.ts +34 -0
  45. package/dist/cli/protocol.d.ts.map +1 -0
  46. package/dist/cli/protocol.js +2 -0
  47. package/dist/cli/protocol.js.map +1 -0
  48. package/dist/cli/selector.d.ts +29 -0
  49. package/dist/cli/selector.d.ts.map +1 -0
  50. package/dist/cli/selector.js +100 -0
  51. package/dist/cli/selector.js.map +1 -0
  52. package/dist/cli/snapshot.d.ts +48 -0
  53. package/dist/cli/snapshot.d.ts.map +1 -0
  54. package/dist/cli/snapshot.js +179 -0
  55. package/dist/cli/snapshot.js.map +1 -0
  56. package/dist/index.d.ts +14 -2
  57. package/dist/index.d.ts.map +1 -1
  58. package/dist/index.js +15 -1
  59. package/dist/index.js.map +1 -1
  60. package/dist/lib/a11y.d.ts +81 -0
  61. package/dist/lib/a11y.d.ts.map +1 -0
  62. package/dist/lib/a11y.js +171 -0
  63. package/dist/lib/a11y.js.map +1 -0
  64. package/dist/lib/bidi/index.d.ts +26 -1
  65. package/dist/lib/bidi/index.d.ts.map +1 -1
  66. package/dist/lib/bidi/index.js +49 -0
  67. package/dist/lib/bidi/index.js.map +1 -1
  68. package/dist/lib/bidi/logs.d.ts +2 -2
  69. package/dist/lib/bidi/logs.d.ts.map +1 -1
  70. package/dist/lib/bidi/logs.js +2 -2
  71. package/dist/lib/bidi/logs.js.map +1 -1
  72. package/dist/lib/bidi/network.d.ts +74 -1
  73. package/dist/lib/bidi/network.d.ts.map +1 -1
  74. package/dist/lib/bidi/network.js +222 -4
  75. package/dist/lib/bidi/network.js.map +1 -1
  76. package/dist/lib/bidi/storage.d.ts +5 -3
  77. package/dist/lib/bidi/storage.d.ts.map +1 -1
  78. package/dist/lib/bidi/storage.js +14 -7
  79. package/dist/lib/bidi/storage.js.map +1 -1
  80. package/dist/lib/bidi/types.d.ts +27 -6
  81. package/dist/lib/bidi/types.d.ts.map +1 -1
  82. package/dist/lib/browser.d.ts +637 -16
  83. package/dist/lib/browser.d.ts.map +1 -1
  84. package/dist/lib/browser.js +1428 -102
  85. package/dist/lib/browser.js.map +1 -1
  86. package/dist/lib/browserContext.d.ts +486 -0
  87. package/dist/lib/browserContext.d.ts.map +1 -0
  88. package/dist/lib/browserContext.js +937 -0
  89. package/dist/lib/browserContext.js.map +1 -0
  90. package/dist/lib/builder.d.ts +3 -0
  91. package/dist/lib/builder.d.ts.map +1 -1
  92. package/dist/lib/builder.js +31 -4
  93. package/dist/lib/builder.js.map +1 -1
  94. package/dist/lib/by.d.ts +18 -2
  95. package/dist/lib/by.d.ts.map +1 -1
  96. package/dist/lib/by.js +20 -2
  97. package/dist/lib/by.js.map +1 -1
  98. package/dist/lib/chrome.d.ts +13 -1
  99. package/dist/lib/chrome.d.ts.map +1 -1
  100. package/dist/lib/chrome.js +13 -17
  101. package/dist/lib/chrome.js.map +1 -1
  102. package/dist/lib/clock.d.ts +115 -0
  103. package/dist/lib/clock.d.ts.map +1 -0
  104. package/dist/lib/clock.js +407 -0
  105. package/dist/lib/clock.js.map +1 -0
  106. package/dist/lib/driver.d.ts +43 -0
  107. package/dist/lib/driver.d.ts.map +1 -1
  108. package/dist/lib/driver.js +120 -0
  109. package/dist/lib/driver.js.map +1 -1
  110. package/dist/lib/driverManager.d.ts +25 -0
  111. package/dist/lib/driverManager.d.ts.map +1 -0
  112. package/dist/lib/driverManager.js +399 -0
  113. package/dist/lib/driverManager.js.map +1 -0
  114. package/dist/lib/elementHandle.d.ts +67 -3
  115. package/dist/lib/elementHandle.d.ts.map +1 -1
  116. package/dist/lib/elementHandle.js +209 -67
  117. package/dist/lib/elementHandle.js.map +1 -1
  118. package/dist/lib/errors.d.ts +71 -0
  119. package/dist/lib/errors.d.ts.map +1 -0
  120. package/dist/lib/errors.js +73 -0
  121. package/dist/lib/errors.js.map +1 -0
  122. package/dist/lib/expect.d.ts +6 -1
  123. package/dist/lib/expect.d.ts.map +1 -1
  124. package/dist/lib/expect.js +55 -37
  125. package/dist/lib/expect.js.map +1 -1
  126. package/dist/lib/firefox.d.ts +18 -0
  127. package/dist/lib/firefox.d.ts.map +1 -0
  128. package/dist/lib/firefox.js +33 -0
  129. package/dist/lib/firefox.js.map +1 -0
  130. package/dist/lib/frame.d.ts +57 -0
  131. package/dist/lib/frame.d.ts.map +1 -0
  132. package/dist/lib/frame.js +277 -0
  133. package/dist/lib/frame.js.map +1 -0
  134. package/dist/lib/keyboard.d.ts +1 -1
  135. package/dist/lib/keyboard.d.ts.map +1 -1
  136. package/dist/lib/keyboard.js +1 -1
  137. package/dist/lib/keyboard.js.map +1 -1
  138. package/dist/lib/locator.d.ts +71 -2
  139. package/dist/lib/locator.d.ts.map +1 -1
  140. package/dist/lib/locator.js +289 -31
  141. package/dist/lib/locator.js.map +1 -1
  142. package/dist/lib/mouse.d.ts +1 -1
  143. package/dist/lib/mouse.d.ts.map +1 -1
  144. package/dist/lib/mouse.js +1 -1
  145. package/dist/lib/mouse.js.map +1 -1
  146. package/dist/lib/page.d.ts +107 -0
  147. package/dist/lib/page.d.ts.map +1 -0
  148. package/dist/lib/page.js +388 -0
  149. package/dist/lib/page.js.map +1 -0
  150. package/dist/lib/tracing.d.ts +128 -0
  151. package/dist/lib/tracing.d.ts.map +1 -0
  152. package/dist/lib/tracing.js +272 -0
  153. package/dist/lib/tracing.js.map +1 -0
  154. package/dist/lib/wait.d.ts.map +1 -1
  155. package/dist/lib/wait.js +14 -1
  156. package/dist/lib/wait.js.map +1 -1
  157. package/dist/lib/webelement.d.ts +2 -0
  158. package/dist/lib/webelement.d.ts.map +1 -1
  159. package/dist/lib/webelement.js +18 -0
  160. package/dist/lib/webelement.js.map +1 -1
  161. package/docs/accessibility.md +177 -0
  162. package/docs/api-reference.md +71 -0
  163. package/docs/assertions.md +182 -0
  164. package/docs/bidi-features.md +462 -0
  165. package/docs/browser-api.md +673 -0
  166. package/docs/browser-context.md +522 -0
  167. package/docs/cli.md +233 -0
  168. package/docs/clock.md +245 -0
  169. package/docs/dialogs.md +143 -0
  170. package/docs/driver-configuration.md +64 -0
  171. package/docs/element-api.md +219 -0
  172. package/docs/emulation.md +155 -0
  173. package/docs/error-codes.md +67 -0
  174. package/docs/getting-started.md +168 -0
  175. package/docs/keyboard-mouse.md +250 -0
  176. package/docs/mcp.md +250 -0
  177. package/docs/mobile-emulation.md +194 -0
  178. package/docs/screenshots.md +146 -0
  179. package/docs/selectors.md +382 -0
  180. package/docs/session-management.md +229 -0
  181. package/docs/tracing.md +282 -0
  182. package/package.json +23 -6
  183. package/skills/craftdriver/SKILL.md +84 -0
  184. package/skills/craftdriver/cheatsheet.md +194 -0
  185. package/skills/craftdriver/cli.md +104 -0
  186. 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 |
@@ -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.1.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
- "@types/ws": "^8.18.1",
72
- "ws": "^8.19.0"
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.