DigitalConsumer.FixMyGlass/playwright-tests/impl/utils/TimingUtils.ts
maguire-arman a2ca9134d1 Initializes Playwright test framework
Sets up the core infrastructure for Playwright-based automated testing, including:
- Docker support for consistent environments
- Azure Pipelines configuration for CI/CD
- Page Object Model structure for maintainable tests
- Test data, validation, and rule engine implementation
- Test configuration for alerts and other scenarios
2025-05-08 15:45:05 -04:00

273 lines
No EOL
12 KiB
TypeScript

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<boolean>, options: Partial<TimeoutOpts & WaitUntilOpts> = {}): Promise<boolean> {
const opts = { ...waitUntilOptDefaults, ...timeoutOptDefaults, ...options }
const 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<any>, options: Partial<TimeoutOpts & WaitUntilOpts> = {}): Promise<any> {
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<boolean>)[], options: Partial<TimeoutOpts & WaitUntilOpts> = {}): Promise<any> {
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<boolean>)[], options: Partial<TimeoutOpts & WaitUntilOpts> = {}): Promise<any> {
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<boolean>)[], options: Partial<TimeoutOpts & WaitUntilOpts> = {}): Promise<any> {
const anyConditionMet = async (): Promise<boolean> => {
return multipleRequiredConditionFunctions.filter(async (fun: (...args: any[]) => Promise<boolean>): Promise<boolean> => 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<void> {
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<boolean>, 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<any>, block2: () => Promise<any>, timeOut: number = 180_000): Promise<void> {
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<void> {
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));
}
}