import { expect, Locator, Page } from "@playwright/test"; import LoggingUtils from "./LoggingUtils"; import { DateTime } from "luxon"; export type TimeoutOpts = { /** * @description timeout Commonly referred to for an entire method; exists to allow developers to specify their own timeout whout overriding defaults */ timeout: number /** * @description timeout_tiny use for exceedingly small waits, as in, waiting for label to contain the test you just typed into it. */ timeoutTiny: number /** * @description timeout_short for relatively quick opperations, such as waiting for a dropdown to render */ timeoutShort: number /** * @description timeout_medium for moderately slow operations, such as a new modal rendering, or a calculated field value being updated, or an API Call */ timeoutMedium: number /** * @description timeout_long for high-risk, slow operations. Waiting for the minicart to load, waiting for login, or waiting for screen-to-screen navigation. */ timeoutLong: number /** * @description for when things are really, really bad. */ timeoutConga: number } export const timeoutOptDefaults: TimeoutOpts = { timeout: 60_000, timeoutTiny: +(process.env.TIMEOUT_TINY ?? 500), timeoutShort: +(process.env.TIMEOUT_SHORT ?? 5000), timeoutMedium: +(process.env.TIMEOUT_MEDIUM ?? 30_000), timeoutLong: +(process.env.TIMEOUT_LONG ?? 180_000), timeoutConga: +(process.env.TIMEOUT_CONGA ?? 500_000), } export type WaitUntilOpts = { delayBetweenChecks: number, continueOnTimeoutError: boolean, anticipatedConditionResult: boolean, conditionName: string, beginWaitingMessage: string, delayBetweenChecksMessage: string, timeoutErrorMessage: string, successMessage: string, ignoreErrorsFromConditionFunction: boolean, } export const waitUntilOptDefaults: WaitUntilOpts = { delayBetweenChecks: 3000, continueOnTimeoutError: false, anticipatedConditionResult: true, conditionName: "", beginWaitingMessage: "", delayBetweenChecksMessage: "", timeoutErrorMessage: "Timed Out", successMessage: "", ignoreErrorsFromConditionFunction: true, } /** * @description repeatedly execute an asynchronous conditional lambda until a given outcome occurs, or the method times-out. Useful for hedgning against GUI race conditions. * @param conditionFunction the condition lambda. Example: ()=>{await return myPage.someButton.isVisible()} * @param options standard Timeout and WaitUntil Options. * @returns true or false - the outcome of the waituntil. */ export async function waitUntil(conditionFunction: (...args: any[]) => Promise, options: Partial = {}): Promise { const opts = { ...waitUntilOptDefaults, ...timeoutOptDefaults, ...options } let timeoutAt = Date.now() + opts.timeout; let waitUntilHasTimedOut = false; let conditionHasBeenMet = false; do { try { const conditionResult = await conditionFunction() conditionHasBeenMet = conditionResult == opts.anticipatedConditionResult } catch (e) { if (!opts.ignoreErrorsFromConditionFunction) { throw e } } waitUntilHasTimedOut = Date.now() > timeoutAt if (!waitUntilHasTimedOut && !conditionHasBeenMet) { await delay(opts.delayBetweenChecks) } else if (waitUntilHasTimedOut && !opts.continueOnTimeoutError) { throw new Error(opts.timeoutErrorMessage) } } while (!conditionHasBeenMet || waitUntilHasTimedOut) return conditionHasBeenMet; } /** * @description Order Matters; WaitUntil each condition passes before moving to the next. All conditions must pass in the expected order. * @param sequentialConditionFunctions an array of async, boolean lambdas to be executed in sqeuance until all have passed. * @param options standard Timeout and WaitUntil options */ export async function waitUntilValueStopsChanging(mercurialValueFunction: (...args: any[]) => Promise, options: Partial = {}): Promise { const opts = { ...waitUntilOptDefaults, ...timeoutOptDefaults, ...options } let lastFoundValue: any = undefined; const valueHasStoppedChanging = async () => { const newFoundValue = await mercurialValueFunction(); if (opts.delayBetweenChecksMessage.length > 0) console.log(`${opts.delayBetweenChecksMessage} - Last Value: ${lastFoundValue}`) const valueIsStable = (newFoundValue != undefined) && (newFoundValue == lastFoundValue); lastFoundValue = newFoundValue; return valueIsStable; } await waitUntil(valueHasStoppedChanging, opts) return lastFoundValue; } /** * @todo EXPIRIMENTAL! NO UNIT TESTS YET! TODO, add - DF, 5/23 * @description Order Matters; WaitUntil each condition passes before moving to the next. All conditions must pass in the expected order. * @param sequentialConditionFunctions an array of async, boolean lambdas to be executed in sqeuance until all have passed. * @param options standard Timeout and WaitUntil options */ export async function waitUntilEach(sequentialConditionFunctions: ((...args: any[]) => Promise)[], options: Partial = {}): Promise { for (const conditionFunction of sequentialConditionFunctions) { await waitUntil(conditionFunction, options); } } /** * @todo EXPIRIMENTAL! NO UNIT TESTS YET! TODO, add - DF, 5/23 * @description Order Matters; WaitUntil each condition passes before moving to the next. All conditions must pass in the expected order. * @param sequentialConditionFunctions an array of async, boolean lambdas to be executed in sequence until all have passed. * @param options standard Timeout and WaitUntil options */ export async function waitUntilAll(sequentialConditionFunctions: ((...args: any[]) => Promise)[], options: Partial = {}): Promise { for (const conditionFunction of sequentialConditionFunctions) { await waitUntil(conditionFunction, options); } } /** * @todo EXPIRIMENTAL! NO UNIT TESTS YET! TODO, add - DF, 5/23 * @description Order DOES NOT Matter; WaitUntil ANY condition passes before completing. Use when multiple conditions can give confidence that sufficient waiting has occured. * @param sequentialConditionFunctions an array of async, boolean lambdas to be executed in psudo-parallel until at least one has passed. * @param options standard Timeout and WaitUntil options */ export async function waitUntilAny(multipleRequiredConditionFunctions: ((...args: any[]) => Promise)[], options: Partial = {}): Promise { const anyConditionMet = async (): Promise => { return multipleRequiredConditionFunctions.filter(async (fun: (...args: any[]) => Promise): Promise => await Function.call(fun)).length > 0; }; waitUntil(anyConditionMet, options) } /** * @param milliseconds delay duration * @param logDelay defaults to false; if true, logs a waiting message. */ export async function delay(milliseconds: number, logDelay = false): Promise { if (logDelay) { console.log(`delaying ${milliseconds} milliseconds before continuing...`) } return new Promise((resolve) => setTimeout(resolve, milliseconds)); } /** * Waits for the page url * @param partialUrl The string we are looking for in the url, to know we have transitioned to the correct page. * @param timeout the amount of milliseconds to wait before giving up. */ export async function waitForUrlPartialMatch(page: Page, firstPartialUrl: string, timeout = 120_000) { const startTime = Date.now(); while (Date.now() - startTime < timeout) { if (page.url().includes(firstPartialUrl)) { return; // URL matches the partial string, exit the function } await page.waitForTimeout(100); // Wait for 100 milliseconds before checking again } throw new Error(`Timed out waiting for URL to match '${firstPartialUrl}'`); } /** * Run a function repeatedly until it returns true or the timeout is reached. * * This function executes the provided asynchronous block function in a loop until it returns true or the specified * timeout duration has elapsed. Between each attempt, it waits for a specified delay. * * @param block - An asynchronous function that returns a boolean value. This function will be executed repeatedly until it returns true. * @param timeout - The maximum duration to keep attempting to run the block function, in milliseconds. Default is 150000 (150 seconds). * @param delayMs - The delay duration between each attempt, in milliseconds. Default is 3000 (3 seconds). * @returns A promise that resolves to a boolean value indicating whether the block function eventually returned true. * * @example * // Example usage: * const blockFunction = async () => { * // Some asynchronous condition check * return await someConditionCheck(); * }; * const result = await runUntilTrue(blockFunction, 10000, 1000); * console.log(result); // Outputs true if blockFunction returned true within the timeout, otherwise false. */ export async function runUntilTrue(block: () => Promise, timeout: number = 150000, delayMs: number = 3000){ const startTime = DateTime.now(); let attempts = 0; let evaluatesToTrue = false; do { // If timeout duration has elapsed, stop making attempts if (DateTime.now().diff(startTime).as('milliseconds') > timeout) { break; } attempts++; // If retrying, wait delay duration if (attempts > 1) await delay(delayMs); evaluatesToTrue = await block(); } while (!evaluatesToTrue); return evaluatesToTrue; } // TODO move this to impl/utils/WaitingUtils when that related pr is available in devleop branch - T.S. 5/20/24 export async function waitForEither(block1: () => Promise, block2: () => Promise, timeOut: number = 180_000): Promise { const startTime = Date.now(); while (true) { try { const result1 = await block1(); const result2 = await block2(); // Check if either result is truthy (i.e., not falsy or undefined) if (result1 || result2) { // At least one block returned a truthy value, resolve the promise return; } } catch (error) { // Handle errors thrown by either block console.error("An error occurred:", error); } // Check if the timeout has been reached if (Date.now() - startTime >= timeOut) { throw new Error(`Timeout of ${timeOut} ms exceeded`); } // Add some delay before checking again await new Promise(resolve => setTimeout(resolve, 1000)); // Adjust delay as needed } } /** * Waits for a specific locator to show up on screen, then disappear. Typically used for things like progress bars. * @param locator The locator we want to become visible and then become hidden */ export async function waitToAppearAndDisappear(locator: Locator): Promise { try { await expect(locator).toBeVisible({ timeout: 60000 }); await expect(locator).toBeHidden({ timeout: 60000 }); } catch (err) { if (err instanceof Error) console.log(LoggingUtils.logFunc(waitToAppearAndDisappear.name, err.message, false)); else console.log(LoggingUtils.logFunc(waitToAppearAndDisappear.name, null, false)); } }