craftdriver 1.3.0 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (132) hide show
  1. package/CHANGELOG.md +17 -3
  2. package/README.md +4 -1
  3. package/dist/cli/dispatcher.js +1 -1
  4. package/dist/cli/dispatcher.js.map +1 -1
  5. package/dist/cli/parseArgs.d.ts +2 -2
  6. package/dist/cli/parseArgs.d.ts.map +1 -1
  7. package/dist/cli/parseArgs.js +1 -1
  8. package/dist/index.d.ts +7 -1
  9. package/dist/index.d.ts.map +1 -1
  10. package/dist/index.js +5 -0
  11. package/dist/index.js.map +1 -1
  12. package/dist/lib/bidi/storage.d.ts +13 -0
  13. package/dist/lib/bidi/storage.d.ts.map +1 -1
  14. package/dist/lib/bidi/storage.js +81 -32
  15. package/dist/lib/bidi/storage.js.map +1 -1
  16. package/dist/lib/browser.d.ts +173 -21
  17. package/dist/lib/browser.d.ts.map +1 -1
  18. package/dist/lib/browser.js +519 -210
  19. package/dist/lib/browser.js.map +1 -1
  20. package/dist/lib/browserContext.d.ts +2 -2
  21. package/dist/lib/browserContext.d.ts.map +1 -1
  22. package/dist/lib/browserContext.js +1 -1
  23. package/dist/lib/builder.d.ts +4 -1
  24. package/dist/lib/builder.d.ts.map +1 -1
  25. package/dist/lib/builder.js +20 -2
  26. package/dist/lib/builder.js.map +1 -1
  27. package/dist/lib/capabilities.d.ts +45 -0
  28. package/dist/lib/capabilities.d.ts.map +1 -0
  29. package/dist/lib/capabilities.js +103 -0
  30. package/dist/lib/capabilities.js.map +1 -0
  31. package/dist/lib/chrome.d.ts +7 -2
  32. package/dist/lib/chrome.d.ts.map +1 -1
  33. package/dist/lib/chrome.js +10 -1
  34. package/dist/lib/chrome.js.map +1 -1
  35. package/dist/lib/driver.d.ts +16 -1
  36. package/dist/lib/driver.d.ts.map +1 -1
  37. package/dist/lib/driver.js +64 -0
  38. package/dist/lib/driver.js.map +1 -1
  39. package/dist/lib/driverManager.d.ts +120 -0
  40. package/dist/lib/driverManager.d.ts.map +1 -1
  41. package/dist/lib/driverManager.js +289 -10
  42. package/dist/lib/driverManager.js.map +1 -1
  43. package/dist/lib/electron.d.ts +60 -0
  44. package/dist/lib/electron.d.ts.map +1 -0
  45. package/dist/lib/electron.js +85 -0
  46. package/dist/lib/electron.js.map +1 -0
  47. package/dist/lib/electronDeeplink.d.ts +22 -0
  48. package/dist/lib/electronDeeplink.d.ts.map +1 -0
  49. package/dist/lib/electronDeeplink.js +118 -0
  50. package/dist/lib/electronDeeplink.js.map +1 -0
  51. package/dist/lib/electronDiagnostics.d.ts +49 -0
  52. package/dist/lib/electronDiagnostics.d.ts.map +1 -0
  53. package/dist/lib/electronDiagnostics.js +195 -0
  54. package/dist/lib/electronDiagnostics.js.map +1 -0
  55. package/dist/lib/electronDialogMock.d.ts +51 -0
  56. package/dist/lib/electronDialogMock.d.ts.map +1 -0
  57. package/dist/lib/electronDialogMock.js +106 -0
  58. package/dist/lib/electronDialogMock.js.map +1 -0
  59. package/dist/lib/electronFuses.d.ts +13 -0
  60. package/dist/lib/electronFuses.d.ts.map +1 -0
  61. package/dist/lib/electronFuses.js +118 -0
  62. package/dist/lib/electronFuses.js.map +1 -0
  63. package/dist/lib/electronMainBridge.d.ts +28 -0
  64. package/dist/lib/electronMainBridge.d.ts.map +1 -0
  65. package/dist/lib/electronMainBridge.js +270 -0
  66. package/dist/lib/electronMainBridge.js.map +1 -0
  67. package/dist/lib/electronMainLogs.d.ts +81 -0
  68. package/dist/lib/electronMainLogs.d.ts.map +1 -0
  69. package/dist/lib/electronMainLogs.js +176 -0
  70. package/dist/lib/electronMainLogs.js.map +1 -0
  71. package/dist/lib/electronMock.d.ts +52 -0
  72. package/dist/lib/electronMock.d.ts.map +1 -0
  73. package/dist/lib/electronMock.js +96 -0
  74. package/dist/lib/electronMock.js.map +1 -0
  75. package/dist/lib/electronRemote.d.ts +106 -0
  76. package/dist/lib/electronRemote.d.ts.map +1 -0
  77. package/dist/lib/electronRemote.js +406 -0
  78. package/dist/lib/electronRemote.js.map +1 -0
  79. package/dist/lib/electronVersions.d.ts +34 -0
  80. package/dist/lib/electronVersions.d.ts.map +1 -0
  81. package/dist/lib/electronVersions.js +90 -0
  82. package/dist/lib/electronVersions.js.map +1 -0
  83. package/dist/lib/errors.d.ts +27 -0
  84. package/dist/lib/errors.d.ts.map +1 -1
  85. package/dist/lib/errors.js +27 -0
  86. package/dist/lib/errors.js.map +1 -1
  87. package/dist/lib/launchTarget.d.ts +30 -0
  88. package/dist/lib/launchTarget.d.ts.map +1 -0
  89. package/dist/lib/launchTarget.js +174 -0
  90. package/dist/lib/launchTarget.js.map +1 -0
  91. package/dist/lib/page.d.ts +1 -0
  92. package/dist/lib/page.d.ts.map +1 -1
  93. package/dist/lib/page.js +1 -1
  94. package/dist/lib/page.js.map +1 -1
  95. package/dist/lib/safari.d.ts +90 -0
  96. package/dist/lib/safari.d.ts.map +1 -0
  97. package/dist/lib/safari.js +136 -0
  98. package/dist/lib/safari.js.map +1 -0
  99. package/dist/lib/service.d.ts +21 -0
  100. package/dist/lib/service.d.ts.map +1 -1
  101. package/dist/lib/service.js +94 -6
  102. package/dist/lib/service.js.map +1 -1
  103. package/dist/lib/tracing.d.ts +1 -1
  104. package/dist/lib/tracing.d.ts.map +1 -1
  105. package/dist/lib/tracing.js.map +1 -1
  106. package/dist/lib/types.d.ts +41 -0
  107. package/dist/lib/types.d.ts.map +1 -1
  108. package/dist/lib/vibiumTrace.d.ts +1 -1
  109. package/dist/lib/vibiumTrace.d.ts.map +1 -1
  110. package/dist/lib/webelement.d.ts.map +1 -1
  111. package/dist/lib/webelement.js +118 -2
  112. package/dist/lib/webelement.js.map +1 -1
  113. package/docs/api-reference.md +39 -2
  114. package/docs/browser-api.md +7 -1
  115. package/docs/driver-configuration.md +54 -0
  116. package/docs/electron.md +419 -0
  117. package/docs/error-codes.md +5 -0
  118. package/docs/getting-started.md +30 -10
  119. package/docs/index.md +5 -1
  120. package/docs/public/examples/displayed.html +86 -0
  121. package/docs/recipes/electron-app-from-another-repo.md +136 -0
  122. package/docs/recipes/electron-deep-link.md +117 -0
  123. package/docs/recipes/electron-mock-apis.md +88 -0
  124. package/docs/recipes/electron-native-dialog.md +105 -0
  125. package/docs/recipes.md +28 -9
  126. package/docs/safari.md +118 -0
  127. package/docs/standards.md +13 -0
  128. package/docs/why-craftdriver.md +1 -1
  129. package/docs/zero-config-drivers.md +12 -0
  130. package/package.json +7 -1
  131. package/skills/craftdriver/SKILL.md +4 -1
  132. package/skills/craftdriver/cheatsheet.md +4 -1
@@ -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 | Use when | Recipe |
25
- | ------------------ | ---------------------------------------------------------------- | --------------------------------------------------------- |
26
- | Find elements | You're new and need to point CraftDriver at the element you want. | [Find Elements On The Page](./recipes/find-elements.md) |
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 | Use when | Recipe |
31
- | ------------------------- | -------------------------------------------------------------------------- | -------------------------------------------------------------------------- |
32
- | 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) |
33
- | 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) |
34
- | 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) |
35
- | Page objects | Selectors and steps are copy-pasted across tests and break together. | [Organize Flows With Page Objects](./recipes/page-objects.md) |
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:
@@ -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.