forked from jsnbuchanan/crowd-funder-for-time-pwa
- Fix font-awesome selector to try multiple variations - Add detailed logging for contact discovery and DOM elements - Add screenshot capture when no contacts found - Add UI state detection (loading, filters) - Fix TypeScript typing issues for proper null safety This should help identify why contacts aren't being found during deletion in the failing Playwright test.
300 lines
12 KiB
TypeScript
300 lines
12 KiB
TypeScript
import { expect, Page } from '@playwright/test';
|
|
|
|
// Import the seed and switch to the user 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 async function importUser(page: Page, id?: string): Promise<string> {
|
|
let seedPhrase, userName, did;
|
|
|
|
// Set seed phrase and DID based on user ID
|
|
switch(id) {
|
|
case '01':
|
|
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';
|
|
break;
|
|
default: // to user 00
|
|
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';
|
|
}
|
|
|
|
// 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('./identity-switcher');
|
|
|
|
await page.goto('./account');
|
|
await page.getByRole('heading', { name: 'Advanced' }).click();
|
|
await page.getByRole('link', { name: 'Switch Identifier' }).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:")');
|
|
}
|
|
|
|
function createContactName(did: string): string {
|
|
return "User " + did.slice(11, 14);
|
|
}
|
|
|
|
export async function deleteContact(page: Page, did: string): Promise<void> {
|
|
console.log('[DEBUG] deleteContact: Starting deletion for DID:', did);
|
|
|
|
await page.goto('./contacts');
|
|
console.log('[DEBUG] deleteContact: Navigated to contacts page');
|
|
|
|
// Wait for the page to load completely
|
|
await page.waitForLoadState('networkidle');
|
|
console.log('[DEBUG] deleteContact: Page load completed');
|
|
|
|
// Check if there are any loading indicators or filters active
|
|
const loadingIndicator = page.locator('[data-testid*="loading"], .loading, .spinner');
|
|
const loadingCount = await loadingIndicator.count();
|
|
console.log('[DEBUG] deleteContact: Loading indicators found:', loadingCount);
|
|
|
|
// Check for any filter or state buttons that might be active
|
|
const showGiveNumbers = page.locator('button:has-text("Hide Actions")');
|
|
const showGiveNumbersExists = await showGiveNumbers.count();
|
|
console.log('[DEBUG] deleteContact: "Hide Actions" button exists (showing give numbers):', showGiveNumbersExists > 0);
|
|
|
|
if (showGiveNumbersExists > 0) {
|
|
console.log('[DEBUG] deleteContact: Clicking "Hide Actions" to show normal view');
|
|
await showGiveNumbers.click();
|
|
await page.waitForTimeout(1000); // Wait for UI to update
|
|
}
|
|
|
|
const contactName = createContactName(did);
|
|
console.log('[DEBUG] deleteContact: Looking for contact with name:', contactName);
|
|
|
|
// First, let's see what contacts are actually on the page
|
|
const contactItems = await page.locator('li[data-testid="contactListItem"]');
|
|
const contactCount = await contactItems.count();
|
|
console.log('[DEBUG] deleteContact: Found contact items on page:', contactCount);
|
|
|
|
// Log all contact names visible on the page
|
|
for (let i = 0; i < contactCount; i++) {
|
|
const contactItem = contactItems.nth(i);
|
|
const nameElement = contactItem.locator('h2');
|
|
const nameText = await nameElement.textContent();
|
|
console.log('[DEBUG] deleteContact: Contact', i, ':', nameText);
|
|
}
|
|
|
|
// If no contacts found, let's take a screenshot to see what's on the page
|
|
if (contactCount === 0) {
|
|
console.log('[DEBUG] deleteContact: No contacts found, taking screenshot');
|
|
await page.screenshot({ path: 'debug-no-contacts.png', fullPage: true });
|
|
console.log('[DEBUG] deleteContact: Screenshot saved as debug-no-contacts.png');
|
|
}
|
|
|
|
// Try to find the contact list item with the expected name
|
|
const contactListItem = page.locator(`li[data-testid="contactListItem"]:has(h2:has-text("${contactName}"))`);
|
|
const contactExists = await contactListItem.count();
|
|
console.log('[DEBUG] deleteContact: Contact with name exists:', contactName, contactExists > 0);
|
|
|
|
if (contactExists === 0) {
|
|
console.error('[DEBUG] deleteContact: Contact not found on page:', contactName);
|
|
throw new Error(`Contact "${contactName}" not found on contacts page`);
|
|
}
|
|
|
|
// Now click the info icon - fix the selector to match actual DOM structure
|
|
// Try different selectors for the info icon
|
|
const infoIconSelectors = [
|
|
`li[data-testid="contactListItem"]:has(h2:has-text("${contactName}")) font-awesome[icon="circle-info"]`,
|
|
`li[data-testid="contactListItem"]:has(h2:has-text("${contactName}")) .fa-circle-info`,
|
|
`li[data-testid="contactListItem"]:has(h2:has-text("${contactName}")) svg.fa-circle-info`,
|
|
`li[data-testid="contactListItem"]:has(h2:has-text("${contactName}")) [class*="fa-circle-info"]`
|
|
];
|
|
|
|
let infoIcon: import('@playwright/test').Locator | null = null;
|
|
let infoIconExists = 0;
|
|
|
|
for (const selector of infoIconSelectors) {
|
|
console.log('[DEBUG] deleteContact: Trying selector:', selector);
|
|
const testIcon = page.locator(selector);
|
|
const testCount = await testIcon.count();
|
|
console.log('[DEBUG] deleteContact: Selector result count:', testCount);
|
|
|
|
if (testCount > 0) {
|
|
infoIcon = testIcon;
|
|
infoIconExists = testCount;
|
|
console.log('[DEBUG] deleteContact: Found working selector:', selector);
|
|
break;
|
|
}
|
|
}
|
|
|
|
console.log('[DEBUG] deleteContact: Info icon exists:', infoIconExists > 0);
|
|
|
|
if (infoIconExists === 0 || !infoIcon) {
|
|
console.error('[DEBUG] deleteContact: Info icon not found for contact:', contactName);
|
|
throw new Error(`Info icon not found for contact "${contactName}"`);
|
|
}
|
|
|
|
// go to the detail page for this contact
|
|
await infoIcon.click();
|
|
console.log('[DEBUG] deleteContact: Clicked info icon, should be on detail page');
|
|
|
|
// Verify we're on the detail page
|
|
const detailPageHeading = page.locator('h1:has-text("Identifier Details")');
|
|
await detailPageHeading.waitFor({ timeout: 10000 });
|
|
console.log('[DEBUG] deleteContact: Confirmed on detail page');
|
|
|
|
// delete the contact
|
|
const deleteButton = page.locator('button > svg.fa-trash-can');
|
|
const deleteButtonExists = await deleteButton.count();
|
|
console.log('[DEBUG] deleteContact: Delete button exists:', deleteButtonExists > 0);
|
|
|
|
if (deleteButtonExists === 0) {
|
|
console.error('[DEBUG] deleteContact: Delete button not found');
|
|
throw new Error('Delete button not found on detail page');
|
|
}
|
|
|
|
await deleteButton.click();
|
|
console.log('[DEBUG] deleteContact: Clicked delete button');
|
|
|
|
// Confirm deletion
|
|
const confirmButton = page.locator('div[role="alert"] button:has-text("Yes")');
|
|
await confirmButton.waitFor({ timeout: 10000 });
|
|
console.log('[DEBUG] deleteContact: Confirmation dialog appeared');
|
|
|
|
await confirmButton.click();
|
|
console.log('[DEBUG] deleteContact: Clicked confirmation button');
|
|
|
|
// for some reason, .isHidden() (without expect) doesn't work
|
|
await expect(page.locator('div[role="alert"] button:has-text("Yes")')).toBeHidden();
|
|
console.log('[DEBUG] deleteContact: Confirmation dialog dismissed, deletion complete');
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
// Generate a new random user and register them.
|
|
// Note that this makes 000 the active user. Use switchToUser to switch to this DID.
|
|
export async function generateAndRegisterEthrUser(page: Page): Promise<string> {
|
|
console.log('[DEBUG] generateAndRegisterEthrUser: Starting user generation');
|
|
const newDid = await generateNewEthrUser(page);
|
|
console.log('[DEBUG] generateAndRegisterEthrUser: Generated new DID:', newDid);
|
|
|
|
await importUser(page, '000'); // switch to user 000
|
|
console.log('[DEBUG] generateAndRegisterEthrUser: Switched to user 000');
|
|
|
|
await page.goto('./contacts');
|
|
console.log('[DEBUG] generateAndRegisterEthrUser: Navigated to contacts page');
|
|
|
|
const contactName = createContactName(newDid);
|
|
console.log('[DEBUG] generateAndRegisterEthrUser: Created contact name:', contactName);
|
|
|
|
const contactInput = `${newDid}, ${contactName}`;
|
|
console.log('[DEBUG] generateAndRegisterEthrUser: Filling contact input with:', contactInput);
|
|
|
|
await page.getByPlaceholder('URL or DID, Name, Public Key').fill(contactInput);
|
|
await page.locator('button > svg.fa-plus').click();
|
|
console.log('[DEBUG] generateAndRegisterEthrUser: Clicked add contact button');
|
|
|
|
// register them
|
|
await page.locator('div[role="alert"] button:has-text("Yes")').click();
|
|
console.log('[DEBUG] generateAndRegisterEthrUser: Clicked registration confirmation');
|
|
|
|
// wait for it to disappear because the next steps may depend on alerts being gone
|
|
await expect(page.locator('div[role="alert"] button:has-text("Yes")')).toBeHidden();
|
|
console.log('[DEBUG] generateAndRegisterEthrUser: Registration dialog dismissed');
|
|
|
|
await expect(page.locator('li', { hasText: contactName })).toBeVisible();
|
|
console.log('[DEBUG] generateAndRegisterEthrUser: Contact is now visible in list:', contactName);
|
|
|
|
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');
|
|
}
|