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
pull/126/head
Matthew Raymer 7 days 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 { test, expect } from '@playwright/test';
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 }) => {
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);
const finalTitle = `Gift ${randomString} from image-share`;
log('STEP', '1. Import test user');
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
await page.goto('./test', { timeout: TIMEOUT });
log('STEP', '3. Upload image file');
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');
await page.getByTestId('fileInput').click();
const fileChooser = await fileChooserPromise;
await fileChooser.setFiles(path.join(__dirname, '..', 'public', 'img', 'icons', 'android-chrome-192x192.png'));
// Wait for file upload to complete
await page.waitForTimeout(2000);
await page.waitForLoadState('networkidle', { timeout: TIMEOUT });
log('STEP', '4. Record gift');
await page.getByRole('button').filter({ hasText: /gift/i }).click();
await page.waitForLoadState('networkidle', { timeout: TIMEOUT });
await captureScreenshot(page, '4-gift-form');
// Click gift button and wait for navigation
await page.getByRole('button').filter({ hasText: /gift/i }).click();
await page.waitForLoadState('networkidle', { timeout: TIMEOUT });
log('STEP', '5. Fill gift details');
await expect(page.getByPlaceholder('What was received')).toBeVisible({ 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
await expect(page.getByPlaceholder('What was received')).toBeVisible({ timeout: TIMEOUT });
await page.getByPlaceholder('What was received').fill(finalTitle);
await page.getByRole('spinbutton').fill('2');
await page.getByRole('button', { name: 'Sign & Send' }).click();
log('STEP', '7. Handle confirmation');
await page.getByTestId('closeOnboardingAndFinish').click();
await expect(page.getByText('That gift was recorded.')).toBeVisible({ timeout: TIMEOUT });
await page.locator('div[role="alert"] button > svg.fa-xmark').click();
await captureScreenshot(page, '7-after-confirmation');
// Wait for onboarding and confirmation
await page.getByTestId('closeOnboardingAndFinish').click();
await expect(page.getByText('That gift was recorded.')).toBeVisible({ timeout: TIMEOUT });
await page.locator('div[role="alert"] button > svg.fa-xmark').click();
log('STEP', '8. Verify on home page');
await page.goto('./');
await page.waitForLoadState('networkidle', { timeout: TIMEOUT });
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
await page.goto('./');
await page.waitForLoadState('networkidle', { timeout: TIMEOUT });
const item1 = page.locator('li').filter({ hasText: finalTitle });
await expect(item1).toBeVisible({ timeout: TIMEOUT });
} catch (error) {
await captureScreenshot(page, `failure-${Date.now()}`);
log('INFO', `Test failed: ${error instanceof Error ? error.message : String(error)}`);
throw error;
}
});
// // 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 { 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
const BASE_TIMEOUT = getOSSpecificTimeout();
const ALERT_TIMEOUT = BASE_TIMEOUT / 6;
const NETWORK_TIMEOUT = BASE_TIMEOUT / 3;
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
test.describe('Contact Management', () => {
// 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 }) => {
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 randomNonZeroNumber = Math.floor(Math.random() * 99) + 1;
if (randomNonZeroNumber <= 0) throw new Error('Failed to generate valid number');
@ -61,21 +90,24 @@ test.describe('Contact Management', () => {
const finalTitle = `Gift ${randomString}`;
const contactName = 'Contact #000 renamed';
const userName = 'User #000';
log('INFO', `Test data generated - Title: ${finalTitle}, Contact: ${contactName}`);
// 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)}`);
}
log('STEP', '1. Import test user');
await importUser(page, '01');
await captureScreenshot(page, '1-after-user-import');
// Add new contact with verification
log('STEP', '2. Add new contact');
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.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);
log('SUCCESS', 'Registration alert handled');
await captureScreenshot(page, '2-after-alert-handled');
// Add a small delay to ensure UI is stable
await page.waitForTimeout(500);
@ -87,18 +119,20 @@ test.describe('Contact Management', () => {
// Ensure no alerts are present before clicking
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 });
// Wait for navigation to contact details page
// After navigation to details
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
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);
log('INFO', `Available page headings: ${headings.join(', ')}`);
// Then look for the actual heading we expect to see
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
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
try {
await recordGift(page, contactName, finalTitle, randomNonZeroNumber);
} 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)}`);
}
@ -133,9 +174,11 @@ test.describe('Contact Management', () => {
await confirmGift(page, finalTitle);
} 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')) {
console.error('Failed to find Edit page heading. Available elements:', await page.locator('*').allInnerTexts());
log('INFO', `Available elements: ${await page.locator('*').allInnerTexts()}`);
}
throw error;
}
@ -170,19 +213,21 @@ async function recordGift(page: Page, contactName: string, title: string, amount
while (retryCount > 0) {
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 Promise.all([
page.waitForLoadState('networkidle', { timeout: TIMEOUT }),
page.waitForLoadState('domcontentloaded', { timeout: TIMEOUT })
]);
await captureScreenshot(page, `gift-recording-home-page-${4 - retryCount}`);
// Handle onboarding first
const onboardingButton = page.getByTestId('closeOnboardingAndFinish');
if (await onboardingButton.isVisible()) {
console.log('Closing onboarding dialog...');
log('STEP', 'Closing onboarding dialog');
await onboardingButton.click();
await expect(onboardingButton).toBeHidden();
await page.waitForTimeout(1000);
@ -193,8 +238,8 @@ async function recordGift(page: Page, contactName: string, title: string, amount
await page.waitForLoadState('networkidle', { timeout: TIMEOUT });
// Debug current state
console.log('Current URL before clicking contact:', await page.url());
console.log('Looking for contact:', contactName);
log('INFO', `Current URL: ${await page.url()}`);
log('INFO', `Looking for contact: ${contactName}`);
// Find and click contact name
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
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
const giftButton = page.locator([
'button:has-text("Record Gift")',
@ -216,21 +264,27 @@ async function recordGift(page: Page, contactName: string, title: string, amount
// Debug UI state
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
const infoIcon = page.locator('svg.fa-circle-info').first();
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 page.waitForLoadState('networkidle', { timeout: TIMEOUT });
await captureScreenshot(page, `post-info-icon-click-${4 - retryCount}`);
}
// Now look for gift button again
if (await giftButton.count() === 0) {
console.log('Gift button not found, taking screenshot');
await page.screenshot({ path: 'test-results/missing-gift-button.png', fullPage: true });
console.log('Page content:', await page.content());
log('INFO', 'Gift button not found, capturing screenshot and page state');
await captureScreenshot(page, `missing-gift-button-${4 - retryCount}`);
// 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');
}
@ -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 });
// If we get here, everything worked
console.log('Gift recording successful');
log('SUCCESS', 'Gift recording completed');
await captureScreenshot(page, `gift-recording-success-${4 - retryCount}`);
return;
} catch (error) {
retryCount--;
console.log(`Gift recording attempt failed, ${retryCount} retries remaining`);
console.error('Error:', error instanceof Error ? error.message : String(error));
log('INFO', `Gift recording attempt failed, ${retryCount} retries remaining`);
log('INFO', `Error details: ${error instanceof Error ? error.message : String(error)}`);
// Take screenshot on failure
if (!page.isClosed()) {
await page.screenshot({
path: `test-results/gift-recording-failure-${4 - retryCount}.png`,
fullPage: true
});
}
await captureScreenshot(page, `gift-recording-failure-attempt-${4 - retryCount}`);
if (retryCount === 0) {
console.error('All gift recording attempts failed');
log('INFO', 'All gift recording attempts failed');
throw error;
}
@ -505,21 +553,17 @@ test('Copy contact to clipboard, then import ', async ({ page, context }, testIn
const isFirefox = await page.evaluate(() => {
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(() => {
return navigator.userAgent.includes('Macintosh') || navigator.userAgent.includes('iPhone');
});
if (isWebkit) {
console.log("Haven't found a way to access clipboard text in Webkit. Skipping.");
log('INFO', 'Webkit detected - clipboard test skipped');
return;
}
console.log("Running test that copies contact details to clipboard.");
log('STEP', 'Running clipboard copy test');
await page.getByTestId('copySelectedContactsButtonTop').click();
const clipboardText = await page.evaluate(async () => {
return navigator.clipboard.readText();

Loading…
Cancel
Save