diff --git a/README.md b/README.md index 6fbf300..433bebe 100644 --- a/README.md +++ b/README.md @@ -121,10 +121,6 @@ throwing an error saying the execution context has been destroyed. This function retries the evaluation several times to see if it can run the evaluation without an error. If it fails after the retries, it throws the error.

-
browserWindowWithRetry(app, page, options)
-

Returns the BrowserWindow object that corresponds to the given Playwright page (with retries).

-

This is basically a wrapper around [app.browserWindow(page)](https://playwright.dev/docs/api/class-electronapplication#electron-application-browser-window) -that retries the operation.

retryUntilTruthy(fn, [timeoutMs], [intervalMs])Promise.<T>

Retries a given function until it returns a truthy value or the timeout is reached.

This offers similar functionality to Playwright's page.waitForFunction() @@ -340,28 +336,6 @@ it throws the error.

| retries | |

the number of times to retry the evaluation

| | retryIntervalMs | |

the interval between retries

| - - -## browserWindowWithRetry(app, page, options) ⇒ -

Returns the BrowserWindow object that corresponds to the given Playwright page (with retries).

-

This is basically a wrapper around [app.browserWindow(page)](https://playwright.dev/docs/api/class-electronapplication#electron-application-browser-window) -that retries the operation.

- -**Kind**: global function -**Returns**:

A promise that resolves to the browser window.

-**Throws**: - --

Will throw an error if all retry attempts fail.

- - -| Param | Description | -| --- | --- | -| app |

The Electron application instance.

| -| page |

The Playwright page instance.

| -| options |

Optional configuration for retries.

| -| options.retries |

The number of retry attempts. Defaults to 5.

| -| options.intervalMs |

The interval between retries in milliseconds. Defaults to 200.

| - ## retryUntilTruthy(fn, [timeoutMs], [intervalMs]) ⇒ Promise.<T> @@ -820,8 +794,8 @@ This function retries a given function until it returns without throwing one of | --- | --- | --- | --- | | fn | function | |

The function to retry.

| | [options] | RetryOptions | {} |

The options for retrying the function.

| -| [options.intervalMs] | number | 200 |

The delay between each retry attempt in milliseconds.

| -| [options.timeoutMs] | number | 5000 |

The maximum time to wait before giving up in milliseconds.

| +| [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** diff --git a/example-project/e2e-tests/e2e.spec.ts b/example-project/e2e-tests/e2e.spec.ts index bb775e4..590ff1b 100644 --- a/example-project/e2e-tests/e2e.spec.ts +++ b/example-project/e2e-tests/e2e.spec.ts @@ -26,6 +26,7 @@ import { ipcRendererInvoke, ipcRendererSend, parseElectronApp, + retryUntilTruthy, stubDialog, waitForMenuItemStatus, } from '../../src' // <-- replace with 'electron-playwright-helpers' @@ -436,3 +437,49 @@ test('dialog.showSaveDialog stubbing', async () => { const filePath2 = await ipcMainInvokeHandler(app, 'get-opened-file') expect(filePath2).toBe('/path/to/new-saved-file.txt') }) + +test.describe('retryUntilTruthy()', () => { + test('retryUntilTruthy() returns true', async () => { + const page = latestPage() + if (!page) { + throw new Error('No page found') + } + const result = await retryUntilTruthy(() => + page.evaluate(() => document.getElementById('new-window')) + ) + expect(result).toBeTruthy() + }) + + test('retryUntilTruthy() timeout when returning false', async () => { + const page = latestPage() + if (!page) { + throw new Error('No page found') + } + await expect( + retryUntilTruthy( + () => + page.evaluate(() => document.getElementById('non-existent-element')), + { timeout: 500 } + ) + ).rejects.toThrow('Timeout after 500ms') + }) + + test('retryUntilTruthy() return truthy after a few iterations', async () => { + const page = latestPage() + if (!page) { + throw new Error('No page found') + } + await expect( + retryUntilTruthy(() => + page.evaluate(() => { + const w = window as Window & { counter?: number } + if (!w.counter) { + w.counter = 0 + } + w.counter++ + return w.counter > 3 + }) + ) + ).resolves.toBeTruthy() + }) +}) diff --git a/src/general_helpers.ts b/src/general_helpers.ts index 106620a..9556475 100644 --- a/src/general_helpers.ts +++ b/src/general_helpers.ts @@ -1,4 +1,4 @@ -import type { ElectronApplication, JSHandle, Page } from 'playwright-core' +import type { ElectronApplication } from 'playwright-core' import type { PageFunctionOn } from 'playwright-core/types/structs' import { retry, RetryOptions } from './utilities' @@ -19,7 +19,7 @@ export async function electronWaitForFunction( electronApp: ElectronApplication, fn: PageFunctionOn, arg?: Arg, - options: RetryOptions = {} + options: Partial = {} ): Promise { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore @@ -47,29 +47,7 @@ export async function evaluateWithRetry( electronApp: ElectronApplication, fn: PageFunctionOn, arg = {} as Arg, - options: RetryOptions = {} + options: Partial = {} ): Promise { return retry(() => electronApp.evaluate(fn, arg), options) } - -/** - * Returns the BrowserWindow object that corresponds to the given Playwright page (with retries). - * - * This is basically a wrapper around `[app.browserWindow(page)](https://playwright.dev/docs/api/class-electronapplication#electron-application-browser-window)` - * that retries the operation. - * - * @param app - The Electron application instance. - * @param page - The Playwright page instance. - * @param options - Optional configuration for retries. - * @param options.retries - The number of retry attempts. Defaults to 5. - * @param options.intervalMs - The interval between retries in milliseconds. Defaults to 200. - * @returns A promise that resolves to the browser window. - * @throws Will throw an error if all retry attempts fail. - */ -export async function browserWindowWithRetry( - app: ElectronApplication, - page: Page, - options: RetryOptions = {} -): Promise { - return retry(() => app.browserWindow(page), options) -} diff --git a/src/menu_helpers.ts b/src/menu_helpers.ts index 421d280..fc9a638 100644 --- a/src/menu_helpers.ts +++ b/src/menu_helpers.ts @@ -17,7 +17,7 @@ import { RetryOptions, retry } from './utilities' export function clickMenuItemById( electronApp: ElectronApplication, id: string, - options: RetryOptions = {} + options: Partial = {} ): Promise { return retry( () => @@ -33,7 +33,7 @@ export function clickMenuItemById( throw new Error(`Menu item with id ${menuId} not found`) } }, id), - options + { disable: true, ...options } ) } @@ -57,7 +57,7 @@ export async function clickMenuItem

( electronApp: ElectronApplication, property: P, value: MenuItemPartial[P], - options: RetryOptions = {} + options: Partial = {} ): Promise { const menuItem = await findMenuItem(electronApp, property, value) if (!menuItem) { @@ -95,7 +95,7 @@ export async function clickMenuItem

( } await mI.click() }, menuItem.commandId), - options + { disable: true, ...options } ) } @@ -114,7 +114,7 @@ export function getMenuItemAttribute( electronApp: ElectronApplication, menuId: string, attribute: T, - options: RetryOptions = {} + options: Partial = {} ): Promise { const attr = attribute as keyof Electron.MenuItem const resultPromise = retry( @@ -175,7 +175,7 @@ export type MenuItemPartial = MenuItemPrimitive & { export function getMenuItemById( electronApp: ElectronApplication, menuId: string, - options: RetryOptions = {} + options: Partial = {} ): Promise { return retry( () => @@ -233,7 +233,7 @@ export function getMenuItemById( */ export function getApplicationMenu( electronApp: ElectronApplication, - options: RetryOptions = {} + options: Partial = {} ): Promise { const promise = retry( () => diff --git a/src/utilities.ts b/src/utilities.ts index 25bccf0..b3f21bd 100644 --- a/src/utilities.ts +++ b/src/utilities.ts @@ -79,15 +79,17 @@ export function addTimeout( export type RetryOptions = { /** The maximum time to wait before giving up (in milliseconds) */ - timeout?: number + timeout: number /** The delay between each retry attempt in milliseconds. Or use "raf" for requestAnimationFrame. */ - poll?: number | 'raf' + poll: number | 'raf' /** * The error message or pattern to match against. Errors that don't match will throw immediately. * If a string or array of strings, the error will throw if it does not contain (one of) the passed string(s). * If a RegExp, the error will throw if it does not match the pattern. */ - errorMatch?: string | string[] | RegExp + errorMatch: string | string[] | RegExp + /** If true, the retry function will be disabled and will throw immediately. */ + disable: boolean } /** @@ -122,22 +124,25 @@ export type RetryOptions = { * @template T The type of the value returned by the function. * @param {Function} fn The function to retry. * @param {RetryOptions} [options={}] The options for retrying the function. - * @param {number} [options.intervalMs=200] The delay between each retry attempt in milliseconds. - * @param {number} [options.timeoutMs=5000] The maximum time to wait before giving up in milliseconds. + * @param {number} [options.timeout=5000] The maximum time to wait before giving up in milliseconds. + * @param {number} [options.poll=200] The delay between each retry attempt in milliseconds. * @param {string|string[]|RegExp} [options.errorMatch=['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. * @returns {Promise} A promise that resolves with the result of the function or rejects with an error or timeout message. */ export async function retry( fn: () => Promise | T, - options: RetryOptions = {} + options: Partial = {} ): Promise { const { poll, timeout, errorMatch } = { ...getRetryOptions(), ...options, } + let lastErr: unknown const startTime = Date.now() - let lastErr: unknown + if (options.disable) { + return fn() + } while (Date.now() - startTime < timeout) { try { @@ -156,7 +161,7 @@ export async function retry( ) { throw err } - if (Date.now() - startTime > timeout) { + if (Date.now() - startTime >= timeout) { continue } if (poll === 'raf') { @@ -175,6 +180,7 @@ export async function retry( } const retryDefaults: RetryOptions = { + disable: false, poll: 200, timeout: 5000, errorMatch: [ @@ -237,6 +243,8 @@ export type RetryUntilTruthyOptions = { retryPoll: number /** The error message or pattern to match against. Errors that don't match will throw immediately. */ retryErrorMatch: string | string[] | RegExp + /** If true, the retry function will be disabled and will throw immediately. */ + retryDisable: boolean } /** @@ -280,11 +288,13 @@ export async function retryUntilTruthy( retryPoll, retryTimeout, retryErrorMatch, + retryDisable, } = options const retryOptions: RetryOptions = { - ...(retryPoll && { poll: retryPoll }), - ...(retryTimeout && { timeout: retryTimeout }), + ...(retryPoll !== undefined && { poll: retryPoll }), + ...(retryTimeout !== undefined && { timeout: retryTimeout }), ...(retryErrorMatch && { errorMatch: retryErrorMatch }), + ...(retryDisable !== undefined && { disable: retryDisable }), } const timeoutTime = Date.now() + timeout while (Date.now() < timeoutTime) {