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
						
					
					
				
			
		
		
		
			
			
			
				
					
				
				
					
				
			
		
		
	
	
							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); | |
| }
 | |
| 
 |