craftdriver 1.3.0 → 1.5.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 (132) hide show
  1. package/CHANGELOG.md +17 -3
  2. package/README.md +4 -1
  3. package/dist/cli/dispatcher.js +1 -1
  4. package/dist/cli/dispatcher.js.map +1 -1
  5. package/dist/cli/parseArgs.d.ts +2 -2
  6. package/dist/cli/parseArgs.d.ts.map +1 -1
  7. package/dist/cli/parseArgs.js +1 -1
  8. package/dist/index.d.ts +7 -1
  9. package/dist/index.d.ts.map +1 -1
  10. package/dist/index.js +5 -0
  11. package/dist/index.js.map +1 -1
  12. package/dist/lib/bidi/storage.d.ts +13 -0
  13. package/dist/lib/bidi/storage.d.ts.map +1 -1
  14. package/dist/lib/bidi/storage.js +81 -32
  15. package/dist/lib/bidi/storage.js.map +1 -1
  16. package/dist/lib/browser.d.ts +173 -21
  17. package/dist/lib/browser.d.ts.map +1 -1
  18. package/dist/lib/browser.js +519 -210
  19. package/dist/lib/browser.js.map +1 -1
  20. package/dist/lib/browserContext.d.ts +2 -2
  21. package/dist/lib/browserContext.d.ts.map +1 -1
  22. package/dist/lib/browserContext.js +1 -1
  23. package/dist/lib/builder.d.ts +4 -1
  24. package/dist/lib/builder.d.ts.map +1 -1
  25. package/dist/lib/builder.js +20 -2
  26. package/dist/lib/builder.js.map +1 -1
  27. package/dist/lib/capabilities.d.ts +45 -0
  28. package/dist/lib/capabilities.d.ts.map +1 -0
  29. package/dist/lib/capabilities.js +103 -0
  30. package/dist/lib/capabilities.js.map +1 -0
  31. package/dist/lib/chrome.d.ts +7 -2
  32. package/dist/lib/chrome.d.ts.map +1 -1
  33. package/dist/lib/chrome.js +10 -1
  34. package/dist/lib/chrome.js.map +1 -1
  35. package/dist/lib/driver.d.ts +16 -1
  36. package/dist/lib/driver.d.ts.map +1 -1
  37. package/dist/lib/driver.js +64 -0
  38. package/dist/lib/driver.js.map +1 -1
  39. package/dist/lib/driverManager.d.ts +120 -0
  40. package/dist/lib/driverManager.d.ts.map +1 -1
  41. package/dist/lib/driverManager.js +289 -10
  42. package/dist/lib/driverManager.js.map +1 -1
  43. package/dist/lib/electron.d.ts +60 -0
  44. package/dist/lib/electron.d.ts.map +1 -0
  45. package/dist/lib/electron.js +85 -0
  46. package/dist/lib/electron.js.map +1 -0
  47. package/dist/lib/electronDeeplink.d.ts +22 -0
  48. package/dist/lib/electronDeeplink.d.ts.map +1 -0
  49. package/dist/lib/electronDeeplink.js +118 -0
  50. package/dist/lib/electronDeeplink.js.map +1 -0
  51. package/dist/lib/electronDiagnostics.d.ts +49 -0
  52. package/dist/lib/electronDiagnostics.d.ts.map +1 -0
  53. package/dist/lib/electronDiagnostics.js +195 -0
  54. package/dist/lib/electronDiagnostics.js.map +1 -0
  55. package/dist/lib/electronDialogMock.d.ts +51 -0
  56. package/dist/lib/electronDialogMock.d.ts.map +1 -0
  57. package/dist/lib/electronDialogMock.js +106 -0
  58. package/dist/lib/electronDialogMock.js.map +1 -0
  59. package/dist/lib/electronFuses.d.ts +13 -0
  60. package/dist/lib/electronFuses.d.ts.map +1 -0
  61. package/dist/lib/electronFuses.js +118 -0
  62. package/dist/lib/electronFuses.js.map +1 -0
  63. package/dist/lib/electronMainBridge.d.ts +28 -0
  64. package/dist/lib/electronMainBridge.d.ts.map +1 -0
  65. package/dist/lib/electronMainBridge.js +270 -0
  66. package/dist/lib/electronMainBridge.js.map +1 -0
  67. package/dist/lib/electronMainLogs.d.ts +81 -0
  68. package/dist/lib/electronMainLogs.d.ts.map +1 -0
  69. package/dist/lib/electronMainLogs.js +176 -0
  70. package/dist/lib/electronMainLogs.js.map +1 -0
  71. package/dist/lib/electronMock.d.ts +52 -0
  72. package/dist/lib/electronMock.d.ts.map +1 -0
  73. package/dist/lib/electronMock.js +96 -0
  74. package/dist/lib/electronMock.js.map +1 -0
  75. package/dist/lib/electronRemote.d.ts +106 -0
  76. package/dist/lib/electronRemote.d.ts.map +1 -0
  77. package/dist/lib/electronRemote.js +406 -0
  78. package/dist/lib/electronRemote.js.map +1 -0
  79. package/dist/lib/electronVersions.d.ts +34 -0
  80. package/dist/lib/electronVersions.d.ts.map +1 -0
  81. package/dist/lib/electronVersions.js +90 -0
  82. package/dist/lib/electronVersions.js.map +1 -0
  83. package/dist/lib/errors.d.ts +27 -0
  84. package/dist/lib/errors.d.ts.map +1 -1
  85. package/dist/lib/errors.js +27 -0
  86. package/dist/lib/errors.js.map +1 -1
  87. package/dist/lib/launchTarget.d.ts +30 -0
  88. package/dist/lib/launchTarget.d.ts.map +1 -0
  89. package/dist/lib/launchTarget.js +174 -0
  90. package/dist/lib/launchTarget.js.map +1 -0
  91. package/dist/lib/page.d.ts +1 -0
  92. package/dist/lib/page.d.ts.map +1 -1
  93. package/dist/lib/page.js +1 -1
  94. package/dist/lib/page.js.map +1 -1
  95. package/dist/lib/safari.d.ts +90 -0
  96. package/dist/lib/safari.d.ts.map +1 -0
  97. package/dist/lib/safari.js +136 -0
  98. package/dist/lib/safari.js.map +1 -0
  99. package/dist/lib/service.d.ts +21 -0
  100. package/dist/lib/service.d.ts.map +1 -1
  101. package/dist/lib/service.js +94 -6
  102. package/dist/lib/service.js.map +1 -1
  103. package/dist/lib/tracing.d.ts +1 -1
  104. package/dist/lib/tracing.d.ts.map +1 -1
  105. package/dist/lib/tracing.js.map +1 -1
  106. package/dist/lib/types.d.ts +41 -0
  107. package/dist/lib/types.d.ts.map +1 -1
  108. package/dist/lib/vibiumTrace.d.ts +1 -1
  109. package/dist/lib/vibiumTrace.d.ts.map +1 -1
  110. package/dist/lib/webelement.d.ts.map +1 -1
  111. package/dist/lib/webelement.js +118 -2
  112. package/dist/lib/webelement.js.map +1 -1
  113. package/docs/api-reference.md +39 -2
  114. package/docs/browser-api.md +7 -1
  115. package/docs/driver-configuration.md +54 -0
  116. package/docs/electron.md +419 -0
  117. package/docs/error-codes.md +5 -0
  118. package/docs/getting-started.md +30 -10
  119. package/docs/index.md +5 -1
  120. package/docs/public/examples/displayed.html +86 -0
  121. package/docs/recipes/electron-app-from-another-repo.md +136 -0
  122. package/docs/recipes/electron-deep-link.md +117 -0
  123. package/docs/recipes/electron-mock-apis.md +88 -0
  124. package/docs/recipes/electron-native-dialog.md +105 -0
  125. package/docs/recipes.md +28 -9
  126. package/docs/safari.md +118 -0
  127. package/docs/standards.md +13 -0
  128. package/docs/why-craftdriver.md +1 -1
  129. package/docs/zero-config-drivers.md +12 -0
  130. package/package.json +7 -1
  131. package/skills/craftdriver/SKILL.md +4 -1
  132. package/skills/craftdriver/cheatsheet.md +4 -1
@@ -279,3 +279,57 @@ const browser = await Browser.launch({
279
279
  }),
280
280
  });
281
281
  ```
282
+
283
+ ## Safari (macOS): driver ships with the browser
284
+
285
+ Everything above (steps 1–9, the download chain, the TTL cache) resolves
286
+ **chromedriver**/**geckodriver** — binaries craftdriver may fetch and cache on
287
+ disk. `browserName: 'safari'` doesn't fit that model at all: Apple ships
288
+ `safaridriver` as part of macOS/Safari itself, so there is no download, no
289
+ version-matching problem, and no cache to warm. This page covers only driver
290
+ **resolution**; for setup (`safaridriver --enable`), capabilities, limitations,
291
+ and Safari Technology Preview, see the [**Safari guide**](./safari.md).
292
+
293
+ `SafariService` resolves the `safaridriver` binary through its own chain —
294
+ first match wins, and none of these steps ever triggers a network call:
295
+
296
+ | Step | Source |
297
+ |---|---|
298
+ | 1 | `binaryPath` passed to `new SafariService({ binaryPath })` |
299
+ | 2 | `CRAFTDRIVER_SAFARIDRIVER_PATH` env var |
300
+ | 3 | `CRAFTDRIVER_DRIVER_PATH` env var (generic fallback, shared with Chrome/Firefox) |
301
+ | 4 | `/usr/bin/safaridriver` (the fixed system location on macOS) |
302
+ | 5 | `safaridriver` on `PATH` |
303
+ | 6 | a clear, actionable error if nothing resolves — craftdriver never downloads a `safaridriver` |
304
+
305
+ To pin **Safari Technology Preview**, point `binaryPath` at its bundled driver
306
+ (`/Applications/Safari Technology Preview.app/Contents/MacOS/safaridriver`) —
307
+ see the [Safari guide](./safari.md#safari-technology-preview).
308
+
309
+ ### Extra `safaridriver` arguments
310
+
311
+ `SafariService` accepts `args`, passed through verbatim after `--port <port>`.
312
+ The most useful one is `--diagnose`, which turns on `safaridriver`'s
313
+ diagnostic logging (written under `~/Library/Logs/com.apple.WebDriver/`) —
314
+ handy when a Safari session fails to start and the driver's own stdout/stderr
315
+ isn't enough to tell why:
316
+
317
+ ```typescript
318
+ const browser = await Browser.launch({
319
+ browserName: 'safari',
320
+ safariService: new SafariService({ args: ['--diagnose'] }),
321
+ });
322
+ ```
323
+
324
+ ### What's different from Chrome/Firefox here
325
+
326
+ - No `CRAFTDRIVER_CACHE_DIR`, `CRAFTDRIVER_DRIVER_TTL`, or `CRAFTDRIVER_OFFLINE`
327
+ behavior applies to Safari — there's nothing cached or downloaded to
328
+ configure.
329
+ - No browser-binary resolution chain (`browserPath` / `CRAFTDRIVER_CHROME_PATH`-
330
+ style) exists for Safari — you select which Safari build's driver to use via
331
+ `SafariService.binaryPath` directly, as shown above, not via a separate
332
+ browser-path option.
333
+ - `enableBiDi: true` is rejected before launch — Safari has no documented
334
+ WebDriver BiDi endpoint to negotiate. See
335
+ [WebDriver Standards → Safari is Classic-only](./standards.md#safari-is-classic-only).
@@ -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
 
@@ -75,29 +75,49 @@ returns the same `Page` the shortcuts would target.
75
75
 
76
76
  ## Launch Options
77
77
 
78
+ The simplest launch runs Chrome:
79
+
80
+ ```typescript
81
+ const browser = await Browser.launch(); // Chrome by default
82
+ ```
83
+
84
+ Pick a browser with `browserName`:
85
+
78
86
  ```typescript
79
- // Chrome (default)
80
87
  const browser = await Browser.launch({
81
- browserName: 'chrome', // 'chrome' | 'chromium' | 'firefox'
88
+ browserName: 'firefox', // 'chrome' | 'chromium' | 'firefox' | 'safari'
82
89
  storageState: './session.json', // optional: pre-load saved session state
83
90
  });
91
+ ```
92
+
93
+ To switch browsers from an environment variable in your own tests, read it
94
+ yourself and pass it to `browserName` — craftdriver doesn't read any browser
95
+ env var for you:
84
96
 
85
- // Firefox
97
+ ```typescript
86
98
  const browser = await Browser.launch({
87
- browserName: 'firefox',
99
+ browserName: process.env.BROWSER_NAME ?? 'chrome',
88
100
  });
89
101
  ```
90
102
 
91
- ### Running tests on Firefox
103
+ ### Safari
92
104
 
93
- Set `BROWSER_NAME=firefox` before running your test command:
105
+ Safari runs on macOS with the same API, as a smaller Classic-only tier. Enable
106
+ automation once (`safaridriver --enable`) and launch:
94
107
 
95
- ```bash
96
- BROWSER_NAME=firefox npx vitest run
97
- # or use the built-in script if your package.json has it:
98
- npm run test:firefox
108
+ ```typescript
109
+ const browser = await Browser.launch({ browserName: 'safari' });
110
+ await browser.navigateTo('http://127.0.0.1:8080/login.html');
111
+ await browser.fill('#username', 'alice');
112
+ await browser.click('#submit');
113
+ await browser.quit();
99
114
  ```
100
115
 
116
+ Safari is macOS-only, headed, and serial (one session at a time), and doesn't
117
+ support BiDi-only features (network mocking, tracing, emulation, …). See the
118
+ [**Safari guide**](./safari.md) for the full picture — setup, what works, and
119
+ Safari Technology Preview.
120
+
101
121
  ### Custom WebDriver binary or port
102
122
 
103
123
  `Browser.launch()` resolves the driver binary automatically. To pin a
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
@@ -30,7 +33,7 @@ features:
30
33
  details: CLI, MCP server, and assistant rules are there when you want coding agents to drive the browser too.
31
34
  ---
32
35
 
33
- ## One Install
36
+ ## Quick Start
34
37
 
35
38
  ```bash
36
39
  npm install craftdriver --save-dev
@@ -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,86 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+
4
+ <head>
5
+ <meta charset="utf-8" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
7
+ <title>Craftdriver Displayedness</title>
8
+ <style>
9
+ body {
10
+ font-family: system-ui, Arial, sans-serif;
11
+ margin: 1rem;
12
+ }
13
+
14
+ /* An ancestor hidden via display:none. Its descendant #hidden-by-ancestor
15
+ has no style of its own but must count as not displayed. */
16
+ #display-none-ancestor {
17
+ display: none;
18
+ }
19
+
20
+ /* Own visibility:hidden. */
21
+ #visibility-hidden {
22
+ visibility: hidden;
23
+ }
24
+
25
+ /* Zero-sized content: no rendered box, overflow clipped. */
26
+ #zero-size {
27
+ width: 0;
28
+ height: 0;
29
+ overflow: hidden;
30
+ }
31
+
32
+ /* A scrollable-but-CLIPPING (overflow:hidden) container. The child at the
33
+ bottom is scrolled out of the container's viewport, so it is clipped and
34
+ not displayed (bot.dom OverflowState.HIDDEN). */
35
+ #clip-container {
36
+ width: 120px;
37
+ height: 80px;
38
+ overflow: hidden;
39
+ border: 1px solid #ccc;
40
+ }
41
+
42
+ #clip-spacer {
43
+ height: 400px;
44
+ }
45
+ </style>
46
+ </head>
47
+
48
+ <body>
49
+ <div class="container">
50
+ <!-- Plainly visible baseline. -->
51
+ <div id="visible-el">I am visible</div>
52
+
53
+ <!-- Hidden by a display:none ancestor. -->
54
+ <div id="display-none-ancestor">
55
+ <div id="hidden-by-ancestor">Hidden because my ancestor is display:none</div>
56
+ </div>
57
+
58
+ <!-- Own display:none. -->
59
+ <div id="display-none-self" style="display: none">Hidden by my own display:none</div>
60
+
61
+ <!-- Own visibility:hidden. -->
62
+ <div id="visibility-hidden">Hidden by my own visibility:hidden</div>
63
+
64
+ <!-- Visibility hidden ancestor with a visible descendant override:
65
+ the child sets visibility:visible and MUST count as displayed. -->
66
+ <div style="visibility: hidden">
67
+ <div id="visibility-visible-override" style="visibility: visible">
68
+ Visible again despite hidden ancestor
69
+ </div>
70
+ </div>
71
+
72
+ <!-- Zero-sized content. -->
73
+ <div id="zero-size">unrenderable</div>
74
+
75
+ <!-- A <br> is legitimately zero-width but should count as displayed. -->
76
+ <br id="line-break" />
77
+
78
+ <!-- Element scrolled out of an overflow:hidden container's viewport. -->
79
+ <div id="clip-container">
80
+ <div id="clip-spacer"></div>
81
+ <div id="scrolled-out-of-clip">I am below the clipped fold</div>
82
+ </div>
83
+ </div>
84
+ </body>
85
+
86
+ </html>