craftdriver 1.2.0 → 1.3.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 +13 -0
- package/README.md +1 -1
- package/dist/cli/dispatcher.d.ts.map +1 -1
- package/dist/cli/dispatcher.js +12 -0
- package/dist/cli/dispatcher.js.map +1 -1
- package/dist/cli/mcp/tools.d.ts +1 -2
- package/dist/cli/mcp/tools.d.ts.map +1 -1
- package/dist/cli/mcp/tools.js +18 -0
- package/dist/cli/mcp/tools.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/lib/browser.d.ts +9 -6
- package/dist/lib/browser.d.ts.map +1 -1
- package/dist/lib/browser.js +94 -73
- package/dist/lib/browser.js.map +1 -1
- package/dist/lib/tracing.d.ts +20 -4
- package/dist/lib/tracing.d.ts.map +1 -1
- package/dist/lib/tracing.js +38 -3
- package/dist/lib/tracing.js.map +1 -1
- package/dist/lib/vibiumTrace.d.ts +14 -0
- package/dist/lib/vibiumTrace.d.ts.map +1 -0
- package/dist/lib/vibiumTrace.js +372 -0
- package/dist/lib/vibiumTrace.js.map +1 -0
- package/docs/api-reference.md +2 -1
- package/docs/mcp.md +17 -3
- package/docs/public/recipes/trace-failing-test.html +16 -0
- package/docs/public/recipes/vitest-vibium-trace.html +16 -0
- package/docs/public/traces/vitest-login.zip +0 -0
- package/docs/recipes/debug-failing-tests-with-traces.md +117 -0
- package/docs/recipes.md +1 -1
- package/docs/tracing.md +102 -24
- package/package.json +4 -8
- package/skills/craftdriver/cheatsheet.md +4 -2
- package/skills/craftdriver/patterns.md +2 -1
- package/docs/recipes/trace-failing-test.md +0 -51
package/docs/tracing.md
CHANGED
|
@@ -39,6 +39,50 @@ artefacts/login/
|
|
|
39
39
|
`try/finally` is the recommended shape, but it's a courtesy — see the
|
|
40
40
|
next section.
|
|
41
41
|
|
|
42
|
+
## Export for Vibium Player
|
|
43
|
+
|
|
44
|
+
Pass a zip path when stopping the trace to create a portable recording that
|
|
45
|
+
opens in [Vibium Player](https://player.vibium.dev/) and Playwright Trace
|
|
46
|
+
Viewer:
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
await browser.startTrace({
|
|
50
|
+
outDir: './artefacts/login-raw',
|
|
51
|
+
title: 'Login flow',
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
try {
|
|
55
|
+
await browser.navigateTo('https://example.com/login');
|
|
56
|
+
await browser.fill('#user', 'alice');
|
|
57
|
+
await browser.click('#submit');
|
|
58
|
+
} finally {
|
|
59
|
+
await browser.stopTrace({ path: './artefacts/login.zip' });
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
The raw `trace.ndjson` remains the crash-resilient source. The zip is produced
|
|
64
|
+
during `stopTrace()` and contains the Vibium/Playwright layout:
|
|
65
|
+
|
|
66
|
+
```text
|
|
67
|
+
login.zip
|
|
68
|
+
├── trace.trace # context-options + before/after actions + browser events
|
|
69
|
+
├── trace.network # HAR-style resource-snapshot events
|
|
70
|
+
└── resources/ # PNG frames referenced by screencast-frame events
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Vibium Player is the recommended online viewer. For local-only inspection:
|
|
74
|
+
|
|
75
|
+
```sh
|
|
76
|
+
npx playwright show-trace ./artefacts/login.zip
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
The export includes Craftdriver actions, screenshots, screenshot-backed frame
|
|
80
|
+
snapshots, navigation/console events, and the network metadata that WebDriver
|
|
81
|
+
BiDi exposes to the tracer. Screenshot-backed snapshots make the main panel of
|
|
82
|
+
Playwright Trace Viewer useful while staying honest: they are not a restorable
|
|
83
|
+
DOM. Response bodies and source files remain future work. Browser action spans
|
|
84
|
+
use real start/end times and preserve thrown action errors.
|
|
85
|
+
|
|
42
86
|
## What happens when a test throws
|
|
43
87
|
|
|
44
88
|
Every recorded event hits disk **before** the next line of your test
|
|
@@ -70,13 +114,14 @@ since `startTrace`) and a `type`:
|
|
|
70
114
|
| `type` | Fields |
|
|
71
115
|
| -------------- | ----------------------------------------------------------- |
|
|
72
116
|
| `meta` | `startedAt` / `endedAt`, `opts` on the start line |
|
|
73
|
-
| `action` | `name`, `args?`, `selector?`
|
|
117
|
+
| `action` | `actionIndex`, `name`, `args?`, `selector?` |
|
|
118
|
+
| `action-end` | `actionIndex`, `error?` |
|
|
74
119
|
| `console` | `level`, `text` |
|
|
75
120
|
| `error` | `text` (uncaught page errors) |
|
|
76
121
|
| `request` | `url`, `method`, `requestId?` |
|
|
77
122
|
| `response` | `url`, `status`, `mimeType?`, `fromCache?`, `requestId?` |
|
|
78
123
|
| `navigation` | `url`, `context?` |
|
|
79
|
-
| `screenshot` | `file` (relative path), `reason` (`'action'` \| `'error'`), `actionIndex?` |
|
|
124
|
+
| `screenshot` | `file` (relative path), `reason` (`'action'` \| `'error'` \| `'final'`), `actionIndex?` |
|
|
80
125
|
|
|
81
126
|
Actions currently logged: `navigateTo`, `goBack`, `goForward`, `reload`,
|
|
82
127
|
`setContent`, `click`, `fill`, `clear`, `acceptDialog`, `dismissDialog`.
|
|
@@ -88,6 +133,8 @@ Screenshots are tied to **meaningful moments**, not a timer:
|
|
|
88
133
|
* Before every logged action — answers *"what did the page look like
|
|
89
134
|
when I clicked?"*
|
|
90
135
|
* On every page error — answers *"what was on screen when it broke?"*
|
|
136
|
+
* Once when `stopTrace()` runs — preserves the state left by the final action
|
|
137
|
+
or failed assertion.
|
|
91
138
|
|
|
92
139
|
A 30-second test with 5 clicks produces 5 PNGs, not 300. Turn it off
|
|
93
140
|
when you only want the JSON log:
|
|
@@ -149,7 +196,7 @@ Drop this into your test folder (e.g. `tests/auto-trace.ts`):
|
|
|
149
196
|
|
|
150
197
|
```ts
|
|
151
198
|
import { beforeEach, afterEach } from 'vitest';
|
|
152
|
-
import { rmSync
|
|
199
|
+
import { rmSync } from 'node:fs';
|
|
153
200
|
import { join } from 'node:path';
|
|
154
201
|
import type { Browser, TraceScreenshotMode } from 'craftdriver';
|
|
155
202
|
|
|
@@ -170,19 +217,15 @@ export function autoTrace(getBrowser: () => Browser): void {
|
|
|
170
217
|
|
|
171
218
|
afterEach(async ({ task }) => {
|
|
172
219
|
const failed = task.result?.state === 'fail';
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
writeFileSync(join(currentDir, 'final.png'), buf);
|
|
179
|
-
} catch { /* browser may already be dead */ }
|
|
180
|
-
}
|
|
181
|
-
try { await getBrowser().stopTrace(); } catch { return; }
|
|
220
|
+
const keep = MODE === 'always' || failed;
|
|
221
|
+
const zipPath = `${currentDir}.zip`;
|
|
222
|
+
try {
|
|
223
|
+
await getBrowser().stopTrace(keep ? { path: zipPath } : undefined);
|
|
224
|
+
} catch { return; }
|
|
182
225
|
if (MODE === 'on-failure' && !failed) {
|
|
183
226
|
rmSync(currentDir, { recursive: true, force: true });
|
|
184
227
|
} else if (failed) {
|
|
185
|
-
console.error(`
|
|
228
|
+
console.error(` 📦 Vibium trace: ${zipPath}`);
|
|
186
229
|
}
|
|
187
230
|
});
|
|
188
231
|
}
|
|
@@ -211,7 +254,7 @@ describe('Login', () => {
|
|
|
211
254
|
await browser.navigateTo('http://127.0.0.1:8080/login.html');
|
|
212
255
|
await browser.fill('#user', 'alice');
|
|
213
256
|
await browser.click('#submit');
|
|
214
|
-
// If this expect throws, ./traces/login/signs-in
|
|
257
|
+
// If this expect throws, ./traces/login/signs-in.zip opens in Vibium Player.
|
|
215
258
|
});
|
|
216
259
|
});
|
|
217
260
|
```
|
|
@@ -238,6 +281,42 @@ craftdriver — and the runner-specific glue (pass/fail detection, hook
|
|
|
238
281
|
order, output paths) belongs on your side. The helper is ~30 lines you
|
|
239
282
|
can read, copy, and adjust to your team's conventions.
|
|
240
283
|
|
|
284
|
+
### One trace for the whole suite
|
|
285
|
+
|
|
286
|
+
If the desired boundary is exactly `beforeAll()` → `afterAll()`, record the
|
|
287
|
+
suite continuously and remember failures in `afterEach()`. A passing suite
|
|
288
|
+
does not create a zip; a failing suite does:
|
|
289
|
+
|
|
290
|
+
```ts
|
|
291
|
+
import { afterAll, afterEach, beforeAll, describe } from 'vitest';
|
|
292
|
+
import { rmSync } from 'node:fs';
|
|
293
|
+
import { Browser } from 'craftdriver';
|
|
294
|
+
|
|
295
|
+
describe('checkout', () => {
|
|
296
|
+
let browser: Browser;
|
|
297
|
+
let failed = false;
|
|
298
|
+
const rawDir = './traces/checkout-raw';
|
|
299
|
+
const zipPath = './traces/checkout-failure.zip';
|
|
300
|
+
|
|
301
|
+
beforeAll(async () => {
|
|
302
|
+
browser = await Browser.launch();
|
|
303
|
+
await browser.startTrace({ outDir: rawDir, title: 'Checkout suite' });
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
afterEach(({ task }) => {
|
|
307
|
+
failed ||= task.result?.state === 'fail';
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
afterAll(async () => {
|
|
311
|
+
await browser.stopTrace(failed ? { path: zipPath } : undefined);
|
|
312
|
+
if (!failed) rmSync(rawDir, { recursive: true, force: true });
|
|
313
|
+
await browser.quit();
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
// ...tests...
|
|
317
|
+
});
|
|
318
|
+
```
|
|
319
|
+
|
|
241
320
|
## API
|
|
242
321
|
|
|
243
322
|
### `browser.startTrace(opts)`
|
|
@@ -249,6 +328,7 @@ interface TraceStartOptions {
|
|
|
249
328
|
network?: boolean; // default true
|
|
250
329
|
console?: boolean; // default true
|
|
251
330
|
screenshots?: boolean | 'auto' | 'off'; // default 'auto'
|
|
331
|
+
title?: string; // viewer title
|
|
252
332
|
}
|
|
253
333
|
```
|
|
254
334
|
|
|
@@ -256,11 +336,12 @@ Creates `outDir` if missing, opens `outDir/trace.ndjson` for writing,
|
|
|
256
336
|
and emits the start `meta` line. Throws if a trace is already running
|
|
257
337
|
or BiDi is not enabled.
|
|
258
338
|
|
|
259
|
-
### `browser.stopTrace()`
|
|
339
|
+
### `browser.stopTrace(opts?)`
|
|
260
340
|
|
|
261
|
-
|
|
262
|
-
and closes the file.
|
|
263
|
-
|
|
341
|
+
Captures the final page state, drains in-flight screenshot captures, writes the closing `meta` line,
|
|
342
|
+
and closes the file. With `{ path: './trace.zip' }`, it also exports a
|
|
343
|
+
Vibium/Playwright-compatible zip. Returns `Promise<void>`. Throws if no
|
|
344
|
+
trace is active.
|
|
264
345
|
|
|
265
346
|
### Cleanup on `browser.quit()`
|
|
266
347
|
|
|
@@ -272,11 +353,8 @@ case. No file is deleted.
|
|
|
272
353
|
|
|
273
354
|
By design, the tracer does not:
|
|
274
355
|
|
|
275
|
-
* produce a self-contained `.zip` or hosted viewer,
|
|
276
356
|
* record video / fixed-interval screencast,
|
|
277
|
-
*
|
|
278
|
-
* capture DOM snapshots or sourcemaps.
|
|
357
|
+
* capture a restorable DOM or sourcemaps.
|
|
279
358
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
handful of PNGs beats a zip you can't open.
|
|
359
|
+
The raw NDJSON stays deliberately small and tail-able; richer portable
|
|
360
|
+
features can be added to the zip format without weakening crash resilience.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "craftdriver",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "Modern WebDriver automation library for NodeJS",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -19,7 +19,6 @@
|
|
|
19
19
|
"test:chromium": "BROWSER_NAME=chromium vitest run",
|
|
20
20
|
"test:recipes": "HEADLESS=true vitest run --config vitest.recipes.config.ts",
|
|
21
21
|
"bench": "HEADLESS=true vitest run --config vitest.perf.config.ts",
|
|
22
|
-
"bench:realapp": "HEADLESS=true vitest run --config vitest.perf.realapp.config.ts",
|
|
23
22
|
"clean": "rimraf dist",
|
|
24
23
|
"serve": "http-server ./examples -a 127.0.0.1 -p 8080 -c-1 --cors",
|
|
25
24
|
"examples:start": "npm run serve",
|
|
@@ -82,13 +81,9 @@
|
|
|
82
81
|
"@semantic-release/git": "^10.0.1",
|
|
83
82
|
"@types/node": "^20.0.0",
|
|
84
83
|
"@types/ws": "^8.18.1",
|
|
85
|
-
"@
|
|
84
|
+
"@types/yazl": "^3.3.1",
|
|
86
85
|
"@typescript-eslint/parser": "^8.1.0",
|
|
87
86
|
"eslint": "^9.8.0",
|
|
88
|
-
"eslint-config-prettier": "^9.1.0",
|
|
89
|
-
"eslint-plugin-import": "^2.29.1",
|
|
90
|
-
"eslint-plugin-n": "^17.10.1",
|
|
91
|
-
"eslint-plugin-promise": "^6.4.0",
|
|
92
87
|
"http-server": "^14.1.1",
|
|
93
88
|
"prettier": "^3.3.3",
|
|
94
89
|
"rimraf": "^6.1.3",
|
|
@@ -99,7 +94,8 @@
|
|
|
99
94
|
},
|
|
100
95
|
"dependencies": {
|
|
101
96
|
"axe-core": "^4.12.1",
|
|
102
|
-
"ws": "^8.21.0"
|
|
97
|
+
"ws": "^8.21.0",
|
|
98
|
+
"yazl": "^3.3.1"
|
|
103
99
|
},
|
|
104
100
|
"overrides": {
|
|
105
101
|
"brace-expansion": "5.0.6",
|
|
@@ -159,8 +159,10 @@ const download = await browser.waitForDownload(() => browser.click('#download'))
|
|
|
159
159
|
```ts
|
|
160
160
|
await browser.screenshot({ path: 'out.png', fullPage: true });
|
|
161
161
|
|
|
162
|
-
await browser.startTrace({ outDir: './artefacts/run' });
|
|
163
|
-
try { /* ... */ } finally {
|
|
162
|
+
await browser.startTrace({ outDir: './artefacts/run', title: 'Smoke flow' });
|
|
163
|
+
try { /* ... */ } finally {
|
|
164
|
+
await browser.stopTrace({ path: './artefacts/run.zip' }); // player.vibium.dev
|
|
165
|
+
}
|
|
164
166
|
```
|
|
165
167
|
|
|
166
168
|
## Accessibility
|
|
@@ -60,6 +60,7 @@ const due = await row.locator('[data-col=due]').text();
|
|
|
60
60
|
```ts
|
|
61
61
|
await browser.startTrace({
|
|
62
62
|
outDir: './artefacts/checkout-fail',
|
|
63
|
+
title: 'Checkout failure',
|
|
63
64
|
screenshots: 'auto',
|
|
64
65
|
network: true,
|
|
65
66
|
console: true,
|
|
@@ -70,7 +71,7 @@ try {
|
|
|
70
71
|
// Trace lands on disk regardless — thrown expects never lose data.
|
|
71
72
|
throw e;
|
|
72
73
|
} finally {
|
|
73
|
-
await browser.stopTrace();
|
|
74
|
+
await browser.stopTrace({ path: './artefacts/checkout-fail.zip' });
|
|
74
75
|
}
|
|
75
76
|
```
|
|
76
77
|
|
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
# Capture Failure Evidence With Tracing
|
|
2
|
-
|
|
3
|
-
When a complex or occasionally-flaky test fails, the message alone rarely tells
|
|
4
|
-
you why. A trace records the actions, navigation, console output, JavaScript
|
|
5
|
-
errors, network events, and screenshots so you can replay what happened. Wrap
|
|
6
|
-
the flow in a helper that starts a trace, keeps it on failure, and always stops
|
|
7
|
-
it. This recipe traces a sign-in against the live
|
|
8
|
-
[login example](https://dtopuzov.github.io/craftdriver/examples/login.html).
|
|
9
|
-
|
|
10
|
-
The `try/catch` here is deliberate — it is what keeps the trace directory when
|
|
11
|
-
the flow throws.
|
|
12
|
-
|
|
13
|
-
```ts
|
|
14
|
-
async function withTrace(name: string, run: () => Promise<void>) {
|
|
15
|
-
const outDir = `./traces/${name}-${Date.now()}`;
|
|
16
|
-
await browser.startTrace({ outDir });
|
|
17
|
-
|
|
18
|
-
try {
|
|
19
|
-
await run();
|
|
20
|
-
} catch (error) {
|
|
21
|
-
console.error(`Trace kept at ${outDir}`);
|
|
22
|
-
throw error;
|
|
23
|
-
} finally {
|
|
24
|
-
await browser.stopTrace().catch(() => undefined);
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
await withTrace('login', async () => {
|
|
29
|
-
await browser.navigateTo('https://dtopuzov.github.io/craftdriver/examples/login.html');
|
|
30
|
-
await browser.getByLabel('Username').fill('alice');
|
|
31
|
-
await browser.getByLabel('Password').fill('secret');
|
|
32
|
-
await browser.getByRole('button', { name: 'Sign in' }).click();
|
|
33
|
-
await browser.expect('#welcome').toContainText('Welcome back, alice!');
|
|
34
|
-
});
|
|
35
|
-
```
|
|
36
|
-
|
|
37
|
-
## Notes
|
|
38
|
-
|
|
39
|
-
- The trace file is NDJSON, so it stays useful even if the test throws before `stopTrace()`.
|
|
40
|
-
- Screenshots are stored under the trace directory.
|
|
41
|
-
- For high-volume suites, turn screenshots off unless a test is under investigation:
|
|
42
|
-
|
|
43
|
-
```ts
|
|
44
|
-
await browser.startTrace({ outDir: './traces/smoke', screenshots: 'off' });
|
|
45
|
-
```
|
|
46
|
-
|
|
47
|
-
## Learn More
|
|
48
|
-
|
|
49
|
-
- [Tracing](../tracing.md)
|
|
50
|
-
- [Screenshots](../screenshots.md)
|
|
51
|
-
- [Console Logs And JavaScript Errors](../browser-logs.md)
|