/** * Phase 2 browser chaos — multi-tab and cross-user isolation in the same * browser process. */ import { test, expect } from '../../fixtures/test'; test.describe('Browser chaos — multi-tab', () => { test('same user in two tabs — SSE delivers to both', async ({ page, context, guest, signIn }) => { const g = await guest('Twin'); await signIn(page, g); await page.goto('/feed'); const tab2 = await context.newPage(); await signIn(tab2, g); await tab2.goto('/feed'); // Both tabs should mount the bottom nav. await expect(page.getByRole('link', { name: 'Galerie' })).toBeVisible(); await expect(tab2.getByRole('link', { name: 'Galerie' })).toBeVisible(); await tab2.close(); }); test('two different users in separate browser contexts have isolated localStorage', async ({ browser, guest }) => { const a = await guest('IsoA'); const b = await guest('IsoB'); const ctxA = await browser.newContext(); const ctxB = await browser.newContext(); const pageA = await ctxA.newPage(); const pageB = await ctxB.newPage(); await pageA.goto('http://localhost:3101/'); await pageA.evaluate(({ jwt, pin, userId, name }) => { localStorage.setItem('eventsnap_jwt', jwt); localStorage.setItem('eventsnap_pin', pin); localStorage.setItem('eventsnap_user_id', userId); localStorage.setItem('eventsnap_display_name', name); }, { jwt: a.jwt, pin: a.pin, userId: a.userId, name: a.displayName }); await pageB.goto('http://localhost:3101/'); await pageB.evaluate(({ jwt, pin, userId, name }) => { localStorage.setItem('eventsnap_jwt', jwt); localStorage.setItem('eventsnap_pin', pin); localStorage.setItem('eventsnap_user_id', userId); localStorage.setItem('eventsnap_display_name', name); }, { jwt: b.jwt, pin: b.pin, userId: b.userId, name: b.displayName }); // Each context sees only its own user. const aUid = await pageA.evaluate(() => localStorage.getItem('eventsnap_user_id')); const bUid = await pageB.evaluate(() => localStorage.getItem('eventsnap_user_id')); expect(aUid).toBe(a.userId); expect(bUid).toBe(b.userId); expect(aUid).not.toBe(bUid); await ctxA.close(); await ctxB.close(); }); test('localStorage is shared across tabs of the same context (real browser behavior)', async ({ context, guest, signIn }) => { // Real browsers share localStorage across tabs of the same origin. Tab A's // removeItem is instantly visible in tab B's localStorage. The UX gap to // document is that tab B's React/Svelte state isn't *re-rendered* until the // next API call or storage-event subscription — which the app doesn't // currently listen for. const g = await guest('LogoutSync'); const pageA = await context.newPage(); const pageB = await context.newPage(); await signIn(pageA, g); await signIn(pageB, g); await pageA.evaluate(() => { localStorage.removeItem('eventsnap_jwt'); localStorage.removeItem('eventsnap_user_id'); }); // Both tabs' localStorage should now show the JWT removed (shared origin). const aGone = await pageA.evaluate(() => !localStorage.getItem('eventsnap_jwt')); const bGone = await pageB.evaluate(() => !localStorage.getItem('eventsnap_jwt')); expect(aGone).toBe(true); expect(bGone).toBe(true); // Tab B's URL: either stayed on /feed (no storage event listener) or has // already routed to /join (a route-guard reactive subscription noticed). // Both are valid; assert it's one or the other rather than coupling to // either specific behavior. expect(['/feed', '/join'].some((p) => pageB.url().includes(p))).toBe(true); }); });