diff --git a/README.md b/README.md
index 433bebe..46c6418 100644
--- a/README.md
+++ b/README.md
@@ -150,34 +150,34 @@ to test your application's behavior when the user selects a file, or cancels the
for all dialog methods. This is useful if you want to ensure that dialogs are not displayed
during your tests. However, you may want to use stubDialog
or stubMultipleDialogs
to
control the return value of specific dialog methods (e.g. showOpenDialog
) during your tests.
-Emit an IPC message to a given window.
This will trigger all ipcRenderer listeners for the message.
This does not transfer data between main and renderer processes.
@@ -494,7 +494,7 @@ control the return value of specific dialog methods (e.g. showOpenDialog
-## ipcMainEmit(electronApp, message, ...args) ⇒ Promise.<boolean>
+## ipcMainEmit(electronApp, message, ...args, retryOptions) ⇒ Promise.<boolean>
Emit an ipcMain message from the main process.
This will trigger all ipcMain listeners for the message.
This does not transfer data between main and renderer processes.
@@ -510,10 +510,11 @@ It simply emits an event in the main process.
| 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) ⇒ Promise.<unknown>
+## ipcMainCallFirstListener(electronApp, message, ...args, retryOptions) ⇒ Promise.<unknown>
Call the first listener for a given ipcMain message in the main process
and return its result.
NOTE: ipcMain listeners usually don't return a value, but we're using
@@ -530,10 +531,11 @@ this to retrieve test data from the main process.
| 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) ⇒ Promise.<unknown>
+## ipcMainInvokeHandler(electronApp, message, ...args, retryOptions) ⇒ Promise.<unknown>
Get the return value of an ipcMain.handle()
function
**Kind**: global function
@@ -545,10 +547,11 @@ this to retrieve test data from the main process.
| 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) ⇒ Promise.<unknown>
+## ipcRendererSend(page, channel, ...args, retryOptions) ⇒ Promise.<unknown>
Send an ipcRenderer.send()
(to main process) from a given window.
Note: nodeIntegration must be true and contextIsolation must be false
in the webPreferences for this BrowserWindow.
@@ -562,10 +565,11 @@ in the webPreferences for this BrowserWindow.
| 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) ⇒ Promise.<unknown>
+## ipcRendererInvoke(page, message, ...args, retryOptions) ⇒ Promise.<unknown>
Send an ipcRenderer.invoke() from a given window.
Note: nodeIntegration must be true and contextIsolation must be false
in the webPreferences for this window
@@ -579,10 +583,11 @@ in the webPreferences for this window
| 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) ⇒ Promise.<unknown>
+## ipcRendererCallFirstListener(page, message, ...args, retryOptions) ⇒ Promise.<unknown>
Call just the first listener for a given ipcRenderer channel in a given window.
UNLIKE MOST Electron ipcRenderer listeners, this function SHOULD return a value.
This function does not send data between main and renderer processes.
@@ -598,10 +603,11 @@ It simply retrieves data from the renderer process.
| 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) ⇒ Promise.<boolean>
+## ipcRendererEmit(page, message, ...args, retryOptions) ⇒ Promise.<boolean>
Emit an IPC message to a given window.
This will trigger all ipcRenderer listeners for the message.
This does not transfer data between main and renderer processes.
@@ -618,6 +624,7 @@ It simply emits an event in the renderer process.
| 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
|
diff --git a/src/ipc_helpers.ts b/src/ipc_helpers.ts
index 7983edf..c28dbc7 100644
--- a/src/ipc_helpers.ts
+++ b/src/ipc_helpers.ts
@@ -1,5 +1,5 @@
import { ElectronApplication, Page } from 'playwright-core'
-import { retry } from './utilities'
+import { isRetryOptions, retry, RetryOptions } from './utilities'
/**
* Send an `ipcRenderer.send()` (to main process) from a given window.
@@ -12,28 +12,34 @@ import { retry } from './utilities'
* @param page {Page} the Playwright Page to send the ipcRenderer.send() from
* @param channel {string} the channel to send the ipcRenderer.send() to
* @param args {...unknown} one or more arguments to send to the `ipcRenderer.send()`
+ * @param retryOptions {RetryOptions} optional last argument - options for retrying upon error
* @returns {Promise}
* @fulfil {unknown} resolves with the result of `ipcRenderer.send()`
*/
export function ipcRendererSend(
page: Page,
channel: string,
- ...args: unknown[]
+ ...args: (unknown | RetryOptions)[]
): Promise {
- return retry(() =>
- page.evaluate(
- ({ channel, args }) => {
- if (!require) {
- throw new Error(
- `Cannot access require() in renderer process. Is nodeIntegration: true?`
- )
- }
- // eslint-disable-next-line @typescript-eslint/no-var-requires
- const { ipcRenderer } = require('electron')
- return ipcRenderer.send(channel, ...args)
- },
- { channel, args }
- )
+ const retryOptions = isRetryOptions(args[args.length - 1])
+ ? (args.pop() as RetryOptions)
+ : undefined
+ return retry(
+ () =>
+ page.evaluate(
+ ({ channel, args }) => {
+ if (!require) {
+ throw new Error(
+ `Cannot access require() in renderer process. Is nodeIntegration: true?`
+ )
+ }
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
+ const { ipcRenderer } = require('electron')
+ return ipcRenderer.send(channel, ...args)
+ },
+ { channel, args }
+ ),
+ retryOptions
)
}
@@ -48,28 +54,34 @@ export function ipcRendererSend(
* @param page {Page} the Playwright Page to send the ipcRenderer.invoke() from
* @param message {string} the channel to send the ipcRenderer.invoke() to
* @param args {...unknown} one or more arguments to send to the ipcRenderer.invoke()
+ * @param retryOptions {RetryOptions} optional last argument - options for retrying upon error
* @returns {Promise}
* @fulfil {unknown} resolves with the result of ipcRenderer.invoke()
*/
export function ipcRendererInvoke(
page: Page,
message: string,
- ...args: unknown[]
+ ...args: (unknown | RetryOptions)[]
): Promise {
- return retry(() =>
- page.evaluate(
- async ({ message, args }) => {
- if (!require) {
- throw new Error(
- `Cannot access require() in renderer process. Is nodeIntegration: true?`
- )
- }
- // eslint-disable-next-line @typescript-eslint/no-var-requires
- const { ipcRenderer } = require('electron')
- return await ipcRenderer.invoke(message, ...args)
- },
- { message, args }
- )
+ const retryOptions = isRetryOptions(args[args.length - 1])
+ ? (args.pop() as RetryOptions)
+ : undefined
+ return retry(
+ () =>
+ page.evaluate(
+ async ({ message, args }) => {
+ if (!require) {
+ throw new Error(
+ `Cannot access require() in renderer process. Is nodeIntegration: true?`
+ )
+ }
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
+ const { ipcRenderer } = require('electron')
+ return await ipcRenderer.invoke(message, ...args)
+ },
+ { message, args }
+ ),
+ retryOptions
)
}
@@ -87,35 +99,41 @@ export function ipcRendererInvoke(
* @param page {Page} The Playwright Page to with the `ipcRenderer.on()` listener
* @param message {string} The channel to call the first listener for
* @param args {...unknown} optional - One or more arguments to send to the ipcRenderer.on() listener
+ * @param retryOptions {RetryOptions} optional - options for retrying upon error
* @returns {Promise}
* @fulfil {unknown} the result of the first `ipcRenderer.on()` listener
*/
export function ipcRendererCallFirstListener(
page: Page,
message: string,
- ...args: unknown[]
+ ...args: (unknown | RetryOptions)[]
): Promise {
- return retry(() =>
- page.evaluate(
- async ({ message, args }) => {
- if (!require) {
- throw new Error(
- `Cannot access require() in renderer process. Is nodeIntegration: true?`
- )
- }
- // eslint-disable-next-line @typescript-eslint/no-var-requires
- const { ipcRenderer } = require('electron')
- if (ipcRenderer.listenerCount(message) > 0) {
- // we send a fake event in place of the ipc event object
- const event = {} as Electron.IpcRendererEvent
- // also, we await in case the listener returns a promise
- return await ipcRenderer.listeners(message)[0](event, ...args)
- } else {
- throw new Error(`No ipcRenderer listeners for '${message}'`)
- }
- },
- { message, args }
- )
+ const retryOptions = isRetryOptions(args[args.length - 1])
+ ? (args.pop() as RetryOptions)
+ : undefined
+ return retry(
+ () =>
+ page.evaluate(
+ async ({ message, args }) => {
+ if (!require) {
+ throw new Error(
+ `Cannot access require() in renderer process. Is nodeIntegration: true?`
+ )
+ }
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
+ const { ipcRenderer } = require('electron')
+ if (ipcRenderer.listenerCount(message) > 0) {
+ // we send a fake event in place of the ipc event object
+ const event = {} as Electron.IpcRendererEvent
+ // also, we await in case the listener returns a promise
+ return await ipcRenderer.listeners(message)[0](event, ...args)
+ } else {
+ throw new Error(`No ipcRenderer listeners for '${message}'`)
+ }
+ },
+ { message, args }
+ ),
+ retryOptions
)
}
@@ -133,6 +151,7 @@ export function ipcRendererCallFirstListener(
* @param page {Page} - the Playwright Page to with the ipcRenderer.on() listener
* @param message {string} - the channel to call all ipcRenderer listeners for
* @param args {...unknown} optional - one or more arguments to send
+ * @param retryOptions {RetryOptions} optional - options for retrying upon error
* @returns {Promise}
* @fulfil {boolean} true if the event was emitted
* @reject {Error} if there are no ipcRenderer listeners for the event
@@ -140,27 +159,32 @@ export function ipcRendererCallFirstListener(
export function ipcRendererEmit(
page: Page,
message: string,
- ...args: unknown[]
+ ...args: (unknown | RetryOptions)[]
): Promise {
- return retry(() =>
- page.evaluate(
- ({ message, args }) => {
- if (!require) {
- throw new Error(
- `Cannot access require() in renderer process. Is nodeIntegration: true?`
- )
- }
- // eslint-disable-next-line @typescript-eslint/no-var-requires
- const { ipcRenderer } = require('electron')
- if (ipcRenderer.listenerCount(message) === 0) {
- throw new Error(`No ipcRenderer listeners for '${message}'`)
- }
- // create a fake event object
- const event = {} as Electron.IpcRendererEvent
- return ipcRenderer.emit(message, event, ...args)
- },
- { message, args }
- )
+ const retryOptions = isRetryOptions(args[args.length - 1])
+ ? (args.pop() as RetryOptions)
+ : undefined
+ return retry(
+ () =>
+ page.evaluate(
+ ({ message, args }) => {
+ if (!require) {
+ throw new Error(
+ `Cannot access require() in renderer process. Is nodeIntegration: true?`
+ )
+ }
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
+ const { ipcRenderer } = require('electron')
+ if (ipcRenderer.listenerCount(message) === 0) {
+ throw new Error(`No ipcRenderer listeners for '${message}'`)
+ }
+ // create a fake event object
+ const event = {} as Electron.IpcRendererEvent
+ return ipcRenderer.emit(message, event, ...args)
+ },
+ { message, args }
+ ),
+ retryOptions
)
}
@@ -176,6 +200,7 @@ export function ipcRendererEmit(
* @param electronApp {ElectronApplication} - the ElectronApplication object from Playwright
* @param message {string} - the channel to call all ipcMain listeners for
* @param args {...unknown} - one or more arguments to send
+ * @param retryOptions {RetryOptions} optional - options for retrying upon error
* @returns {Promise}
* @fulfil {boolean} true if there were listeners for this message
* @reject {Error} if there are no ipcMain listeners for the event
@@ -183,21 +208,26 @@ export function ipcRendererEmit(
export function ipcMainEmit(
electronApp: ElectronApplication,
message: string,
- ...args: unknown[]
+ ...args: (unknown | RetryOptions)[]
): Promise {
- return retry(() =>
- electronApp.evaluate(
- ({ ipcMain }, { message, args }) => {
- if (ipcMain.listeners(message).length > 0) {
- // fake ipcMainEvent
- const event = {} as Electron.IpcMainEvent
- return ipcMain.emit(message, event, ...args)
- } else {
- throw new Error(`No ipcMain listeners for '${message}'`)
- }
- },
- { message, args }
- )
+ const retryOptions = isRetryOptions(args[args.length - 1])
+ ? (args.pop() as RetryOptions)
+ : undefined
+ return retry(
+ () =>
+ electronApp.evaluate(
+ ({ ipcMain }, { message, args }) => {
+ if (ipcMain.listeners(message).length > 0) {
+ // fake ipcMainEvent
+ const event = {} as Electron.IpcMainEvent
+ return ipcMain.emit(message, event, ...args)
+ } else {
+ throw new Error(`No ipcMain listeners for '${message}'`)
+ }
+ },
+ { message, args }
+ ),
+ retryOptions
)
}
@@ -215,6 +245,7 @@ export function ipcMainEmit(
* @param electronApp {ElectronApplication} - the ElectronApplication object from Playwright
* @param message {string} - the channel to call the first listener for
* @param args {...unknown} - one or more arguments to send
+ * @param retryOptions {RetryOptions} optional - options for retrying upon error
* @returns {Promise}
* @fulfil {unknown} resolves with the result of the function
* @reject {Error} if there are no ipcMain listeners for the event
@@ -222,21 +253,26 @@ export function ipcMainEmit(
export async function ipcMainCallFirstListener(
electronApp: ElectronApplication,
message: string,
- ...args: unknown[]
+ ...args: (unknown | RetryOptions)[]
): Promise {
- return retry(() =>
- electronApp.evaluate(
- async ({ ipcMain }, { message, args }) => {
- if (ipcMain.listenerCount(message) > 0) {
- // fake ipcMainEvent
- const event = {} as Electron.IpcMainEvent
- return await ipcMain.listeners(message)[0](event, ...args)
- } else {
- throw new Error(`No listeners for message ${message}`)
- }
- },
- { message, args }
- )
+ const retryOptions = isRetryOptions(args[args.length - 1])
+ ? (args.pop() as RetryOptions)
+ : undefined
+ return retry(
+ () =>
+ electronApp.evaluate(
+ async ({ ipcMain }, { message, args }) => {
+ if (ipcMain.listenerCount(message) > 0) {
+ // fake ipcMainEvent
+ const event = {} as Electron.IpcMainEvent
+ return await ipcMain.listeners(message)[0](event, ...args)
+ } else {
+ throw new Error(`No listeners for message ${message}`)
+ }
+ },
+ { message, args }
+ ),
+ retryOptions
)
}
@@ -263,43 +299,49 @@ type IpcMainWithHandlers = Electron.IpcMain & {
* @param electronApp {ElectronApplication} - the ElectronApplication object from Playwright
* @param message {string} - the channel to call the first listener for
* @param args {...unknown} - one or more arguments to send
+ * @param retryOptions {RetryOptions} optional - options for retrying upon error
* @returns {Promise}
* @fulfil {unknown} resolves with the result of the function called in main process
*/
export async function ipcMainInvokeHandler(
electronApp: ElectronApplication,
message: string,
- ...args: unknown[]
+ ...args: (unknown | RetryOptions)[]
): Promise {
- return retry(() =>
- electronApp.evaluate(
- async ({ ipcMain }, { message, args }) => {
- const ipcMainWH = ipcMain as IpcMainWithHandlers
- // this is all a bit of a hack, so let's test as we go
- if (!ipcMainWH._invokeHandlers) {
- throw new Error(`Cannot access ipcMain._invokeHandlers`)
- }
- const handler = ipcMainWH._invokeHandlers.get(message)
- if (!handler) {
- throw new Error(`No ipcMain handler registered for '${message}'`)
- }
- // in electron <= 24, the event object's _reply() method is called
- let e24reply: unknown
- const e = {} as IpcMainInvokeEventWithReply
- e._reply = (value: unknown) => {
- e24reply = value
- }
- e._throw = function (error: Error) {
- throw error
- }
- // in electron >= 25, we can simply call the handler
- const e25reply = await handler(e, ...args)
+ const retryOptions = isRetryOptions(args[args.length - 1])
+ ? (args.pop() as RetryOptions)
+ : undefined
+ return retry(
+ () =>
+ electronApp.evaluate(
+ async ({ ipcMain }, { message, args }) => {
+ const ipcMainWH = ipcMain as IpcMainWithHandlers
+ // this is all a bit of a hack, so let's test as we go
+ if (!ipcMainWH._invokeHandlers) {
+ throw new Error(`Cannot access ipcMain._invokeHandlers`)
+ }
+ const handler = ipcMainWH._invokeHandlers.get(message)
+ if (!handler) {
+ throw new Error(`No ipcMain handler registered for '${message}'`)
+ }
+ // in electron <= 24, the event object's _reply() method is called
+ let e24reply: unknown
+ const e = {} as IpcMainInvokeEventWithReply
+ e._reply = (value: unknown) => {
+ e24reply = value
+ }
+ e._throw = function (error: Error) {
+ throw error
+ }
+ // in electron >= 25, we can simply call the handler
+ const e25reply = await handler(e, ...args)
- // return the value from the event object if it exists
- // otherwise return the value from the handler
- return (await e24reply) ?? e25reply
- },
- { message, args }
- )
+ // return the value from the event object if it exists
+ // otherwise return the value from the handler
+ return (await e24reply) ?? e25reply
+ },
+ { message, args }
+ ),
+ retryOptions
)
}
diff --git a/src/utilities.ts b/src/utilities.ts
index b78f418..1737b65 100644
--- a/src/utilities.ts
+++ b/src/utilities.ts
@@ -252,6 +252,16 @@ export function resetRetryOptions(): void {
Object.assign(currentRetryOptions, retryDefaults)
}
+export function isRetryOptions(options: unknown): options is RetryOptions {
+ if (typeof options !== 'object' || options === null) {
+ // if it's not an object
+ return false
+ }
+ const validKeys = Object.keys(retryDefaults)
+ // if every one of the keys in the passed object is a valid key
+ return Object.keys(options).every((key) => validKeys.includes(key))
+}
+
export type RetryUntilTruthyOptions = {
/** The maximum time (milliseconds) to wait for a truthy result. Default 5000. */
timeout: number