From bf56ed40b45a51686fa6c61ea787491c98649591 Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Thu, 21 May 2026 10:53:38 -0700 Subject: [PATCH] docs(electron): document mocking the native main-process dialog API Adds a recipe to class-electron.md showing how to stub `dialog.showOpenDialog` / `showSaveDialog` / `showMessageBox` (and their sync variants) via `electronApplication.evaluate()`, since Playwright does not intercept the native Electron Dialog API automatically. Refs https://github.com/microsoft/playwright/issues/8278 --- docs/src/electron-api/class-electron.md | 28 ++++++++++++++++++++ packages/playwright-client/types/types.d.ts | 29 +++++++++++++++++++++ packages/playwright-core/types/types.d.ts | 29 +++++++++++++++++++++ 3 files changed, 86 insertions(+) diff --git a/docs/src/electron-api/class-electron.md b/docs/src/electron-api/class-electron.md index 1df55a214a252..60e64080e0551 100644 --- a/docs/src/electron-api/class-electron.md +++ b/docs/src/electron-api/class-electron.md @@ -51,6 +51,34 @@ If you are not able to launch Electron and it will end up in timeouts during lau * Ensure that `nodeCliInspect` ([FuseV1Options.EnableNodeCliInspectArguments](https://www.electronjs.org/docs/latest/tutorial/fuses#nodecliinspect)) fuse is **not** set to `false`. +**Mocking native dialogs:** + +Playwright does not intercept the native Electron [dialog](https://www.electronjs.org/docs/latest/api/dialog) API +(`dialog.showOpenDialog`, `dialog.showSaveDialog`, `dialog.showMessageBox`, etc.) because those calls happen in the +Electron main process and go straight to OS APIs. Use [`method: ElectronApplication.evaluate`] to replace the +relevant methods in the main process so tests run deterministically without any OS-level UI: + +```js +// Stub the open dialog to always return a fixed path. +await electronApp.evaluate(({ dialog }, filePaths) => { + dialog.showOpenDialog = () => Promise.resolve({ canceled: false, filePaths }); +}, ['/path/to/file.txt']); + +// Stub the save dialog. +await electronApp.evaluate(({ dialog }, filePath) => { + dialog.showSaveDialog = () => Promise.resolve({ canceled: false, filePath }); +}, '/path/to/saved.txt'); + +// Stub showMessageBox to click the first button. +await electronApp.evaluate(({ dialog }) => { + dialog.showMessageBox = () => Promise.resolve({ response: 0, checkboxChecked: false }); +}); +``` + +The replacement persists until the application is closed. Synchronous variants +(`showOpenDialogSync`, `showSaveDialogSync`, `showMessageBoxSync`) can be stubbed the same way — just return the +value directly instead of a `Promise`. + ## async method: Electron.launch * since: v1.9 - returns: <[ElectronApplication]> diff --git a/packages/playwright-client/types/types.d.ts b/packages/playwright-client/types/types.d.ts index 05fe4dc968db1..f0df995ecb5c7 100644 --- a/packages/playwright-client/types/types.d.ts +++ b/packages/playwright-client/types/types.d.ts @@ -22195,6 +22195,35 @@ export interface WebStorage { * - Ensure that `nodeCliInspect` * ([FuseV1Options.EnableNodeCliInspectArguments](https://www.electronjs.org/docs/latest/tutorial/fuses#nodecliinspect)) * fuse is **not** set to `false`. + * + * **Mocking native dialogs:** + * + * Playwright does not intercept the native Electron [dialog](https://www.electronjs.org/docs/latest/api/dialog) API + * (`dialog.showOpenDialog`, `dialog.showSaveDialog`, `dialog.showMessageBox`, etc.) because those calls happen in the + * Electron main process and go straight to OS APIs. Use + * [electronApplication.evaluate(pageFunction[, arg])](https://playwright.dev/docs/api/class-electronapplication#electron-application-evaluate) + * to replace the relevant methods in the main process so tests run deterministically without any OS-level UI: + * + * ```js + * // Stub the open dialog to always return a fixed path. + * await electronApp.evaluate(({ dialog }, filePaths) => { + * dialog.showOpenDialog = () => Promise.resolve({ canceled: false, filePaths }); + * }, ['/path/to/file.txt']); + * + * // Stub the save dialog. + * await electronApp.evaluate(({ dialog }, filePath) => { + * dialog.showSaveDialog = () => Promise.resolve({ canceled: false, filePath }); + * }, '/path/to/saved.txt'); + * + * // Stub showMessageBox to click the first button. + * await electronApp.evaluate(({ dialog }) => { + * dialog.showMessageBox = () => Promise.resolve({ response: 0, checkboxChecked: false }); + * }); + * ``` + * + * The replacement persists until the application is closed. Synchronous variants (`showOpenDialogSync`, + * `showSaveDialogSync`, `showMessageBoxSync`) can be stubbed the same way — just return the value directly instead of + * a `Promise`. */ export interface Electron { /** diff --git a/packages/playwright-core/types/types.d.ts b/packages/playwright-core/types/types.d.ts index 05fe4dc968db1..f0df995ecb5c7 100644 --- a/packages/playwright-core/types/types.d.ts +++ b/packages/playwright-core/types/types.d.ts @@ -22195,6 +22195,35 @@ export interface WebStorage { * - Ensure that `nodeCliInspect` * ([FuseV1Options.EnableNodeCliInspectArguments](https://www.electronjs.org/docs/latest/tutorial/fuses#nodecliinspect)) * fuse is **not** set to `false`. + * + * **Mocking native dialogs:** + * + * Playwright does not intercept the native Electron [dialog](https://www.electronjs.org/docs/latest/api/dialog) API + * (`dialog.showOpenDialog`, `dialog.showSaveDialog`, `dialog.showMessageBox`, etc.) because those calls happen in the + * Electron main process and go straight to OS APIs. Use + * [electronApplication.evaluate(pageFunction[, arg])](https://playwright.dev/docs/api/class-electronapplication#electron-application-evaluate) + * to replace the relevant methods in the main process so tests run deterministically without any OS-level UI: + * + * ```js + * // Stub the open dialog to always return a fixed path. + * await electronApp.evaluate(({ dialog }, filePaths) => { + * dialog.showOpenDialog = () => Promise.resolve({ canceled: false, filePaths }); + * }, ['/path/to/file.txt']); + * + * // Stub the save dialog. + * await electronApp.evaluate(({ dialog }, filePath) => { + * dialog.showSaveDialog = () => Promise.resolve({ canceled: false, filePath }); + * }, '/path/to/saved.txt'); + * + * // Stub showMessageBox to click the first button. + * await electronApp.evaluate(({ dialog }) => { + * dialog.showMessageBox = () => Promise.resolve({ response: 0, checkboxChecked: false }); + * }); + * ``` + * + * The replacement persists until the application is closed. Synchronous variants (`showOpenDialogSync`, + * `showSaveDialogSync`, `showMessageBoxSync`) can be stubbed the same way — just return the value directly instead of + * a `Promise`. */ export interface Electron { /**