craftdriver 1.2.0 → 1.4.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 (113) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/README.md +2 -1
  3. package/dist/cli/dispatcher.d.ts.map +1 -1
  4. package/dist/cli/dispatcher.js +13 -1
  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 +7 -2
  11. package/dist/index.d.ts.map +1 -1
  12. package/dist/index.js +4 -0
  13. package/dist/index.js.map +1 -1
  14. package/dist/lib/browser.d.ts +156 -27
  15. package/dist/lib/browser.d.ts.map +1 -1
  16. package/dist/lib/browser.js +474 -212
  17. package/dist/lib/browser.js.map +1 -1
  18. package/dist/lib/capabilities.d.ts +35 -0
  19. package/dist/lib/capabilities.d.ts.map +1 -0
  20. package/dist/lib/capabilities.js +58 -0
  21. package/dist/lib/capabilities.js.map +1 -0
  22. package/dist/lib/chrome.d.ts +7 -2
  23. package/dist/lib/chrome.d.ts.map +1 -1
  24. package/dist/lib/chrome.js +10 -1
  25. package/dist/lib/chrome.js.map +1 -1
  26. package/dist/lib/driverManager.d.ts +92 -0
  27. package/dist/lib/driverManager.d.ts.map +1 -1
  28. package/dist/lib/driverManager.js +229 -10
  29. package/dist/lib/driverManager.js.map +1 -1
  30. package/dist/lib/electron.d.ts +60 -0
  31. package/dist/lib/electron.d.ts.map +1 -0
  32. package/dist/lib/electron.js +85 -0
  33. package/dist/lib/electron.js.map +1 -0
  34. package/dist/lib/electronDeeplink.d.ts +22 -0
  35. package/dist/lib/electronDeeplink.d.ts.map +1 -0
  36. package/dist/lib/electronDeeplink.js +118 -0
  37. package/dist/lib/electronDeeplink.js.map +1 -0
  38. package/dist/lib/electronDiagnostics.d.ts +49 -0
  39. package/dist/lib/electronDiagnostics.d.ts.map +1 -0
  40. package/dist/lib/electronDiagnostics.js +195 -0
  41. package/dist/lib/electronDiagnostics.js.map +1 -0
  42. package/dist/lib/electronDialogMock.d.ts +51 -0
  43. package/dist/lib/electronDialogMock.d.ts.map +1 -0
  44. package/dist/lib/electronDialogMock.js +106 -0
  45. package/dist/lib/electronDialogMock.js.map +1 -0
  46. package/dist/lib/electronFuses.d.ts +13 -0
  47. package/dist/lib/electronFuses.d.ts.map +1 -0
  48. package/dist/lib/electronFuses.js +118 -0
  49. package/dist/lib/electronFuses.js.map +1 -0
  50. package/dist/lib/electronMainBridge.d.ts +28 -0
  51. package/dist/lib/electronMainBridge.d.ts.map +1 -0
  52. package/dist/lib/electronMainBridge.js +270 -0
  53. package/dist/lib/electronMainBridge.js.map +1 -0
  54. package/dist/lib/electronMainLogs.d.ts +81 -0
  55. package/dist/lib/electronMainLogs.d.ts.map +1 -0
  56. package/dist/lib/electronMainLogs.js +176 -0
  57. package/dist/lib/electronMainLogs.js.map +1 -0
  58. package/dist/lib/electronMock.d.ts +52 -0
  59. package/dist/lib/electronMock.d.ts.map +1 -0
  60. package/dist/lib/electronMock.js +96 -0
  61. package/dist/lib/electronMock.js.map +1 -0
  62. package/dist/lib/electronRemote.d.ts +106 -0
  63. package/dist/lib/electronRemote.d.ts.map +1 -0
  64. package/dist/lib/electronRemote.js +406 -0
  65. package/dist/lib/electronRemote.js.map +1 -0
  66. package/dist/lib/electronVersions.d.ts +34 -0
  67. package/dist/lib/electronVersions.d.ts.map +1 -0
  68. package/dist/lib/electronVersions.js +90 -0
  69. package/dist/lib/electronVersions.js.map +1 -0
  70. package/dist/lib/errors.d.ts +27 -0
  71. package/dist/lib/errors.d.ts.map +1 -1
  72. package/dist/lib/errors.js +27 -0
  73. package/dist/lib/errors.js.map +1 -1
  74. package/dist/lib/launchTarget.d.ts +30 -0
  75. package/dist/lib/launchTarget.d.ts.map +1 -0
  76. package/dist/lib/launchTarget.js +112 -0
  77. package/dist/lib/launchTarget.js.map +1 -0
  78. package/dist/lib/page.d.ts +1 -0
  79. package/dist/lib/page.d.ts.map +1 -1
  80. package/dist/lib/page.js +1 -1
  81. package/dist/lib/page.js.map +1 -1
  82. package/dist/lib/service.d.ts +21 -0
  83. package/dist/lib/service.d.ts.map +1 -1
  84. package/dist/lib/service.js +94 -6
  85. package/dist/lib/service.js.map +1 -1
  86. package/dist/lib/tracing.d.ts +20 -4
  87. package/dist/lib/tracing.d.ts.map +1 -1
  88. package/dist/lib/tracing.js +38 -3
  89. package/dist/lib/tracing.js.map +1 -1
  90. package/dist/lib/vibiumTrace.d.ts +14 -0
  91. package/dist/lib/vibiumTrace.d.ts.map +1 -0
  92. package/dist/lib/vibiumTrace.js +372 -0
  93. package/dist/lib/vibiumTrace.js.map +1 -0
  94. package/docs/api-reference.md +38 -2
  95. package/docs/browser-api.md +7 -1
  96. package/docs/electron.md +419 -0
  97. package/docs/error-codes.md +5 -0
  98. package/docs/index.md +4 -0
  99. package/docs/mcp.md +17 -3
  100. package/docs/public/recipes/trace-failing-test.html +16 -0
  101. package/docs/public/recipes/vitest-vibium-trace.html +16 -0
  102. package/docs/public/traces/vitest-login.zip +0 -0
  103. package/docs/recipes/debug-failing-tests-with-traces.md +117 -0
  104. package/docs/recipes/electron-app-from-another-repo.md +136 -0
  105. package/docs/recipes/electron-deep-link.md +117 -0
  106. package/docs/recipes/electron-mock-apis.md +88 -0
  107. package/docs/recipes/electron-native-dialog.md +105 -0
  108. package/docs/recipes.md +29 -10
  109. package/docs/tracing.md +102 -24
  110. package/package.json +8 -8
  111. package/skills/craftdriver/cheatsheet.md +4 -2
  112. package/skills/craftdriver/patterns.md +2 -1
  113. package/docs/recipes/trace-failing-test.md +0 -51
@@ -0,0 +1,419 @@
1
+ # Testing Electron Apps
2
+
3
+ CraftDriver can launch a packaged Electron app and test its **renderer** with
4
+ the same `Browser` API used for web pages: locators, clicks, form fills,
5
+ assertions, viewport screenshots, element screenshots, `evaluate()`, and
6
+ page/window helpers.
7
+
8
+ For apps you control, CraftDriver can also access the main process and replace
9
+ native open, save, and message dialogs with deterministic test results.
10
+
11
+ ## Quick Start
12
+
13
+ Launch the packaged executable and pass the Electron version used by the app:
14
+
15
+ ```ts
16
+ import { Browser, By } from 'craftdriver';
17
+
18
+ const browser = await Browser.launch({
19
+ electron: {
20
+ appBinaryPath: '/path/to/YourApp',
21
+ version: '43.1.0',
22
+ },
23
+ });
24
+
25
+ try {
26
+ await browser.find(By.testId('app-title')).expect().toHaveText('Your App');
27
+ await browser.click(By.testId('nav-settings'));
28
+ await browser.fill(By.testId('name'), 'Ada');
29
+ await browser.click(By.testId('save'));
30
+ await browser.find(By.testId('status')).expect().toHaveText('Saved');
31
+ } finally {
32
+ await browser.quit();
33
+ }
34
+ ```
35
+
36
+ Electron launches default to Classic WebDriver. The app loads its own UI, so you
37
+ usually do **not** call `navigateTo()`.
38
+
39
+ For a fuller project layout, see
40
+ [Test An Electron App From Another Repo](./recipes/electron-app-from-another-repo.md).
41
+
42
+ ## Mock Native Dialogs
43
+
44
+ Native operating-system dialogs are outside the DOM, so WebDriver cannot click
45
+ their buttons or select their files. Mock the Electron main-process method before
46
+ the renderer action that opens it:
47
+
48
+ ```ts
49
+ const browser = await Browser.launch({
50
+ electron: {
51
+ appBinaryPath: '/path/to/YourApp',
52
+ version: '43.1.0',
53
+ mainProcess: true,
54
+ },
55
+ });
56
+
57
+ const openDialog = await browser.electron.mockDialog('showOpenDialog', {
58
+ canceled: false,
59
+ filePaths: ['/fixtures/invoice.pdf'],
60
+ });
61
+
62
+ await browser.click(By.testId('choose-file'));
63
+ await browser.find(By.testId('selected-file')).expect().toHaveText('/fixtures/invoice.pdf');
64
+
65
+ expect(await openDialog.getCallCount()).toBe(1);
66
+ expect(await openDialog.getCalls()).toEqual([
67
+ {
68
+ options: {
69
+ title: 'Choose an invoice',
70
+ properties: ['openFile'],
71
+ },
72
+ },
73
+ ]);
74
+
75
+ await openDialog.restore();
76
+ ```
77
+
78
+ `mockDialog()` supports Electron's asynchronous `showOpenDialog`,
79
+ `showSaveDialog`, and `showMessageBox` methods. The supplied result has the same
80
+ shape as the real Electron result:
81
+
82
+ ```ts
83
+ await browser.electron.mockDialog('showSaveDialog', {
84
+ canceled: false,
85
+ filePath: '/fixtures/export.csv',
86
+ });
87
+
88
+ await browser.electron.mockDialog('showMessageBox', {
89
+ response: 1,
90
+ checkboxChecked: false,
91
+ });
92
+ ```
93
+
94
+ The returned mock provides `getCalls()`, `getCallCount()`, `clearCalls()`, and
95
+ `restore()`. CraftDriver also restores active dialog mocks during
96
+ `browser.quit()`. Call records contain the dialog options; Electron's optional
97
+ parent `BrowserWindow` is intentionally excluded because it cannot cross the
98
+ test-process boundary.
99
+
100
+ See [Mock A Native Electron File Dialog](./recipes/electron-native-dialog.md)
101
+ for the complete renderer, preload, main-process, and test flow.
102
+
103
+ ## Mock Any Electron API
104
+
105
+ `mockDialog()` is a typed convenience over a general primitive: `mock()` replaces
106
+ **any** `electron.<api>.<fn>` main-process method with a scripted return value and
107
+ a call recorder. Reach for it when your app calls into Electron beyond dialogs —
108
+ `shell.openExternal`, `app.getPath`, `safeStorage.encryptString`, `clipboard.*`,
109
+ and so on.
110
+
111
+ ```ts
112
+ // Stop a "Open in browser" button from actually launching a browser, and assert
113
+ // the app asked to open the right URL — without leaving the test machine.
114
+ const openExternal = await browser.electron.mock('shell', 'openExternal', true);
115
+
116
+ await browser.click(By.testId('open-docs'));
117
+
118
+ expect(await openExternal.getCalls()).toEqual([{ args: ['https://example.com/docs'] }]);
119
+ await openExternal.restore();
120
+ ```
121
+
122
+ The returned mock mirrors the dialog mock: `getCalls()` (each call is
123
+ `{ args: [...] }`), `getCallCount()`, `clearCalls()`, `mockReturnValue(value)` to
124
+ re-script the return, and `restore()`. Active mocks are also restored on
125
+ `browser.quit()`.
126
+
127
+ Notes:
128
+
129
+ - The scripted value is returned **as-is**, not wrapped in a Promise, so it works
130
+ for both synchronous methods (`app.getName()`) and `await`ed asynchronous ones
131
+ (`safeStorage`-style APIs). For an async method, pass the already-resolved value.
132
+ - Recorded call arguments and the return value must be JSON-serializable; complex
133
+ values (a `BrowserWindow`, a `Buffer`) are recorded as a descriptive placeholder
134
+ rather than crossing the process boundary.
135
+ - Mocking the same `api.fn` twice throws until you `restore()` the first mock.
136
+
137
+ See [Mock Electron APIs](./recipes/electron-mock-apis.md) for `shell.openExternal`
138
+ and `clipboard.writeText` flows end to end.
139
+
140
+ ## Test Deep Links (Custom Protocols)
141
+
142
+ If your app registers a custom URL scheme (`myapp://…`), `triggerDeeplink()` opens
143
+ one against the **running** app exactly as the OS would when a browser, email, or
144
+ another app hands off the link — so your real `open-url` / `second-instance`
145
+ handler runs. It is fire-and-forget: it resolves once the OS launcher is spawned,
146
+ so assert the effect through your app afterwards.
147
+
148
+ ```ts
149
+ await browser.electron.triggerDeeplink('myapp://open?file=test.txt');
150
+
151
+ // Assert however your app surfaces the link — a renderer element, a main-process
152
+ // log, or state read back with executeMain.
153
+ await browser.find(By.testId('deeplink-result')).expect().toHaveText('myapp://open?file=test.txt');
154
+ ```
155
+
156
+ Your **app** must:
157
+
158
+ - register the protocol — `app.setAsDefaultProtocolClient('myapp')` plus the
159
+ packager's protocol declaration (electron-builder `protocols:` / Forge
160
+ `protocols`), which writes the macOS `CFBundleURLTypes` and Windows registry
161
+ entries; and
162
+ - hold the single-instance lock (`app.requestSingleInstanceLock()`) and read the
163
+ URL from `open-url` (macOS) and `second-instance` argv (Windows, Linux).
164
+
165
+ Platform notes:
166
+
167
+ - **macOS** delivers the URL to the running instance via `open-url`; no extra
168
+ routing is needed. The bundle must be known to LaunchServices (installed, or
169
+ registered with `lsregister`).
170
+ - **Windows / Linux** would launch a *second* process; craftdriver appends the
171
+ running app's user-data dir as a `userData` query parameter (auto-detected via
172
+ the main process) so the single-instance lock routes the link to the test
173
+ instance. Windows also needs `appBinaryPath` set on launch.
174
+ - Only custom protocols are accepted; `http`/`https`/`file` throw `INVALID_ARGUMENT`.
175
+
176
+ See [Test An Electron Deep Link](./recipes/electron-deep-link.md) for the complete
177
+ app + test flow.
178
+
179
+ ### Can this test a production build?
180
+
181
+ Yes, when that packaged build accepts Electron's `--inspect` argument. Electron
182
+ controls this with the `EnableNodeCliInspectArguments` fuse, which is enabled by
183
+ default. CraftDriver launches the existing executable with a local,
184
+ session-scoped inspector and replaces the dialog function in memory. It does not
185
+ patch the executable or require a test hook in your application code. Code
186
+ signing by itself does not prevent this.
187
+
188
+ Security-hardened applications often disable this fuse. CraftDriver cannot turn
189
+ it back on after packaging or signing, and renderer-only automation will continue
190
+ to work but `mock()`, `mockDialog()`, and `executeMain()` will not. When that
191
+ happens CraftDriver reads the fuse straight from the packaged binary and the error
192
+ says so precisely — `ELECTRON_MAIN_UNAVAILABLE` with `detail.fuseStatus:
193
+ 'disabled'` and the fix below — instead of a generic "inspector unreachable".
194
+ Build a separate test artifact with the fuse enabled **before code signing**:
195
+
196
+ ```js
197
+ const { FusesPlugin } = require('@electron-forge/plugin-fuses');
198
+ const { FuseV1Options, FuseVersion } = require('@electron/fuses');
199
+
200
+ module.exports = {
201
+ // ...your normal Forge configuration
202
+ plugins: [
203
+ new FusesPlugin({
204
+ version: FuseVersion.V1,
205
+ [FuseV1Options.EnableNodeCliInspectArguments]: true,
206
+ }),
207
+ ],
208
+ };
209
+ ```
210
+
211
+ For another packager, use `@electron/fuses` in its pre-sign packaging hook and
212
+ set `FuseV1Options.EnableNodeCliInspectArguments` to `true`. Keep the production
213
+ artifact hardened if that is your security policy; the test artifact should be
214
+ stored and distributed like other internal test tooling.
215
+
216
+ This is build instrumentation, not application instrumentation. Your app should
217
+ continue to use Electron normally, for example `dialog.showOpenDialog(...)` in an
218
+ IPC handler. One limitation is that CraftDriver replaces the method on
219
+ `electron.dialog`; if your app copies that function into another variable before
220
+ the test installs the mock, the copied reference cannot be replaced. Prefer
221
+ calling `dialog.showOpenDialog(...)` at the point of use.
222
+
223
+ ## Package The App First
224
+
225
+ `appBinaryPath` must point to the packaged app executable. Do not point it at an
226
+ Electron source directory, a `.app` bundle root, a `.dmg`, or an installer.
227
+
228
+ Typical executable paths:
229
+
230
+ | OS | `appBinaryPath` |
231
+ | ------- | ----------------------------------------------- |
232
+ | macOS | `YourApp.app/Contents/MacOS/YourApp` |
233
+ | Windows | `YourApp.exe` |
234
+ | Linux | the unpacked binary, e.g. `yourapp` (lowercase) |
235
+
236
+ If you point chromedriver at an unpackaged app directory, Electron can start as
237
+ a plain Chromium window and your app UI will not load. Package with your normal
238
+ Electron Forge, electron-builder, or release-build command first.
239
+
240
+ ## Match The Driver To Electron
241
+
242
+ Electron bundles its own Chromium. The chromedriver used for the test must match
243
+ that bundled Chromium major, not the Chrome installed on the machine.
244
+
245
+ The recommended setup is:
246
+
247
+ ```ts
248
+ await Browser.launch({
249
+ electron: {
250
+ appBinaryPath: '/path/to/YourApp',
251
+ version: '43.1.0',
252
+ },
253
+ });
254
+ ```
255
+
256
+ With `version`, CraftDriver maps the Electron version to the bundled Chromium
257
+ major and downloads a matching Chrome-for-Testing chromedriver.
258
+
259
+ Other valid driver sources:
260
+
261
+ - `electron.chromedriverPath` when you already have a matching chromedriver.
262
+ - `CRAFTDRIVER_ELECTRON_CHROMEDRIVER_PATH` for a CI-wide override.
263
+ - `electron-chromedriver` installed in the test project.
264
+ - macOS packaged app detection. If you omit `version`, CraftDriver can read the
265
+ Electron version from a packaged `.app` and resolve the driver from that.
266
+
267
+ For Linux and Windows packaged apps, pass `version` or an explicit driver path.
268
+ CraftDriver does not use a system Chrome driver for Electron, because it would
269
+ often be the wrong Chromium version.
270
+
271
+ Version-based driver resolution works for Electron majors known to CraftDriver
272
+ and Chromium majors published by Chrome for Testing. If your app uses an older,
273
+ newer, or custom Electron/Chromium build, use `chromedriverPath`.
274
+
275
+ ## Windows And Splash Screens
276
+
277
+ Many desktop apps open a splash window first, then replace it with the main
278
+ window. Wait for the real window by title or URL:
279
+
280
+ ```ts
281
+ const browser = await Browser.launch({
282
+ electron: { appBinaryPath: '/path/to/YourApp', version: '43.1.0' },
283
+ });
284
+
285
+ const main = await browser.waitForPage({ title: /Your App/ });
286
+ await main.find(By.testId('app-root')).expect().toBeVisible();
287
+ ```
288
+
289
+ The returned `Page` is bound to that window. In Electron's default Classic mode,
290
+ `waitForPage({ title | url })` also makes the matched window current, so
291
+ top-level calls such as `browser.find(...)` and `browser.click(...)` target it.
292
+
293
+ Use `browser.pages()` to list open top-level windows. If the current splash
294
+ window closes and exactly one window remains, `browser.activePage()` recovers to
295
+ that remaining window. If several windows remain, select one explicitly with
296
+ `waitForPage()` or `pages()`.
297
+
298
+ ## Electron API Differences
299
+
300
+ Most renderer-facing APIs work the same way they do for browser tests. The main
301
+ differences are launch shape and protocol defaults.
302
+
303
+ Use these Electron-specific launch options:
304
+
305
+ ```ts
306
+ await Browser.launch({
307
+ electron: {
308
+ appBinaryPath: '/path/to/YourApp',
309
+ version: '43.1.0',
310
+ args: ['--lang=de-DE'],
311
+ },
312
+ });
313
+ ```
314
+
315
+ Do not combine `electron` with browser-only launch options:
316
+
317
+ | Browser option | Electron alternative |
318
+ | ---------------------------------- | -------------------------------------------------------------------------------------------------- |
319
+ | `browserName` | Omit it; Electron uses its bundled Chromium. |
320
+ | `browserPath` | Use `electron.appBinaryPath`. |
321
+ | top-level `args` | Use `electron.args`. |
322
+ | `chromeService` / `firefoxService` | Use `electronService` only when custom driver service settings are needed. |
323
+ | `mobileEmulation` | Not applicable to desktop Electron apps; use `setViewportSize()` for responsive renderer coverage. |
324
+
325
+ Electron is also always a GUI app in this V1 flow. CraftDriver does not add
326
+ headless flags for Electron, even if `HEADLESS=true`.
327
+
328
+ ## BiDi-Only Features
329
+
330
+ Electron defaults to Classic WebDriver because that keeps the packaged app's
331
+ initial UI loaded and ready to test.
332
+
333
+ Some CraftDriver APIs require BiDi. For Electron, you must launch with
334
+ `enableBiDi: true` before using them:
335
+
336
+ - `browser.logs`
337
+ - `browser.network`, `waitForRequest()`, and `waitForResponse()`
338
+ - `browser.startTrace()`
339
+ - `browser.openPage()`
340
+ - `browser.newContext()`, `browser.contexts()`, and `browser.defaultContext`
341
+ - `browser.addInitScript()`
342
+ - `browser.grantPermissions()`, `setGeolocation()`, and `emulate()`
343
+ - `browser.screenshot({ fullPage: true })`
344
+
345
+ BiDi has an Electron-specific caveat: chromedriver resets the initial renderer
346
+ to `about:blank` during BiDi negotiation. Only opt in when you need one of the
347
+ features above and can navigate to a known renderer URL:
348
+
349
+ ```ts
350
+ const browser = await Browser.launch({
351
+ electron: { appBinaryPath: '/path/to/YourApp', version: '43.1.0' },
352
+ enableBiDi: true,
353
+ });
354
+
355
+ await browser.navigateTo('file:///path/inside/app/index.html');
356
+ ```
357
+
358
+ Viewport and element screenshots do not require BiDi.
359
+
360
+ ## CI Notes
361
+
362
+ Electron needs a display:
363
+
364
+ - On Linux CI, run under a virtual display such as `xvfb-run`.
365
+ - On macOS and Windows hosted runners, a display is usually already available.
366
+ - Do not pass headless flags to Electron.
367
+ - For disposable Linux CI fixtures, use `electron.args: ['--no-sandbox']` if the
368
+ unpacked app cannot start its sandbox.
369
+
370
+ ```ts
371
+ await Browser.launch({
372
+ electron: {
373
+ appBinaryPath,
374
+ version: '43.1.0',
375
+ args: process.platform === 'linux' ? ['--no-sandbox'] : [],
376
+ },
377
+ });
378
+ ```
379
+
380
+ Prefer an exact Electron version in CI so driver resolution is repeatable.
381
+
382
+ ## Compatibility
383
+
384
+ The automated Electron suite currently covers Electron **43.1.0** (Chromium 150) on Linux x64 and Windows x64. A real macOS production app, Fiddler
385
+ Everywhere 7.8.0 (Electron 39.8.6 / Chromium 142), has also been driven
386
+ manually.
387
+
388
+ Other Electron versions are best-effort until they are added to the tested
389
+ matrix.
390
+
391
+ ## Troubleshooting
392
+
393
+ Set `CRAFTDRIVER_DEBUG=1` to print the selected app path, protocol, platform,
394
+ Electron version when known, chromedriver path, driver source, and detected
395
+ driver version. Arguments and environment values are not printed.
396
+
397
+ Common failures:
398
+
399
+ - **The app opens as a blank/plain Chromium window** — package the app and point
400
+ `appBinaryPath` at the packaged executable, not the source directory.
401
+ - **Session creation fails with a driver mismatch** — pass the app's Electron
402
+ version or an explicit matching chromedriver.
403
+ - **The driver is for the wrong CPU architecture** — use a chromedriver for the
404
+ host architecture.
405
+ - **Linux CI exits during launch** — run under `xvfb-run`; for disposable
406
+ unpacked fixtures, try `electron.args: ['--no-sandbox']`.
407
+ - **macOS blocks launch** — confirm the app executable can run on that machine.
408
+ If you provide a custom `chromedriverPath`, confirm that binary can run too.
409
+ Using `electron.version` lets CraftDriver resolve the Chrome-for-Testing driver
410
+ instead of relying on a local driver file.
411
+ - **Manual launch opens Node.js instead of your app** — if
412
+ `ELECTRON_RUN_AS_NODE=1` is set in your shell, an Electron executable runs like
413
+ Node.js and does not open the desktop app window. CraftDriver removes that
414
+ variable when it launches your app. If you run the app yourself from a
415
+ terminal, unset it first:
416
+
417
+ ```bash
418
+ env -u ELECTRON_RUN_AS_NODE /path/to/YourApp
419
+ ```
@@ -49,6 +49,11 @@ try {
49
49
  | `UNSUPPORTED` | Feature exists but is unavailable on this browser/transport (e.g. Chromium-only over Firefox, or a BiDi-only feature with BiDi disabled). | Enable BiDi (`enableBiDi: true`) or switch browser. |
50
50
  | `STATE_INVALID` | Method called in the wrong state (e.g. `stopTrace()` without `startTrace()`). | Call the prerequisite first. |
51
51
  | `DRIVER_ERROR` | A WebDriver command returned a protocol error (non-200 response) — e.g. `stale element reference`, `element click intercepted`, `invalid selector` — or a transport-level failure. | `error.detail.webDriverError` carries the W3C JSON error code and `error.detail.webDriverMessage` the driver's message; recovery loops match on `webDriverError`. Also inspect `error.cause`. |
52
+ | `ELECTRON_DRIVER_MISMATCH` | The resolved chromedriver can't drive the Electron app, caught **before** a session is created. | `error.detail.kind` is `'chromium-major'` (driver major ≠ the app's bundled Chromium; see `driverMajor` / `expectedChromiumMajor` / `electronVersion`) or `'arch'` (`driverArch` ≠ `runtimeArch`). The `hint` names the fix. |
53
+ | `ELECTRON_LAUNCH_FAILED` | The Electron app process exited during session creation ("Chrome instance exited"). | `error.detail` carries the diagnosed cause (`macSigning: 'unsigned'`/`'adhoc'` on macOS, `sandboxDisabled: false` on Linux); `hint` is the top remediation; the message appends the chromedriver output tail; `error.cause` is the original driver error. |
54
+ | `ELECTRON_MAIN_UNAVAILABLE` | `browser.electron.executeMain(...)` couldn't reach the app's main process. | Launch with `electron: { mainProcess: true }`; ensure the app build keeps the `EnableNodeCliInspectArguments` fuse enabled (default). `hint` names the fix. |
55
+ | `ELECTRON_MAIN_THREW` | The `executeMain(...)` callback threw inside the Electron main process. | The main-process exception text is in the message; treat like a failed assertion about main-process state. |
56
+ | `ELECTRON_DEEPLINK_FAILED` | `browser.electron.triggerDeeplink(url)` could not open the custom-protocol URL. | An unsupported platform, a missing `appBinaryPath` on Windows, or the OS `open`/`gio`/`rundll32` launcher failed (`error.detail.command`, `error.cause`). An invalid or `http(s)`/`file` URL throws `INVALID_ARGUMENT` instead. |
52
57
 
53
58
  ## Stability
54
59
 
package/docs/index.md CHANGED
@@ -13,6 +13,9 @@ hero:
13
13
  - theme: brand
14
14
  text: Get Started
15
15
  link: /getting-started
16
+ - theme: alt
17
+ text: Recipes
18
+ link: /recipes
16
19
  - theme: alt
17
20
  text: API Reference
18
21
  link: /api-reference
@@ -55,6 +58,7 @@ await browser.quit();
55
58
  | You want to... | Start |
56
59
  | --------------------------------- | --------------------------------------- |
57
60
  | Write browser automation | [Getting Started](./getting-started.md) |
61
+ | Solve a common testing workflow | [Recipes](./recipes.md) |
58
62
  | Give an AI coding agent a browser | [AI Agent Guide](./agents.md) |
59
63
 
60
64
  ## Good Stuff To Read Next
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>
@@ -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)