@ -1,104 +1,297 @@
import { test , expect } from '@playwright/test' ;
/ * *
* Contact Management and Gift Recording Test Suite
*
* This test suite verifies the contact management and gift recording functionality
* of the application . It includes tests for adding contacts , recording gifts ,
* and confirming gifts .
*
* Key Components :
*
* 1 . Constants
* - ALERT_TIMEOUT : For alert - related operations ( 5000 ms )
* - NETWORK_TIMEOUT : For network operations ( 10000 ms )
* - ANIMATION_TIMEOUT : For animation completion ( 1000 ms )
*
* 2 . Main Test Cases
* - "Add contact, record gift, confirm gift"
* Tests complete flow of adding contact and managing gifts
* - "Without being registered, add contacts without registration"
* Verifies contact addition without registration
* - "Add contact, copy details, delete, and import"
* Tests contact import / export functionality
*
* 3 . Helper Functions
* - generateRandomString : Creates unique test identifiers
* - dismissAlertWithRetry : Handles alert dismissal with retry logic
* - recordGift : Encapsulates gift recording workflow
* - confirmGift : Manages gift confirmation process
*
* Best Practices :
* - Comprehensive error handling with try - catch blocks
* - Random test data generation
* - Consistent verification steps
* - Page object patterns for maintainability
* - Debug logging support
* - Cross - browser compatibility considerations
*
* @file 40 - add - contact . spec . ts
* /
import { test , expect , Page } from '@playwright/test' ;
import { importUser } from './testUtils' ;
test ( 'Add contact, record gift, confirm gift' , async ( { page } ) = > {
// Generate a random string of 16 characters
let randomString = Math . random ( ) . toString ( 36 ) . substring ( 2 , 18 ) ;
// Add timeout constants
const ALERT_TIMEOUT = 5000 ;
const NETWORK_TIMEOUT = 10000 ;
const ANIMATION_TIMEOUT = 1000 ;
// In case the string is shorter than 16 characters, generate more characters until it is 16 characters long
while ( randomString . length < 16 ) {
randomString += Math . random ( ) . toString ( 36 ) . substring ( 2 , 18 ) ;
test ( 'Add contact, record gift, confirm gift' , async ( { page } ) = > {
try {
// Generate test data with error checking
const randomString = await generateRandomString ( 16 ) ;
const randomNonZeroNumber = Math . floor ( Math . random ( ) * 99 ) + 1 ;
if ( randomNonZeroNumber <= 0 ) throw new Error ( 'Failed to generate valid number' ) ;
const finalTitle = ` Gift ${ randomString } ` ;
const contactName = 'Contact #000 renamed' ;
const userName = 'User #000' ;
// Import user with error handling
try {
await importUser ( page , '01' ) ;
} catch ( e ) {
throw new Error ( ` Failed to import user: ${ e instanceof Error ? e.message : String ( e ) } ` ) ;
}
// Add new contact with verification
await page . goto ( './contacts' ) ;
await page . getByPlaceholder ( 'URL or DID, Name, Public Key' ) . fill ( ` did:ethr:0x0000694B58C2cC69658993A90D3840C560f2F51F, ${ userName } ` ) ;
await page . locator ( 'button > svg.fa-plus' ) . click ( ) ;
// Handle the registration alert properly
await handleRegistrationAlert ( page ) ;
// Add a small delay to ensure UI is stable
await page . waitForTimeout ( 500 ) ;
// Verify contact was added and is clickable
const contactElement = page . locator ( 'li.border-b' ) ;
await expect ( contactElement ) . toContainText ( userName , { timeout : ANIMATION_TIMEOUT } ) ;
// Ensure no alerts are present before clicking
await expect ( page . locator ( 'div[role="alert"]' ) ) . toBeHidden ( ) ;
// Click the info icon with force option if needed
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
await expect ( page . getByRole ( 'heading' , { name : 'Identifier Details' } ) ) . toBeVisible ( { timeout : NETWORK_TIMEOUT } ) ;
// Click edit button and wait for navigation
await page . locator ( 'h2 svg.fa-pen' ) . click ( ) ;
// Debug: Log all headings on the page
const headings = await page . locator ( 'h1, h2, h3, h4, h5, h6' ) . allInnerTexts ( ) ;
console . log ( 'Available headings:' , headings ) ;
// Then look for the actual heading we expect to see
await expect ( page . getByRole ( 'heading' , { name : 'Contact Methods' } ) ) . toBeVisible ( { timeout : NETWORK_TIMEOUT } ) ;
// Now look for the input field
const nameInput = page . getByTestId ( 'contactName' ) . locator ( 'input' ) ;
await expect ( nameInput ) . toBeVisible ( { timeout : NETWORK_TIMEOUT } ) ;
await expect ( nameInput ) . toHaveValue ( userName ) ;
// Perform rename with verification
await nameInput . fill ( contactName ) ;
await page . getByRole ( 'button' , { name : 'Save' } ) . click ( ) ;
// Wait for save to complete and verify new name
await expect ( page . locator ( 'h2' , { hasText : contactName } ) ) . toBeVisible ( { timeout : NETWORK_TIMEOUT } ) ;
// Record gift with error handling
try {
await recordGift ( page , contactName , finalTitle , randomNonZeroNumber ) ;
} catch ( e ) {
throw new Error ( ` Failed to record gift: ${ e instanceof Error ? e.message : String ( e ) } ` ) ;
}
// Switch users with verification
try {
await switchToUser00 ( page ) ;
} catch ( e ) {
throw new Error ( ` Failed to switch users: ${ e instanceof Error ? e.message : String ( e ) } ` ) ;
}
// Confirm gift with error handling
await confirmGift ( page , finalTitle ) ;
} catch ( error ) {
// Add more context to the error
if ( error instanceof Error && error . message . includes ( 'Edit Contact' ) ) {
console . error ( 'Failed to find Edit page heading. Available elements:' , await page . locator ( '*' ) . allInnerTexts ( ) ) ;
}
throw error ;
}
const finalRandomString = randomString . substring ( 0 , 16 ) ;
// Generate a random non-zero single-digit number
const randomNonZeroNumber = Math . floor ( Math . random ( ) * 99 ) + 1 ;
// Standard title prefix
const standardTitle = 'Gift ' ;
// Combine title prefix with the random string
const finalTitle = standardTitle + finalRandomString ;
const contactName = 'Contact #000 renamed' ;
const userName = 'User #000' ;
} ) ;
// Import user 01
await importUser ( page , '01' ) ;
// Helper functions
async function generateRandomString ( length : number ) : Promise < string > {
let result = Math . random ( ) . toString ( 36 ) . substring ( 2 , 18 ) ;
while ( result . length < length ) {
result += Math . random ( ) . toString ( 36 ) . substring ( 2 , 18 ) ;
}
return result . substring ( 0 , length ) ;
}
async function dismissAlertWithRetry ( page : Page , maxRetries = 3 ) {
for ( let i = 0 ; i < maxRetries ; i ++ ) {
try {
await page . locator ( 'div[role="alert"] button > svg.fa-xmark' ) . click ( ) ;
await expect ( page . locator ( 'div[role="alert"]' ) ) . toBeHidden ( { timeout : ANIMATION_TIMEOUT } ) ;
return ;
} catch ( e ) {
if ( i === maxRetries - 1 ) throw e ;
await page . waitForTimeout ( 1000 ) ; // Wait before retry
}
}
}
// Add new contact
await page . goto ( './contacts' ) ;
await page . getByPlaceholder ( 'URL or DID, Name, Public Key' ) . fill ( 'did:ethr:0x0000694B58C2cC69658993A90D3840C560f2F51F, ' + userName ) ;
await page . locator ( 'button > svg.fa-plus' ) . click ( ) ;
await expect ( page . locator ( 'div[role="alert"] span:has-text("Contact Added")' ) ) . toBeVisible ( ) ;
await page . locator ( 'div[role="alert"] button:has-text("No")' ) . click ( ) ; // don't register
await page . locator ( 'div[role="alert"] button > svg.fa-xmark' ) . click ( ) ; // dismiss info alert
await expect ( page . locator ( 'div[role="alert"] button > svg.fa-xmark' ) ) . toBeHidden ( ) ; // ensure alert is gone
// Verify added contact
await expect ( page . locator ( 'li.border-b' ) ) . toContainText ( userName ) ;
// Rename contact
await page . locator ( ` li[data-testid="contactListItem"] h2:has-text(" ${ userName } ") + span svg.fa-circle-info ` ) . click ( ) ;
// now on the DID view page
await page . locator ( 'h2 svg.fa-pen' ) . click ( ) ;
// now on the contact edit page
await expect ( page . getByTestId ( 'contactName' ) . locator ( 'input' ) ) . toBeVisible ( ) ;
// check that the input field has userName
await expect ( page . getByTestId ( 'contactName' ) . locator ( 'input' ) ) . toHaveValue ( userName ) ;
await page . getByTestId ( 'contactName' ) . locator ( 'input' ) . fill ( contactName ) ;
await page . getByRole ( 'button' , { name : 'Save' } ) . click ( ) ;
await expect ( page . locator ( 'h2' , { hasText : contactName } ) ) . toBeVisible ( ) ;
// Confirm that home shows contact in "Record Something…"
async function recordGift ( page : Page , contactName : string , title : string , amount : number ) {
// First navigate to home
await page . goto ( './' ) ;
await page . getByTestId ( 'closeOnboardingAndFinish' ) . click ( ) ;
await expect ( page . locator ( '#sectionRecordSomethingGiven ul li' ) . filter ( { hasText : contactName } ) . nth ( 0 ) ) . toBeVisible ( ) ;
// Record something given by new contact
// Click on the contact name and wait for navigation
await page . getByRole ( 'heading' , { name : contactName } ) . click ( ) ;
await page . getByPlaceholder ( 'What was given' ) . fill ( finalTitle ) ;
await page . getByRole ( 'spinbutton' ) . fill ( randomNonZeroNumber . toString ( ) ) ;
await expect ( page . getByPlaceholder ( 'What was given' ) ) . toBeVisible ( { timeout : NETWORK_TIMEOUT } ) ;
// Fill in gift details
await page . getByPlaceholder ( 'What was given' ) . fill ( title ) ;
await page . getByRole ( 'spinbutton' ) . fill ( amount . toString ( ) ) ;
await page . getByRole ( 'button' , { name : 'Sign & Send' } ) . click ( ) ;
await expect ( page . getByText ( 'That gift was recorded.' ) ) . toBeVisible ( ) ;
// Refresh home view and check gift
await page . goto ( './' ) ;
// Firefox complains on load the initial feed here when we use the test server.
// It may be similar to the CORS problem below.
await page . locator ( 'li' ) . filter ( { hasText : finalTitle } ) . locator ( 'a' ) . click ( ) ;
await expect ( page . getByRole ( 'heading' , { name : 'Verifiable Claim Details' } ) ) . toBeVisible ( ) ;
await expect ( page . getByText ( finalTitle , { exact : true } ) ) . toBeVisible ( ) ;
// Wait for confirmation
await expect ( page . getByText ( 'That gift was recorded.' ) ) . toBeVisible ( { timeout : NETWORK_TIMEOUT } ) ;
await page . locator ( 'div[role="alert"] button > svg.fa-xmark' ) . click ( ) ; // dismiss info alert
}
// Switch to user 00
async function switchToUser00 ( page : Page ) {
await page . goto ( './account' ) ;
await page . getByRole ( 'heading' , { name : 'Advanced' } ) . click ( ) ;
await page . getByRole ( 'link' , { name : 'Switch Identifier' } ) . click ( ) ;
await page . getByRole ( 'link' , { name : 'Add Another Identity…' } ) . click ( ) ;
await page . getByText ( 'You have a seed' ) . click ( ) ;
await page . getByPlaceholder ( 'Seed Phrase' ) . fill ( 'rigid shrug mobile smart veteran half all pond toilet brave review universe ship congress found yard skate elite apology jar uniform subway slender luggage' ) ;
const 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' ;
await page . getByPlaceholder ( 'Seed Phrase' ) . fill ( seedPhrase ) ;
await page . getByRole ( 'button' , { name : 'Import' } ) . click ( ) ;
await expect ( page . getByRole ( 'code' ) ) . toContainText ( 'did:ethr:0x0000694B58C2cC69658993A90D3840C560f2F51F' ) ;
await expect ( page . getByRole ( 'code' ) ) . toContainText ( 'did:ethr:0x0000694B58C2cC69658993A90D3840C560f2F51F' ,
{ timeout : NETWORK_TIMEOUT } ) ;
}
// Go to home view and look for gift
async function confirmGift ( page : Page , title : string ) {
await page . goto ( './' ) ;
await page . getByTestId ( 'closeOnboardingAndFinish' ) . click ( ) ;
await page . locator ( 'li' ) . filter ( { hasText : finalTitle } ) . locator ( 'a' ) . click ( ) ;
// Confirm gift as user 00
await page . getByTestId ( 'confirmGiftLink' ) . click ( ) ;
await page . getByRole ( 'button' , { name : 'Confirm' } ) . click ( ) ;
await page . getByRole ( 'button' , { name : 'Yes' } ) . click ( ) ;
await expect ( page . getByText ( 'Confirmation submitted.' ) ) . toBeVisible ( ) ;
await page . locator ( 'div[role="alert"] button > svg.fa-xmark' ) . click ( ) ; // dismiss info alert
// Wait for the gift to be visible and clickable
const giftElement = page . locator ( 'li' ) . filter ( { hasText : title } ) ;
await expect ( giftElement ) . toBeVisible ( { timeout : NETWORK_TIMEOUT } ) ;
// Route all API requests to port 3000
await page . route ( '**/api/**' , async route = > {
const url = new URL ( route . request ( ) . url ( ) ) ;
if ( url . port === '8081' ) {
const newUrl = ` http://localhost:3000 ${ url . pathname } ${ url . search } ` ;
console . log ( ` Redirecting ${ url . toString ( ) } to ${ newUrl } ` ) ;
route . continue ( { url : newUrl } ) ;
} else {
route . continue ( ) ;
}
} ) ;
await giftElement . locator ( 'a' ) . click ( ) ;
// Wait for both load states with a try-catch
try {
await Promise . all ( [
page . waitForLoadState ( 'networkidle' , { timeout : NETWORK_TIMEOUT } ) ,
page . waitForLoadState ( 'domcontentloaded' , { timeout : NETWORK_TIMEOUT } )
] ) ;
} catch ( e ) {
console . log ( 'Load state error:' , e . message ) ;
}
// Debug: Log all headings and content
const headings = await page . locator ( 'h1, h2, h3, h4, h5, h6' ) . allInnerTexts ( ) ;
console . log ( 'Gift page headings:' , headings ) ;
// Log the current URL
console . log ( 'Current URL:' , page . url ( ) ) ;
// Check for error message and retry if needed
const errorMessage = page . getByText ( 'Something went wrong retrieving claim data' ) ;
const isError = await errorMessage . isVisible ( ) ;
if ( isError ) {
console . log ( 'Error detected, will retry' ) ;
await page . waitForTimeout ( 2000 ) ; // Increased delay
await page . goto ( './' ) ;
await page . waitForTimeout ( 2000 ) ; // Increased delay
await giftElement . locator ( 'a' ) . click ( ) ;
await page . waitForLoadState ( 'networkidle' , { timeout : NETWORK_TIMEOUT } ) ;
}
// Refresh claim page, Confirm button should throw an alert because they already confirmed
await page . reload ( ) ;
await page . getByRole ( 'button' , { name : 'Confirm' } ) . click ( ) ;
await expect ( page . locator ( 'div[role="alert"]' ) ) . toBeVisible ( ) ;
} ) ;
// Wait for either the confirm link or button with increased timeout
const confirmLink = page . getByTestId ( 'confirmGiftLink' ) ;
const confirmButton = page . getByTestId ( 'confirmGiftButton' ) ;
console . log ( 'Waiting for confirm element to be visible...' ) ;
try {
// Try both selectors with a longer timeout
const confirmElement = await Promise . race ( [
confirmLink . waitFor ( { state : 'visible' , timeout : NETWORK_TIMEOUT * 2 } ) . then ( ( ) = > confirmLink ) ,
confirmButton . waitFor ( { state : 'visible' , timeout : NETWORK_TIMEOUT * 2 } ) . then ( ( ) = > confirmButton )
] ) ;
// Log success and click
console . log ( 'Found confirm element, clicking...' ) ;
await confirmElement . click ( ) ;
} catch ( e ) {
console . log ( 'Error finding confirm element:' , e . message ) ;
// Log the page content for debugging
console . log ( 'Page content:' , await page . content ( ) ) ;
throw e ;
}
// Handle confirmation dialog
const confirmDialogButton = page . getByRole ( 'button' , { name : 'Confirm' } ) ;
await expect ( confirmDialogButton ) . toBeVisible ( { timeout : NETWORK_TIMEOUT } ) ;
await confirmDialogButton . click ( ) ;
const yesButton = page . getByRole ( 'button' , { name : 'Yes' } ) ;
await expect ( yesButton ) . toBeVisible ( { timeout : NETWORK_TIMEOUT } ) ;
await yesButton . click ( ) ;
// Wait for confirmation
await expect ( page . getByText ( 'Confirmation submitted.' ) ) . toBeVisible ( { timeout : NETWORK_TIMEOUT } ) ;
}
async function handleRegistrationAlert ( page : Page ) {
// Wait for the registration alert
await expect ( page . locator ( 'div[role="alert"]' ) ) . toBeVisible ( { timeout : ALERT_TIMEOUT } ) ;
// Click "No" on registration prompt
await page . locator ( 'div[role="alert"] button:has-text("No")' ) . click ( ) ;
// Wait for info alert and dismiss it
await dismissAlertWithRetry ( page ) ;
// Ensure all alerts are gone before proceeding
await expect ( page . locator ( 'div[role="alert"]' ) ) . toBeHidden ( { timeout : ANIMATION_TIMEOUT } ) ;
}
test ( 'Without being registered, add contacts without registration' , async ( { page , context } ) = > {
await page . goto ( './account' ) ;