Skip to content

Commit

Permalink
Merge pull request #59 from spaceagetv/fix/beta/various
Browse files Browse the repository at this point in the history
fix: disable retry + remove browserWindowWithRetry
  • Loading branch information
jjeff authored Nov 18, 2024
2 parents 100836b + 14a410e commit 2e9875f
Show file tree
Hide file tree
Showing 5 changed files with 79 additions and 70 deletions.
30 changes: 2 additions & 28 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.</p></dd>
<dt><a href="#browserWindowWithRetry">browserWindowWithRetry(app, page, options)</a> ⇒</dt>
<dd><p>Returns the BrowserWindow object that corresponds to the given Playwright page (with retries).</p>
<p>This is basically a wrapper around <code>[app.browserWindow(page)](https://playwright.dev/docs/api/class-electronapplication#electron-application-browser-window)</code>
that retries the operation.</p></dd>
<dt><a href="#retryUntilTruthy">retryUntilTruthy(fn, [timeoutMs], [intervalMs])</a> ⇒ <code>Promise.&lt;T&gt;</code></dt>
<dd><p>Retries a given function until it returns a truthy value or the timeout is reached.</p>
<p>This offers similar functionality to Playwright's <a href="https://playwright.dev/docs/api/class-page#page-wait-for-function"><code>page.waitForFunction()</code></a>
Expand Down Expand Up @@ -340,28 +336,6 @@ it throws the error.</p>
| retries | | <p>the number of times to retry the evaluation</p> |
| retryIntervalMs | | <p>the interval between retries</p> |

<a name="browserWindowWithRetry"></a>

## browserWindowWithRetry(app, page, options) ⇒
<p>Returns the BrowserWindow object that corresponds to the given Playwright page (with retries).</p>
<p>This is basically a wrapper around <code>[app.browserWindow(page)](https://playwright.dev/docs/api/class-electronapplication#electron-application-browser-window)</code>
that retries the operation.</p>

**Kind**: global function
**Returns**: <p>A promise that resolves to the browser window.</p>
**Throws**:

- <p>Will throw an error if all retry attempts fail.</p>


| Param | Description |
| --- | --- |
| app | <p>The Electron application instance.</p> |
| page | <p>The Playwright page instance.</p> |
| options | <p>Optional configuration for retries.</p> |
| options.retries | <p>The number of retry attempts. Defaults to 5.</p> |
| options.intervalMs | <p>The interval between retries in milliseconds. Defaults to 200.</p> |

<a name="retryUntilTruthy"></a>

## retryUntilTruthy(fn, [timeoutMs], [intervalMs]) ⇒ <code>Promise.&lt;T&gt;</code>
Expand Down Expand Up @@ -820,8 +794,8 @@ This function retries a given function until it returns without throwing one of
| --- | --- | --- | --- |
| fn | <code>function</code> | | <p>The function to retry.</p> |
| [options] | <code>RetryOptions</code> | <code>{}</code> | <p>The options for retrying the function.</p> |
| [options.intervalMs] | <code>number</code> | <code>200</code> | <p>The delay between each retry attempt in milliseconds.</p> |
| [options.timeoutMs] | <code>number</code> | <code>5000</code> | <p>The maximum time to wait before giving up in milliseconds.</p> |
| [options.timeout] | <code>number</code> | <code>5000</code> | <p>The maximum time to wait before giving up in milliseconds.</p> |
| [options.poll] | <code>number</code> | <code>200</code> | <p>The delay between each retry attempt in milliseconds.</p> |
| [options.errorMatch] | <code>string</code> \| <code>Array.&lt;string&gt;</code> \| <code>RegExp</code> | <code>&quot;[&#x27;context or browser has been closed&#x27;, &#x27;Promise was collected&#x27;, &#x27;Execution context was destroyed&#x27;]&quot;</code> | <p>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.</p> |

**Example**
Expand Down
47 changes: 47 additions & 0 deletions example-project/e2e-tests/e2e.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import {
ipcRendererInvoke,
ipcRendererSend,
parseElectronApp,
retryUntilTruthy,
stubDialog,
waitForMenuItemStatus,
} from '../../src' // <-- replace with 'electron-playwright-helpers'
Expand Down Expand Up @@ -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()
})
})
28 changes: 3 additions & 25 deletions src/general_helpers.ts
Original file line number Diff line number Diff line change
@@ -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'

Expand All @@ -19,7 +19,7 @@ export async function electronWaitForFunction<R, Arg>(
electronApp: ElectronApplication,
fn: PageFunctionOn<typeof Electron.CrossProcessExports, Arg, R>,
arg?: Arg,
options: RetryOptions = {}
options: Partial<RetryOptions> = {}
): Promise<void> {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
Expand Down Expand Up @@ -47,29 +47,7 @@ export async function evaluateWithRetry<R, Arg>(
electronApp: ElectronApplication,
fn: PageFunctionOn<typeof Electron.CrossProcessExports, Arg, R>,
arg = {} as Arg,
options: RetryOptions = {}
options: Partial<RetryOptions> = {}
): Promise<R> {
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<JSHandle> {
return retry(() => app.browserWindow(page), options)
}
14 changes: 7 additions & 7 deletions src/menu_helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import { RetryOptions, retry } from './utilities'
export function clickMenuItemById(
electronApp: ElectronApplication,
id: string,
options: RetryOptions = {}
options: Partial<RetryOptions> = {}
): Promise<unknown> {
return retry(
() =>
Expand All @@ -33,7 +33,7 @@ export function clickMenuItemById(
throw new Error(`Menu item with id ${menuId} not found`)
}
}, id),
options
{ disable: true, ...options }
)
}

Expand All @@ -57,7 +57,7 @@ export async function clickMenuItem<P extends keyof MenuItemPartial>(
electronApp: ElectronApplication,
property: P,
value: MenuItemPartial[P],
options: RetryOptions = {}
options: Partial<RetryOptions> = {}
): Promise<unknown> {
const menuItem = await findMenuItem(electronApp, property, value)
if (!menuItem) {
Expand Down Expand Up @@ -95,7 +95,7 @@ export async function clickMenuItem<P extends keyof MenuItemPartial>(
}
await mI.click()
}, menuItem.commandId),
options
{ disable: true, ...options }
)
}

Expand All @@ -114,7 +114,7 @@ export function getMenuItemAttribute<T extends keyof Electron.MenuItem>(
electronApp: ElectronApplication,
menuId: string,
attribute: T,
options: RetryOptions = {}
options: Partial<RetryOptions> = {}
): Promise<Electron.MenuItem[T]> {
const attr = attribute as keyof Electron.MenuItem
const resultPromise = retry(
Expand Down Expand Up @@ -175,7 +175,7 @@ export type MenuItemPartial = MenuItemPrimitive & {
export function getMenuItemById(
electronApp: ElectronApplication,
menuId: string,
options: RetryOptions = {}
options: Partial<RetryOptions> = {}
): Promise<MenuItemPartial> {
return retry(
() =>
Expand Down Expand Up @@ -233,7 +233,7 @@ export function getMenuItemById(
*/
export function getApplicationMenu(
electronApp: ElectronApplication,
options: RetryOptions = {}
options: Partial<RetryOptions> = {}
): Promise<MenuItemPartial[] | undefined> {
const promise = retry(
() =>
Expand Down
30 changes: 20 additions & 10 deletions src/utilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,15 +79,17 @@ export function addTimeout<T extends HelperFunctionName>(

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
}

/**
Expand Down Expand Up @@ -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<T>} A promise that resolves with the result of the function or rejects with an error or timeout message.
*/
export async function retry<T>(
fn: () => Promise<T> | T,
options: RetryOptions = {}
options: Partial<RetryOptions> = {}
): Promise<T> {
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 {
Expand All @@ -156,7 +161,7 @@ export async function retry<T>(
) {
throw err
}
if (Date.now() - startTime > timeout) {
if (Date.now() - startTime >= timeout) {
continue
}
if (poll === 'raf') {
Expand All @@ -175,6 +180,7 @@ export async function retry<T>(
}

const retryDefaults: RetryOptions = {
disable: false,
poll: 200,
timeout: 5000,
errorMatch: [
Expand Down Expand Up @@ -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
}

/**
Expand Down Expand Up @@ -280,11 +288,13 @@ export async function retryUntilTruthy<T>(
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) {
Expand Down

0 comments on commit 2e9875f

Please sign in to comment.