/** * @file Bulk Gift Recording Test Suite * @description Tests TimeSafari's gift recording functionality under load by creating * multiple gift records in sequence. Limited to 9 gifts to stay under 30-second timeout. * * This test verifies: * 1. Scalability * - System handles multiple gift recordings (9) * - Performance remains stable across iterations * - No data corruption during bulk operations * * 2. Data Integrity * - Each gift has unique identifiers * - All gifts properly stored and retrievable * - No cross-contamination between gift data * * 3. UI/UX Stability * - Interface remains responsive during bulk operations * - Success notifications display correctly * - Alert dismissal works consistently * * Test Flow: * 1. Setup Phase * - Generate arrays of unique strings for titles * - Generate array of random numbers for amounts * - Import User 00 (test account) * * 2. Bulk Recording (9 iterations) * - Navigate to home * - Handle first-time onboarding dialog * - Select recipient (Unnamed/Unknown) * - Fill gift details from arrays * - Sign and submit * - Verify success * - Dismiss notification * - Verify gift in list (optimized) * * Test Data: * - Gift Count: 9 (optimized for timeout limits) * - Title Format: "Gift [unique-string]" * - Amount: Random numbers array * - Recipient: "Unnamed/Unknown" (constant) * * Key Selectors: * - Gift input: '[placeholder="What was given"]' * - Amount input: '[role="spinbutton"]' * - Submit button: '[name="Sign & Send"]' * - Success alert: 'div[role="alert"]' * - Alert dismiss: 'button > svg.fa-xmark' * * Performance Considerations: * - Limited to 9 gifts to avoid timeout * - Handles UI lag between operations * - Manages memory usage during bulk operations * - Optimized navigation: single page.goto() per iteration * - Efficient verification: waits for DOM updates instead of full page reload * * Error Handling: * - Closes onboarding dialog only on first iteration * - Verifies each gift individually * - Maintains operation even if individual recordings fail * * Related Files: * - Gift recording view: src/views/RecordGiftView.vue * - JWT creation: sw_scripts/safari-notifications.js * - Endorser API: src/libs/endorserServer.ts * - Test utilities: ./testUtils.ts * * @see Documentation in usage-guide.md for gift recording workflows * @requires @playwright/test * @requires ./testUtils - For user management and array generation * * @example * ```typescript * // Generate test data arrays * const uniqueStrings = await createUniqueStringsArray(giftCount); * const randomNumbers = await createRandomNumbersArray(giftCount); * * // Record gifts in sequence * for (let i = 0; i < giftCount; i++) { * await page.goto('./'); * await page.getByPlaceholder('What was given').fill(finalTitles[i]); * await page.getByRole('spinbutton').fill(finalNumbers[i].toString()); * await page.getByRole('button', { name: 'Sign & Send' }).click(); * } * ``` */ import { test, expect } from '@playwright/test'; import { importUserFromAccount, createUniqueStringsArray, createRandomNumbersArray } from './testUtils'; import { createPerformanceCollector, attachPerformanceData, assertPerformanceMetrics } from './performanceUtils'; test('Record 9 new gifts', async ({ page }, testInfo) => { test.slow(); // Set timeout longer // STEP 1: Initialize the performance collector const perfCollector = await createPerformanceCollector(page); const giftCount = 9; const standardTitle = 'Gift '; const finalTitles: string[] = []; const finalNumbers: number[] = []; // STEP 2: Create arrays for field input await perfCollector.measureUserAction('generate-test-data', async () => { const uniqueStrings = await createUniqueStringsArray(giftCount); const randomNumbers = await createRandomNumbersArray(giftCount); // Populate arrays for (let i = 0; i < giftCount; i++) { finalTitles.push(standardTitle + uniqueStrings[i]); finalNumbers.push(randomNumbers[i]); } }); // STEP 3: Import user 00 await perfCollector.measureUserAction('import-user-account', async () => { await importUserFromAccount(page, '00'); }); // STEP 4: Initial navigation and metrics collection await perfCollector.measureUserAction('initial-navigation', async () => { await page.goto('./'); }); const initialMetrics = await perfCollector.collectNavigationMetrics('initial-home-load'); await testInfo.attach('initial-page-load-metrics', { contentType: 'application/json', body: JSON.stringify(initialMetrics, null, 2) }); // STEP 5: Record new gifts with optimized navigation for (let i = 0; i < giftCount; i++) { // Only navigate on first iteration if (i === 0) { await perfCollector.measureUserAction(`navigate-home-iteration-${i + 1}`, async () => { await page.goto('./', { waitUntil: 'networkidle' }); }); await perfCollector.measureUserAction('close-onboarding', async () => { await page.getByTestId('closeOnboardingAndFinish').click(); }); } else { // For subsequent iterations, just wait for the page to be ready await perfCollector.measureUserAction(`wait-for-page-ready-iteration-${i + 1}`, async () => { await page.waitForLoadState('domcontentloaded'); }); } await perfCollector.measureUserAction(`select-recipient-iteration-${i + 1}`, async () => { await page.getByRole('button', { name: 'Person' }).click(); await page.getByRole('listitem').filter({ hasText: 'Unnamed' }).locator('svg').click(); }); await perfCollector.measureUserAction(`fill-gift-details-iteration-${i + 1}`, async () => { await page.getByPlaceholder('What was given').fill(finalTitles[i]); await page.getByRole('spinbutton').fill(finalNumbers[i].toString()); }); await perfCollector.measureUserAction(`submit-gift-iteration-${i + 1}`, async () => { await page.getByRole('button', { name: 'Sign & Send' }).click(); // Wait for success and dismiss await expect(page.getByText('That gift was recorded.')).toBeVisible(); await page.locator('div[role="alert"] button > svg.fa-xmark').click(); }); // Optimized verification: use real-time DOM monitoring instead of page reload await perfCollector.measureUserAction(`verify-gift-in-list-iteration-${i + 1}`, async () => { // Wait for any network activity to settle after gift submission await page.waitForLoadState('networkidle', { timeout: 3000 }); // Real-time DOM monitoring: wait for the gift to appear in the activity list await page.waitForFunction( (giftTitle) => { const activityList = document.querySelector('ul#listLatestActivity'); if (!activityList) return false; const listItems = activityList.querySelectorAll('li'); for (const item of listItems) { if (item.textContent?.includes(giftTitle)) { return true; } } return false; }, finalTitles[i], { timeout: 8000 } ); // Additional verification: ensure the gift is actually visible await expect(page.locator('ul#listLatestActivity li') .filter({ hasText: finalTitles[i] }) .first()) .toBeVisible({ timeout: 2000 }); }); } // STEP 6: Attach and validate performance data const { webVitals, performanceReport, summary } = await attachPerformanceData(testInfo, perfCollector); // Calculate average navigation time only if we have metrics if (perfCollector.navigationMetrics.length > 0) { const avgNavigationTime = perfCollector.navigationMetrics.reduce((sum, nav) => sum + nav.metrics.loadComplete, 0) / perfCollector.navigationMetrics.length; assertPerformanceMetrics(webVitals, initialMetrics, avgNavigationTime); } else { // If no navigation metrics, just validate web vitals assertPerformanceMetrics(webVitals, initialMetrics, 0); } });