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
|
@@ -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
|
|
|
@@ -50,3 +54,18 @@ For exact signatures, use the linked feature docs and the
|
|
|
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) |
|
package/docs/safari.md
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# Safari
|
|
2
|
+
|
|
3
|
+
CraftDriver drives **real desktop Safari on macOS** through W3C WebDriver
|
|
4
|
+
Classic.
|
|
5
|
+
|
|
6
|
+
## One-time setup
|
|
7
|
+
|
|
8
|
+
Safari's driver (`safaridriver`) ships with macOS — **nothing to install or
|
|
9
|
+
download**. You only have to turn on automation **once per machine**:
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
safaridriver --enable
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
This is the single prerequisite.
|
|
16
|
+
|
|
17
|
+
CraftDriver never runs `--enable` for you. If automation isn't enabled, a
|
|
18
|
+
launch fails immediately with that exact remedy rather than hanging.
|
|
19
|
+
|
|
20
|
+
## Quick start
|
|
21
|
+
|
|
22
|
+
```typescript
|
|
23
|
+
import { Browser } from 'craftdriver';
|
|
24
|
+
|
|
25
|
+
const browser = await Browser.launch({ browserName: 'safari' });
|
|
26
|
+
await browser.navigateTo('http://127.0.0.1:8080/login.html');
|
|
27
|
+
await browser.getByLabel('Username').fill('alice');
|
|
28
|
+
await browser.getByLabel('Password').fill('secret');
|
|
29
|
+
await browser.getByRole('button', { name: 'Sign in' }).click();
|
|
30
|
+
await browser.expect('#welcome').toHaveText('Welcome back, alice!');
|
|
31
|
+
await browser.quit();
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Same API as every other browser — locators, actions, assertions, and
|
|
35
|
+
auto-waiting all work unchanged.
|
|
36
|
+
|
|
37
|
+
## What works
|
|
38
|
+
|
|
39
|
+
Navigation, locators, and element queries · click / fill / clear / keyboard /
|
|
40
|
+
desktop mouse · `evaluate()` (sync & async JS) · frames and iframes · window &
|
|
41
|
+
popup enumeration · **imperative dialogs** (`getAlertText` / `acceptAlert` /
|
|
42
|
+
`dismissAlert` / `sendAlertText`) · viewport and element screenshots · cookies
|
|
43
|
+
and storage (via the standard Classic cookie endpoints) · accessibility checks.
|
|
44
|
+
|
|
45
|
+
## What isn't supported
|
|
46
|
+
|
|
47
|
+
Safari exposes no WebDriver BiDi endpoint, so everything event- or
|
|
48
|
+
BiDi-driven is unavailable — and each of these throws a clear
|
|
49
|
+
[`UNSUPPORTED`](./error-codes.md) error immediately, never a silent hang:
|
|
50
|
+
|
|
51
|
+
- network mocking / interception and request/response waits
|
|
52
|
+
- console & JavaScript-error capture, tracing
|
|
53
|
+
- BiDi user contexts and emulation (permissions, geolocation,
|
|
54
|
+
locale/timezone, offline, color-scheme)
|
|
55
|
+
- init/preload scripts and the virtual clock
|
|
56
|
+
- event-driven dialogs (`onDialog` / `waitForDialog`) — use the imperative
|
|
57
|
+
methods above
|
|
58
|
+
- full-page screenshots and `openPage()`
|
|
59
|
+
- CraftDriver-managed downloads (`waitForDownload`)
|
|
60
|
+
|
|
61
|
+
Desktop Safari is also **not** iPhone/iPad Safari — there is no mobile or
|
|
62
|
+
touch emulation.
|
|
63
|
+
|
|
64
|
+
## Limitations to plan around
|
|
65
|
+
|
|
66
|
+
- **macOS-only.** There is no Safari for Linux or Windows, and none is coming.
|
|
67
|
+
Trying to launch Safari off macOS throws `UNSUPPORTED`.
|
|
68
|
+
- **Headed only.** No headless mode — every launch opens a visible automation
|
|
69
|
+
window (with a purple banner). Don't interact with it while a test runs.
|
|
70
|
+
Setting `HEADLESS=true` with Safari is rejected up front.
|
|
71
|
+
- **Serial only.** Only one Safari WebDriver session can be active on a Mac at
|
|
72
|
+
a time. A second concurrent launch is refused with an actionable error
|
|
73
|
+
(never a hang) — run Safari tests serially.
|
|
74
|
+
|
|
75
|
+
## Safari Technology Preview
|
|
76
|
+
|
|
77
|
+
STP ships its own `safaridriver`; point `SafariService` at it. This is also
|
|
78
|
+
the one way to get a **second concurrent session** on one Mac — stable Safari
|
|
79
|
+
and STP each run one session at the same time.
|
|
80
|
+
|
|
81
|
+
```typescript
|
|
82
|
+
import { Browser, SafariService } from 'craftdriver';
|
|
83
|
+
|
|
84
|
+
const browser = await Browser.launch({
|
|
85
|
+
browserName: 'safari',
|
|
86
|
+
safariService: new SafariService({
|
|
87
|
+
binaryPath: '/Applications/Safari Technology Preview.app/Contents/MacOS/safaridriver',
|
|
88
|
+
}),
|
|
89
|
+
});
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Enable automation once for STP too: `/Applications/Safari Technology Preview.app/Contents/MacOS/safaridriver --enable`.
|
|
93
|
+
|
|
94
|
+
## Running Safari tests
|
|
95
|
+
|
|
96
|
+
Run Safari **serially** — one session at a time. With Vitest that's
|
|
97
|
+
`--maxWorkers=1`:
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
vitest run --maxWorkers=1
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Keep it headed (don't set `HEADLESS`), on macOS with Remote Automation enabled.
|
|
104
|
+
In CI, use a macOS runner; parallelism across Safari needs **separate macOS
|
|
105
|
+
hosts**.
|
|
106
|
+
|
|
107
|
+
## Troubleshooting
|
|
108
|
+
|
|
109
|
+
| Symptom | Fix |
|
|
110
|
+
| --- | --- |
|
|
111
|
+
| `You must enable 'Allow remote automation'…` | Run `safaridriver --enable` once (see [setup](#one-time-setup)). |
|
|
112
|
+
| Second launch fails while one session is open | Expected — Safari is serial. Serialize your tests, or use STP for a second session. |
|
|
113
|
+
| `Could not find 'safaridriver'` | You're not on macOS, or Safari is missing. For a non-standard install, set `CRAFTDRIVER_SAFARIDRIVER_PATH` or `SafariService({ binaryPath })`. |
|
|
114
|
+
|
|
115
|
+
## See also
|
|
116
|
+
|
|
117
|
+
- [Driver Configuration → Safari](./driver-configuration.md#safari-macos-driver-ships-with-the-browser) — the no-download driver-resolution chain.
|
|
118
|
+
- [WebDriver Standards](./standards.md#safari-is-classic-only) — why Safari is Classic-only.
|
package/docs/standards.md
CHANGED
|
@@ -16,6 +16,19 @@ That protocol choice is the center of the project. CraftDriver tries to provide
|
|
|
16
16
|
|
|
17
17
|
CraftDriver picks the practical protocol path behind a single public API. You call `browser.click()`, `browser.navigateTo()`, `browser.network.mock()`, or `browser.startTrace()`; the library handles the transport details.
|
|
18
18
|
|
|
19
|
+
### Safari is Classic-only
|
|
20
|
+
|
|
21
|
+
`browserName: 'safari'` always uses WebDriver Classic and never negotiates
|
|
22
|
+
`webSocketUrl`. This isn't a temporary gap CraftDriver plans to close — Apple
|
|
23
|
+
does not currently document a supported WebDriver BiDi endpoint for Safari or
|
|
24
|
+
Safari Technology Preview, so there's no BiDi transport to opt into. CraftDriver
|
|
25
|
+
rejects `enableBiDi: true` for Safari with a clear `UNSUPPORTED` error before
|
|
26
|
+
launch rather than silently downgrading, and every BiDi-only method (network,
|
|
27
|
+
logs, tracing, contexts, permissions/geolocation/emulation, full-page
|
|
28
|
+
screenshots, event-driven dialogs) fails the same way when called on a Safari
|
|
29
|
+
session. See the [Safari guide](./safari.md) for what remains available on
|
|
30
|
+
Classic.
|
|
31
|
+
|
|
19
32
|
## Why It Matters
|
|
20
33
|
|
|
21
34
|
Standards-based automation gives projects a few useful properties:
|
package/docs/why-craftdriver.md
CHANGED
|
@@ -5,7 +5,7 @@ CraftDriver is for writing boringly reliable automation against real browsers, w
|
|
|
5
5
|
## What It Cares About
|
|
6
6
|
|
|
7
7
|
- 🍺 **Focused Node.js API** - browser automation without a giant framework around it.
|
|
8
|
-
- 🧭 **Real browsers** - drives installed Chrome, Chromium, and Firefox instead of patched browser engine builds.
|
|
8
|
+
- 🧭 **Real browsers** - drives installed Chrome, Chromium, and Firefox instead of patched browser engine builds, plus real ([Safari on macOS](./safari.md)).
|
|
9
9
|
- 🌐 **Standards that age well** - W3C WebDriver standards stay stable while browser-private protocols change.
|
|
10
10
|
- 🚦 **Readable, auto-waited flows** - role, label, text, test id, CSS, XPath, click, fill, and expect.
|
|
11
11
|
- 📡 **Network control** - mock, block, intercept, and wait for browser requests and responses.
|
|
@@ -62,3 +62,15 @@ Manual configuration is useful when:
|
|
|
62
62
|
[Driver Configuration → Browser Binary Configuration](./driver-configuration.md#browser-binary-configuration)
|
|
63
63
|
|
|
64
64
|
See [Driver Configuration](./driver-configuration.md) for the full reference.
|
|
65
|
+
|
|
66
|
+
## Safari: nothing to resolve, nothing to download
|
|
67
|
+
|
|
68
|
+
The auto-download model above doesn't apply to `browserName: 'safari'`.
|
|
69
|
+
`safaridriver` ships as part of macOS/Safari itself — there's no version to
|
|
70
|
+
match, no binary to fetch, and no cache to warm. CraftDriver only ever
|
|
71
|
+
**locates** the driver Apple already installed. The single prerequisite is
|
|
72
|
+
turning automation on once per Mac with `safaridriver --enable`.
|
|
73
|
+
|
|
74
|
+
See the [**Safari guide**](./safari.md) for setup and capabilities, and
|
|
75
|
+
[Driver Configuration → Safari](./driver-configuration.md#safari-macos-driver-ships-with-the-browser)
|
|
76
|
+
for the driver-resolution chain.
|