craftdriver 1.3.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 (91) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/README.md +1 -0
  3. package/dist/cli/dispatcher.js +1 -1
  4. package/dist/cli/dispatcher.js.map +1 -1
  5. package/dist/index.d.ts +6 -1
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +4 -0
  8. package/dist/index.js.map +1 -1
  9. package/dist/lib/browser.d.ts +147 -21
  10. package/dist/lib/browser.d.ts.map +1 -1
  11. package/dist/lib/browser.js +380 -139
  12. package/dist/lib/browser.js.map +1 -1
  13. package/dist/lib/capabilities.d.ts +35 -0
  14. package/dist/lib/capabilities.d.ts.map +1 -0
  15. package/dist/lib/capabilities.js +58 -0
  16. package/dist/lib/capabilities.js.map +1 -0
  17. package/dist/lib/chrome.d.ts +7 -2
  18. package/dist/lib/chrome.d.ts.map +1 -1
  19. package/dist/lib/chrome.js +10 -1
  20. package/dist/lib/chrome.js.map +1 -1
  21. package/dist/lib/driverManager.d.ts +92 -0
  22. package/dist/lib/driverManager.d.ts.map +1 -1
  23. package/dist/lib/driverManager.js +229 -10
  24. package/dist/lib/driverManager.js.map +1 -1
  25. package/dist/lib/electron.d.ts +60 -0
  26. package/dist/lib/electron.d.ts.map +1 -0
  27. package/dist/lib/electron.js +85 -0
  28. package/dist/lib/electron.js.map +1 -0
  29. package/dist/lib/electronDeeplink.d.ts +22 -0
  30. package/dist/lib/electronDeeplink.d.ts.map +1 -0
  31. package/dist/lib/electronDeeplink.js +118 -0
  32. package/dist/lib/electronDeeplink.js.map +1 -0
  33. package/dist/lib/electronDiagnostics.d.ts +49 -0
  34. package/dist/lib/electronDiagnostics.d.ts.map +1 -0
  35. package/dist/lib/electronDiagnostics.js +195 -0
  36. package/dist/lib/electronDiagnostics.js.map +1 -0
  37. package/dist/lib/electronDialogMock.d.ts +51 -0
  38. package/dist/lib/electronDialogMock.d.ts.map +1 -0
  39. package/dist/lib/electronDialogMock.js +106 -0
  40. package/dist/lib/electronDialogMock.js.map +1 -0
  41. package/dist/lib/electronFuses.d.ts +13 -0
  42. package/dist/lib/electronFuses.d.ts.map +1 -0
  43. package/dist/lib/electronFuses.js +118 -0
  44. package/dist/lib/electronFuses.js.map +1 -0
  45. package/dist/lib/electronMainBridge.d.ts +28 -0
  46. package/dist/lib/electronMainBridge.d.ts.map +1 -0
  47. package/dist/lib/electronMainBridge.js +270 -0
  48. package/dist/lib/electronMainBridge.js.map +1 -0
  49. package/dist/lib/electronMainLogs.d.ts +81 -0
  50. package/dist/lib/electronMainLogs.d.ts.map +1 -0
  51. package/dist/lib/electronMainLogs.js +176 -0
  52. package/dist/lib/electronMainLogs.js.map +1 -0
  53. package/dist/lib/electronMock.d.ts +52 -0
  54. package/dist/lib/electronMock.d.ts.map +1 -0
  55. package/dist/lib/electronMock.js +96 -0
  56. package/dist/lib/electronMock.js.map +1 -0
  57. package/dist/lib/electronRemote.d.ts +106 -0
  58. package/dist/lib/electronRemote.d.ts.map +1 -0
  59. package/dist/lib/electronRemote.js +406 -0
  60. package/dist/lib/electronRemote.js.map +1 -0
  61. package/dist/lib/electronVersions.d.ts +34 -0
  62. package/dist/lib/electronVersions.d.ts.map +1 -0
  63. package/dist/lib/electronVersions.js +90 -0
  64. package/dist/lib/electronVersions.js.map +1 -0
  65. package/dist/lib/errors.d.ts +27 -0
  66. package/dist/lib/errors.d.ts.map +1 -1
  67. package/dist/lib/errors.js +27 -0
  68. package/dist/lib/errors.js.map +1 -1
  69. package/dist/lib/launchTarget.d.ts +30 -0
  70. package/dist/lib/launchTarget.d.ts.map +1 -0
  71. package/dist/lib/launchTarget.js +112 -0
  72. package/dist/lib/launchTarget.js.map +1 -0
  73. package/dist/lib/page.d.ts +1 -0
  74. package/dist/lib/page.d.ts.map +1 -1
  75. package/dist/lib/page.js +1 -1
  76. package/dist/lib/page.js.map +1 -1
  77. package/dist/lib/service.d.ts +21 -0
  78. package/dist/lib/service.d.ts.map +1 -1
  79. package/dist/lib/service.js +94 -6
  80. package/dist/lib/service.js.map +1 -1
  81. package/docs/api-reference.md +37 -2
  82. package/docs/browser-api.md +7 -1
  83. package/docs/electron.md +419 -0
  84. package/docs/error-codes.md +5 -0
  85. package/docs/index.md +4 -0
  86. package/docs/recipes/electron-app-from-another-repo.md +136 -0
  87. package/docs/recipes/electron-deep-link.md +117 -0
  88. package/docs/recipes/electron-mock-apis.md +88 -0
  89. package/docs/recipes/electron-native-dialog.md +105 -0
  90. package/docs/recipes.md +28 -9
  91. package/package.json +5 -1
@@ -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
@@ -0,0 +1,136 @@
1
+ # Test An Electron App From Another Repo
2
+
3
+ Use this when your Electron application is built in one repository, but your
4
+ CraftDriver tests live in another repository or package. The important part is
5
+ that the test launches the **packaged executable** produced by the app repo.
6
+
7
+ This recipe assumes a sibling checkout:
8
+
9
+ ```text
10
+ ~/git/
11
+ my-electron-app/
12
+ my-electron-tests/
13
+ ```
14
+
15
+ ## Build the app first
16
+
17
+ From the app repo, produce the packaged app the same way CI or release builds do:
18
+
19
+ ```bash
20
+ cd ~/git/my-electron-app
21
+ npm ci
22
+ npm run build
23
+ npm run package
24
+ ```
25
+
26
+ The exact command depends on your app. The output is usually under `dist/`,
27
+ `out/`, or `release/`. Point CraftDriver at the executable inside that output,
28
+ not at the source directory.
29
+
30
+ ## Keep the app path in one helper
31
+
32
+ In the test repo, create a small fixture helper so every test uses the same
33
+ path and Electron version:
34
+
35
+ ```ts
36
+ // tests/fixtures/electron-app.ts
37
+ import os from 'node:os';
38
+ import path from 'node:path';
39
+ import { fileURLToPath } from 'node:url';
40
+ import { Browser } from 'craftdriver';
41
+
42
+ const here = path.dirname(fileURLToPath(import.meta.url));
43
+ const workspace = path.resolve(here, '..', '..', '..');
44
+ const appRepo = path.join(workspace, 'my-electron-app');
45
+
46
+ export function appBinaryPath(): string {
47
+ const override = process.env.MY_APP_BINARY;
48
+ if (override) return override;
49
+
50
+ switch (`${process.platform}-${process.arch}`) {
51
+ case 'darwin-arm64':
52
+ case 'darwin-x64':
53
+ return path.join(appRepo, 'dist/mac/MyApp.app/Contents/MacOS/MyApp');
54
+ case 'win32-x64':
55
+ return path.join(appRepo, 'dist/win-unpacked/MyApp.exe');
56
+ case 'linux-x64':
57
+ return path.join(appRepo, 'dist/linux-unpacked/my-app');
58
+ default:
59
+ throw new Error(`Unsupported Electron test platform: ${os.platform()}-${os.arch()}`);
60
+ }
61
+ }
62
+
63
+ export function launchApp(): Promise<Browser> {
64
+ return Browser.launch({
65
+ electron: {
66
+ appBinaryPath: appBinaryPath(),
67
+ version: process.env.MY_APP_ELECTRON_VERSION ?? '43.1.0',
68
+ args: process.platform === 'linux' ? ['--no-sandbox'] : [],
69
+ },
70
+ });
71
+ }
72
+ ```
73
+
74
+ Use `MY_APP_BINARY` when CI downloads a prebuilt app artifact instead of building
75
+ from a sibling checkout.
76
+
77
+ ## Write the test like a normal renderer test
78
+
79
+ Electron defaults to Classic WebDriver, so the app loads itself. Do not call
80
+ `navigateTo()` unless you explicitly opted into BiDi and know the renderer URL.
81
+
82
+ ```ts
83
+ // tests/settings.test.ts
84
+ import { afterEach, describe, it } from 'vitest';
85
+ import { Browser, By } from 'craftdriver';
86
+ import { launchApp } from './fixtures/electron-app';
87
+
88
+ describe('settings window', () => {
89
+ let browser: Browser | undefined;
90
+
91
+ afterEach(async () => {
92
+ await browser?.quit();
93
+ });
94
+
95
+ it('saves the profile name', async () => {
96
+ browser = await launchApp();
97
+
98
+ const main = await browser.waitForPage({ title: /My App/ });
99
+ await main.find(By.testId('app-title')).expect().toHaveText('My App');
100
+
101
+ await main.click(By.testId('nav-settings'));
102
+ await main.fill(By.testId('profile-name'), 'Ada Lovelace');
103
+ await main.click(By.testId('save-settings'));
104
+
105
+ await main.find(By.testId('settings-status')).expect().toHaveText('Saved');
106
+ });
107
+ });
108
+ ```
109
+
110
+ Prefer stable `data-testid` attributes in the Electron renderer. You can inspect
111
+ or debug the app with DevTools while designing selectors, but the test should
112
+ interact with the packaged UI the way a user would.
113
+
114
+ ## CI shape
115
+
116
+ A common CI flow is:
117
+
118
+ 1. Check out or download the Electron app.
119
+ 2. Build/package the app, or download a packaged artifact.
120
+ 3. Set `MY_APP_BINARY` and `MY_APP_ELECTRON_VERSION`.
121
+ 4. Run the CraftDriver tests.
122
+
123
+ On Linux, run the tests under Xvfb:
124
+
125
+ ```bash
126
+ xvfb-run -a npm test
127
+ ```
128
+
129
+ On macOS and Windows hosted runners, a display is already available. Do not pass
130
+ headless flags to Electron; it is a desktop GUI app.
131
+
132
+ ## Learn More
133
+
134
+ - [Testing Electron Apps](../electron.md)
135
+ - [Browser API](../browser-api.md)
136
+ - [Vitest Hooks](./vitest-browser-lifecycle.md)