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.
		
		
		
		
		
			
		
			
				
					
					
						
							423 lines
						
					
					
						
							14 KiB
						
					
					
				
			
		
		
		
			
			
			
				
					
				
				
					
				
			
		
		
	
	
							423 lines
						
					
					
						
							14 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();
							 | 
						|
								
							 | 
						|
								  // PHASE 1 FIX: Wait for registration status to settle
							 | 
						|
								  // This ensures that components have the correct isRegistered status
							 | 
						|
								  await waitForRegistrationStatusToSettle(page);
							 | 
						|
								
							 | 
						|
								  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();
							 | 
						|
								  
							 | 
						|
								  // PHASE 1 FIX: Wait for registration check to complete and update UI elements
							 | 
						|
								  // This ensures that components like InviteOneView have the correct isRegistered status
							 | 
						|
								  await waitForRegistrationStatusToSettle(page);
							 | 
						|
								  
							 | 
						|
								  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
							 | 
						|
								// Note that this only generates up to 10 characters
							 | 
						|
								export async function generateRandomString(length: number): Promise<string> {
							 | 
						|
								  return Math.random()
							 | 
						|
								    .toString(36) // base 36 only generates up to 10 characters
							 | 
						|
								    .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 = 5; // max of 10; see generateRandomString
							 | 
						|
								
							 | 
						|
								  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);
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								/**
							 | 
						|
								 * PHASE 1 FIX: Wait for registration status to settle
							 | 
						|
								 * 
							 | 
						|
								 * This function addresses the timing issue where:
							 | 
						|
								 * 1. User imports identity → Database shows isRegistered: false
							 | 
						|
								 * 2. HomeView loads → Starts async registration check
							 | 
						|
								 * 3. Other views load → Use cached isRegistered: false
							 | 
						|
								 * 4. Async check completes → Updates database to isRegistered: true
							 | 
						|
								 * 5. But other views don't re-check → Plus buttons don't appear
							 | 
						|
								 * 
							 | 
						|
								 * This function waits for the async registration check to complete
							 | 
						|
								 * without interfering with test navigation.
							 | 
						|
								 */
							 | 
						|
								export async function waitForRegistrationStatusToSettle(page: Page): Promise<void> {
							 | 
						|
								  try {
							 | 
						|
								    // Wait for the initial registration check to complete
							 | 
						|
								    // This is indicated by the "Checking" text disappearing from usage limits
							 | 
						|
								    await expect(
							 | 
						|
								      page.locator("#sectionUsageLimits").getByText("Checking")
							 | 
						|
								    ).toBeHidden({ timeout: 15000 });
							 | 
						|
								    
							 | 
						|
								    // Before navigating back to the page, we'll trigger a registration check
							 | 
						|
								    // by navigating to home and waiting for the registration process to complete
							 | 
						|
								
							 | 
						|
								    const currentUrl = page.url();
							 | 
						|
								    
							 | 
						|
								    // Navigate to home to trigger the registration check
							 | 
						|
								    await page.goto('./');
							 | 
						|
								    await page.waitForLoadState('networkidle');
							 | 
						|
								    
							 | 
						|
								    // Wait for the registration check to complete by monitoring the usage limits section
							 | 
						|
								    // This ensures the async registration check has finished
							 | 
						|
								    await page.waitForFunction(() => {
							 | 
						|
								      const usageLimits = document.querySelector('#sectionUsageLimits');
							 | 
						|
								      if (!usageLimits) return true; // No usage limits section, assume ready
							 | 
						|
								      
							 | 
						|
								      // Check if the "Checking..." spinner is gone
							 | 
						|
								      const checkingSpinner = usageLimits.querySelector('.fa-spin');
							 | 
						|
								      if (checkingSpinner) return false; // Still loading
							 | 
						|
								      
							 | 
						|
								      // Check if we have actual content (not just the spinner)
							 | 
						|
								      const hasContent = usageLimits.querySelector('p') || usageLimits.querySelector('button');
							 | 
						|
								      return hasContent !== null; // Has actual content, not just spinner
							 | 
						|
								    }, { timeout: 10000 });
							 | 
						|
								    
							 | 
						|
								    // Also navigate to account page to ensure activeDid is set and usage limits are loaded
							 | 
						|
								    await page.goto('./account');
							 | 
						|
								    await page.waitForLoadState('networkidle');
							 | 
						|
								    
							 | 
						|
								    // Wait for the usage limits section to be visible and loaded
							 | 
						|
								    await page.waitForFunction(() => {
							 | 
						|
								      const usageLimits = document.querySelector('#sectionUsageLimits');
							 | 
						|
								      if (!usageLimits) return false; // Section should exist on account page
							 | 
						|
								      
							 | 
						|
								      // Check if the "Checking..." spinner is gone
							 | 
						|
								      const checkingSpinner = usageLimits.querySelector('.fa-spin');
							 | 
						|
								      if (checkingSpinner) return false; // Still loading
							 | 
						|
								      
							 | 
						|
								      // Check if we have actual content (not just the spinner)
							 | 
						|
								      const hasContent = usageLimits.querySelector('p') || usageLimits.querySelector('button');
							 | 
						|
								      return hasContent !== null; // Has actual content, not just spinner
							 | 
						|
								    }, { timeout: 15000 });
							 | 
						|
								    
							 | 
						|
								    // Navigate back to the original page if it wasn't home
							 | 
						|
								    if (!currentUrl.includes('/')) {
							 | 
						|
								      await page.goto(currentUrl);
							 | 
						|
								      await page.waitForLoadState('networkidle');
							 | 
						|
								    }
							 | 
						|
								    
							 | 
						|
								  } catch (error) {
							 | 
						|
								    // Registration status check timed out, continuing anyway
							 | 
						|
								    // This may indicate the user is not registered or there's a server issue
							 | 
						|
								  }
							 | 
						|
								}
							 | 
						|
								
							 |