/** * SSE reconnection after tab background. USER_JOURNEYS.md §17 / edge cases. * * The app closes the EventSource on `document.hidden` and reopens it (minting a * fresh ticket + new EventSource) when the tab becomes visible again — see * frontend/src/lib/sse.ts. We assert the reconnect by counting stream-open * requests rather than event delivery, which keeps the test fast and independent * of the reverse proxy's SSE buffering. */ import { test, expect } from '../../fixtures/test'; /** Count EventSource opens (GET /api/v1/stream?ticket=…), not the ticket POST. */ function trackStreamOpens(page: import('@playwright/test').Page): () => number { let n = 0; page.on('request', (req) => { if (req.method() === 'GET' && req.url().includes('/api/v1/stream?')) n++; }); return () => n; } test.describe('Feed — SSE behavior', () => { test('backgrounding then foregrounding the tab opens a fresh SSE connection', async ({ page, guest, signIn }) => { const g = await guest('SseReconnect'); const streamOpens = trackStreamOpens(page); await signIn(page, g); // lands on /feed, which calls connectSse() on mount // Initial connection established. await expect.poll(streamOpens, { timeout: 10_000 }).toBeGreaterThanOrEqual(1); const afterInitial = streamOpens(); // Background: the visibility handler reads document.hidden, so override that // (not just visibilityState) before dispatching, or the close never fires. await page.evaluate(() => { Object.defineProperty(document, 'hidden', { configurable: true, get: () => true }); Object.defineProperty(document, 'visibilityState', { configurable: true, get: () => 'hidden' }); document.dispatchEvent(new Event('visibilitychange')); }); // Foreground again → connectSse() mints a new ticket and opens a new EventSource. await page.evaluate(() => { Object.defineProperty(document, 'hidden', { configurable: true, get: () => false }); Object.defineProperty(document, 'visibilityState', { configurable: true, get: () => 'visible' }); document.dispatchEvent(new Event('visibilitychange')); }); // A reconnect means a brand-new stream GET beyond the initial one. await expect.poll(streamOpens, { timeout: 10_000 }).toBeGreaterThan(afterInitial); // And the app is still functional. await expect(page.getByRole('link', { name: 'Galerie' })).toBeVisible(); }); });