electron-playwright-helpers
v2.1.0
Published
Helper functions for Electron end-to-end testing using Playwright
Maintainers
Readme
Electron Playwright Helpers
Helper functions to make it easier to use Playwright for end-to-end testing with
Electron. Parse packaged Electron projects so you can run tests on them. Click Electron menu items, send IPC messages, get menu structures, stub dialog.showOpenDialog() results, etc.
Installation
npm i -D electron-playwright-helpersUsage
For a full example of how to use this library, see the electron-playwright-example project. But here's a quick example:
Javascript:
const eph = require('electron-playwright-helpers')
// - or cherry pick -
const { findLatestBuild, parseElectronApp, clickMenuItemById } = require('electron-playwright-helpers')
let electronApp: ElectronApplication
test.beforeAll(async () => {
// find the latest build in the out directory
const latestBuild = findLatestBuild()
// parse the packaged Electron app and find paths and other info
const appInfo = parseElectronApp(latestBuild)
electronApp = await electron.launch({
args: [appInfo.main], // main file from package.json
executablePath: appInfo.executable // path to the Electron executable
})
})
test.afterAll(async () => {
await electronApp.close()
})
test('open a file', async () => {
// stub electron dialog so dialog.showOpenDialog()
// will return a file path without opening a dialog
await eph.stubDialog(electronApp, 'showOpenDialog', { filePaths: ['/path/to/file'] })
// call the click method of menu item in the Electron app's application menu
await eph.clickMenuItemById(electronApp, 'open-file')
// get the result of an ipcMain.handle() function
const result = await eph.ipcMainInvokeHandler(electronApp, 'get-open-file-path')
// result should be the file path
expect(result).toBe('/path/to/file')
})Typescript:
import * as eph from 'electron-playwright-helpers'
// - or cherry pick -
import { electronWaitForFunction, ipcMainCallFirstListener, clickMenuItemById } from 'electron-playwright-helpers'
// then same as Javascript aboveContributing
Yes, please! Pull requests are always welcome. Feel free to add or suggest new features, fix bugs, etc.
Please use Conventional Commit messages for your commits. This project uses semantic-release to automatically publish new versions to NPM. The commit messages are used to determine the version number and changelog. We're also using Prettier as our code format and ESlint to enforce formatting, so please make sure your code is formatted before submitting a PR.
Migrating from v1.x to v2.0
Version 2.0 introduces significant improvements to handle flakiness issues that appeared with Electron 27+ and Playwright. Starting with Electron 27, Playwright's evaluate() calls became unreliable, often throwing errors like "context or browser has been closed", "Promise was collected", or "Execution context was destroyed" seemingly at random.
What's New in v2.0
Built-in Retry Logic
All helper functions now automatically retry operations that fail due to Playwright context issues. This happens transparently - your existing code will work without changes, but will be more reliable.
New Utility Functions
retry(fn, options)- Wrap any Playwright call to automatically retry on context errorsretryUntilTruthy(fn, options)- Like Playwright'spage.waitForFunction()but with automatic retry on errorssetRetryOptions(options)- Configure default retry behavior globallygetRetryOptions()- Get current retry configurationresetRetryOptions()- Reset retry options to defaults
Conditional Dialog Stubbing (New!)
stubDialogMatchers(app, stubs, options)- Stub dialogs with conditional matching based on dialog optionsclearDialogMatchers(app)- Clear dialog matcher stubs
Breaking Changes
1. Node.js 18+ Required
Version 2.0 requires Node.js 18 or later due to modern JavaScript features like structuredClone().
2. IPC Helper Function Signatures
IPC helpers now accept an optional RetryOptions object as the last argument:
// v1.x
await ipcRendererSend(page, 'my-channel', arg1, arg2)
// v2.0 - still works exactly the same
await ipcRendererSend(page, 'my-channel', arg1, arg2)
// v2.0 - with retry options
await ipcRendererSend(page, 'my-channel', arg1, arg2, { timeout: 10000 })This applies to: ipcRendererSend, ipcRendererInvoke, ipcRendererEmit, ipcRendererCallFirstListener, ipcMainEmit, ipcMainCallFirstListener, ipcMainInvokeHandler
3. Menu Helper Function Signatures
Menu helpers now accept an optional RetryOptions object:
// v1.x
await clickMenuItemById(electronApp, 'my-menu-item')
// v2.0 - still works exactly the same
await clickMenuItemById(electronApp, 'my-menu-item')
// v2.0 - with retry options
await clickMenuItemById(electronApp, 'my-menu-item', { timeout: 10000 })Migration Steps
For most projects, upgrading is straightforward:
- Update Node.js to version 18 or later
- Update the package:
npm install electron-playwright-helpers@latest - Test your suite - existing code should work without changes
Customizing Retry Behavior
If you need to adjust retry behavior globally:
import { setRetryOptions, resetRetryOptions } from 'electron-playwright-helpers'
// Increase timeout for slow CI environments
setRetryOptions({
timeout: 10000, // 10 seconds (default: 5000)
poll: 500, // poll every 500ms (default: 200)
})
// Reset to defaults
resetRetryOptions()Or disable retries for specific calls:
await ipcRendererSend(page, 'channel', arg, { disable: true })Using the New Retry Functions
If you have custom Playwright evaluate() calls that aren't using our helpers, wrap them with retry():
import { retry, retryUntilTruthy } from 'electron-playwright-helpers'
// Wrap evaluate calls to handle context errors
const result = await retry(() =>
electronApp.evaluate(({ app }) => app.getName())
)
// Wait for a condition with automatic error recovery
await retryUntilTruthy(() =>
page.evaluate(() => document.body.classList.contains('ready'))
)Additional Resources
- Electron Playwright Example - an example of how to use this library
- Playwright Electron Class - Playwright API docs for Electron
- Electron API - Electron API documentation
API
Constants
Functions
Typedefs
dialogMatcherDefaults
Kind: global constant
toSerializableMatcher()
Kind: global function
matchesPattern()
Kind: global function
findLatestBuild(buildDirectory) ⇒ string
Kind: global function
Returns: string -
| Param | Type | Default | Description | | --- | --- | --- | --- | | buildDirectory | string | "out" | optional - the directory to search for the latest build (path/name relative to package root or full path starting with /). Defaults to out. |
parseElectronApp(buildDir) ⇒ ElectronAppInfo
Kind: global function
Returns: ElectronAppInfo - metadata about the app
| Param | Type | Description | | --- | --- | --- | | buildDir | string | absolute path to the build directory or the app itself |
electronWaitForFunction(electronApp, fn, arg) ⇒ Promise.<void>
Kind: global function
Fulfil: void Resolves when the function returns true
| Param | Type | Description | | --- | --- | --- | | electronApp | ElectronApplication | the Playwright ElectronApplication | | fn | function | the function to evaluate in the main process - must return a boolean | | arg | Any | optional - an argument to pass to the function |
evaluateWithRetry(electronApp, fn, arg, retries, retryIntervalMs) ⇒ Promise.<R>
Kind: global function
Returns: Promise.<R> -
| Param | Type | Description | | --- | --- | --- | | electronApp | ElectronApplication | the Playwright ElectronApplication | | fn | function | the function to evaluate in the main process | | arg | Any | an argument to pass to the function | | retries | | the number of times to retry the evaluation | | retryIntervalMs | | the interval between retries |
isSerializedNativeImageSuccess()
Kind: global function
isSerializedNativeImageError()
Kind: global function
retryUntilTruthy(fn, [timeoutMs], [intervalMs]) ⇒ Promise.<T>
Kind: global function
Returns: Promise.<T> -
- Error
| Param | Type | Default | Description | | --- | --- | --- | --- | | fn | function | | The function to retry. It can return a promise or a value. It should NOT return void/undefined. | | [timeoutMs] | number | 5000 | The maximum time in milliseconds to keep retrying the function. Defaults to 5000ms. | | [intervalMs] | number | 100 | The delay between each retry attempt in milliseconds. Defaults to 100ms. | | [options.retryTimeout] | number | 5000 | The maximum time in milliseconds to wait for an individual try to return a result. Defaults to 5000ms. | | [options.retryPoll] | number | 200 | The delay between each retry attempt in milliseconds. Defaults to 200ms. | | [options.retryErrorMatch] | string | Array.<string> | RegExp | | The error message or pattern to match against. Errors that don't match will throw immediately. |
Example
test('my test', async () => {
// this will fail immediately if Playwright's context gets weird:
const oldWay = await page.waitForFunction(() => document.body.classList.contains('ready'))
// this will not fail if Playwright's context gets weird:
const newWay = await retryUntilTruthy(() =>
page.evaluate(() => document.body.classList.contains('ready'))
)
})matchesPattern()
Kind: global function
ElectronAppInfo
Kind: global typedef
Properties
| Name | Type | Description | | --- | --- | --- | | executable | string | path to the Electron executable | | main | string | path to the main (JS) file | | name | string | name of the your application | | resourcesDir | string | path to the resources directory | | asar | boolean | whether the app is packaged as an asar archive | | platform | string | 'darwin', 'linux', or 'win32' | | arch | string | 'x64', 'x32', or 'arm64' | | packageJson | PackageJson | the JSON.parse()'d contents of the package.json file. |
stubDialog(app, method, value) ⇒ Promise.<void>
Kind: global function
Returns: Promise.<void> - A promise that resolves when the mock is applied.
Category: Dialog
Fullfil: void - A promise that resolves when the mock is applied.
See: stubMultipleDialogs
| Param | Type | Description | | --- | --- | --- | | app | ElectronApplication | The Playwright ElectronApplication instance. | | method | String | The dialog method to mock. | | value | ReturnType.<Electron.Dialog> | The value that your application will receive when calling this dialog method. See the Electron docs for the return value of each method. |
Example
await stubDialog(app, 'showOpenDialog', {
filePaths: ['/path/to/file'],
canceled: false,
})
await clickMenuItemById(app, 'open-file')
// when time your application calls dialog.showOpenDialog,
// it will return the value you specifiedstubMultipleDialogs(app, mocks) ⇒ Promise.<void>
Kind: global function
Returns: Promise.<void> - A promise that resolves when the mocks are applied.
Category: Dialog
Fullfil: void - A promise that resolves when the mocks are applied.
| Param | Type | Description | | --- | --- | --- | | app | ElectronApplication | The Playwright ElectronApplication instance. | | mocks | Array.<DialogMethodStubPartial> | An array of dialog method mocks to apply. |
Example
await stubMultipleDialogs(app, [
{
method: 'showOpenDialog',
value: {
filePaths: ['/path/to/file1', '/path/to/file2'],
canceled: false,
},
},
{
method: 'showSaveDialog',
value: {
filePath: '/path/to/file',
canceled: false,
},
},
])
await clickMenuItemById(app, 'save-file')
// when your application calls dialog.showSaveDialog,
// it will return the value you specifiedstubAllDialogs(app) ⇒ Promise.<void>
Kind: global function
Returns: Promise.<void> - A promise that resolves when the mocks are applied.
Category: Dialog
Fullfil: void - A promise that resolves when the mocks are applied.
See: stubDialog
| Param | Type | Description | | --- | --- | --- | | app | ElectronApplication | The Playwright ElectronApplication instance. |
stubDialogMatchers(app, stubs, options) ⇒
Kind: global function
Returns: A promise that resolves when the stubs are applied.
Category: Dialog
| Param | Description | | --- | --- | | app | The Playwright ElectronApplication instance. | | stubs | Array of dialog matcher stubs to apply. | | options | Optional configuration. |
Example
// Set up multiple dialog stubs at the start of your test
await stubDialogMatchers(app, [
{
method: 'showMessageBox',
matcher: { title: /delete/i, buttons: /yes/i },
value: { response: 1 }, // Click "Yes" for delete dialogs
},
{
method: 'showMessageBox',
matcher: { title: /save/i },
value: { response: 0 }, // Click "Save" for save dialogs
},
{
method: 'showOpenDialog',
matcher: { title: 'Select Image' },
value: { filePaths: ['/path/to/image.png'], canceled: false },
},
{
method: 'showOpenDialog',
matcher: {}, // Match all other open dialogs
value: { canceled: true },
},
])clearDialogMatchers(app) ⇒
Kind: global function
Returns: A promise that resolves when the stubs are cleared.
Category: Dialog
| Param | Description | | --- | --- | | app | The Playwright ElectronApplication instance. |
ipcMainEmit(electronApp, message, ...args, retryOptions) ⇒ Promise.<boolean>
Kind: global function
Category: IPCMain
Fulfil: boolean true if there were listeners for this message
Reject: Error if there are no ipcMain listeners for the event
| Param | Type | Description | | --- | --- | --- | | electronApp | ElectronApplication | the ElectronApplication object from Playwright | | message | string | the channel to call all ipcMain listeners for | | ...args | unknown | one or more arguments to send | | retryOptions | RetryOptions | optional - options for retrying upon error |
ipcMainCallFirstListener(electronApp, message, ...args, retryOptions) ⇒ Promise.<unknown>
Kind: global function
Category: IPCMain
Fulfil: unknown resolves with the result of the function
Reject: Error if there are no ipcMain listeners for the event
| Param | Type | Description | | --- | --- | --- | | electronApp | ElectronApplication | the ElectronApplication object from Playwright | | message | string | the channel to call the first listener for | | ...args | unknown | one or more arguments to send | | retryOptions | RetryOptions | optional - options for retrying upon error |
ipcMainInvokeHandler(electronApp, message, ...args, retryOptions) ⇒ Promise.<unknown>
Kind: global function
Category: IPCMain
Fulfil: unknown resolves with the result of the function called in main process
| Param | Type | Description | | --- | --- | --- | | electronApp | ElectronApplication | the ElectronApplication object from Playwright | | message | string | the channel to call the first listener for | | ...args | unknown | one or more arguments to send | | retryOptions | RetryOptions | optional - options for retrying upon error |
ipcRendererSend(page, channel, ...args, retryOptions) ⇒ Promise.<unknown>
Kind: global function
Category: IPCRenderer
Fulfil: unknown resolves with the result of ipcRenderer.send()
| Param | Type | Description | | --- | --- | --- | | page | Page | the Playwright Page to send the ipcRenderer.send() from | | channel | string | the channel to send the ipcRenderer.send() to | | ...args | unknown | one or more arguments to send to the ipcRenderer.send() | | retryOptions | RetryOptions | optional last argument - options for retrying upon error |
ipcRendererInvoke(page, message, ...args, retryOptions) ⇒ Promise.<unknown>
Kind: global function
Category: IPCRenderer
Fulfil: unknown resolves with the result of ipcRenderer.invoke()
| Param | Type | Description | | --- | --- | --- | | page | Page | the Playwright Page to send the ipcRenderer.invoke() from | | message | string | the channel to send the ipcRenderer.invoke() to | | ...args | unknown | one or more arguments to send to the ipcRenderer.invoke() | | retryOptions | RetryOptions | optional last argument - options for retrying upon error |
ipcRendererCallFirstListener(page, message, ...args, retryOptions) ⇒ Promise.<unknown>
Kind: global function
Category: IPCRenderer
Fulfil: unknown the result of the first ipcRenderer.on() listener
| Param | Type | Description | | --- | --- | --- | | page | Page | The Playwright Page to with the ipcRenderer.on() listener | | message | string | The channel to call the first listener for | | ...args | unknown | optional - One or more arguments to send to the ipcRenderer.on() listener | | retryOptions | RetryOptions | optional - options for retrying upon error |
ipcRendererEmit(page, message, ...args, retryOptions) ⇒ Promise.<boolean>
Kind: global function
Category: IPCRenderer
Fulfil: boolean true if the event was emitted
Reject: Error if there are no ipcRenderer listeners for the event
| Param | Type | Description | | --- | --- | --- | | page | Page | the Playwright Page to with the ipcRenderer.on() listener | | message | string | the channel to call all ipcRenderer listeners for | | ...args | unknown | optional - one or more arguments to send | | retryOptions | RetryOptions | optional - options for retrying upon error |
clickMenuItemById(electronApp, id) ⇒ Promise.<void>
Kind: global function
Category: Menu
Fulfil: void resolves with the result of the click() method - probably undefined
| Param | Type | Description | | --- | --- | --- | | electronApp | ElectronApplication | the Electron application object (from Playwright) | | id | string | the id of the MenuItem to click |
clickMenuItem(electronApp, property, value) ⇒ Promise.<void>
Kind: global function
Category: Menu
Fulfil: void resolves with the result of the click() method - probably undefined
| Param | Type | Description | | --- | --- | --- | | electronApp | ElectronApplication | the Electron application object (from Playwright) | | property | String | a property of the MenuItem to search for | | value | String | Number | Boolean | the value of the property to search for |
getMenuItemAttribute(electronApp, menuId, attribute) ⇒ Promise.<string>
Kind: global function
Category: Menu
Fulfil: string resolves with the attribute value
| Param | Type | Description | | --- | --- | --- | | electronApp | ElectronApplication | the Electron application object (from Playwright) | | menuId | string | the id of the MenuItem to retrieve the attribute from | | attribute | string | the attribute to retrieve |
getMenuItemById(electronApp, menuId) ⇒ Promise.<MenuItemPartial>
Kind: global function
Category: Menu
Fulfil: MenuItemPartial the MenuItem with the given id
| Param | Type | Description | | --- | --- | --- | | electronApp | ElectronApplication | the Electron application object (from Playwright) | | menuId | string | the id of the MenuItem to retrieve |
getApplicationMenu(electronApp) ⇒ Promise.<Array.<MenuItemPartial>>
Kind: global function
Category: Menu
Fulfil: MenuItemPartial[] an array of MenuItem-like objects
| Param | Type | Description | | --- | --- | --- | | electronApp | ElectronApplication | the Electron application object (from Playwright) |
findMenuItem(electronApp, property, value, menuItems) ⇒ Promise.<MenuItemPartial>
Kind: global function
Category: Menu
Fulfil: MenuItemPartial the first MenuItem with the given property and value
| Param | Type | Description | | --- | --- | --- | | electronApp | ElectronApplication | the Electron application object (from Playwright) | | property | string | the property to search for | | value | string | the value to search for | | menuItems | MenuItemPartial | Array.<MenuItemPartial> | optional - single MenuItem or array - if not provided, will be retrieved from the application menu |
waitForMenuItem(electronApp, id) ⇒ Promise.<void>
Kind: global function
Category: Menu
Fulfil: void resolves when the MenuItem is found
| Param | Type | Description | | --- | --- | --- | | electronApp | ElectronApplication | the Electron application object (from Playwright) | | id | string | the id of the MenuItem to wait for |
waitForMenuItemStatus(electronApp, id, property, value) ⇒ Promise.<void>
Kind: global function
Category: Menu
Fulfil: void resolves when the MenuItem with correct status is found
| Param | Type | Description | | --- | --- | --- | | electronApp | ElectronApplication | the Electron application object (from Playwright) | | id | string | the id of the MenuItem to wait for | | property | string | the property to search for | | value | string | number | boolean | the value to search for |
addTimeoutToPromise(promise, timeoutMs, timeoutMessage) ⇒ Promise.<T>
Kind: global function
Returns: Promise.<T> - the result of the original promise if it resolves before the timeout
Category: Utilities
See: addTimeout
| Param | Default | Description | | --- | --- | --- | | promise | | the promise to add a timeout to - must be a Promise | | timeoutMs | 5000 | the timeout in milliseconds - defaults to 5000 | | timeoutMessage | | optional - the message to return if the timeout is reached |
addTimeout(functionName, timeoutMs, timeoutMessage, ...args) ⇒ Promise.<T>
Kind: global function
Returns: Promise.<T> - the result of the helper function if it resolves before the timeout
Category: Utilities
| Param | Default | Description | | --- | --- | --- | | functionName | | the name of the helper function to call | | timeoutMs | 5000 | the timeout in milliseconds - defaults to 5000 | | timeoutMessage | | optional - the message to return if the timeout is reached | | ...args | | any arguments to pass to the helper function |
retry(fn, [options]) ⇒ Promise.<T>
Kind: global function
Returns: Promise.<T> - A promise that resolves with the result of the function or rejects with an error or timeout message.
Category: Utilities
| Param | Type | Default | Description | | --- | --- | --- | --- | | fn | function | | The function to retry. | | [options] | RetryOptions | {} | The options for retrying the function. | | [options.timeout] | number | 5000 | The maximum time to wait before giving up in milliseconds. | | [options.poll] | number | 200 | The delay between each retry attempt in milliseconds. | | [options.errorMatch] | string | Array.<string> | RegExp | "['context or browser has been closed', 'Promise was collected', 'Execution context was destroyed']" | String(s) or regex to match against error message. If the error does not match, it will throw immediately. If it does match, it will retry. |
Example
You can simply wrap your Playwright calls in this function to make them more reliable:
test('my test', async () => {
// instead of this:
const oldWayRenderer = await page.evaluate(() => document.body.classList.contains('active'))
const oldWayMain = await electronApp.evaluate(({}) => document.body.classList.contains('active'))
// use this:
const newWay = await retry(() =>
page.evaluate(() => document.body.classList.contains('active'))
)
// note the `() =>` in front of the original function call
// and the `await` keyword in front of `retry`,
// but NOT in front of `page.evaluate`
})setRetryOptions(options) ⇒
Kind: global function
Returns: The updated retry options.
Category: Utilities
| Param | Description | | --- | --- | | options | A partial object containing the retry options to be set. |
getRetryOptions() ⇒
Kind: global function
Returns: The current retry options.
Category: Utilities
resetRetryOptions()
Kind: global function
Category: Utilities
errToString(err) ⇒
Kind: global function
Returns: A string representation of the error.
Category: Utilities
| Param | Description | | --- | --- | | err | The unknown error to be converted to a string. |
getWindowByUrl(electronApp, pattern, options) ⇒
Kind: global function
Returns: An array of matching Pages
Category: Window Helpers
| Param | Description | | --- | --- | | electronApp | The Playwright ElectronApplication | | pattern | A string (substring match) or RegExp to match against the URL | | options | Options with all: true to return all matches |
Example
const allSettingsWindows = await getWindowByUrl(app, '/settings', { all: true })getWindowByTitle(electronApp, pattern, options) ⇒
Kind: global function
Returns: An array of matching Pages
Category: Window Helpers
| Param | Description | | --- | --- | | electronApp | The Playwright ElectronApplication | | pattern | A string (substring match) or RegExp to match against the title | | options | Options with all: true to return all matches |
Example
const allNumberedWindows = await getWindowByTitle(app, /Window \d+/, { all: true })getWindowByMatcher(electronApp, matcher, options) ⇒
Kind: global function
Returns: An array of matching Pages
Category: Window Helpers
| Param | Description | | --- | --- | | electronApp | The Playwright ElectronApplication | | matcher | A function that receives a Page and returns true if it matches | | options | Options with all: true to return all matches |
Example
const allLargeWindows = await getWindowByMatcher(app, async (page) => {
const size = await page.viewportSize()
return size && size.width > 1000
}, { all: true })waitForWindowByUrl(electronApp, pattern, options) ⇒
Kind: global function
Returns: The matching Page
Category: Window Helpers
Throws:
| Param | Description | | --- | --- | | electronApp | The Playwright ElectronApplication | | pattern | A string (substring match) or RegExp to match against the URL | | options | Optional timeout and interval settings |
Example
// Click something that opens a new window, then wait for it
await page.click('#open-settings')
const settingsWindow = await waitForWindowByUrl(app, '/settings', { timeout: 5000 })waitForWindowByTitle(electronApp, pattern, options) ⇒
Kind: global function
Returns: The matching Page
Category: Window Helpers
Throws:
| Param | Description | | --- | --- | | electronApp | The Playwright ElectronApplication | | pattern | A string (substring match) or RegExp to match against the title | | options | Optional timeout and interval settings |
Example
// Wait for a window with a specific title to appear
const prefsWindow = await waitForWindowByTitle(app, 'Preferences', { timeout: 5000 })waitForWindowByMatcher(electronApp, matcher, options) ⇒
Kind: global function
Returns: The matching Page
Category: Window Helpers
Throws:
| Param | Description | | --- | --- | | electronApp | The Playwright ElectronApplication | | matcher | A function that receives a Page and returns true if it matches | | options | Optional timeout and interval settings |
Example
const window = await waitForWindowByMatcher(app, async (page) => {
const title = await page.title()
return title.startsWith('Document:')
}, { timeout: 10000 })