@ -40,12 +40,39 @@
import { test , expect , Page } from '@playwright/test' ;
import { test , expect , Page } from '@playwright/test' ;
import { importUser , getOSSpecificTimeout } from './testUtils' ;
import { importUser , getOSSpecificTimeout } from './testUtils' ;
const TEST_NAME = 'add-contact' ;
// Logging utility function - outputs clean, parseable log format
const log = ( type : 'INFO' | 'STEP' | 'SUCCESS' | 'WAIT' , message : string ) = > {
const timestamp = new Date ( ) . toISOString ( ) . split ( 'T' ) [ 1 ] . slice ( 0 , - 1 ) ; // HH:MM:SS format
console . log ( ` ${ timestamp } ${ type . padEnd ( 7 ) } ${ message } ` ) ;
} ;
// Update timeout constants for Linux
// Update timeout constants for Linux
const BASE_TIMEOUT = getOSSpecificTimeout ( ) ;
const BASE_TIMEOUT = getOSSpecificTimeout ( ) ;
const ALERT_TIMEOUT = BASE_TIMEOUT / 6 ;
const ALERT_TIMEOUT = BASE_TIMEOUT / 6 ;
const NETWORK_TIMEOUT = BASE_TIMEOUT / 3 ;
const NETWORK_TIMEOUT = BASE_TIMEOUT / 3 ;
const ANIMATION_TIMEOUT = 1000 ;
const ANIMATION_TIMEOUT = 1000 ;
// Screenshot helper function
async function captureScreenshot ( page : Page , name : string ) {
if ( ! page . isClosed ( ) ) {
// Screenshots are stored in test-results directory
// Example: test-results/add-contact-test-start.png
const filename = ` test-results/ ${ TEST_NAME } - ${ name . replace ( /\s+/g , '-' ) } .png ` ;
log ( 'INFO' , ` Capturing screenshot: ${ filename } ` ) ;
// Ensure directory exists
const fs = require ( 'fs' ) ;
if ( ! fs . existsSync ( 'test-results' ) ) {
fs . mkdirSync ( 'test-results' , { recursive : true } ) ;
}
await page . screenshot ( { path : filename , fullPage : true } ) ;
return filename ;
}
}
// Add test configuration to increase timeout
// Add test configuration to increase timeout
test . describe ( 'Contact Management' , ( ) = > {
test . describe ( 'Contact Management' , ( ) = > {
// Increase timeout for all tests in this group
// Increase timeout for all tests in this group
@ -53,7 +80,9 @@ test.describe('Contact Management', () => {
test ( 'Add contact, record gift, confirm gift' , async ( { page } ) = > {
test ( 'Add contact, record gift, confirm gift' , async ( { page } ) = > {
try {
try {
// Generate test data with error checking
log ( 'INFO' , '▶ Starting: Add Contact and Gift Recording Test' ) ;
await captureScreenshot ( page , 'test-start' ) ;
const randomString = await generateRandomString ( 16 ) ;
const randomString = await generateRandomString ( 16 ) ;
const randomNonZeroNumber = Math . floor ( Math . random ( ) * 99 ) + 1 ;
const randomNonZeroNumber = Math . floor ( Math . random ( ) * 99 ) + 1 ;
if ( randomNonZeroNumber <= 0 ) throw new Error ( 'Failed to generate valid number' ) ;
if ( randomNonZeroNumber <= 0 ) throw new Error ( 'Failed to generate valid number' ) ;
@ -61,21 +90,24 @@ test.describe('Contact Management', () => {
const finalTitle = ` Gift ${ randomString } ` ;
const finalTitle = ` Gift ${ randomString } ` ;
const contactName = 'Contact #000 renamed' ;
const contactName = 'Contact #000 renamed' ;
const userName = 'User #000' ;
const userName = 'User #000' ;
log ( 'INFO' , ` Test data generated - Title: ${ finalTitle } , Contact: ${ contactName } ` ) ;
// Import user with error handling
log ( 'STEP' , '1. Import test user' ) ;
try {
await importUser ( page , '01' ) ;
await importUser ( page , '01' ) ;
} catch ( e ) {
await captureScreenshot ( page , '1-after-user-import' ) ;
throw new Error ( ` Failed to import user: ${ e instanceof Error ? e.message : String ( e ) } ` ) ;
}
// Add new contact with verification
log ( 'STEP' , '2. Add new contact' ) ;
await page . goto ( './contacts' ) ;
await page . goto ( './contacts' ) ;
await captureScreenshot ( page , '2-contacts-page' ) ;
await page . getByPlaceholder ( 'URL or DID, Name, Public Key' ) . fill ( ` did:ethr:0x0000694B58C2cC69658993A90D3840C560f2F51F, ${ userName } ` ) ;
await page . getByPlaceholder ( 'URL or DID, Name, Public Key' ) . fill ( ` did:ethr:0x0000694B58C2cC69658993A90D3840C560f2F51F, ${ userName } ` ) ;
await page . locator ( 'button > svg.fa-plus' ) . click ( ) ;
await page . locator ( 'button > svg.fa-plus' ) . click ( ) ;
await captureScreenshot ( page , '2-after-contact-added' ) ;
// Handle the registration alert properly
log ( 'WAIT' , 'Handling registration alert...' ) ;
await handleRegistrationAlert ( page ) ;
await handleRegistrationAlert ( page ) ;
log ( 'SUCCESS' , 'Registration alert handled' ) ;
await captureScreenshot ( page , '2-after-alert-handled' ) ;
// Add a small delay to ensure UI is stable
// Add a small delay to ensure UI is stable
await page . waitForTimeout ( 500 ) ;
await page . waitForTimeout ( 500 ) ;
@ -87,18 +119,20 @@ test.describe('Contact Management', () => {
// Ensure no alerts are present before clicking
// Ensure no alerts are present before clicking
await expect ( page . locator ( 'div[role="alert"]' ) ) . toBeHidden ( ) ;
await expect ( page . locator ( 'div[role="alert"]' ) ) . toBeHidden ( ) ;
// Click the info icon with force option if needed
// Before clicking info icon
await captureScreenshot ( page , '3-before-info-click' ) ;
await page . locator ( ` li[data-testid="contactListItem"] h2:has-text(" ${ userName } ") + span svg.fa-circle-info ` ) . click ( { force : true } ) ;
await page . locator ( ` li[data-testid="contactListItem"] h2:has-text(" ${ userName } ") + span svg.fa-circle-info ` ) . click ( { force : true } ) ;
// Wait for navigation to contact details page
// After navigation to details
await expect ( page . getByRole ( 'heading' , { name : 'Identifier Details' } ) ) . toBeVisible ( { timeout : NETWORK_TIMEOUT } ) ;
await expect ( page . getByRole ( 'heading' , { name : 'Identifier Details' } ) ) . toBeVisible ( { timeout : NETWORK_TIMEOUT } ) ;
await captureScreenshot ( page , '3-contact-details' ) ;
// Click edit button and wait for navigation
// Click edit button and wait for navigation
await page . locator ( 'h2 svg.fa-pen' ) . click ( ) ;
await page . locator ( 'h2 svg.fa-pen' ) . click ( ) ;
// Debug: Log all headings on the page
// Debug: Log all headings on the page
const headings = await page . locator ( 'h1, h2, h3, h4, h5, h6' ) . allInnerTexts ( ) ;
const headings = await page . locator ( 'h1, h2, h3, h4, h5, h6' ) . allInnerTexts ( ) ;
console . log ( 'Available headings:' , headings ) ;
log ( 'INFO' , ` Available page headings: ${ headings . join ( ', ' ) } ` ) ;
// Then look for the actual heading we expect to see
// Then look for the actual heading we expect to see
await expect ( page . getByRole ( 'heading' , { name : 'Contact Methods' } ) ) . toBeVisible ( { timeout : NETWORK_TIMEOUT } ) ;
await expect ( page . getByRole ( 'heading' , { name : 'Contact Methods' } ) ) . toBeVisible ( { timeout : NETWORK_TIMEOUT } ) ;
@ -115,10 +149,17 @@ test.describe('Contact Management', () => {
// Wait for save to complete and verify new name
// Wait for save to complete and verify new name
await expect ( page . locator ( 'h2' , { hasText : contactName } ) ) . toBeVisible ( { timeout : NETWORK_TIMEOUT } ) ;
await expect ( page . locator ( 'h2' , { hasText : contactName } ) ) . toBeVisible ( { timeout : NETWORK_TIMEOUT } ) ;
// Add screenshot before attempting gift recording
log ( 'STEP' , 'Preparing to record gift' ) ;
await captureScreenshot ( page , 'pre-gift-recording-attempt' ) ;
// Record gift with error handling
// Record gift with error handling
try {
try {
await recordGift ( page , contactName , finalTitle , randomNonZeroNumber ) ;
await recordGift ( page , contactName , finalTitle , randomNonZeroNumber ) ;
} catch ( e ) {
} catch ( e ) {
// Capture state when gift recording fails
await captureScreenshot ( page , 'gift-recording-failure' ) ;
log ( 'INFO' , ` Gift recording failed: ${ e instanceof Error ? e.message : String ( e ) } ` ) ;
throw new Error ( ` Failed to record gift: ${ e instanceof Error ? e.message : String ( e ) } ` ) ;
throw new Error ( ` Failed to record gift: ${ e instanceof Error ? e.message : String ( e ) } ` ) ;
}
}
@ -133,9 +174,11 @@ test.describe('Contact Management', () => {
await confirmGift ( page , finalTitle ) ;
await confirmGift ( page , finalTitle ) ;
} catch ( error ) {
} catch ( error ) {
// Add more context to the error
// Capture failure state
await captureScreenshot ( page , ` failure- ${ Date . now ( ) } ` ) ;
log ( 'INFO' , ` Test failed: ${ error instanceof Error ? error.message : String ( error ) } ` ) ;
if ( error instanceof Error && error . message . includes ( 'Edit Contact' ) ) {
if ( error instanceof Error && error . message . includes ( 'Edit Contact' ) ) {
console . error ( 'Failed to find Edit page heading. Available elements:' , await page . locator ( '*' ) . allInnerTexts ( ) ) ;
log ( 'INFO' , ` Available elements: ${ await page . locator ( '*' ) . allInnerTexts ( ) } ` ) ;
}
}
throw error ;
throw error ;
}
}
@ -170,19 +213,21 @@ async function recordGift(page: Page, contactName: string, title: string, amount
while ( retryCount > 0 ) {
while ( retryCount > 0 ) {
try {
try {
console . log ( ` Gift recording attempt ${ 4 - retryCount } /3 ` ) ;
log ( 'STEP' , ` Gift recording attempt ${ 4 - retryCount } /3 ` ) ;
await captureScreenshot ( page , ` gift-recording-start-attempt- ${ 4 - retryCount } ` ) ;
// First navigate to home and ensure it's loaded
log ( 'STEP' , 'Navigate to home page' ) ;
await page . goto ( './' , { timeout : TIMEOUT } ) ;
await page . goto ( './' , { timeout : TIMEOUT } ) ;
await Promise . all ( [
await Promise . all ( [
page . waitForLoadState ( 'networkidle' , { timeout : TIMEOUT } ) ,
page . waitForLoadState ( 'networkidle' , { timeout : TIMEOUT } ) ,
page . waitForLoadState ( 'domcontentloaded' , { timeout : TIMEOUT } )
page . waitForLoadState ( 'domcontentloaded' , { timeout : TIMEOUT } )
] ) ;
] ) ;
await captureScreenshot ( page , ` gift-recording-home-page- ${ 4 - retryCount } ` ) ;
// Handle onboarding first
// Handle onboarding first
const onboardingButton = page . getByTestId ( 'closeOnboardingAndFinish' ) ;
const onboardingButton = page . getByTestId ( 'closeOnboardingAndFinish' ) ;
if ( await onboardingButton . isVisible ( ) ) {
if ( await onboardingButton . isVisible ( ) ) {
console . log ( 'Closing onboarding dialog... ') ;
log ( 'STEP' , 'Closing onboarding dialog ') ;
await onboardingButton . click ( ) ;
await onboardingButton . click ( ) ;
await expect ( onboardingButton ) . toBeHidden ( ) ;
await expect ( onboardingButton ) . toBeHidden ( ) ;
await page . waitForTimeout ( 1000 ) ;
await page . waitForTimeout ( 1000 ) ;
@ -193,8 +238,8 @@ async function recordGift(page: Page, contactName: string, title: string, amount
await page . waitForLoadState ( 'networkidle' , { timeout : TIMEOUT } ) ;
await page . waitForLoadState ( 'networkidle' , { timeout : TIMEOUT } ) ;
// Debug current state
// Debug current state
console . log ( 'Current URL before clicking contact:' , await page . url ( ) ) ;
log ( 'INFO' , ` Current URL: ${ await page . url ( ) } ` ) ;
console . log ( 'Looking for contact:' , contactName ) ;
log ( 'INFO' , ` Looking for contact: ${ contactName } ` ) ;
// Find and click contact name
// Find and click contact name
const contactHeading = page . getByRole ( 'heading' , { name : contactName } ) . first ( ) ;
const contactHeading = page . getByRole ( 'heading' , { name : contactName } ) . first ( ) ;
@ -203,7 +248,10 @@ async function recordGift(page: Page, contactName: string, title: string, amount
// Wait for navigation
// Wait for navigation
await page . waitForLoadState ( 'networkidle' , { timeout : TIMEOUT } ) ;
await page . waitForLoadState ( 'networkidle' , { timeout : TIMEOUT } ) ;
console . log ( 'Current URL after clicking contact:' , await page . url ( ) ) ;
log ( 'INFO' , ` Current URL after clicking contact: ${ await page . url ( ) } ` ) ;
// Before looking for gift button
await captureScreenshot ( page , ` pre-gift-button-search- ${ 4 - retryCount } ` ) ;
// Look for gift recording UI elements
// Look for gift recording UI elements
const giftButton = page . locator ( [
const giftButton = page . locator ( [
@ -216,21 +264,27 @@ async function recordGift(page: Page, contactName: string, title: string, amount
// Debug UI state
// Debug UI state
const allButtons = await page . locator ( 'button, a' ) . allInnerTexts ( ) ;
const allButtons = await page . locator ( 'button, a' ) . allInnerTexts ( ) ;
console . log ( 'Available buttons:' , allButtons ) ;
log ( 'INFO' , ` Available buttons: ${ allButtons . join ( ', ' ) } ` ) ;
// Check if we need to click info first
// Check if we need to click info first
const infoIcon = page . locator ( 'svg.fa-circle-info' ) . first ( ) ;
const infoIcon = page . locator ( 'svg.fa-circle-info' ) . first ( ) ;
if ( await infoIcon . isVisible ( ) ) {
if ( await infoIcon . isVisible ( ) ) {
console . log ( 'Found info icon, clicking it first' ) ;
log ( 'STEP' , 'Clicking info icon' ) ;
await captureScreenshot ( page , ` pre-info-icon-click- ${ 4 - retryCount } ` ) ;
await infoIcon . click ( ) ;
await infoIcon . click ( ) ;
await page . waitForLoadState ( 'networkidle' , { timeout : TIMEOUT } ) ;
await page . waitForLoadState ( 'networkidle' , { timeout : TIMEOUT } ) ;
await captureScreenshot ( page , ` post-info-icon-click- ${ 4 - retryCount } ` ) ;
}
}
// Now look for gift button again
// Now look for gift button again
if ( await giftButton . count ( ) === 0 ) {
if ( await giftButton . count ( ) === 0 ) {
console . log ( 'Gift button not found, taking screenshot' ) ;
log ( 'INFO' , 'Gift button not found, capturing screenshot and page state' ) ;
await page . screenshot ( { path : 'test-results/missing-gift-button.png' , fullPage : true } ) ;
await captureScreenshot ( page , ` missing-gift-button- ${ 4 - retryCount } ` ) ;
console . log ( 'Page content:' , await page . content ( ) ) ;
// Capture more debug info
log ( 'INFO' , ` Current URL: ${ await page . url ( ) } ` ) ;
log ( 'INFO' , ` Page title: ${ await page . title ( ) } ` ) ;
const visibleElements = await page . locator ( 'button, a, h1, h2, h3, div[role="button"]' ) . allInnerTexts ( ) ;
log ( 'INFO' , ` Visible interactive elements: ${ visibleElements . join ( ', ' ) } ` ) ;
throw new Error ( 'Gift button not found on page' ) ;
throw new Error ( 'Gift button not found on page' ) ;
}
}
@ -272,25 +326,19 @@ async function recordGift(page: Page, contactName: string, title: string, amount
await expect ( page . getByText ( 'That gift was recorded.' ) ) . toBeVisible ( { timeout : 1000 } ) ;
await expect ( page . getByText ( 'That gift was recorded.' ) ) . toBeVisible ( { timeout : 1000 } ) ;
// If we get here, everything worked
log ( 'SUCCESS' , 'Gift recording completed' ) ;
console . log ( 'Gift recording successful' ) ;
await captureScreenshot ( page , ` gift-recording-success- ${ 4 - retryCount } ` ) ;
return ;
return ;
} catch ( error ) {
} catch ( error ) {
retryCount -- ;
retryCount -- ;
console . log ( ` Gift recording attempt failed, ${ retryCount } retries remaining ` ) ;
log ( 'INFO' , ` Gift recording attempt failed, ${ retryCount } retries remaining ` ) ;
console . error ( 'Error:' , error instanceof Error ? error.message : String ( error ) ) ;
log ( 'INFO' , ` Error details: ${ error instanceof Error ? error.message : String ( error ) } ` ) ;
// Take screenshot on failure
await captureScreenshot ( page , ` gift-recording-failure-attempt- ${ 4 - retryCount } ` ) ;
if ( ! page . isClosed ( ) ) {
await page . screenshot ( {
path : ` test-results/gift-recording-failure- ${ 4 - retryCount } .png ` ,
fullPage : true
} ) ;
}
if ( retryCount === 0 ) {
if ( retryCount === 0 ) {
console . error ( 'All gift recording attempts failed' ) ;
log ( 'INFO' , 'All gift recording attempts failed' ) ;
throw error ;
throw error ;
}
}
@ -505,21 +553,17 @@ test('Copy contact to clipboard, then import ', async ({ page, context }, testIn
const isFirefox = await page . evaluate ( ( ) = > {
const isFirefox = await page . evaluate ( ( ) = > {
return navigator . userAgent . includes ( 'Firefox' ) ;
return navigator . userAgent . includes ( 'Firefox' ) ;
} ) ;
} ) ;
if ( isFirefox ) {
// Firefox doesn't grant permissions like this but it works anyway.
} else {
await context . grantPermissions ( [ 'clipboard-read' ] ) ;
}
const isWebkit = await page . evaluate ( ( ) = > {
const isWebkit = await page . evaluate ( ( ) = > {
return navigator . userAgent . includes ( 'Macintosh' ) || navigator . userAgent . includes ( 'iPhone' ) ;
return navigator . userAgent . includes ( 'Macintosh' ) || navigator . userAgent . includes ( 'iPhone' ) ;
} ) ;
} ) ;
if ( isWebkit ) {
if ( isWebkit ) {
console . log ( "Haven't found a way to access clipboard text in Webkit. Skipping." ) ;
log ( 'INFO' , 'Webkit detected - clipboard test skipped' ) ;
return ;
return ;
}
}
console . log ( "Running test that copies contact details to clipboard." ) ;
log ( 'STEP' , 'Running clipboard copy test' ) ;
await page . getByTestId ( 'copySelectedContactsButtonTop' ) . click ( ) ;
await page . getByTestId ( 'copySelectedContactsButtonTop' ) . click ( ) ;
const clipboardText = await page . evaluate ( async ( ) = > {
const clipboardText = await page . evaluate ( async ( ) = > {
return navigator . clipboard . readText ( ) ;
return navigator . clipboard . readText ( ) ;