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
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# Test An Electron App From Another Repo
|
|
2
|
+
|
|
3
|
+
Use this when your Electron application is built in one repository, but your
|
|
4
|
+
CraftDriver tests live in another repository or package. The important part is
|
|
5
|
+
that the test launches the **packaged executable** produced by the app repo.
|
|
6
|
+
|
|
7
|
+
This recipe assumes a sibling checkout:
|
|
8
|
+
|
|
9
|
+
```text
|
|
10
|
+
~/git/
|
|
11
|
+
my-electron-app/
|
|
12
|
+
my-electron-tests/
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Build the app first
|
|
16
|
+
|
|
17
|
+
From the app repo, produce the packaged app the same way CI or release builds do:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
cd ~/git/my-electron-app
|
|
21
|
+
npm ci
|
|
22
|
+
npm run build
|
|
23
|
+
npm run package
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
The exact command depends on your app. The output is usually under `dist/`,
|
|
27
|
+
`out/`, or `release/`. Point CraftDriver at the executable inside that output,
|
|
28
|
+
not at the source directory.
|
|
29
|
+
|
|
30
|
+
## Keep the app path in one helper
|
|
31
|
+
|
|
32
|
+
In the test repo, create a small fixture helper so every test uses the same
|
|
33
|
+
path and Electron version:
|
|
34
|
+
|
|
35
|
+
```ts
|
|
36
|
+
// tests/fixtures/electron-app.ts
|
|
37
|
+
import os from 'node:os';
|
|
38
|
+
import path from 'node:path';
|
|
39
|
+
import { fileURLToPath } from 'node:url';
|
|
40
|
+
import { Browser } from 'craftdriver';
|
|
41
|
+
|
|
42
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
43
|
+
const workspace = path.resolve(here, '..', '..', '..');
|
|
44
|
+
const appRepo = path.join(workspace, 'my-electron-app');
|
|
45
|
+
|
|
46
|
+
export function appBinaryPath(): string {
|
|
47
|
+
const override = process.env.MY_APP_BINARY;
|
|
48
|
+
if (override) return override;
|
|
49
|
+
|
|
50
|
+
switch (`${process.platform}-${process.arch}`) {
|
|
51
|
+
case 'darwin-arm64':
|
|
52
|
+
case 'darwin-x64':
|
|
53
|
+
return path.join(appRepo, 'dist/mac/MyApp.app/Contents/MacOS/MyApp');
|
|
54
|
+
case 'win32-x64':
|
|
55
|
+
return path.join(appRepo, 'dist/win-unpacked/MyApp.exe');
|
|
56
|
+
case 'linux-x64':
|
|
57
|
+
return path.join(appRepo, 'dist/linux-unpacked/my-app');
|
|
58
|
+
default:
|
|
59
|
+
throw new Error(`Unsupported Electron test platform: ${os.platform()}-${os.arch()}`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function launchApp(): Promise<Browser> {
|
|
64
|
+
return Browser.launch({
|
|
65
|
+
electron: {
|
|
66
|
+
appBinaryPath: appBinaryPath(),
|
|
67
|
+
version: process.env.MY_APP_ELECTRON_VERSION ?? '43.1.0',
|
|
68
|
+
args: process.platform === 'linux' ? ['--no-sandbox'] : [],
|
|
69
|
+
},
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Use `MY_APP_BINARY` when CI downloads a prebuilt app artifact instead of building
|
|
75
|
+
from a sibling checkout.
|
|
76
|
+
|
|
77
|
+
## Write the test like a normal renderer test
|
|
78
|
+
|
|
79
|
+
Electron defaults to Classic WebDriver, so the app loads itself. Do not call
|
|
80
|
+
`navigateTo()` unless you explicitly opted into BiDi and know the renderer URL.
|
|
81
|
+
|
|
82
|
+
```ts
|
|
83
|
+
// tests/settings.test.ts
|
|
84
|
+
import { afterEach, describe, it } from 'vitest';
|
|
85
|
+
import { Browser, By } from 'craftdriver';
|
|
86
|
+
import { launchApp } from './fixtures/electron-app';
|
|
87
|
+
|
|
88
|
+
describe('settings window', () => {
|
|
89
|
+
let browser: Browser | undefined;
|
|
90
|
+
|
|
91
|
+
afterEach(async () => {
|
|
92
|
+
await browser?.quit();
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('saves the profile name', async () => {
|
|
96
|
+
browser = await launchApp();
|
|
97
|
+
|
|
98
|
+
const main = await browser.waitForPage({ title: /My App/ });
|
|
99
|
+
await main.find(By.testId('app-title')).expect().toHaveText('My App');
|
|
100
|
+
|
|
101
|
+
await main.click(By.testId('nav-settings'));
|
|
102
|
+
await main.fill(By.testId('profile-name'), 'Ada Lovelace');
|
|
103
|
+
await main.click(By.testId('save-settings'));
|
|
104
|
+
|
|
105
|
+
await main.find(By.testId('settings-status')).expect().toHaveText('Saved');
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Prefer stable `data-testid` attributes in the Electron renderer. You can inspect
|
|
111
|
+
or debug the app with DevTools while designing selectors, but the test should
|
|
112
|
+
interact with the packaged UI the way a user would.
|
|
113
|
+
|
|
114
|
+
## CI shape
|
|
115
|
+
|
|
116
|
+
A common CI flow is:
|
|
117
|
+
|
|
118
|
+
1. Check out or download the Electron app.
|
|
119
|
+
2. Build/package the app, or download a packaged artifact.
|
|
120
|
+
3. Set `MY_APP_BINARY` and `MY_APP_ELECTRON_VERSION`.
|
|
121
|
+
4. Run the CraftDriver tests.
|
|
122
|
+
|
|
123
|
+
On Linux, run the tests under Xvfb:
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
xvfb-run -a npm test
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
On macOS and Windows hosted runners, a display is already available. Do not pass
|
|
130
|
+
headless flags to Electron; it is a desktop GUI app.
|
|
131
|
+
|
|
132
|
+
## Learn More
|
|
133
|
+
|
|
134
|
+
- [Testing Electron Apps](../electron.md)
|
|
135
|
+
- [Browser API](../browser-api.md)
|
|
136
|
+
- [Vitest Hooks](./vitest-browser-lifecycle.md)
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# Test An Electron Deep Link
|
|
2
|
+
|
|
3
|
+
Use this pattern when your app registers a custom URL scheme (`myapp://…`) and you
|
|
4
|
+
want to verify it handles a link the OS delivers from outside the app. CraftDriver
|
|
5
|
+
opens the link through the real OS launcher, so your production `open-url` /
|
|
6
|
+
`second-instance` handler runs unchanged.
|
|
7
|
+
|
|
8
|
+
## Application code
|
|
9
|
+
|
|
10
|
+
Register the protocol and route incoming links to the window. This is normal
|
|
11
|
+
production code, not test instrumentation:
|
|
12
|
+
|
|
13
|
+
```js
|
|
14
|
+
const { app, BrowserWindow } = require('electron');
|
|
15
|
+
|
|
16
|
+
const SCHEME = 'myapp';
|
|
17
|
+
let mainWindow = null;
|
|
18
|
+
let lastDeeplink = null;
|
|
19
|
+
|
|
20
|
+
app.setAsDefaultProtocolClient(SCHEME);
|
|
21
|
+
|
|
22
|
+
function handleDeeplink(url) {
|
|
23
|
+
if (!url) return;
|
|
24
|
+
lastDeeplink = url;
|
|
25
|
+
if (mainWindow && !mainWindow.isDestroyed()) mainWindow.webContents.send('deeplink', url);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// macOS: the running instance receives the URL here.
|
|
29
|
+
app.on('open-url', (event, url) => {
|
|
30
|
+
event.preventDefault();
|
|
31
|
+
handleDeeplink(url);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
// Windows/Linux: a deep link launches a second process. Hold the single-instance
|
|
35
|
+
// lock so its argv is routed back into the running instance instead of opening a
|
|
36
|
+
// duplicate window.
|
|
37
|
+
if (!app.requestSingleInstanceLock()) {
|
|
38
|
+
app.quit();
|
|
39
|
+
} else {
|
|
40
|
+
app.on('second-instance', (_event, argv) => {
|
|
41
|
+
handleDeeplink(argv.find((a) => a.startsWith(`${SCHEME}://`)));
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Declare the scheme so the packaged app is registered with the OS. With
|
|
47
|
+
electron-builder:
|
|
48
|
+
|
|
49
|
+
```yaml
|
|
50
|
+
# electron-builder.yml
|
|
51
|
+
protocols:
|
|
52
|
+
- name: My App Protocol
|
|
53
|
+
schemes:
|
|
54
|
+
- myapp
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Forward the link to the renderer through a narrow preload API:
|
|
58
|
+
|
|
59
|
+
```js
|
|
60
|
+
const { contextBridge, ipcRenderer } = require('electron');
|
|
61
|
+
|
|
62
|
+
contextBridge.exposeInMainWorld('myapp', {
|
|
63
|
+
onDeeplink: (cb) => ipcRenderer.on('deeplink', (_event, url) => cb(url)),
|
|
64
|
+
});
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
```js
|
|
68
|
+
// renderer
|
|
69
|
+
window.myapp?.onDeeplink((url) => {
|
|
70
|
+
document.querySelector('[data-testid="deeplink-result"]').textContent = url;
|
|
71
|
+
});
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Test code
|
|
75
|
+
|
|
76
|
+
```ts
|
|
77
|
+
import { Browser, By } from 'craftdriver';
|
|
78
|
+
import { expect, test } from 'vitest';
|
|
79
|
+
|
|
80
|
+
test('handles an incoming deep link', async () => {
|
|
81
|
+
const browser = await Browser.launch({
|
|
82
|
+
electron: {
|
|
83
|
+
appBinaryPath: process.env.APP_PATH!, // required on Windows to route the link
|
|
84
|
+
version: process.env.ELECTRON_VERSION!,
|
|
85
|
+
mainProcess: true, // lets craftdriver auto-detect the user-data dir (Windows/Linux)
|
|
86
|
+
},
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
try {
|
|
90
|
+
const url = 'myapp://open?file=test.txt';
|
|
91
|
+
await browser.electron.triggerDeeplink(url);
|
|
92
|
+
|
|
93
|
+
// triggerDeeplink is fire-and-forget: assert the effect through the app.
|
|
94
|
+
await browser.find(By.testId('deeplink-result')).expect().toHaveText(url);
|
|
95
|
+
|
|
96
|
+
// Only custom protocols are valid targets.
|
|
97
|
+
await expect(browser.electron.triggerDeeplink('https://example.com')).rejects.toThrow();
|
|
98
|
+
} finally {
|
|
99
|
+
await browser.quit();
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
The reference example implements this exact flow (scheme `craftdriver-example://`)
|
|
105
|
+
in [`craftdriver-examples`](https://github.com/dtopuzov/craftdriver-examples/tree/main/electron).
|
|
106
|
+
|
|
107
|
+
## Notes
|
|
108
|
+
|
|
109
|
+
- **macOS** needs the app bundle known to LaunchServices — installed in
|
|
110
|
+
`/Applications`, or registered once with `lsregister -f MyApp.app`.
|
|
111
|
+
- **Windows / Linux** launch a second process for the link; craftdriver appends the
|
|
112
|
+
running app's user-data dir as a `userData` query parameter so the single-instance
|
|
113
|
+
lock hands the link to the test instance. Your app should honor that parameter
|
|
114
|
+
when it sets its user-data path if you run isolated instances.
|
|
115
|
+
- Like `mockDialog()` and `executeMain()`, deep-link routing that inspects the app
|
|
116
|
+
via the main process needs the `EnableNodeCliInspectArguments` fuse enabled; the
|
|
117
|
+
OS launch itself does not.
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# Mock Electron APIs (shell, clipboard, app…)
|
|
2
|
+
|
|
3
|
+
Native dialogs have a typed helper (`mockDialog()`), but plenty of Electron
|
|
4
|
+
behavior you want to keep out of a test run doesn't: opening the user's browser
|
|
5
|
+
(`shell.openExternal`), writing the system clipboard (`clipboard.writeText`),
|
|
6
|
+
resolving OS paths (`app.getPath`), and so on. `browser.electron.mock()` replaces
|
|
7
|
+
any `electron.<api>.<fn>` main-process method with a scripted return and a call
|
|
8
|
+
recorder, so you can drive the real UI and assert what it asked the OS to do —
|
|
9
|
+
without the OS actually doing it.
|
|
10
|
+
|
|
11
|
+
Needs main-process access (`electron: { mainProcess: true }`), like `executeMain`
|
|
12
|
+
and `mockDialog`.
|
|
13
|
+
|
|
14
|
+
## Stop a "share" button from launching a browser
|
|
15
|
+
|
|
16
|
+
Application code — a normal renderer → preload → IPC → `shell.openExternal` flow:
|
|
17
|
+
|
|
18
|
+
```js
|
|
19
|
+
// main.js
|
|
20
|
+
const { ipcMain, shell } = require('electron');
|
|
21
|
+
ipcMain.handle('share:open', (_event, url) => shell.openExternal(url));
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
```js
|
|
25
|
+
// preload.js
|
|
26
|
+
const { contextBridge, ipcRenderer } = require('electron');
|
|
27
|
+
contextBridge.exposeInMainWorld('share', { open: (url) => ipcRenderer.invoke('share:open', url) });
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Test — mock the final `shell.openExternal`, click, and assert the URL it received:
|
|
31
|
+
|
|
32
|
+
```ts
|
|
33
|
+
import { Browser, By } from 'craftdriver';
|
|
34
|
+
import { expect, test } from 'vitest';
|
|
35
|
+
|
|
36
|
+
test('share button opens the right URL without launching a browser', async () => {
|
|
37
|
+
const browser = await Browser.launch({
|
|
38
|
+
electron: { appBinaryPath: process.env.APP_PATH!, mainProcess: true },
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
// shell.openExternal resolves to true on success — script that.
|
|
43
|
+
const openExternal = await browser.electron.mock('shell', 'openExternal', true);
|
|
44
|
+
|
|
45
|
+
await browser.click(By.testId('share-btn'));
|
|
46
|
+
|
|
47
|
+
expect(await openExternal.getCalls()).toEqual([{ args: ['https://example.com/report/42'] }]);
|
|
48
|
+
await openExternal.restore();
|
|
49
|
+
} finally {
|
|
50
|
+
await browser.quit();
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Each recorded call is `{ args: [...] }` with the (JSON-safe) arguments the app
|
|
56
|
+
passed. The rest of the handle mirrors `mockDialog`: `getCallCount()`,
|
|
57
|
+
`clearCalls()`, `mockReturnValue(value)` to re-script mid-test, and `restore()`
|
|
58
|
+
(also restored on `browser.quit()`).
|
|
59
|
+
|
|
60
|
+
## Assert what the app copied to the clipboard
|
|
61
|
+
|
|
62
|
+
```ts
|
|
63
|
+
const writeText = await browser.electron.mock('clipboard', 'writeText');
|
|
64
|
+
|
|
65
|
+
await browser.click(By.testId('copy-link-btn'));
|
|
66
|
+
|
|
67
|
+
expect(await writeText.getCalls()).toEqual([{ args: ['https://example.com/report/42'] }]);
|
|
68
|
+
await writeText.restore();
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Here no return value is scripted — `clipboard.writeText` returns nothing, so the
|
|
72
|
+
mock returns `undefined` and you assert purely on the recorded arguments.
|
|
73
|
+
|
|
74
|
+
## Notes
|
|
75
|
+
|
|
76
|
+
- The scripted value is returned **as-is** (not wrapped in a Promise), so it works
|
|
77
|
+
for synchronous methods (`app.getName()`, `app.getPath()`) and `await`ed
|
|
78
|
+
asynchronous ones (`shell.openExternal`). Pass the already-resolved value for an
|
|
79
|
+
async method.
|
|
80
|
+
- Arguments and the return value must be JSON-serializable; a non-serializable
|
|
81
|
+
argument (a `BrowserWindow`, a `Buffer`) is recorded as a descriptive placeholder
|
|
82
|
+
rather than crossing the process boundary.
|
|
83
|
+
- `mock()` targets **object-namespace** methods (`shell`, `clipboard`, `app`,
|
|
84
|
+
`dialog`, …). Class-based APIs such as `Notification` or `Menu` are not covered by
|
|
85
|
+
it today — mock the app code that *constructs* them instead, or drive them through
|
|
86
|
+
your own IPC surface.
|
|
87
|
+
- Reach for the typed [`mockDialog()`](../electron.md#mock-native-dialogs) for file,
|
|
88
|
+
save, and message dialogs; it validates the result shape for you.
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# Mock A Native Electron File Dialog
|
|
2
|
+
|
|
3
|
+
Use this pattern when a renderer action opens Electron's operating-system file
|
|
4
|
+
picker. The real app remains structured normally: a sandboxed renderer calls a
|
|
5
|
+
narrow preload API, the preload sends IPC, and the main process opens the native
|
|
6
|
+
dialog. The test replaces only the final `dialog.showOpenDialog()` call.
|
|
7
|
+
|
|
8
|
+
## Application code
|
|
9
|
+
|
|
10
|
+
Register the dialog in the main process:
|
|
11
|
+
|
|
12
|
+
```js
|
|
13
|
+
const { BrowserWindow, dialog, ipcMain } = require('electron');
|
|
14
|
+
|
|
15
|
+
ipcMain.handle('native-dialog:open-file', (event) => {
|
|
16
|
+
const parent = BrowserWindow.fromWebContents(event.sender);
|
|
17
|
+
const options = {
|
|
18
|
+
title: 'Choose a text file',
|
|
19
|
+
properties: ['openFile'],
|
|
20
|
+
filters: [{ name: 'Text files', extensions: ['txt', 'md'] }],
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
return parent ? dialog.showOpenDialog(parent, options) : dialog.showOpenDialog(options);
|
|
24
|
+
});
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Expose only the operation the renderer needs:
|
|
28
|
+
|
|
29
|
+
```js
|
|
30
|
+
const { contextBridge, ipcRenderer } = require('electron');
|
|
31
|
+
|
|
32
|
+
contextBridge.exposeInMainWorld('craftdriverExample', {
|
|
33
|
+
openFile: () => ipcRenderer.invoke('native-dialog:open-file'),
|
|
34
|
+
});
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
The renderer can use the returned value like the real Electron result:
|
|
38
|
+
|
|
39
|
+
```js
|
|
40
|
+
document.querySelector('[data-testid="open-native-file-btn"]').addEventListener('click', async () => {
|
|
41
|
+
const result = await window.craftdriverExample.openFile();
|
|
42
|
+
document.querySelector('[data-testid="native-dialog-result"]').textContent = result.canceled
|
|
43
|
+
? 'cancelled'
|
|
44
|
+
: result.filePaths.join(', ');
|
|
45
|
+
});
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Test code
|
|
49
|
+
|
|
50
|
+
Opt in to main-process access and install the mock before clicking:
|
|
51
|
+
|
|
52
|
+
```ts
|
|
53
|
+
import { Browser, By } from 'craftdriver';
|
|
54
|
+
import { expect, test } from 'vitest';
|
|
55
|
+
|
|
56
|
+
test('selects an invoice without opening an OS dialog', async () => {
|
|
57
|
+
const browser = await Browser.launch({
|
|
58
|
+
electron: {
|
|
59
|
+
appBinaryPath: process.env.APP_PATH!,
|
|
60
|
+
version: process.env.ELECTRON_VERSION!,
|
|
61
|
+
mainProcess: true,
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
try {
|
|
66
|
+
const dialog = await browser.electron.mockDialog('showOpenDialog', {
|
|
67
|
+
canceled: false,
|
|
68
|
+
filePaths: ['/fixtures/invoice.txt'],
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
await browser.click(By.testId('open-native-file-btn'));
|
|
72
|
+
await browser
|
|
73
|
+
.find(By.testId('native-dialog-result'))
|
|
74
|
+
.expect()
|
|
75
|
+
.toHaveText('/fixtures/invoice.txt');
|
|
76
|
+
|
|
77
|
+
// The click reached the main process: the mock recorded the exact options the
|
|
78
|
+
// ipcMain handler passed to dialog.showOpenDialog — no OS dialog ever opened.
|
|
79
|
+
expect(await dialog.getCalls()).toEqual([
|
|
80
|
+
{
|
|
81
|
+
options: {
|
|
82
|
+
title: 'Choose a text file',
|
|
83
|
+
properties: ['openFile'],
|
|
84
|
+
filters: [{ name: 'Text files', extensions: ['txt', 'md'] }],
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
]);
|
|
88
|
+
} finally {
|
|
89
|
+
await browser.quit();
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
The reference example implements this exact flow in
|
|
95
|
+
[`craftdriver-examples`](https://github.com/dtopuzov/craftdriver-examples/tree/main/electron).
|
|
96
|
+
|
|
97
|
+
## Production and hardened builds
|
|
98
|
+
|
|
99
|
+
No application test hook is required. This works against an unchanged packaged
|
|
100
|
+
production artifact when its `EnableNodeCliInspectArguments` Electron fuse is
|
|
101
|
+
enabled. The fuse is enabled by default, but security-hardened builds may disable
|
|
102
|
+
it. A disabled fuse cannot be changed after the app is signed; package a separate
|
|
103
|
+
test artifact with that fuse enabled. See
|
|
104
|
+
[Testing Electron Apps](../electron.md#can-this-test-a-production-build) for the
|
|
105
|
+
Forge configuration and security boundary.
|
package/docs/recipes.md
CHANGED
|
@@ -16,23 +16,27 @@ launched `browser` — unless it shows a `Browser.launch(...)` call itself. See
|
|
|
16
16
|
[Vitest Hooks recipe](./recipes/vitest-browser-lifecycle.md) for the surrounding
|
|
17
17
|
setup.
|
|
18
18
|
|
|
19
|
+
The [Electron recipes](#electron) are different: they show the project shape for
|
|
20
|
+
driving a packaged desktop app, so they use paths you adapt to your app repo
|
|
21
|
+
rather than the hosted browser examples.
|
|
22
|
+
|
|
19
23
|
For exact signatures, use the linked feature docs and the
|
|
20
24
|
[API reference](./api-reference.md).
|
|
21
25
|
|
|
22
26
|
## Start Here
|
|
23
27
|
|
|
24
|
-
| Scenario
|
|
25
|
-
|
|
|
26
|
-
| Find elements
|
|
28
|
+
| Scenario | Use when | Recipe |
|
|
29
|
+
| ------------- | ----------------------------------------------------------------- | ------------------------------------------------------- |
|
|
30
|
+
| Find elements | You're new and need to point CraftDriver at the element you want. | [Find Elements On The Page](./recipes/find-elements.md) |
|
|
27
31
|
|
|
28
32
|
## Test Structure
|
|
29
33
|
|
|
30
|
-
| Scenario
|
|
31
|
-
|
|
|
32
|
-
| Vitest browser lifecycle
|
|
33
|
-
| Login once, reuse session
|
|
34
|
-
| Multi-user flows
|
|
35
|
-
| Page objects
|
|
34
|
+
| Scenario | Use when | Recipe |
|
|
35
|
+
| ------------------------------ | -------------------------------------------------------------------------- | ------------------------------------------------------------------------------------- |
|
|
36
|
+
| Vitest browser lifecycle | You want one browser per test file and a fresh page per test. | [Use CraftDriver With Vitest Hooks](./recipes/vitest-browser-lifecycle.md) |
|
|
37
|
+
| Login once, reuse session | Login UI is slow or noisy and most tests start signed in. | [Log In Once And Reuse The Session](./recipes/login-once-reuse-session.md) |
|
|
38
|
+
| Multi-user flows | You need Alice and Bob signed in at the same time without leaking cookies. | [Test Multi-User Workflows](./recipes/multi-user-contexts.md) |
|
|
39
|
+
| Page objects | Selectors and steps are copy-pasted across tests and break together. | [Organize Flows With Page Objects](./recipes/page-objects.md) |
|
|
36
40
|
|
|
37
41
|
## App Behavior
|
|
38
42
|
|
|
@@ -49,4 +53,19 @@ For exact signatures, use the linked feature docs and the
|
|
|
49
53
|
| ----------------------------- | ------------------------------------------------------------------------------- | ------------------------------------------------------------------------ |
|
|
50
54
|
| Accessibility regression gate | CI should fail on serious page or component accessibility issues. | [Run Accessibility Gates](./recipes/accessibility-gate.md) |
|
|
51
55
|
| Console and JavaScript errors | Tests should fail if the browser reports unexpected client-side errors. | [Fail On Console And JavaScript Errors](./recipes/console-error-gate.md) |
|
|
52
|
-
|
|
|
56
|
+
| Debug failing tests | You need the actions, screenshots, logs, and network activity behind a failure. | [Use Traces To Debug Failing Tests](./recipes/debug-failing-tests-with-traces.md) |
|
|
57
|
+
|
|
58
|
+
## Electron
|
|
59
|
+
|
|
60
|
+
Driving a packaged Electron desktop app. These use paths you adapt to your app
|
|
61
|
+
repo rather than the hosted browser examples, and the mocking/deep-link recipes
|
|
62
|
+
need main-process access (`electron: { mainProcess: true }`). See
|
|
63
|
+
[Testing Electron Apps](./electron.md) for setup, drivers, and the security
|
|
64
|
+
boundary.
|
|
65
|
+
|
|
66
|
+
| Scenario | Use when | Recipe |
|
|
67
|
+
| ------------------------------ | -------------------------------------------------------------------------- | ------------------------------------------------------------------------------------- |
|
|
68
|
+
| Electron app from another repo | Your packaged Electron app is built in one repo and tested from another. | [Test An Electron App From Another Repo](./recipes/electron-app-from-another-repo.md) |
|
|
69
|
+
| Electron native dialog | A renderer flow opens an operating-system file, save, or message dialog. | [Mock A Native Electron File Dialog](./recipes/electron-native-dialog.md) |
|
|
70
|
+
| Electron API mocking | A flow calls `shell.openExternal`, `clipboard.writeText`, or similar. | [Mock Electron APIs](./recipes/electron-mock-apis.md) |
|
|
71
|
+
| Electron deep link | Your app registers a `myapp://` scheme and must handle links from the OS. | [Test An Electron Deep Link](./recipes/electron-deep-link.md) |
|