/** * Phase 2 browser chaos — what happens when the browser drops state mid-session? * * Real users: Safari ITP, "Clear browsing data", incognito mode expiring, * extensions that wipe storage on tab close. The app must NEVER white-screen * or expose other users' data when its own state vanishes. */ import { test, expect } from '../../fixtures/test'; import { readStorage, clearLocalStorage, clearAllStorage } from '../../helpers/storage-helpers'; test.describe('Browser chaos — storage purge', () => { test('localStorage.clear() mid-session → next nav goes to /join, no crash', async ({ page, guest, signIn }) => { const g = await guest('Purge1'); await signIn(page, g); await page.goto('/feed'); await expect(page.getByRole('link', { name: 'Galerie' })).toBeVisible(); // Listen for any unhandled page errors so a crash is visible. const errors: Error[] = []; page.on('pageerror', (e) => errors.push(e)); await clearLocalStorage(page); await page.goto('/feed'); // The app may redirect to /join, render an empty feed with a "sign in" prompt, or // surface the join screen inline. Any of these is fine — the assertion is "no crash". expect(errors.filter((e) => !e.message.includes('AbortError'))).toHaveLength(0); // Eventually the user lands somewhere they can recover from. const url = new URL(page.url()); expect(['/join', '/feed', '/recover', '/']).toContain(url.pathname); }); test('cookies cleared mid-session — JWT in localStorage still works (no cookie dependency)', async ({ page, guest, signIn }) => { const g = await guest('Purge2'); await signIn(page, g); await page.goto('/feed'); await page.context().clearCookies(); // The api.ts client reads from localStorage, not cookies, so a /me/context call should still work. const stillAuthed = await page.evaluate(async () => { const res = await fetch('/api/v1/me/context', { headers: { Authorization: `Bearer ${localStorage.getItem('eventsnap_jwt')}` }, }); return res.status; }); expect(stillAuthed).toBe(200); }); test('sessionStorage cleared has no effect on auth (auth lives in localStorage)', async ({ page, guest, signIn }) => { const g = await guest('Purge3'); await signIn(page, g); await page.goto('/feed'); await page.evaluate(() => sessionStorage.clear()); await page.reload(); const storage = await readStorage(page); expect(storage.jwt).toBeTruthy(); // localStorage survived await expect(page.getByRole('link', { name: 'Galerie' })).toBeVisible(); }); test('clearAllStorage on /admin forces re-login', async ({ page, api }) => { const adminJwt = await api.adminLogin(); await page.goto('/'); await page.evaluate((j) => localStorage.setItem('eventsnap_jwt', j), adminJwt); await page.goto('/admin'); await clearAllStorage(page); await page.goto('/admin'); // The admin layout should bounce them to /admin/login when the JWT is gone. await page.waitForURL(/admin\/login|join/, { timeout: 5_000 }); }); test('PIN survives clearAuth (intentional per auth.ts comment)', async ({ page, guest, signIn }) => { const g = await guest('PurgePin'); await signIn(page, g); await page.goto('/account'); // Simulate clearAuth() — clears JWT + user_id but keeps PIN so the user can recover. await page.evaluate(() => { localStorage.removeItem('eventsnap_jwt'); localStorage.removeItem('eventsnap_user_id'); }); const remaining = await readStorage(page); expect(remaining.jwt).toBeNull(); expect(remaining.userId).toBeNull(); expect(remaining.pin).toBe(g.pin); }); });