craftdriver 1.3.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.
Files changed (91) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/README.md +1 -0
  3. package/dist/cli/dispatcher.js +1 -1
  4. package/dist/cli/dispatcher.js.map +1 -1
  5. package/dist/index.d.ts +6 -1
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +4 -0
  8. package/dist/index.js.map +1 -1
  9. package/dist/lib/browser.d.ts +147 -21
  10. package/dist/lib/browser.d.ts.map +1 -1
  11. package/dist/lib/browser.js +380 -139
  12. package/dist/lib/browser.js.map +1 -1
  13. package/dist/lib/capabilities.d.ts +35 -0
  14. package/dist/lib/capabilities.d.ts.map +1 -0
  15. package/dist/lib/capabilities.js +58 -0
  16. package/dist/lib/capabilities.js.map +1 -0
  17. package/dist/lib/chrome.d.ts +7 -2
  18. package/dist/lib/chrome.d.ts.map +1 -1
  19. package/dist/lib/chrome.js +10 -1
  20. package/dist/lib/chrome.js.map +1 -1
  21. package/dist/lib/driverManager.d.ts +92 -0
  22. package/dist/lib/driverManager.d.ts.map +1 -1
  23. package/dist/lib/driverManager.js +229 -10
  24. package/dist/lib/driverManager.js.map +1 -1
  25. package/dist/lib/electron.d.ts +60 -0
  26. package/dist/lib/electron.d.ts.map +1 -0
  27. package/dist/lib/electron.js +85 -0
  28. package/dist/lib/electron.js.map +1 -0
  29. package/dist/lib/electronDeeplink.d.ts +22 -0
  30. package/dist/lib/electronDeeplink.d.ts.map +1 -0
  31. package/dist/lib/electronDeeplink.js +118 -0
  32. package/dist/lib/electronDeeplink.js.map +1 -0
  33. package/dist/lib/electronDiagnostics.d.ts +49 -0
  34. package/dist/lib/electronDiagnostics.d.ts.map +1 -0
  35. package/dist/lib/electronDiagnostics.js +195 -0
  36. package/dist/lib/electronDiagnostics.js.map +1 -0
  37. package/dist/lib/electronDialogMock.d.ts +51 -0
  38. package/dist/lib/electronDialogMock.d.ts.map +1 -0
  39. package/dist/lib/electronDialogMock.js +106 -0
  40. package/dist/lib/electronDialogMock.js.map +1 -0
  41. package/dist/lib/electronFuses.d.ts +13 -0
  42. package/dist/lib/electronFuses.d.ts.map +1 -0
  43. package/dist/lib/electronFuses.js +118 -0
  44. package/dist/lib/electronFuses.js.map +1 -0
  45. package/dist/lib/electronMainBridge.d.ts +28 -0
  46. package/dist/lib/electronMainBridge.d.ts.map +1 -0
  47. package/dist/lib/electronMainBridge.js +270 -0
  48. package/dist/lib/electronMainBridge.js.map +1 -0
  49. package/dist/lib/electronMainLogs.d.ts +81 -0
  50. package/dist/lib/electronMainLogs.d.ts.map +1 -0
  51. package/dist/lib/electronMainLogs.js +176 -0
  52. package/dist/lib/electronMainLogs.js.map +1 -0
  53. package/dist/lib/electronMock.d.ts +52 -0
  54. package/dist/lib/electronMock.d.ts.map +1 -0
  55. package/dist/lib/electronMock.js +96 -0
  56. package/dist/lib/electronMock.js.map +1 -0
  57. package/dist/lib/electronRemote.d.ts +106 -0
  58. package/dist/lib/electronRemote.d.ts.map +1 -0
  59. package/dist/lib/electronRemote.js +406 -0
  60. package/dist/lib/electronRemote.js.map +1 -0
  61. package/dist/lib/electronVersions.d.ts +34 -0
  62. package/dist/lib/electronVersions.d.ts.map +1 -0
  63. package/dist/lib/electronVersions.js +90 -0
  64. package/dist/lib/electronVersions.js.map +1 -0
  65. package/dist/lib/errors.d.ts +27 -0
  66. package/dist/lib/errors.d.ts.map +1 -1
  67. package/dist/lib/errors.js +27 -0
  68. package/dist/lib/errors.js.map +1 -1
  69. package/dist/lib/launchTarget.d.ts +30 -0
  70. package/dist/lib/launchTarget.d.ts.map +1 -0
  71. package/dist/lib/launchTarget.js +112 -0
  72. package/dist/lib/launchTarget.js.map +1 -0
  73. package/dist/lib/page.d.ts +1 -0
  74. package/dist/lib/page.d.ts.map +1 -1
  75. package/dist/lib/page.js +1 -1
  76. package/dist/lib/page.js.map +1 -1
  77. package/dist/lib/service.d.ts +21 -0
  78. package/dist/lib/service.d.ts.map +1 -1
  79. package/dist/lib/service.js +94 -6
  80. package/dist/lib/service.js.map +1 -1
  81. package/docs/api-reference.md +37 -2
  82. package/docs/browser-api.md +7 -1
  83. package/docs/electron.md +419 -0
  84. package/docs/error-codes.md +5 -0
  85. package/docs/index.md +4 -0
  86. package/docs/recipes/electron-app-from-another-repo.md +136 -0
  87. package/docs/recipes/electron-deep-link.md +117 -0
  88. package/docs/recipes/electron-mock-apis.md +88 -0
  89. package/docs/recipes/electron-native-dialog.md +105 -0
  90. package/docs/recipes.md +28 -9
  91. package/package.json +5 -1
@@ -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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "craftdriver",
3
- "version": "1.3.0",
3
+ "version": "1.4.0",
4
4
  "description": "Modern WebDriver automation library for NodeJS",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -18,6 +18,7 @@
18
18
  "test:chrome": "BROWSER_NAME=chrome vitest run",
19
19
  "test:chromium": "BROWSER_NAME=chromium vitest run",
20
20
  "test:recipes": "HEADLESS=true vitest run --config vitest.recipes.config.ts",
21
+ "test:electron": "vitest run --config vitest.electron.config.ts",
21
22
  "bench": "HEADLESS=true vitest run --config vitest.perf.config.ts",
22
23
  "clean": "rimraf dist",
23
24
  "serve": "http-server ./examples -a 127.0.0.1 -p 8080 -c-1 --cors",
@@ -39,6 +40,9 @@
39
40
  "firefox",
40
41
  "chrome",
41
42
  "chromium",
43
+ "electron",
44
+ "electron-testing",
45
+ "desktop-app-testing",
42
46
  "automation",
43
47
  "browser-automation",
44
48
  "e2e",