Files
EventSnap/e2e/specs/08-browser-chaos/storage-purge.spec.ts
fabi bbdfae09a0 chore(e2e): add ESLint + Prettier; fix real findings; dedupe BASE
The Playwright suite had no linter and no formatter — only tsc. Add flat-config ESLint
(typescript-eslint, type-aware) and Prettier (2-space, matching the suite's style).

Rules keep the ones that catch real TEST bugs and drop the noise:
  - no-floating-promises KEPT — an un-awaited request/assertion can let a test end before it runs,
    passing vacuously. It caught one: the SSE reader loop in sse-listener is now explicitly `void`.
  - no-unused-vars KEPT — caught three dead bindings (an unused adminToken fixture arg, an unused
    `api` arg, an unused JPEG_MAGIC import), all removed.
  - no-explicit-any OFF — all test code; `any` is the honest type for an untyped res.json() body or
    a page.evaluate() return.
  - no-empty-pattern OFF — Playwright's dependency-free fixtures are `async ({}, use) => {}`.

Refactor: `const BASE = process.env.E2E_FRONTEND_URL ?? '...'` was redeclared verbatim in 23
files — extracted to helpers/env.ts and imported, so a port/scheme change is one edit not a sweep.

Then `prettier --write`. Verified: eslint clean, tsc clean, prettier clean, desktop suite 210
passed / 1 skipped. (One mobile spec flaked once under retries:0 — a pre-existing cross-test
reflow-timing vector from the flakiness audit, not this change: the each-key edit is stable across
16 isolated runs and a clean full mobile re-run.)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 20:45:59 +02:00

109 lines
3.7 KiB
TypeScript

/**
* 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);
});
});