# EventSnap E2E Suite Playwright-driven end-to-end tests for the EventSnap stack. The suite spins up an isolated docker-compose stack on ports `:3101` (Caddy → frontend + backend) and `:55432` (Postgres), and exercises the SvelteKit frontend against a real Rust backend with rate limits and quotas disabled. **Phases 1, 2, and 3-mobile-gestures are landed**: - **Phase 1** — happy-path coverage of every documented user journey, plus a smoke matrix across nine browser/UA profiles to catch engine-level divergences. - **Phase 2** — adversarial inputs (XSS, SQL-injection, JWT forgery, MIME spoofing, oversize, brute-force) and browser chaos (storage purge, offline/slow-3G, multi-tab, clock skew, no-JS, quota exhaustion). - **Phase 3 (gestures only)** — touch-target audit, safe-area structural check, long-press → ContextSheet, double-tap → like, viewport reflow, plus `test.fixme` stubs for planned gestures (lightbox swipe, swipe-down dismiss, pull-to-refresh). Phase 3 real-device compat (Android emulator + Samsung Internet via `connectOverCDP`, BrowserStack), visual regression, and a11y audits are sketched in the **Roadmap** at the bottom. ## Quickstart ```bash cd e2e npm install npm run install:browsers # one-time: ~500 MB across chromium/firefox/webkit # 1. Boot the test stack (rebuilds backend + frontend Docker images) npm run stack:up # 2. Wait ~20s for migrations + warmup, then run tests npm run test:e2e # full Phase 1 suite on chromium-desktop npm run test:e2e:smoke # cross-UA smoke matrix (~9 projects × 1 test) npm run test:e2e:ui # interactive Playwright UI mode # 3. After: tear the stack down (deletes volumes) npm run stack:down ``` The CI workflow at `.github/workflows/e2e.yml` runs both jobs on every PR. ## What's tested Every spec covers a journey from [`docs/USER_JOURNEYS.md`](../docs/USER_JOURNEYS.md) or a security/chaos scenario. One folder per area: | Folder | Phase | Journeys / Topic | Tests | Notes | |---|---|---|---|---| | `specs/01-auth/` | 1 | §1, §2, §3, §11, §15 | 13 | Join, recover, PIN lockout, admin login, leave event. | | `specs/02-upload/` | 1 | §5, §6, §18 | 5 | Gallery picker, multi-file, rate-limit, admin toggle. | | `specs/03-feed/` | 1 | §7, §8, §17 | 5 | Like/comment SSE, filter chips, SSE reconnect. | | `specs/04-host/` | 1 | §9 | 5 | Event lock, ban/unban, role change. | | `specs/05-admin/` | 1 | §11, §16 | 11 | Config validation, foundational auth guards, stats. | | `specs/06-export/` | 1 | §12 | 3 | Status, release, download stub. | | `specs/__smoke/` | 1 | (matrix) | 1 × 9 UAs | `@smoke`-tagged happy-path on every UA project. | | `specs/07-adversarial/` | **2** | Input attacks, file upload boundaries, JWT forgery, brute-force, deep authorization, small DDoS | ~40 | See breakdown below. | | `specs/08-browser-chaos/` | **2** | Storage purge, IndexedDB, offline/slow-3G, multi-tab, no-JS, clock skew, quota | ~20 | See breakdown below. | | `specs/09-mobile/` | **3** | Touch-target audit, safe-area, long-press, double-tap, viewport reflow, fixme stubs | 23 | Runs only on `chromium-mobile` (Pixel 7 viewport). See below. | ### Phase 2 — adversarial (`specs/07-adversarial/`) - **`xss-injection.spec.ts`** — 13 tests. Six XSS payloads × display-name path + four SQLi patterns + length/encoding edge cases (NUL byte, RTL override, caption overflow). Asserts `window.__xssFired` never gets set and no `dialog` event fires. - **`ui-rendering.spec.ts`** — 2 tests. Belt-and-braces: even when a script- payload sits in localStorage as the user's display name, rendering through `/account` keeps it as text. - **`file-upload-attacks.spec.ts`** — 9 tests. ELF body claimed as JPEG, oversize image vs `max_image_size_mb`, zero-byte, missing file field, path-traversal filename, NUL filename, `application/*` declared category bypass, SVG-with-script. - **`auth-tampering.spec.ts`** — 8 tests. `alg:none` forging admin role, signature tamper, payload tamper with original signature, logged-out session reuse, header without `Bearer `, missing Authorization, PIN brute-force lockout, admin password brute-force (documented finding — no lockout today, bcrypt cost is the only defense). - **`authorization-deep.spec.ts`** — 6 tests. Cross-user comment delete, banned user across like/comment/feed-read, host→admin escalation attempts. - **`ddos.spec.ts`** — 4 small-scale abuse tests. 20 parallel /join, 10 MB comment body, 10 concurrent SSE streams, malformed JSON. ### Phase 2 — browser chaos (`specs/08-browser-chaos/`) - **`storage-purge.spec.ts`** — 5 tests. `localStorage.clear()` mid-session, cookies cleared (JWT in localStorage still works), sessionStorage cleared, admin force-relogin, PIN intentionally survives clearAuth. - **`indexeddb.spec.ts`** — 2 tests. Drop all IDB databases mid-session; stub IDB to undefined before navigation. - **`offline-network.spec.ts`** — 4 tests. `setOffline(true)` → reconnect, slow-3G via `page.route` delay, intermittent 503s, 429 from server (no infinite retry storm). - **`multi-tab.spec.ts`** — 3 tests. Same user two tabs, two users two contexts (storage isolated), logout in tab A doesn't sync to tab B (documented gap). - **`environment.spec.ts`** — 5 tests. JS disabled, localStorage quota exhausted, hostile CSS hiding nav, clock skew ±1h / -2d. Pending tests covering features that need a Node-side multipart upload helper are marked `test.fixme` and will activate when that helper lands. ## Browser & UA matrix | Project | Engine | UA / Device | Why | |---|---|---|---| | `chromium-desktop` | Chromium | Desktop Chrome | Baseline. Full suite runs here. | | `chromium-pixel7` | Chromium | Pixel 7 device descriptor | Chrome Android. | | `chromium-galaxy-s22` | Chromium | Galaxy viewport + Samsung phone UA | Chrome on Samsung hardware. | | `samsung-internet` | Chromium | Galaxy viewport + SamsungBrowser UA | **Tier-A Samsung Internet baseline.** | | `edge-android` | Chromium | Pixel viewport + EdgA UA | Edge Mobile (Blink-based). | | `chrome-ios` | Chromium | iPhone viewport + CriOS UA | Chrome iOS (actually WebKit, but UA differs). | | `webkit-iphone` | WebKit | iPhone 14 Pro | Real iOS Safari engine. | | `firefox-android` | Firefox | Pixel viewport + Firefox Android UA | Gecko engine. | | `firefox-desktop` | Firefox | Desktop Firefox | FF-specific quirks. | Only the `@smoke` happy-path runs across all projects (controlled by `grep` in `playwright.config.ts`). The full Phase 1 suite is `chromium-desktop`-only by default to keep CI under 15 min. ### Samsung Internet — three escalation tiers Samsung Internet ships on every Galaxy phone (~5% of mobile traffic in DE). It's **Blink-based**, so Tier-A catches ~90% of regressions. Real Samsung divergences (Smart Switch save-data mode, dark-mode injection, custom autoplay, in-browser ad blocking) are only reproducible at Tier B+: - **Tier A** *(this repo, free, in CI)*: Playwright Chromium with the Samsung Internet user-agent + Galaxy viewport. See the `samsung-internet` project in `playwright.config.ts`. - **Tier B** *(free, manual, future)*: Android Studio emulator on Linux → install Samsung Internet APK → enable `--remote-debugging-port=9222` → `chromium.connectOverCDP('http://localhost:9222')`. Setup docs live in `docs/samsung-emulator.md` (to be written). - **Tier C** *(paid, optional)*: BrowserStack or LambdaTest cloud devices. Real Galaxy S22/S23 hardware via Playwright's cloud integration. ## Test isolation Every test runs against a **freshly truncated database**: 1. `global-setup.ts` waits for `/health`, logs in admin, and disables every rate-limit and quota toggle via `PATCH /admin/config`. 2. The auto-fixture `truncate` in `fixtures/test.ts` calls `POST /api/v1/admin/__truncate` before every test. 3. The truncate endpoint is only registered when the backend is started with `EVENTSNAP_TEST_MODE=1` (see `backend/src/main.rs` and `backend/src/handlers/test_admin.rs`). Production builds return 404. Single-worker by design (`workers: 1` in the config). Per-worker isolated DBs are a Phase-2+ change. ## Architecture ``` e2e/ ├── docker-compose.test.yml # Isolated test stack: db :55432, caddy :3101 ├── Caddyfile.test # Proxies /api/* /media/* /health to backend ├── playwright.config.ts # UA matrix + smoke grep ├── global-setup.ts # admin login, rate-limit disable ├── global-teardown.ts # (no-op; use `npm run stack:down`) ├── fixtures/ │ ├── api-client.ts # Typed wrapper over /api/v1/* │ ├── db.ts # Direct Postgres escape hatch (locked-PIN, etc.) │ ├── test.ts # Central test.extend (guest, host, signIn fixtures) │ └── media/ # sample.jpg, sample.mp4, not-an-image.jpg ├── helpers/ │ ├── sse-listener.ts # Async SSE iterator with waitForEvent() │ ├── storage-helpers.ts # localStorage/sessionStorage helpers │ └── fake-media.ts # Camera permissions (Chromium only) ├── page-objects/ │ ├── join-page.ts # /join │ ├── recover-page.ts # /recover │ ├── admin-login-page.ts # /admin/login │ ├── feed-page.ts # /feed + bottom nav │ ├── upload-sheet.ts # UploadSheet.svelte + /upload │ ├── lightbox.ts # LightboxModal.svelte │ ├── account-page.ts # /account │ ├── host-dashboard.ts # /host │ ├── admin-dashboard.ts # /admin │ └── export-page.ts # /export └── specs/ ├── __smoke/ # @smoke cross-UA matrix (1 spec) ├── 01-auth/ ├── 02-upload/ ├── 03-feed/ ├── 04-host/ ├── 05-admin/ └── 06-export/ ``` ## Debugging a failure - `npm run test:e2e:ui` — interactive UI with time-travel and selector probe. - `npm run test:e2e:headed` — watch the browser run live. - `npm run test:e2e:debug` — Playwright inspector with breakpoints. - `npm run stack:logs` — tail backend + Postgres logs during a failure. - `playwright-report/index.html` — opens the HTML report (auto-generated on every run). - Trace files (`test-results/**/trace.zip`) drag-and-drop into `https://trace.playwright.dev`. ## Conventions - **One assertion per `expect`**. Bundling multiple expects in one statement loses the line-level failure context. - **Wait on data, not time**. Use `expect.poll` for DB checks; never `waitForTimeout` in production specs. - **`@smoke` tag** on each suite's happiest path so the matrix run stays under 2 min. - **`test.fixme`** for features that need infrastructure not yet built (Node-side multipart upload helper, real video fixtures, etc.). Fixme tests don't fail the suite but show up in the report. - **Page objects own selectors**. Specs never use raw locators. - **German text in assertions** is fine — it's not going to change frequently. When it does, the page object is the only file to update. ## Roadmap ### Phase 2 — Adversarial & browser chaos ✅ landed See the **What's tested** table above and the per-file breakdown. Known findings surfaced (documented in tests, not silent failures): 1. `/admin/login` has no rate-limit or lockout — bcrypt cost is the only defense. 2. `localStorage` 'storage' event is not listened for, so logout in tab A doesn't synchronously sign out tab B (the next 401 from any API call clears it). 3. SVG uploads currently pass the magic-byte check (depends on `infer`'s detection coverage) — consider adding `X-Content-Type-Options: nosniff` + CSP on `/media/*` if SVGs are ever expected as user content. ### Phase 3 — Mobile gestures (`specs/09-mobile/`) ✅ landed Runs only on the `chromium-mobile` project (Pixel 7 device descriptor with `hasTouch` and `isMobile`). The `chromium-desktop` project explicitly ignores this folder via `testIgnore` in [playwright.config.ts](playwright.config.ts). - **`touch-targets.spec.ts`** — 4 tests. Audits ≥ 44×44 px on bottom nav, FAB, join submit, admin-login submit, PIN-modal buttons. Uses `expect.soft` so a single failure surfaces the actual bounding-box dimensions instead of stopping the suite. - **`safe-area.spec.ts`** — 4 tests. Asserts `env(safe-area-inset-bottom)` is present in the inline style of every bottom-anchored UI element (bottom nav, UploadSheet, ContextSheet), and that the nav stays flush with the viewport bottom on a no-notch emulated device. - **`gestures-longpress.spec.ts`** — 3 tests. A 600 ms hold on a FeedListCard opens the ContextSheet; a 200 ms tap does not; the click-suppression logic prevents the lightbox from also opening at pointer-up. Driven via `page.mouse.down/up` because the `longpress` action listens for pointer events (mouse/touch/pen unified). - **`gestures-doubletap.spec.ts`** — 2 tests. Double-tap on a feed card image button records a like; double-tap inside the lightbox triggers the heart-burst animation and records a like. Assertions read the like count back via `/api/v1/feed` so they don't couple to specific badge markup. - **`viewport-reflow.spec.ts`** — 5 tests. Portrait, landscape, narrow (320×568), phablet (480×1024) — each asserts the bottom nav is visible, the FAB stays roughly centered, and there's no horizontal overflow on ``. Plus a rotation test that confirms auth survives a viewport resize. - **`planned-gestures.spec.ts`** — 5 **`test.fixme`** stubs documenting the contracts for gestures from journey §17 that aren't shipped yet (lightbox swipe L/R, swipe-down to dismiss UploadSheet, pull-to-refresh, long-press on a comment). Flip `test.fixme` to `test` when wiring each gesture. #### Driving gestures: the `helpers/touch.ts` module - `longPress(page, locator, durationMs)` — holds the pointer down for the duration. Default 600 ms beats the action's 500 ms threshold. - `doubleTap(page, locator)` — two `mouse.down/up` pairs within the `doubletap` action's 300 ms window. - `swipe(page, from, to, steps)` — gradual mouse-driven move (used by the fixme stubs once swipe gestures land). - `inlineStyle(locator)` / `computedStyle(locator, prop)` — read raw `style` attributes (where `env(...)` strings live) and computed values. ### Phase 3 — Real-device compat & visual / a11y (not landed) - Long-press own/other post, swipe lightbox L/R, swipe-down dismiss, pull-to-refresh, double-tap like. - Safe-area inset visual diff on iPhone notch. - Touch-target ≥ 44 px audit. - Tier B Samsung Internet via `connectOverCDP` on Android Studio emulator. - Tier C BrowserStack integration (paid, optional). - `@axe-core/playwright` accessibility audits. - Visual regression with screenshot diffs. ### Out of scope (handed to other tools) - Load testing → k6 / Vegeta. - API contract testing → backend `cargo test` integration tests. - Static asset auditing → Lighthouse CI.