From 5550d6a411df2bc41480a0329d61e52acda15dcb Mon Sep 17 00:00:00 2001 From: Matthew Raymer Date: Sat, 15 Feb 2025 12:50:54 +0000 Subject: [PATCH] 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 --- .../35-record-gift-from-image-share.spec.ts | 135 ++++++++++++++---- test-playwright/40-add-contact.spec.ts | 134 +++++++++++------ 2 files changed, 193 insertions(+), 76 deletions(-) diff --git a/test-playwright/35-record-gift-from-image-share.spec.ts b/test-playwright/35-record-gift-from-image-share.spec.ts index f05ac1c7..460760da 100644 --- a/test-playwright/35-record-gift-from-image-share.spec.ts +++ b/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. diff --git a/test-playwright/40-add-contact.spec.ts b/test-playwright/40-add-contact.spec.ts index 32e18905..2aff9d18 100644 --- a/test-playwright/40-add-contact.spec.ts +++ b/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();