forked from trent_larson/crowd-funder-for-time-pwa
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
This commit is contained in:
@@ -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`;
|
||||
let randomString = Math.random().toString(36).substring(2, 8);
|
||||
const finalTitle = `Gift ${randomString} from image-share`;
|
||||
log('INFO', `Generated test gift title: ${finalTitle}`);
|
||||
|
||||
await importUser(page, '00');
|
||||
log('STEP', '1. Import test user');
|
||||
await importUser(page, '00');
|
||||
await captureScreenshot(page, '1-after-user-import');
|
||||
|
||||
// Record something given with increased timeout
|
||||
await page.goto('./test', { timeout: TIMEOUT });
|
||||
log('STEP', '2. Navigate to test page');
|
||||
await page.goto('./test', { timeout: TIMEOUT });
|
||||
await captureScreenshot(page, '2-test-page');
|
||||
|
||||
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', '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');
|
||||
|
||||
// Click gift button and wait for navigation
|
||||
await page.getByRole('button').filter({ hasText: /gift/i }).click();
|
||||
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');
|
||||
|
||||
// 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', '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 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', '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');
|
||||
|
||||
// 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 });
|
||||
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');
|
||||
|
||||
} 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.
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user