You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

338 lines
10 KiB

import { expect, Page, Locator } from "@playwright/test";
// Get test user data based on the ID.
// '01' -> user 111
// otherwise -> user 000
// (... which is a weird convention but I haven't taken the time to change it)
export function getTestUserData(id?: string): {
seedPhrase: string;
userName: string;
did: string;
} {
switch (id) {
case "01":
return {
seedPhrase:
"island fever beef wine urban aim vacant quit afford total poem flame service calm better adult neither color gaze forum month sister imitate excite",
userName: "User One",
did: "did:ethr:0x111d15564f824D56C7a07b913aA7aDd03382aA39",
};
default: // to user 00
return {
seedPhrase:
"rigid shrug mobile smart veteran half all pond toilet brave review universe ship congress found yard skate elite apology jar uniform subway slender luggage",
userName: "User Zero",
did: "did:ethr:0x0000694B58C2cC69658993A90D3840C560f2F51F",
};
}
}
export async function importUserFromAccount(page: Page, id?: string): Promise<string> {
// Navigate to AccountViewView to use the Identity Switcher
await page.goto("./account");
// Click "Show Advanced Settings" to reveal the identity switcher
await page.getByTestId("advancedSettings").click();
// Use the identity switcher to add User Zero
await page.locator("#switch-identity-link").click();
await page.locator("#start-link").click();
// Select "You have a seed" option
await page.getByText("You have a seed").click();
// Get User Zero's seed phrase using the new method
const userZeroData = getTestUserData(id);
// Enter User Zero's seed phrase
await page.getByPlaceholder("Seed Phrase").fill(userZeroData.seedPhrase);
await page.getByRole("button", { name: "Import" }).click();
return userZeroData.did;
}
// Import the seed and switch to the user based on the ID.
export async function importUser(page: Page, id?: string): Promise<string> {
const userData = getTestUserData(id);
const { seedPhrase, userName, did } = userData;
// Import ID
await page.goto("./start");
await page.getByText("You have a seed").click();
await page.getByPlaceholder("Seed Phrase").fill(seedPhrase);
await page.getByRole("button", { name: "Import" }).click();
// Check DID
await expect(page.getByRole("code")).toContainText(did);
// ... and ensure the app retrieves the registration status
await expect(
page.locator("#sectionUsageLimits").getByText("Checking")
).toBeHidden();
return did;
}
export async function importUserAndCloseOnboarding(
page: Page,
id?: string
): Promise<string> {
const did = await importUser(page, id);
await page.goto("./");
await page.getByTestId("closeOnboardingAndFinish").click();
return did;
}
// This is to switch to someone already in the identity table. It doesn't include registration.
export async function switchToUser(page: Page, did: string): Promise<void> {
// This is the direct approach but users have to tap on things so we'll do that instead.
await page.goto("./account");
// Wait for the page to load and the advanced settings element to be visible
await page.waitForLoadState('networkidle');
await page.getByTestId("advancedSettings").waitFor({ state: 'visible' });
const switchIdentityLink = page.locator("#switch-identity-link");
if (await switchIdentityLink.isHidden()) {
await page.getByTestId("advancedSettings").click();
await switchIdentityLink.click();
} else {
await switchIdentityLink.click();
}
const didElem = await page.locator(`code:has-text("${did}")`);
await didElem.isVisible();
await didElem.click();
// wait for the switch to happen and the account page to fully load
await page.getByTestId("didWrapper").locator('code:has-text("did:")');
}
export function createContactName(did: string): string {
return "User " + did.slice(11, 14);
}
export async function deleteContact(page: Page, did: string): Promise<void> {
await page.goto("./contacts");
const contactName = createContactName(did);
// go to the detail page for this contact
await page
.locator(
`li[data-testid="contactListItem"] h2:has-text("${contactName}") + div svg.fa-circle-info`
)
.click();
// delete the contact
await page.locator("button > svg.fa-trash-can").click();
await page.locator('div[role="alert"] button:has-text("Yes")').click();
// for some reason, .isHidden() (without expect) doesn't work
await expect(
page.locator('div[role="alert"] button:has-text("Yes")')
).toBeHidden();
}
export async function generateNewEthrUser(page: Page): Promise<string> {
await page.goto("./start");
await page.getByTestId("newSeed").click();
await expect(page.locator('span:has-text("Created")')).toBeVisible();
await page.goto("./account");
const didElem = await page
.getByTestId("didWrapper")
.locator('code:has-text("did:")');
const newDid = await didElem.innerText();
return newDid;
}
// Function to generate a random string of specified length
export async function generateRandomString(length: number): Promise<string> {
return Math.random()
.toString(36)
.substring(2, 2 + length);
}
// Function to create an array of unique strings
export async function createUniqueStringsArray(
count: number
): Promise<string[]> {
const stringsArray: string[] = [];
const stringLength = 16;
for (let i = 0; i < count; i++) {
let randomString = await generateRandomString(stringLength);
stringsArray.push(randomString);
}
return stringsArray;
}
// Function to create an array of two-digit non-zero numbers
export async function createRandomNumbersArray(
count: number
): Promise<number[]> {
const numbersArray: number[] = [];
for (let i = 0; i < count; i++) {
let randomNumber = Math.floor(Math.random() * 99) + 1;
numbersArray.push(randomNumber);
}
return numbersArray;
}
export function isLinuxEnvironment() {
return process.platform === "linux";
}
export function getOSSpecificTimeout(): number {
// Increase base timeout for Linux
const isLinux = process.platform === "linux";
return isLinux ? 180000 : 60000; // 3 minutes for Linux, 1 minute for others
}
export function getOSSpecificConfig() {
if (isLinuxEnvironment()) {
return {
retries: 2,
timeout: 90000, // Increased global timeout
expect: {
timeout: 30000, // Increased expect timeout
},
// Add video recording for failed tests on Linux
use: {
video: "retain-on-failure",
trace: "retain-on-failure",
},
};
}
return {};
}
// Add helper for test grouping
export function isResourceIntensiveTest(testPath: string): boolean {
return (
testPath.includes("35-record-gift-from-image-share") ||
testPath.includes("40-add-contact")
);
}
// Retry logic for load-sensitive operations
export async function retryOperation<T>(
operation: () => Promise<T>,
maxRetries: number = 3,
baseDelay: number = 1000,
description: string = 'operation'
): Promise<T> {
let lastError: Error;
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
return await operation();
} catch (error) {
lastError = error as Error;
if (attempt === maxRetries) {
console.log(`${description} failed after ${maxRetries} attempts`);
throw error;
}
// Exponential backoff with jitter
const delay = baseDelay * Math.pow(2, attempt - 1) + Math.random() * 500;
console.log(`⚠️ ${description} failed (attempt ${attempt}/${maxRetries}), retrying in ${Math.round(delay)}ms...`);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
throw lastError!;
}
// Specific retry wrappers for common operations
export async function retryWaitForSelector(
page: Page,
selector: string,
options?: { timeout?: number; state?: 'attached' | 'detached' | 'visible' | 'hidden' }
): Promise<void> {
const timeout = options?.timeout || getOSSpecificTimeout();
await retryOperation(
() => page.waitForSelector(selector, { ...options, timeout }),
3,
1000,
`waitForSelector(${selector})`
);
}
export async function retryWaitForLoadState(
page: Page,
state: 'load' | 'domcontentloaded' | 'networkidle',
options?: { timeout?: number }
): Promise<void> {
const timeout = options?.timeout || getOSSpecificTimeout();
await retryOperation(
() => page.waitForLoadState(state, { ...options, timeout }),
2,
2000,
`waitForLoadState(${state})`
);
}
export async function retryClick(
page: Page,
locator: Locator,
options?: { timeout?: number }
): Promise<void> {
const timeout = options?.timeout || getOSSpecificTimeout();
await retryOperation(
async () => {
await locator.waitFor({ state: 'visible', timeout });
await locator.click();
},
3,
1000,
`click(${locator.toString()})`
);
}
// Adaptive timeout utilities for load-sensitive operations
export function getAdaptiveTimeout(baseTimeout: number, multiplier: number = 1.5): number {
// Check if we're in a high-load environment
const isHighLoad = process.env.NODE_ENV === 'test' &&
(process.env.CI || process.env.TEST_LOAD_STRESS);
// Check system memory usage (if available)
const memoryUsage = process.memoryUsage();
const memoryPressure = memoryUsage.heapUsed / memoryUsage.heapTotal;
// Adjust timeout based on load indicators
let loadMultiplier = 1.0;
if (isHighLoad) {
loadMultiplier = 2.0;
} else if (memoryPressure > 0.8) {
loadMultiplier = 1.5;
} else if (memoryPressure > 0.6) {
loadMultiplier = 1.2;
}
return Math.floor(baseTimeout * loadMultiplier * multiplier);
}
export function getFirefoxTimeout(baseTimeout: number): number {
// Firefox typically needs more time, especially under load
return getAdaptiveTimeout(baseTimeout, 2.0);
}
export function getNetworkIdleTimeout(): number {
return getAdaptiveTimeout(5000, 1.5);
}
export function getElementWaitTimeout(): number {
return getAdaptiveTimeout(10000, 1.3);
}
export function getPageLoadTimeout(): number {
return getAdaptiveTimeout(30000, 1.4);
}