/** * @fileoverview Tests for the new activity/offers system in TimeSafari * * CRITICAL UNDERSTANDING: Offer Acknowledgment System * =================================================== * * This file tests the offer acknowledgment mechanism, which was clarified through * systematic debugging investigation. Key findings: * * 1. POINTER-BASED TRACKING: * - TimeSafari uses `lastAckedOfferToUserJwtId` to track the last acknowledged offer * - Offers with IDs newer than this pointer are considered "new" and counted * - The UI shows count of offers newer than the pointer * * 2. TWO DISMISSAL MECHANISMS: * a) COMPLETE DISMISSAL (used in this test): * - Triggered by: Expanding offers section (clicking chevron) * - Method: expandOffersToUserAndMarkRead() * - Action: Sets lastAckedOfferToUserJwtId = newOffersToUser[0].jwtId (newest) * - Result: ALL offers marked as read, count becomes 0 (hidden) * * b) SELECTIVE DISMISSAL: * - Triggered by: Clicking "Keep all above as new offers" * - Method: markOffersAsReadStartingWith(jwtId) * - Action: Sets lastAckedOfferToUserJwtId = nextOffer.jwtId (partial) * - Result: Only offers above clicked offer marked as read * * 3. BROWSER COMPATIBILITY: * - Initially appeared to be Chromium-specific issue * - Investigation revealed test logic error, not browser incompatibility * - Both Chromium and Firefox now pass consistently * * @author Matthew Raymer * @since Investigation completed 2024-12-27 */ import { test, expect } from '@playwright/test'; import { importUser, generateNewEthrUser, switchToUser } from './testUtils'; test('New offers for another user', async ({ page }) => { const user01Did = await generateNewEthrUser(page); await page.goto('./'); await page.getByTestId('closeOnboardingAndFinish').click(); await expect(page.getByTestId('newDirectOffersActivityNumber')).toBeHidden(); await importUser(page, '00'); await page.goto('./contacts'); await page.getByPlaceholder('URL or DID, Name, Public Key').fill(user01Did + ', A Friend'); await expect(page.locator('button > svg.fa-plus')).toBeVisible(); await page.locator('button > svg.fa-plus').click(); await expect(page.locator('div[role="alert"] span:has-text("Contact Added")')).toBeVisible(); await page.locator('div[role="alert"] button:has-text("No")').click(); // don't register await page.locator('div[role="alert"] button > svg.fa-xmark').click(); // dismiss info alert await expect(page.locator('div[role="alert"] button > svg.fa-xmark')).toBeHidden(); // ensure alert is gone // show buttons to make offers directly to people await page.getByRole('button').filter({ hasText: /See Actions/i }).click(); // make an offer directly to user 1 // Generate a random string of 3 characters, skipping the "0." at the beginning const randomString1 = Math.random().toString(36).substring(2, 5); await page.getByTestId('offerButton').click(); await page.getByTestId('inputDescription').fill(`help of ${randomString1} from #000`); await page.getByTestId('inputOfferAmount').fill('1'); await page.getByRole('button', { name: 'Sign & Send' }).click(); await expect(page.getByText('That offer was recorded.')).toBeVisible(); await page.locator('div[role="alert"] button > svg.fa-xmark').click(); // dismiss info alert await expect(page.locator('div[role="alert"] button > svg.fa-xmark')).toBeHidden(); // ensure alert is gone // make another offer to user 1 const randomString2 = Math.random().toString(36).substring(2, 5); await page.getByTestId('offerButton').click(); await page.getByTestId('inputDescription').fill(`help of ${randomString2} from #000`); await page.getByTestId('inputOfferAmount').fill('3'); await page.getByRole('button', { name: 'Sign & Send' }).click(); await expect(page.getByText('That offer was recorded.')).toBeVisible(); await page.locator('div[role="alert"] button > svg.fa-xmark').click(); // dismiss info alert await expect(page.locator('div[role="alert"] button > svg.fa-xmark')).toBeHidden(); // ensure alert is gone // as user 1, go to the home page and check that two offers are shown as new console.log('[DEBUG] 60-new-activity: Switching to user01Did:', user01Did); await switchToUser(page, user01Did); console.log('[DEBUG] 60-new-activity: Switch completed, navigating to home page'); await page.goto('./'); console.log('[DEBUG] 60-new-activity: Navigated to home page'); // Wait for page to load completely await page.waitForLoadState('networkidle'); console.log('[DEBUG] 60-new-activity: Page load completed'); // Add systematic debugging for the newDirectOffersActivityNumber element console.log('[DEBUG] 60-new-activity: Looking for newDirectOffersActivityNumber element'); // Check if the element exists at all const offerNumElem = page.getByTestId('newDirectOffersActivityNumber'); const elementExists = await offerNumElem.count(); console.log('[DEBUG] 60-new-activity: newDirectOffersActivityNumber element count:', elementExists); // Check if parent containers exist const offerContainer = page.locator('[data-testid="newDirectOffersActivityNumber"]').locator('..'); const containerExists = await offerContainer.count(); console.log('[DEBUG] 60-new-activity: Parent container exists:', containerExists > 0); if (containerExists > 0) { const containerVisible = await offerContainer.isVisible(); console.log('[DEBUG] 60-new-activity: Parent container visible:', containerVisible); } // Look for any elements with test IDs that might be related const allTestIds = page.locator('[data-testid]'); const testIdCount = await allTestIds.count(); console.log('[DEBUG] 60-new-activity: Found', testIdCount, 'elements with test IDs'); for (let i = 0; i < Math.min(testIdCount, 20); i++) { const element = allTestIds.nth(i); const testId = await element.getAttribute('data-testid'); const isVisible = await element.isVisible(); const textContent = await element.textContent(); console.log(`[DEBUG] 60-new-activity: TestID ${i}: "${testId}" (visible: ${isVisible}, text: "${textContent?.trim()}")`); } // Check for the specific elements mentioned in HomeView.vue const newOffersSection = page.locator('div:has([data-testid="newDirectOffersActivityNumber"])'); const newOffersSectionExists = await newOffersSection.count(); console.log('[DEBUG] 60-new-activity: New offers section exists:', newOffersSectionExists > 0); // Check for loading states const loadingIndicators = page.locator('.fa-spinner, .fa-spin, [class*="loading"]'); const loadingCount = await loadingIndicators.count(); console.log('[DEBUG] 60-new-activity: Loading indicators found:', loadingCount); // Wait a bit longer and check again console.log('[DEBUG] 60-new-activity: Waiting additional 3 seconds for offers to load'); await page.waitForTimeout(3000); const elementExistsAfterWait = await offerNumElem.count(); console.log('[DEBUG] 60-new-activity: newDirectOffersActivityNumber element count after wait:', elementExistsAfterWait); if (elementExistsAfterWait === 0) { console.log('[DEBUG] 60-new-activity: Element still not found, taking screenshot'); await page.screenshot({ path: 'debug-missing-offers-element.png', fullPage: true }); console.log('[DEBUG] 60-new-activity: Screenshot saved as debug-missing-offers-element.png'); // Check page URL and state const currentUrl = page.url(); console.log('[DEBUG] 60-new-activity: Current URL:', currentUrl); // Check if we're actually logged in as the right user const didElement = page.getByTestId('didWrapper'); const didElementExists = await didElement.count(); if (didElementExists > 0) { const currentDid = await didElement.textContent(); console.log('[DEBUG] 60-new-activity: Current DID on page:', currentDid?.trim()); console.log('[DEBUG] 60-new-activity: Expected DID:', user01Did); } } let offerNumElemForTest = page.getByTestId('newDirectOffersActivityNumber'); await expect(offerNumElemForTest).toHaveText('2'); // click on the number of new offers to go to the list page await offerNumElemForTest.click(); await expect(page.getByText('New Offers To You', { exact: true })).toBeVisible(); await page.getByTestId('showOffersToUser').locator('div > svg.fa-chevron-right').click(); // note that they show in reverse chronologicalorder await expect(page.getByText(`help of ${randomString2} from #000`)).toBeVisible(); await expect(page.getByText(`help of ${randomString1} from #000`)).toBeVisible(); /** * OFFER ACKNOWLEDGMENT MECHANISM DOCUMENTATION * * TimeSafari uses a pointer-based system to track which offers are "new": * - `lastAckedOfferToUserJwtId` stores the ID of the last acknowledged offer * - Offers newer than this pointer are considered "new" and counted * * Two dismissal mechanisms exist: * 1. COMPLETE DISMISSAL: Expanding the offers section calls expandOffersToUserAndMarkRead() * which sets lastAckedOfferToUserJwtId = newOffersToUser[0].jwtId (newest offer) * Result: ALL offers marked as read, count goes to 0 * * 2. SELECTIVE DISMISSAL: "Keep all above" calls markOffersAsReadStartingWith(jwtId) * which sets lastAckedOfferToUserJwtId = nextOffer.jwtId (partial dismissal) * Result: Only offers above the clicked offer are marked as read * * This test uses mechanism #1 (expansion) for complete dismissal. * The expansion already happened when we clicked the chevron above. */ console.log('[DEBUG] 60-new-activity: Offers section already expanded, marking all offers as read'); console.log('[DEBUG] 60-new-activity: Expansion calls expandOffersToUserAndMarkRead() -> sets lastAckedOfferToUserJwtId to newest offer'); // now see that all offers are dismissed since we expanded the section console.log('[DEBUG] 60-new-activity: Going back to home page to check offers are dismissed'); await page.goto('./'); // Add debugging for the final check await page.waitForLoadState('networkidle'); console.log('[DEBUG] 60-new-activity: Page loaded for final check'); const offerNumElemFinal = page.getByTestId('newDirectOffersActivityNumber'); const elementExistsFinal = await offerNumElemFinal.count(); console.log('[DEBUG] 60-new-activity: newDirectOffersActivityNumber element count (final check):', elementExistsFinal); if (elementExistsFinal > 0) { const finalIsVisible = await offerNumElemFinal.isVisible(); const finalText = await offerNumElemFinal.textContent(); console.log('[DEBUG] 60-new-activity: Final element visible:', finalIsVisible); console.log('[DEBUG] 60-new-activity: Final element text:', finalText); if (finalIsVisible) { console.log('[DEBUG] 60-new-activity: Element is still visible when it should be hidden'); await page.screenshot({ path: 'debug-offers-still-visible-final.png', fullPage: true }); console.log('[DEBUG] 60-new-activity: Screenshot saved as debug-offers-still-visible-final.png'); } } // wait until the list with ID listLatestActivity has at least one visible item await page.locator('#listLatestActivity li').first().waitFor({ state: 'visible' }); console.log('[DEBUG] 60-new-activity: Activity list loaded'); await expect(page.getByTestId('newDirectOffersActivityNumber')).toBeHidden(); });