Initial commit of the Playwright test framework, including: - Configuration files for Playwright, Docker, and Sauce Labs - Test case structure and page object models - CI/CD pipeline configuration - Utilities and business logic implementations This framework will be used for end-to-end testing of the FMG application.
273 lines
No EOL
12 KiB
TypeScript
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));
|
|
}
|
|
} |