craftdriver 1.2.0 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +20 -0
- package/README.md +2 -1
- package/dist/cli/dispatcher.d.ts.map +1 -1
- package/dist/cli/dispatcher.js +13 -1
- package/dist/cli/dispatcher.js.map +1 -1
- package/dist/cli/mcp/tools.d.ts +1 -2
- package/dist/cli/mcp/tools.d.ts.map +1 -1
- package/dist/cli/mcp/tools.js +18 -0
- package/dist/cli/mcp/tools.js.map +1 -1
- package/dist/index.d.ts +7 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/lib/browser.d.ts +156 -27
- package/dist/lib/browser.d.ts.map +1 -1
- package/dist/lib/browser.js +474 -212
- package/dist/lib/browser.js.map +1 -1
- package/dist/lib/capabilities.d.ts +35 -0
- package/dist/lib/capabilities.d.ts.map +1 -0
- package/dist/lib/capabilities.js +58 -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/driverManager.d.ts +92 -0
- package/dist/lib/driverManager.d.ts.map +1 -1
- package/dist/lib/driverManager.js +229 -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 +112 -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/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 +20 -4
- package/dist/lib/tracing.d.ts.map +1 -1
- package/dist/lib/tracing.js +38 -3
- package/dist/lib/tracing.js.map +1 -1
- package/dist/lib/vibiumTrace.d.ts +14 -0
- package/dist/lib/vibiumTrace.d.ts.map +1 -0
- package/dist/lib/vibiumTrace.js +372 -0
- package/dist/lib/vibiumTrace.js.map +1 -0
- package/docs/api-reference.md +38 -2
- package/docs/browser-api.md +7 -1
- package/docs/electron.md +419 -0
- package/docs/error-codes.md +5 -0
- package/docs/index.md +4 -0
- package/docs/mcp.md +17 -3
- package/docs/public/recipes/trace-failing-test.html +16 -0
- package/docs/public/recipes/vitest-vibium-trace.html +16 -0
- package/docs/public/traces/vitest-login.zip +0 -0
- package/docs/recipes/debug-failing-tests-with-traces.md +117 -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 +29 -10
- package/docs/tracing.md +102 -24
- package/package.json +8 -8
- package/skills/craftdriver/cheatsheet.md +4 -2
- package/skills/craftdriver/patterns.md +2 -1
- package/docs/recipes/trace-failing-test.md +0 -51
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/index.md
CHANGED
|
@@ -13,6 +13,9 @@ hero:
|
|
|
13
13
|
- theme: brand
|
|
14
14
|
text: Get Started
|
|
15
15
|
link: /getting-started
|
|
16
|
+
- theme: alt
|
|
17
|
+
text: Recipes
|
|
18
|
+
link: /recipes
|
|
16
19
|
- theme: alt
|
|
17
20
|
text: API Reference
|
|
18
21
|
link: /api-reference
|
|
@@ -55,6 +58,7 @@ await browser.quit();
|
|
|
55
58
|
| You want to... | Start |
|
|
56
59
|
| --------------------------------- | --------------------------------------- |
|
|
57
60
|
| Write browser automation | [Getting Started](./getting-started.md) |
|
|
61
|
+
| Solve a common testing workflow | [Recipes](./recipes.md) |
|
|
58
62
|
| Give an AI coding agent a browser | [AI Agent Guide](./agents.md) |
|
|
59
63
|
|
|
60
64
|
## Good Stuff To Read Next
|
package/docs/mcp.md
CHANGED
|
@@ -53,7 +53,7 @@ goose configure # add craftdriver as a stdio server
|
|
|
53
53
|
|
|
54
54
|
## Tools
|
|
55
55
|
|
|
56
|
-
Compact set —
|
|
56
|
+
Compact set — 15 tools, one line each. Long help lives in the schema
|
|
57
57
|
description; clients render it in the model's context once per session.
|
|
58
58
|
|
|
59
59
|
| Tool | Purpose |
|
|
@@ -70,11 +70,25 @@ description; clients render it in the model's context once per session.
|
|
|
70
70
|
| `browser_pages` | List open pages (id, url, title). |
|
|
71
71
|
| `browser_snapshot` | **Sanitized DOM summary with refs.** Use `ref=eN` as the selector for subsequent calls. |
|
|
72
72
|
| `browser_screenshot` | Capture PNG to a file (auto-allocated under the per-session artifact dir; never inlined). |
|
|
73
|
+
| `browser_trace` | Start/stop tracing and export a Vibium Player compatible zip. |
|
|
73
74
|
| `browser_status` | Browser up? Which URL is active? |
|
|
74
75
|
| `browser_advanced_eval` | Evaluate JS in the page. Last resort. |
|
|
75
76
|
|
|
76
|
-
|
|
77
|
-
|
|
77
|
+
Trace a complete agent-driven session with two calls:
|
|
78
|
+
|
|
79
|
+
```jsonc
|
|
80
|
+
{ "name": "browser_trace", "arguments": {
|
|
81
|
+
"action": "start", "out_dir": "./traces/agent-raw", "title": "Agent flow"
|
|
82
|
+
} }
|
|
83
|
+
// ...browser_navigate / browser_fill / browser_click calls...
|
|
84
|
+
{ "name": "browser_trace", "arguments": {
|
|
85
|
+
"action": "stop", "path": "./traces/agent-flow.zip"
|
|
86
|
+
} }
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
The zip opens at [player.vibium.dev](https://player.vibium.dev/). The raw
|
|
90
|
+
directory remains available if the MCP client disconnects before the stop
|
|
91
|
+
call; in that case there is no finalized zip.
|
|
78
92
|
|
|
79
93
|
## Selector syntax
|
|
80
94
|
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta http-equiv="refresh" content="0; url=./debug-failing-tests-with-traces">
|
|
6
|
+
<link
|
|
7
|
+
rel="canonical"
|
|
8
|
+
href="https://dtopuzov.github.io/craftdriver/recipes/debug-failing-tests-with-traces"
|
|
9
|
+
>
|
|
10
|
+
<title>Vitest traces moved</title>
|
|
11
|
+
<script>location.replace('./debug-failing-tests-with-traces')</script>
|
|
12
|
+
</head>
|
|
13
|
+
<body>
|
|
14
|
+
<p>This recipe moved to <a href="./debug-failing-tests-with-traces">Debug With Traces</a>.</p>
|
|
15
|
+
</body>
|
|
16
|
+
</html>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta http-equiv="refresh" content="0; url=./debug-failing-tests-with-traces">
|
|
6
|
+
<link
|
|
7
|
+
rel="canonical"
|
|
8
|
+
href="https://dtopuzov.github.io/craftdriver/recipes/debug-failing-tests-with-traces"
|
|
9
|
+
>
|
|
10
|
+
<title>Vitest trace recipe moved</title>
|
|
11
|
+
<script>location.replace('./debug-failing-tests-with-traces')</script>
|
|
12
|
+
</head>
|
|
13
|
+
<body>
|
|
14
|
+
<p>This recipe moved to <a href="./debug-failing-tests-with-traces">Debug With Traces</a>.</p>
|
|
15
|
+
</body>
|
|
16
|
+
</html>
|
|
Binary file
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# Use Traces To Debug Failing Tests
|
|
2
|
+
|
|
3
|
+
A failed assertion tells you what was wrong at the end of a test, but often not
|
|
4
|
+
how the browser got there. A trace preserves the actions, screenshots, console
|
|
5
|
+
and JavaScript events, navigation, and network activity that led to the
|
|
6
|
+
failure.
|
|
7
|
+
|
|
8
|
+
## A Shared Trace Format
|
|
9
|
+
|
|
10
|
+
[Jason Huggins' SeleniumConf talk](https://www.youtube.com/watch?v=c-OGZK3F0tk&t=1530s)
|
|
11
|
+
was one of the reasons Craftdriver adopted portable browser traces instead of
|
|
12
|
+
a Craftdriver-only viewer format.
|
|
13
|
+
|
|
14
|
+
The exported zip follows the Playwright-style
|
|
15
|
+
recording layout also used by tools like
|
|
16
|
+
[Vibium](https://github.com/VibiumDev/vibium/blob/main/docs/explanation/recording-format.md)
|
|
17
|
+
and WDIO, so the same artifact can be opened in viewers such as
|
|
18
|
+
[Vibium Player](https://player.vibium.dev/).
|
|
19
|
+
|
|
20
|
+
Craftdriver still records a crash-resilient raw NDJSON trace first, then
|
|
21
|
+
packages it into the shared zip format when a test runner asks for an artifact.
|
|
22
|
+
|
|
23
|
+
## Keep A Trace When A Vitest Test Fails
|
|
24
|
+
|
|
25
|
+
Start one browser for the suite, but scope tracing to the test hooks.
|
|
26
|
+
`afterEach()` knows whether the test failed, so it can keep only the useful zip:
|
|
27
|
+
|
|
28
|
+
```ts
|
|
29
|
+
import { afterAll, afterEach, beforeAll, beforeEach, describe, it } from 'vitest';
|
|
30
|
+
import { rmSync } from 'node:fs';
|
|
31
|
+
import path from 'node:path';
|
|
32
|
+
import { Browser } from 'craftdriver';
|
|
33
|
+
|
|
34
|
+
describe('login', () => {
|
|
35
|
+
let browser: Browser;
|
|
36
|
+
const rawTraceDir = path.resolve('test-results/traces/login-raw');
|
|
37
|
+
const traceZip = path.resolve('test-results/traces/login-failure.zip');
|
|
38
|
+
|
|
39
|
+
beforeAll(async () => {
|
|
40
|
+
browser = await Browser.launch();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
beforeEach(async () => {
|
|
44
|
+
await browser.startTrace({
|
|
45
|
+
outDir: rawTraceDir,
|
|
46
|
+
title: 'Failing login test',
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
afterEach(async ({ task }) => {
|
|
51
|
+
const failed = task.result?.state === 'fail';
|
|
52
|
+
await browser.stopTrace(failed ? { path: traceZip } : undefined);
|
|
53
|
+
if (!failed) rmSync(rawTraceDir, { recursive: true, force: true });
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
afterAll(async () => browser.quit());
|
|
57
|
+
|
|
58
|
+
it('fills and submits the login form', async () => {
|
|
59
|
+
await browser.navigateTo(
|
|
60
|
+
'https://dtopuzov.github.io/craftdriver/examples/login.html',
|
|
61
|
+
);
|
|
62
|
+
await browser.fill('#username', 'alice');
|
|
63
|
+
await browser.fill('#password', 'secret');
|
|
64
|
+
await browser.click('#submit');
|
|
65
|
+
await browser.expect('#welcome').toContainText('Welcome back, alice!');
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
A passing run removes the raw trace and leaves no artifact. After a normal
|
|
71
|
+
Vitest failure, the evidence is kept here:
|
|
72
|
+
|
|
73
|
+
```text
|
|
74
|
+
test-results/
|
|
75
|
+
└── traces/
|
|
76
|
+
├── login-raw/ # trace.ndjson and evidence screenshots
|
|
77
|
+
└── login-failure.zip # open this in Vibium Player
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
For the best online experience, drop the zip into
|
|
81
|
+
[Vibium Player](https://player.vibium.dev/). To keep the trace entirely local,
|
|
82
|
+
use Playwright Trace Viewer:
|
|
83
|
+
|
|
84
|
+
```sh
|
|
85
|
+
npx playwright show-trace test-results/traces/login-failure.zip
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Craftdriver includes screenshot-backed frame snapshots, so Playwright's main
|
|
89
|
+
browser panel shows the captured page state. It is a visual snapshot rather
|
|
90
|
+
than a restorable DOM, so the DOM/locator inspector is intentionally limited.
|
|
91
|
+
|
|
92
|
+
If the test process is killed before `afterEach()` runs, a zip cannot be
|
|
93
|
+
finalized, but the append-only `trace.ndjson` and completed screenshots remain
|
|
94
|
+
in `login-raw/`.
|
|
95
|
+
|
|
96
|
+
## Try A Trace Before Creating A Failure
|
|
97
|
+
|
|
98
|
+
1. [Download the sample login trace](https://dtopuzov.github.io/craftdriver/traces/vitest-login.zip).
|
|
99
|
+
2. Open [Vibium Player](https://player.vibium.dev/).
|
|
100
|
+
3. Drop `vitest-login.zip` onto the player and step through the login.
|
|
101
|
+
|
|
102
|
+
`stopTrace()` captures the final page state, so a failed assertion is visible
|
|
103
|
+
as the last player frame. The sample was generated by the runnable Vitest proof
|
|
104
|
+
for this recipe against the same published login page. To exercise the failure
|
|
105
|
+
workflow yourself, change the expected message to `Welcome back, bob!`, run the
|
|
106
|
+
test, and open the resulting `login-failure.zip`.
|
|
107
|
+
|
|
108
|
+
Trace files can contain screenshots, URLs, console messages, network metadata,
|
|
109
|
+
selectors, and entered values. Use test credentials and review a trace before
|
|
110
|
+
publishing it outside your team.
|
|
111
|
+
|
|
112
|
+
## Learn More
|
|
113
|
+
|
|
114
|
+
- [Tracing](../tracing.md)
|
|
115
|
+
- [Use CraftDriver With Vitest Hooks](./vitest-browser-lifecycle.md)
|
|
116
|
+
- [Vibium Player](https://player.vibium.dev/)
|
|
117
|
+
- [Jason Huggins on standardized tracing](https://www.youtube.com/watch?v=c-OGZK3F0tk&t=1530s)
|