Browse Source

feat(tests): Add structured logging and screenshot capture

Enhances test debugging capabilities by adding:
- Structured logging with timestamps and categories (INFO/STEP/SUCCESS/WAIT)
- Systematic screenshot capture at key test steps
- Comprehensive file documentation headers
- Improved error handling with visual state capture
- Organized test step numbering and progress tracking

Key improvements:
- Added screenshot helper function with consistent naming
- Ensured test-results directory exists before captures
- Replaced console.log with structured logging utility
- Added try-catch blocks with failure state capture
- Improved debugging info for gift button not found error

Tests affected:
- 35-record-gift-from-image-share.spec.ts
- 40-add-contact.spec.ts

Note: Screenshots are stored in test-results/ with test-specific prefixes
Matthew Raymer 1 week ago
parent
commit
5550d6a411
  1. 135
      test-playwright/35-record-gift-from-image-share.spec.ts
  2. 134
      test-playwright/40-add-contact.spec.ts

135
test-playwright/35-record-gift-from-image-share.spec.ts

@ -1,47 +1,120 @@
/**
* @file Image Share Gift Recording Test
* @description End-to-end test suite for verifying the gift recording functionality
* through image sharing. Tests the complete flow from image upload to gift
* verification on the home page.
*
* Key test scenarios:
* - Image upload functionality
* - Gift recording form interaction
* - Onboarding flow handling
* - Success confirmation
* - Gift visibility on home page
*
* @author Matthew Raymer
* @created 2024
*
* Test Environment Requirements:
* - Requires test user data (user '00')
* - Needs access to test image files in public/img/icons
* - Assumes service worker is properly configured
*
* @note There is a commented-out test for service worker photo-sharing functionality
* that could be implemented in the future.
*/
import path from 'path'; import path from 'path';
import { test, expect } from '@playwright/test'; import { test, expect } from '@playwright/test';
import { importUser, getOSSpecificTimeout } from './testUtils'; import { importUser, getOSSpecificTimeout } from './testUtils';
import { Page } from '@playwright/test';
const TEST_NAME = 'record-gift-from-image-share';
// 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}`);
};
// Screenshot helper function
async function captureScreenshot(page: Page, name: string) {
if (!page.isClosed()) {
const filename = `test-results/${TEST_NAME}-${name.replace(/\s+/g, '-')}.png`;
log('INFO', `Capturing screenshot: ${filename}`);
await page.screenshot({ path: filename, fullPage: true });
return filename;
}
}
test('Record item given from image-share', async ({ page }) => { test('Record item given from image-share', async ({ page }) => {
const TIMEOUT = getOSSpecificTimeout(); try {
log('INFO', '▶ Starting: Image Share Gift Recording Test');
await captureScreenshot(page, 'test-start');
const TIMEOUT = getOSSpecificTimeout();
log('INFO', `Using OS-specific timeout: ${TIMEOUT}ms`);
let randomString = Math.random().toString(36).substring(2, 8);
const finalTitle = `Gift ${randomString} from image-share`;
log('INFO', `Generated test gift title: ${finalTitle}`);
let randomString = Math.random().toString(36).substring(2, 8); log('STEP', '1. Import test user');
const finalTitle = `Gift ${randomString} from image-share`; await importUser(page, '00');
await captureScreenshot(page, '1-after-user-import');
await importUser(page, '00'); log('STEP', '2. Navigate to test page');
await page.goto('./test', { timeout: TIMEOUT });
await captureScreenshot(page, '2-test-page');
// Record something given with increased timeout log('STEP', '3. Upload image file');
await page.goto('./test', { timeout: TIMEOUT }); const fileChooserPromise = page.waitForEvent('filechooser');
await page.getByTestId('fileInput').click();
const fileChooser = await fileChooserPromise;
const testImagePath = path.join(__dirname, '..', 'public', 'img', 'icons', 'android-chrome-192x192.png');
log('INFO', `Uploading test image from: ${testImagePath}`);
await fileChooser.setFiles(testImagePath);
await captureScreenshot(page, '3-before-upload');
log('WAIT', 'Upload in progress...');
await page.waitForTimeout(2000);
await page.waitForLoadState('networkidle', { timeout: TIMEOUT });
log('SUCCESS', 'Upload complete');
await captureScreenshot(page, '3-after-upload');
const fileChooserPromise = page.waitForEvent('filechooser'); log('STEP', '4. Record gift');
await page.getByTestId('fileInput').click(); await page.getByRole('button').filter({ hasText: /gift/i }).click();
const fileChooser = await fileChooserPromise; await page.waitForLoadState('networkidle', { timeout: TIMEOUT });
await fileChooser.setFiles(path.join(__dirname, '..', 'public', 'img', 'icons', 'android-chrome-192x192.png')); await captureScreenshot(page, '4-gift-form');
// Wait for file upload to complete
await page.waitForTimeout(2000);
await page.waitForLoadState('networkidle', { timeout: TIMEOUT });
// Click gift button and wait for navigation log('STEP', '5. Fill gift details');
await page.getByRole('button').filter({ hasText: /gift/i }).click(); await expect(page.getByPlaceholder('What was received')).toBeVisible({ timeout: TIMEOUT });
await page.waitForLoadState('networkidle', { timeout: TIMEOUT }); await page.getByPlaceholder('What was received').fill(finalTitle);
await page.getByRole('spinbutton').fill('2');
await captureScreenshot(page, '5-filled-form');
log('STEP', '6. Submit gift');
await page.getByRole('button', { name: 'Sign & Send' }).click();
await captureScreenshot(page, '6-after-submit');
// Wait for form to be ready log('STEP', '7. Handle confirmation');
await expect(page.getByPlaceholder('What was received')).toBeVisible({ timeout: TIMEOUT }); await page.getByTestId('closeOnboardingAndFinish').click();
await page.getByPlaceholder('What was received').fill(finalTitle); await expect(page.getByText('That gift was recorded.')).toBeVisible({ timeout: TIMEOUT });
await page.getByRole('spinbutton').fill('2'); await page.locator('div[role="alert"] button > svg.fa-xmark').click();
await page.getByRole('button', { name: 'Sign & Send' }).click(); await captureScreenshot(page, '7-after-confirmation');
// Wait for onboarding and confirmation log('STEP', '8. Verify on home page');
await page.getByTestId('closeOnboardingAndFinish').click(); await page.goto('./');
await expect(page.getByText('That gift was recorded.')).toBeVisible({ timeout: TIMEOUT }); await page.waitForLoadState('networkidle', { timeout: TIMEOUT });
await page.locator('div[role="alert"] button > svg.fa-xmark').click(); const item1 = page.locator('li').filter({ hasText: finalTitle });
await expect(item1).toBeVisible({ timeout: TIMEOUT });
await captureScreenshot(page, '8-home-page-verification');
log('SUCCESS', '✓ Test completed successfully');
// Verify on home page } catch (error) {
await page.goto('./'); await captureScreenshot(page, `failure-${Date.now()}`);
await page.waitForLoadState('networkidle', { timeout: TIMEOUT }); log('INFO', `Test failed: ${error instanceof Error ? error.message : String(error)}`);
const item1 = page.locator('li').filter({ hasText: finalTitle }); throw error;
await expect(item1).toBeVisible({ timeout: TIMEOUT }); }
}); });
// // I believe there's a way to test this service worker feature. // // I believe there's a way to test this service worker feature.

134
test-playwright/40-add-contact.spec.ts

@ -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'); await captureScreenshot(page, '1-after-user-import');
} catch (e) {
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,8 +248,11 @@ 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([
'button:has-text("Record Gift")', 'button:has-text("Record Gift")',
@ -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();

Loading…
Cancel
Save