From 3f7f9beca162265953d3dbb7507276f5056c3805 Mon Sep 17 00:00:00 2001 From: Jeff Robbins Date: Mon, 18 Nov 2024 11:19:15 -0500 Subject: [PATCH 1/3] fix: disable option for retry --- example-project/e2e-tests/e2e.spec.ts | 47 +++++++++++++++++++++++++++ src/general_helpers.ts | 6 ++-- src/menu_helpers.ts | 10 +++--- src/utilities.ts | 30 +++++++++++------ 4 files changed, 75 insertions(+), 18 deletions(-) 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..ce75b7e 100644 --- a/src/general_helpers.ts +++ b/src/general_helpers.ts @@ -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,7 +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) } @@ -69,7 +69,7 @@ export async function evaluateWithRetry( export async function browserWindowWithRetry( app: ElectronApplication, page: Page, - options: RetryOptions = {} + options: Partial = {} ): Promise { return retry(() => app.browserWindow(page), options) } diff --git a/src/menu_helpers.ts b/src/menu_helpers.ts index 421d280..c6adde1 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( () => @@ -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) { @@ -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) { From 1419d819965c1dbeace20cb28835336c83f12164 Mon Sep 17 00:00:00 2001 From: Jeff Robbins Date: Mon, 18 Nov 2024 11:19:54 -0500 Subject: [PATCH 2/3] refactor: remove browserWindowWithRetry() --- README.md | 30 ++---------------------------- src/general_helpers.ts | 22 ---------------------- 2 files changed, 2 insertions(+), 50 deletions(-) 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/src/general_helpers.ts b/src/general_helpers.ts index ce75b7e..9de17a8 100644 --- a/src/general_helpers.ts +++ b/src/general_helpers.ts @@ -51,25 +51,3 @@ export async function evaluateWithRetry( ): 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: Partial = {} -): Promise { - return retry(() => app.browserWindow(page), options) -} From 14a410ea0cf2a33bc251de5f8e2d555ba9670930 Mon Sep 17 00:00:00 2001 From: Jeff Robbins Date: Mon, 18 Nov 2024 13:48:46 -0500 Subject: [PATCH 3/3] =?UTF-8?q?fix:=20disable=20retry=20on=20menuItemClick?= =?UTF-8?q?=20=C6=92s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/general_helpers.ts | 2 +- src/menu_helpers.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/general_helpers.ts b/src/general_helpers.ts index 9de17a8..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' diff --git a/src/menu_helpers.ts b/src/menu_helpers.ts index c6adde1..fc9a638 100644 --- a/src/menu_helpers.ts +++ b/src/menu_helpers.ts @@ -33,7 +33,7 @@ export function clickMenuItemById( throw new Error(`Menu item with id ${menuId} not found`) } }, id), - options + { disable: true, ...options } ) } @@ -95,7 +95,7 @@ export async function clickMenuItem

( } await mI.click() }, menuItem.commandId), - options + { disable: true, ...options } ) }