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.
- package/CHANGELOG.md +17 -3
- package/README.md +4 -1
- package/dist/cli/dispatcher.js +1 -1
- package/dist/cli/dispatcher.js.map +1 -1
- package/dist/cli/parseArgs.d.ts +2 -2
- package/dist/cli/parseArgs.d.ts.map +1 -1
- package/dist/cli/parseArgs.js +1 -1
- package/dist/index.d.ts +7 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -1
- package/dist/lib/bidi/storage.d.ts +13 -0
- package/dist/lib/bidi/storage.d.ts.map +1 -1
- package/dist/lib/bidi/storage.js +81 -32
- package/dist/lib/bidi/storage.js.map +1 -1
- package/dist/lib/browser.d.ts +173 -21
- package/dist/lib/browser.d.ts.map +1 -1
- package/dist/lib/browser.js +519 -210
- package/dist/lib/browser.js.map +1 -1
- package/dist/lib/browserContext.d.ts +2 -2
- package/dist/lib/browserContext.d.ts.map +1 -1
- package/dist/lib/browserContext.js +1 -1
- package/dist/lib/builder.d.ts +4 -1
- package/dist/lib/builder.d.ts.map +1 -1
- package/dist/lib/builder.js +20 -2
- package/dist/lib/builder.js.map +1 -1
- package/dist/lib/capabilities.d.ts +45 -0
- package/dist/lib/capabilities.d.ts.map +1 -0
- package/dist/lib/capabilities.js +103 -0
- package/dist/lib/capabilities.js.map +1 -0
- package/dist/lib/chrome.d.ts +7 -2
- package/dist/lib/chrome.d.ts.map +1 -1
- package/dist/lib/chrome.js +10 -1
- package/dist/lib/chrome.js.map +1 -1
- package/dist/lib/driver.d.ts +16 -1
- package/dist/lib/driver.d.ts.map +1 -1
- package/dist/lib/driver.js +64 -0
- package/dist/lib/driver.js.map +1 -1
- package/dist/lib/driverManager.d.ts +120 -0
- package/dist/lib/driverManager.d.ts.map +1 -1
- package/dist/lib/driverManager.js +289 -10
- package/dist/lib/driverManager.js.map +1 -1
- package/dist/lib/electron.d.ts +60 -0
- package/dist/lib/electron.d.ts.map +1 -0
- package/dist/lib/electron.js +85 -0
- package/dist/lib/electron.js.map +1 -0
- package/dist/lib/electronDeeplink.d.ts +22 -0
- package/dist/lib/electronDeeplink.d.ts.map +1 -0
- package/dist/lib/electronDeeplink.js +118 -0
- package/dist/lib/electronDeeplink.js.map +1 -0
- package/dist/lib/electronDiagnostics.d.ts +49 -0
- package/dist/lib/electronDiagnostics.d.ts.map +1 -0
- package/dist/lib/electronDiagnostics.js +195 -0
- package/dist/lib/electronDiagnostics.js.map +1 -0
- package/dist/lib/electronDialogMock.d.ts +51 -0
- package/dist/lib/electronDialogMock.d.ts.map +1 -0
- package/dist/lib/electronDialogMock.js +106 -0
- package/dist/lib/electronDialogMock.js.map +1 -0
- package/dist/lib/electronFuses.d.ts +13 -0
- package/dist/lib/electronFuses.d.ts.map +1 -0
- package/dist/lib/electronFuses.js +118 -0
- package/dist/lib/electronFuses.js.map +1 -0
- package/dist/lib/electronMainBridge.d.ts +28 -0
- package/dist/lib/electronMainBridge.d.ts.map +1 -0
- package/dist/lib/electronMainBridge.js +270 -0
- package/dist/lib/electronMainBridge.js.map +1 -0
- package/dist/lib/electronMainLogs.d.ts +81 -0
- package/dist/lib/electronMainLogs.d.ts.map +1 -0
- package/dist/lib/electronMainLogs.js +176 -0
- package/dist/lib/electronMainLogs.js.map +1 -0
- package/dist/lib/electronMock.d.ts +52 -0
- package/dist/lib/electronMock.d.ts.map +1 -0
- package/dist/lib/electronMock.js +96 -0
- package/dist/lib/electronMock.js.map +1 -0
- package/dist/lib/electronRemote.d.ts +106 -0
- package/dist/lib/electronRemote.d.ts.map +1 -0
- package/dist/lib/electronRemote.js +406 -0
- package/dist/lib/electronRemote.js.map +1 -0
- package/dist/lib/electronVersions.d.ts +34 -0
- package/dist/lib/electronVersions.d.ts.map +1 -0
- package/dist/lib/electronVersions.js +90 -0
- package/dist/lib/electronVersions.js.map +1 -0
- package/dist/lib/errors.d.ts +27 -0
- package/dist/lib/errors.d.ts.map +1 -1
- package/dist/lib/errors.js +27 -0
- package/dist/lib/errors.js.map +1 -1
- package/dist/lib/launchTarget.d.ts +30 -0
- package/dist/lib/launchTarget.d.ts.map +1 -0
- package/dist/lib/launchTarget.js +174 -0
- package/dist/lib/launchTarget.js.map +1 -0
- package/dist/lib/page.d.ts +1 -0
- package/dist/lib/page.d.ts.map +1 -1
- package/dist/lib/page.js +1 -1
- package/dist/lib/page.js.map +1 -1
- package/dist/lib/safari.d.ts +90 -0
- package/dist/lib/safari.d.ts.map +1 -0
- package/dist/lib/safari.js +136 -0
- package/dist/lib/safari.js.map +1 -0
- package/dist/lib/service.d.ts +21 -0
- package/dist/lib/service.d.ts.map +1 -1
- package/dist/lib/service.js +94 -6
- package/dist/lib/service.js.map +1 -1
- package/dist/lib/tracing.d.ts +1 -1
- package/dist/lib/tracing.d.ts.map +1 -1
- package/dist/lib/tracing.js.map +1 -1
- package/dist/lib/types.d.ts +41 -0
- package/dist/lib/types.d.ts.map +1 -1
- package/dist/lib/vibiumTrace.d.ts +1 -1
- package/dist/lib/vibiumTrace.d.ts.map +1 -1
- package/dist/lib/webelement.d.ts.map +1 -1
- package/dist/lib/webelement.js +118 -2
- package/dist/lib/webelement.js.map +1 -1
- package/docs/api-reference.md +39 -2
- package/docs/browser-api.md +7 -1
- package/docs/driver-configuration.md +54 -0
- package/docs/electron.md +419 -0
- package/docs/error-codes.md +5 -0
- package/docs/getting-started.md +30 -10
- package/docs/index.md +5 -1
- package/docs/public/examples/displayed.html +86 -0
- package/docs/recipes/electron-app-from-another-repo.md +136 -0
- package/docs/recipes/electron-deep-link.md +117 -0
- package/docs/recipes/electron-mock-apis.md +88 -0
- package/docs/recipes/electron-native-dialog.md +105 -0
- package/docs/recipes.md +28 -9
- package/docs/safari.md +118 -0
- package/docs/standards.md +13 -0
- package/docs/why-craftdriver.md +1 -1
- package/docs/zero-config-drivers.md +12 -0
- package/package.json +7 -1
- package/skills/craftdriver/SKILL.md +4 -1
- 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).
|
package/docs/electron.md
ADDED
|
@@ -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
|
+
```
|
package/docs/error-codes.md
CHANGED
|
@@ -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/getting-started.md
CHANGED
|
@@ -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: '
|
|
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
|
-
|
|
97
|
+
```typescript
|
|
86
98
|
const browser = await Browser.launch({
|
|
87
|
-
browserName: '
|
|
99
|
+
browserName: process.env.BROWSER_NAME ?? 'chrome',
|
|
88
100
|
});
|
|
89
101
|
```
|
|
90
102
|
|
|
91
|
-
###
|
|
103
|
+
### Safari
|
|
92
104
|
|
|
93
|
-
|
|
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
|
-
```
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
-
##
|
|
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>
|