craftdriver 1.1.1 → 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.
Files changed (52) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/README.md +1 -1
  3. package/dist/cli/dispatcher.d.ts.map +1 -1
  4. package/dist/cli/dispatcher.js +12 -0
  5. package/dist/cli/dispatcher.js.map +1 -1
  6. package/dist/cli/mcp/tools.d.ts +1 -2
  7. package/dist/cli/mcp/tools.d.ts.map +1 -1
  8. package/dist/cli/mcp/tools.js +18 -0
  9. package/dist/cli/mcp/tools.js.map +1 -1
  10. package/dist/index.d.ts +1 -1
  11. package/dist/index.d.ts.map +1 -1
  12. package/dist/index.js.map +1 -1
  13. package/dist/lib/bidi/index.d.ts +4 -5
  14. package/dist/lib/bidi/index.d.ts.map +1 -1
  15. package/dist/lib/bidi/index.js +18 -8
  16. package/dist/lib/bidi/index.js.map +1 -1
  17. package/dist/lib/bidi/logs.d.ts +12 -1
  18. package/dist/lib/bidi/logs.d.ts.map +1 -1
  19. package/dist/lib/bidi/logs.js +20 -8
  20. package/dist/lib/bidi/logs.js.map +1 -1
  21. package/dist/lib/browser.d.ts +9 -15
  22. package/dist/lib/browser.d.ts.map +1 -1
  23. package/dist/lib/browser.js +94 -77
  24. package/dist/lib/browser.js.map +1 -1
  25. package/dist/lib/tracing.d.ts +20 -4
  26. package/dist/lib/tracing.d.ts.map +1 -1
  27. package/dist/lib/tracing.js +38 -3
  28. package/dist/lib/tracing.js.map +1 -1
  29. package/dist/lib/vibiumTrace.d.ts +14 -0
  30. package/dist/lib/vibiumTrace.d.ts.map +1 -0
  31. package/dist/lib/vibiumTrace.js +372 -0
  32. package/dist/lib/vibiumTrace.js.map +1 -0
  33. package/docs/api-reference.md +2 -1
  34. package/docs/browser-api.md +0 -1
  35. package/docs/browser-logs.md +8 -23
  36. package/docs/mcp.md +17 -3
  37. package/docs/public/recipes/trace-failing-test.html +16 -0
  38. package/docs/public/recipes/vitest-vibium-trace.html +16 -0
  39. package/docs/public/traces/vitest-login.zip +0 -0
  40. package/docs/recipes/console-error-gate.md +6 -9
  41. package/docs/recipes/debug-failing-tests-with-traces.md +117 -0
  42. package/docs/recipes/file-upload-download.md +1 -4
  43. package/docs/recipes/login-once-reuse-session.md +2 -5
  44. package/docs/recipes/mobile-flow-with-network-and-logs.md +2 -6
  45. package/docs/recipes/vitest-browser-lifecycle.md +1 -4
  46. package/docs/recipes.md +1 -1
  47. package/docs/tracing.md +104 -26
  48. package/package.json +4 -8
  49. package/skills/craftdriver/SKILL.md +3 -2
  50. package/skills/craftdriver/cheatsheet.md +6 -3
  51. package/skills/craftdriver/patterns.md +4 -6
  52. package/docs/recipes/trace-failing-test.md +0 -51
@@ -82,6 +82,7 @@ Capture console output, JavaScript errors, and trace artifacts.
82
82
  | `JavaScriptError` | type | — | [browser-logs](browser-logs.md) |
83
83
  | `LogMessage` | type | — | [browser-logs](browser-logs.md) |
84
84
  | `TraceStartOptions` | type | — | [tracing](tracing.md) |
85
+ | `TraceStopOptions` | type | — | [tracing](tracing.md) |
85
86
  | `TraceScreenshotMode` | type | Screenshot mode for tracing. | [tracing](tracing.md) |
86
87
  | `TraceEvent` | type | — | [tracing](tracing.md) |
87
88
 
@@ -130,4 +131,4 @@ Handle stable CraftDriver errors or customize browser driver services.
130
131
  | `FirefoxService` | class | — | [getting-started](getting-started.md) |
131
132
  | `FirefoxServiceOptions` | type | — | [getting-started](getting-started.md) |
132
133
 
133
- Total exports: **60**.
134
+ Total exports: **61**.
@@ -29,7 +29,6 @@ const browser = await Browser.launch({
29
29
  |--------|------|---------|-------------|
30
30
  | `browserName` | `'chrome' \| 'chromium' \| 'firefox'` | `'chrome'` | Browser to launch |
31
31
  | `enableBiDi` | `boolean` | `true` | Use WebDriver BiDi. Set `false` only when running against a browser that does not support it — every BiDi-only feature then throws a `requires BiDi` error if called. |
32
- | `captureLogs` | `boolean` | `false` | Start console/error log capture immediately at launch instead of lazily on first `browser.logs`/`onConsole`/`onError`/`waitForConsole` touch. See [Console Logs And JavaScript Errors](./browser-logs.md). |
33
32
  | `storageState` | `string` | — | Path to a session-state JSON file to load on startup |
34
33
  | `mobileEmulation` | `MobileEmulation \| DeviceName` | — | Mobile device emulation settings (Chrome/Chromium only) |
35
34
  | `downloadsDir` | `string` | temp dir | Directory where downloaded files are saved |
@@ -12,35 +12,20 @@ emitted a known log message.
12
12
 
13
13
  ## Capture Timing
14
14
 
15
- Capture is lazy by default. CraftDriver subscribes to log events the first time
16
- you touch `browser.logs.onLog()`, `.onConsole()`, `.onError()`, `.on()`,
17
- `.waitForConsole()`, or `.waitForError()`.
18
-
19
- Messages emitted before that first touch are not captured, so this can return an
20
- empty array:
15
+ Capture is automatic. CraftDriver subscribes to log events at launch, so
16
+ messages emitted at any point — including during the first navigation, before you
17
+ ever touch `browser.logs` — are captured. There is nothing to enable and no
18
+ listener to pre-arm:
21
19
 
22
20
  ```typescript
23
21
  await browser.navigateTo('https://example.com');
24
- const messages = browser.logs.getMessages();
25
- ```
26
-
27
- Start capture before the action that logs:
28
-
29
- ```typescript
30
- const warning = browser.logs.waitForConsole((msg) => msg.level === 'warn');
31
-
32
22
  await browser.getByRole('button', { name: 'Show warning' }).click();
33
- expect((await warning).text).toContain('Heads up');
23
+ const messages = browser.logs.getMessages(); // includes the warning
34
24
  ```
35
25
 
36
- Or start capture immediately at launch:
37
-
38
- ```typescript
39
- const browser = await Browser.launch({
40
- browserName: 'chrome',
41
- captureLogs: true,
42
- });
43
- ```
26
+ The subscription rides the connect-time batch, so it costs no extra round trip
27
+ and no measurable overhead. Use `clearLogs()` if you want to assert only on
28
+ messages emitted after a certain point.
44
29
 
45
30
  ## Read Collected Messages
46
31
 
package/docs/mcp.md CHANGED
@@ -53,7 +53,7 @@ goose configure # add craftdriver as a stdio server
53
53
 
54
54
  ## Tools
55
55
 
56
- Compact set — 14 tools, one line each. Long help lives in the schema
56
+ Compact set — 15 tools, one line each. Long help lives in the schema
57
57
  description; clients render it in the model's context once per session.
58
58
 
59
59
  | Tool | Purpose |
@@ -70,11 +70,25 @@ description; clients render it in the model's context once per session.
70
70
  | `browser_pages` | List open pages (id, url, title). |
71
71
  | `browser_snapshot` | **Sanitized DOM summary with refs.** Use `ref=eN` as the selector for subsequent calls. |
72
72
  | `browser_screenshot` | Capture PNG to a file (auto-allocated under the per-session artifact dir; never inlined). |
73
+ | `browser_trace` | Start/stop tracing and export a Vibium Player compatible zip. |
73
74
  | `browser_status` | Browser up? Which URL is active? |
74
75
  | `browser_advanced_eval` | Evaluate JS in the page. Last resort. |
75
76
 
76
- `browser_trace` (start/stop/explain) and trace resources are slated
77
- for a future release alongside richer trace introspection.
77
+ Trace a complete agent-driven session with two calls:
78
+
79
+ ```jsonc
80
+ { "name": "browser_trace", "arguments": {
81
+ "action": "start", "out_dir": "./traces/agent-raw", "title": "Agent flow"
82
+ } }
83
+ // ...browser_navigate / browser_fill / browser_click calls...
84
+ { "name": "browser_trace", "arguments": {
85
+ "action": "stop", "path": "./traces/agent-flow.zip"
86
+ } }
87
+ ```
88
+
89
+ The zip opens at [player.vibium.dev](https://player.vibium.dev/). The raw
90
+ directory remains available if the MCP client disconnects before the stop
91
+ call; in that case there is no finalized zip.
78
92
 
79
93
  ## Selector syntax
80
94
 
@@ -0,0 +1,16 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta http-equiv="refresh" content="0; url=./debug-failing-tests-with-traces">
6
+ <link
7
+ rel="canonical"
8
+ href="https://dtopuzov.github.io/craftdriver/recipes/debug-failing-tests-with-traces"
9
+ >
10
+ <title>Vitest traces moved</title>
11
+ <script>location.replace('./debug-failing-tests-with-traces')</script>
12
+ </head>
13
+ <body>
14
+ <p>This recipe moved to <a href="./debug-failing-tests-with-traces">Debug With Traces</a>.</p>
15
+ </body>
16
+ </html>
@@ -0,0 +1,16 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta http-equiv="refresh" content="0; url=./debug-failing-tests-with-traces">
6
+ <link
7
+ rel="canonical"
8
+ href="https://dtopuzov.github.io/craftdriver/recipes/debug-failing-tests-with-traces"
9
+ >
10
+ <title>Vitest trace recipe moved</title>
11
+ <script>location.replace('./debug-failing-tests-with-traces')</script>
12
+ </head>
13
+ <body>
14
+ <p>This recipe moved to <a href="./debug-failing-tests-with-traces">Debug With Traces</a>.</p>
15
+ </body>
16
+ </html>
@@ -1,17 +1,14 @@
1
1
  # Fail On Console And JavaScript Errors
2
2
 
3
3
  A flow can look correct in the DOM while quietly throwing in the console — a
4
- failed fetch, an undefined access, a rejected promise. Capture logs at launch,
5
- run the flow, then assert no JavaScript errors were reported so those failures
6
- break the test instead of reaching users. This recipe uses the live
4
+ failed fetch, an undefined access, a rejected promise. Logs are captured
5
+ automatically from launch, so you can run the flow and then assert no JavaScript
6
+ errors were reported, breaking the test instead of reaching users. This recipe
7
+ uses the live
7
8
  [console errors example](https://dtopuzov.github.io/craftdriver/examples/console-errors.html).
8
- It enables `captureLogs` at launch, so it shows the `launch` call.
9
9
 
10
10
  ```ts
11
- const browser = await Browser.launch({
12
- browserName: 'chrome',
13
- captureLogs: true,
14
- });
11
+ const browser = await Browser.launch();
15
12
 
16
13
  await browser.navigateTo('https://dtopuzov.github.io/craftdriver/examples/console-errors.html');
17
14
  browser.logs.clearLogs();
@@ -38,7 +35,7 @@ await logged;
38
35
 
39
36
  ## Notes
40
37
 
41
- - `assertNoErrors()` checks captured JavaScript errors, so keep `captureLogs: true`.
38
+ - `assertNoErrors()` checks captured JavaScript errors — capture is automatic, nothing to enable.
42
39
  - Call `clearLogs()` before the flow so errors from earlier tests don't bleed in.
43
40
  - Use `waitForConsole()` / `waitForError()` when a specific message is the thing you're asserting.
44
41
 
@@ -0,0 +1,117 @@
1
+ # Use Traces To Debug Failing Tests
2
+
3
+ A failed assertion tells you what was wrong at the end of a test, but often not
4
+ how the browser got there. A trace preserves the actions, screenshots, console
5
+ and JavaScript events, navigation, and network activity that led to the
6
+ failure.
7
+
8
+ ## A Shared Trace Format
9
+
10
+ [Jason Huggins' SeleniumConf talk](https://www.youtube.com/watch?v=c-OGZK3F0tk&t=1530s)
11
+ was one of the reasons Craftdriver adopted portable browser traces instead of
12
+ a Craftdriver-only viewer format.
13
+
14
+ The exported zip follows the Playwright-style
15
+ recording layout also used by tools like
16
+ [Vibium](https://github.com/VibiumDev/vibium/blob/main/docs/explanation/recording-format.md)
17
+ and WDIO, so the same artifact can be opened in viewers such as
18
+ [Vibium Player](https://player.vibium.dev/).
19
+
20
+ Craftdriver still records a crash-resilient raw NDJSON trace first, then
21
+ packages it into the shared zip format when a test runner asks for an artifact.
22
+
23
+ ## Keep A Trace When A Vitest Test Fails
24
+
25
+ Start one browser for the suite, but scope tracing to the test hooks.
26
+ `afterEach()` knows whether the test failed, so it can keep only the useful zip:
27
+
28
+ ```ts
29
+ import { afterAll, afterEach, beforeAll, beforeEach, describe, it } from 'vitest';
30
+ import { rmSync } from 'node:fs';
31
+ import path from 'node:path';
32
+ import { Browser } from 'craftdriver';
33
+
34
+ describe('login', () => {
35
+ let browser: Browser;
36
+ const rawTraceDir = path.resolve('test-results/traces/login-raw');
37
+ const traceZip = path.resolve('test-results/traces/login-failure.zip');
38
+
39
+ beforeAll(async () => {
40
+ browser = await Browser.launch();
41
+ });
42
+
43
+ beforeEach(async () => {
44
+ await browser.startTrace({
45
+ outDir: rawTraceDir,
46
+ title: 'Failing login test',
47
+ });
48
+ });
49
+
50
+ afterEach(async ({ task }) => {
51
+ const failed = task.result?.state === 'fail';
52
+ await browser.stopTrace(failed ? { path: traceZip } : undefined);
53
+ if (!failed) rmSync(rawTraceDir, { recursive: true, force: true });
54
+ });
55
+
56
+ afterAll(async () => browser.quit());
57
+
58
+ it('fills and submits the login form', async () => {
59
+ await browser.navigateTo(
60
+ 'https://dtopuzov.github.io/craftdriver/examples/login.html',
61
+ );
62
+ await browser.fill('#username', 'alice');
63
+ await browser.fill('#password', 'secret');
64
+ await browser.click('#submit');
65
+ await browser.expect('#welcome').toContainText('Welcome back, alice!');
66
+ });
67
+ });
68
+ ```
69
+
70
+ A passing run removes the raw trace and leaves no artifact. After a normal
71
+ Vitest failure, the evidence is kept here:
72
+
73
+ ```text
74
+ test-results/
75
+ └── traces/
76
+ ├── login-raw/ # trace.ndjson and evidence screenshots
77
+ └── login-failure.zip # open this in Vibium Player
78
+ ```
79
+
80
+ For the best online experience, drop the zip into
81
+ [Vibium Player](https://player.vibium.dev/). To keep the trace entirely local,
82
+ use Playwright Trace Viewer:
83
+
84
+ ```sh
85
+ npx playwright show-trace test-results/traces/login-failure.zip
86
+ ```
87
+
88
+ Craftdriver includes screenshot-backed frame snapshots, so Playwright's main
89
+ browser panel shows the captured page state. It is a visual snapshot rather
90
+ than a restorable DOM, so the DOM/locator inspector is intentionally limited.
91
+
92
+ If the test process is killed before `afterEach()` runs, a zip cannot be
93
+ finalized, but the append-only `trace.ndjson` and completed screenshots remain
94
+ in `login-raw/`.
95
+
96
+ ## Try A Trace Before Creating A Failure
97
+
98
+ 1. [Download the sample login trace](https://dtopuzov.github.io/craftdriver/traces/vitest-login.zip).
99
+ 2. Open [Vibium Player](https://player.vibium.dev/).
100
+ 3. Drop `vitest-login.zip` onto the player and step through the login.
101
+
102
+ `stopTrace()` captures the final page state, so a failed assertion is visible
103
+ as the last player frame. The sample was generated by the runnable Vitest proof
104
+ for this recipe against the same published login page. To exercise the failure
105
+ workflow yourself, change the expected message to `Welcome back, bob!`, run the
106
+ test, and open the resulting `login-failure.zip`.
107
+
108
+ Trace files can contain screenshots, URLs, console messages, network metadata,
109
+ selectors, and entered values. Use test credentials and review a trace before
110
+ publishing it outside your team.
111
+
112
+ ## Learn More
113
+
114
+ - [Tracing](../tracing.md)
115
+ - [Use CraftDriver With Vitest Hooks](./vitest-browser-lifecycle.md)
116
+ - [Vibium Player](https://player.vibium.dev/)
117
+ - [Jason Huggins on standardized tracing](https://www.youtube.com/watch?v=c-OGZK3F0tk&t=1530s)
@@ -10,10 +10,7 @@ then downloads a report from the
10
10
  It sets `downloadsDir` at launch, so it shows the `launch` call.
11
11
 
12
12
  ```ts
13
- const browser = await Browser.launch({
14
- browserName: 'chrome',
15
- downloadsDir: '.tmp/downloads',
16
- });
13
+ const browser = await Browser.launch({ downloadsDir: '.tmp/downloads' });
17
14
 
18
15
  await browser.navigateTo('https://dtopuzov.github.io/craftdriver/examples/upload.html');
19
16
  await browser.find('#file-input').setInputFiles('tests/fixtures/sample.txt');
@@ -15,7 +15,7 @@ Run this once as a setup step. It is one of the few recipes that shows
15
15
  ```ts
16
16
  import { Browser } from 'craftdriver';
17
17
 
18
- const browser = await Browser.launch({ browserName: 'chrome' });
18
+ const browser = await Browser.launch();
19
19
 
20
20
  await browser.navigateTo('https://dtopuzov.github.io/craftdriver/examples/login.html');
21
21
  await browser.getByLabel('Username').fill('alice');
@@ -32,10 +32,7 @@ await browser.quit();
32
32
  Launch with `storageState` and the browser starts already signed in.
33
33
 
34
34
  ```ts
35
- const browser = await Browser.launch({
36
- browserName: 'chrome',
37
- storageState: '.auth/alice.json',
38
- });
35
+ const browser = await Browser.launch({ storageState: '.auth/alice.json' });
39
36
 
40
37
  await browser.navigateTo('https://dtopuzov.github.io/craftdriver/examples/login.html');
41
38
  await browser.expect('#welcome').toContainText('Welcome back, alice!');
@@ -9,11 +9,7 @@ Because mobile emulation is a launch-time capability, this recipe shows the
9
9
  `launch` call.
10
10
 
11
11
  ```ts
12
- const browser = await Browser.launch({
13
- browserName: 'chrome', // mobile emulation is Chrome/Chromium only
14
- mobileEmulation: 'Pixel 7',
15
- captureLogs: true,
16
- });
12
+ const browser = await Browser.launch({ mobileEmulation: 'Pixel 7' });
17
13
 
18
14
  // ?bidi=true makes the demo page issue real requests for interception.
19
15
  await browser.navigateTo('https://dtopuzov.github.io/craftdriver/examples/network.html?bidi=true');
@@ -33,7 +29,7 @@ browser.logs.assertNoErrors();
33
29
 
34
30
  - Mobile emulation is currently Chrome/Chromium only.
35
31
  - Mock before the action if the page reads data during that step.
36
- - Keep `captureLogs: true` so `assertNoErrors()` has something to check.
32
+ - Console/error capture is automatic, so `assertNoErrors()` always has the full picture.
37
33
 
38
34
  ## Learn More
39
35
 
@@ -16,10 +16,7 @@ describe('login page', () => {
16
16
  let browser: Browser;
17
17
 
18
18
  beforeAll(async () => {
19
- browser = await Browser.launch({
20
- browserName: 'chrome',
21
- captureLogs: true,
22
- });
19
+ browser = await Browser.launch();
23
20
  });
24
21
 
25
22
  beforeEach(async () => {
package/docs/recipes.md CHANGED
@@ -49,4 +49,4 @@ For exact signatures, use the linked feature docs and the
49
49
  | ----------------------------- | ------------------------------------------------------------------------------- | ------------------------------------------------------------------------ |
50
50
  | Accessibility regression gate | CI should fail on serious page or component accessibility issues. | [Run Accessibility Gates](./recipes/accessibility-gate.md) |
51
51
  | Console and JavaScript errors | Tests should fail if the browser reports unexpected client-side errors. | [Fail On Console And JavaScript Errors](./recipes/console-error-gate.md) |
52
- | Evidence on failure | You want a replayable trail of actions, network, logs, errors, and screenshots. | [Capture Failure Evidence With Tracing](./recipes/trace-failing-test.md) |
52
+ | Debug failing tests | You need the actions, screenshots, logs, and network activity behind a failure. | [Use Traces To Debug Failing Tests](./recipes/debug-failing-tests-with-traces.md) |
package/docs/tracing.md CHANGED
@@ -14,7 +14,7 @@ process crash cannot lose the evidence that led up to it.
14
14
  ```ts
15
15
  import { chromium } from 'craftdriver';
16
16
 
17
- const browser = await chromium.launch({ enableBiDi: true });
17
+ const browser = await chromium.launch();
18
18
 
19
19
  await browser.startTrace({ outDir: './artefacts/login' });
20
20
  try {
@@ -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, writeFileSync } from 'node:fs';
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
- // 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; }
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(` 📁 trace: ${currentDir}`);
228
+ console.error(` 📦 Vibium trace: ${zipPath}`);
186
229
  }
187
230
  });
188
231
  }
@@ -202,7 +245,7 @@ import { autoTrace } from './auto-trace';
202
245
 
203
246
  describe('Login', () => {
204
247
  let browser: Browser;
205
- beforeAll(async () => { browser = await Browser.launch({ enableBiDi: true }); });
248
+ beforeAll(async () => { browser = await Browser.launch(); });
206
249
  afterAll(async () => { await browser.quit(); });
207
250
 
208
251
  autoTrace(() => browser); // ← that's it
@@ -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/trace.ndjson is on disk.
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
- 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.
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
- * emit HAR (the NDJSON already holds request/response events),
278
- * capture DOM snapshots or sourcemaps.
357
+ * capture a restorable DOM or sourcemaps.
279
358
 
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.
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.1.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
- "@typescript-eslint/eslint-plugin": "^8.1.0",
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",
@@ -34,8 +34,9 @@ break the moment markup shifts.
34
34
  3. `instanceof CraftdriverError` is true on every public throw;
35
35
  `instanceof Error` is also true.
36
36
  4. BiDi features (`network`, `logs`, tracing, init scripts, true load
37
- events) require `Browser.launch({ enableBiDi: true })`. The error
38
- code on the wrong transport is `UNSUPPORTED`.
37
+ events) work out of the box — `enableBiDi` defaults to `true`. They only
38
+ break if you explicitly pass `enableBiDi: false`; the error code on the
39
+ wrong transport is `UNSUPPORTED`.
39
40
  5. Tests fetch from the example server. Start it in a separate
40
41
  terminal: `npm run examples:start` before `npm test`.
41
42