Files
EventSnap/e2e/playwright.config.ts
fabi 3f74c65787 ci: run the checks that only ran on a laptop; mobile in CI; retries 0
CI ran Playwright (chromium-desktop) + the dependency audit and nothing else. cargo test,
clippy, the frontend vitest suite, svelte-check and the e2e typecheck were all green on a
developer's machine and gated NOTHING.

  checks.yml (new): cargo test (with a Postgres service; --test-threads bounded so the
    sqlx per-test DBs can't exhaust connections) + clippy; vitest + svelte-check; e2e tsc.
  e2e.yml: also run the chromium-mobile project — 22 tests (focus traps, touch targets,
    safe-area, viewport reflow) that never ran in CI on a phone-first app.
  playwright.config: widen webkit-iphone beyond @smoke (2 specs) to the core guest journeys
    (auth/upload/feed) — iOS Safari is the PRIMARY browser here; and retries 2 → 0.

retries:0 is deliberate and against the usual advice: this repo's real bugs are races, a
race is indistinguishable from a flake from outside, and a retry resolves that ambiguity
toward "flake" every time — it took a real 3%-flaky product bug from 1-in-33 to 1-in-37000,
green. A flake here is a bug report.

Not gated: cargo fmt (the tree has never been rustfmt'd — a 112-file reformat would bury
every real diff; do it separately). WebKit widening is unverified locally (needs libavif16
via sudo), so its first CI run may surface real iOS bugs.

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

175 lines
7.3 KiB
TypeScript

import { defineConfig, devices } from '@playwright/test';
/**
* EventSnap E2E config.
*
* Defaults assume the test stack is already up (`npm run stack:up`). The
* stack is *not* started by webServer because Playwright would clobber the
* compose volumes between retries. CI calls `npm run stack:up` once before
* `npm run test:e2e` runs.
*
* UA matrix: the `@smoke` happy-path runs across every project to catch
* engine-level divergences. The rest of the suite only runs against
* `chromium-desktop` to keep the wall-clock reasonable.
*/
// camera/microphone/clipboard are Chromium-only permissions; passing them to
// firefox/webkit projects throws "Unknown permission: camera" and fails the
// whole test before it runs. Granted per-Chromium-project below instead of in
// the global `use` block.
const CHROMIUM_PERMISSIONS = ['camera', 'microphone', 'clipboard-read', 'clipboard-write'];
export default defineConfig({
testDir: './specs',
outputDir: './test-results',
fullyParallel: false, // Single shared backend → tests TRUNCATE between, so don't run in parallel.
forbidOnly: !!process.env.CI,
// NO RETRIES — not even in CI. This is deliberate and it is the opposite of the usual advice.
//
// The standard case for retries is "the environment is flaky, the product isn't." That argument
// does not hold here. This backend's real bugs ARE races (the last several fixes were all export
// concurrency), and from the outside a race is indistinguishable from a flake — so a retry
// resolves that ambiguity, silently, in favour of "flake", every single time.
//
// The numbers make it concrete. A test that fails 3% of runs reports a bug roughly 1 run in 33.
// Under `retries: 2` it fails the build only when it fails three times in a row: ~1 in 37,000.
// We had exactly such a test, and it was reporting a REAL user-facing bug (a suggestion button
// that committed on mousedown and destroyed itself mid-click). Retries would have buried it
// forever, and buried it GREEN, so nobody would even have seen an amber.
//
// Worse, a retry does not re-run the failing conditions — it runs a CLEANER environment (the
// interfering background worker from the previous test has since finished). So the mechanism is
// specifically good at hiding exactly the cross-test contamination this suite is prone to.
//
// A flake here is a bug report. Treat it as one.
retries: 0,
workers: 1, // One worker. Multi-worker needs per-worker isolated DBs (Phase 2+).
reporter: [
['list'],
['html', { open: 'never', outputFolder: './playwright-report' }],
],
globalSetup: './global-setup.ts',
globalTeardown: './global-teardown.ts',
timeout: 60_000,
expect: { timeout: 10_000 },
use: {
baseURL: process.env.E2E_FRONTEND_URL ?? 'http://localhost:3101',
trace: 'retain-on-failure',
screenshot: 'only-on-failure',
video: 'retain-on-failure',
actionTimeout: 10_000,
navigationTimeout: 30_000,
// No camera/mic/clipboard here — those are Chromium-only and are granted on
// the Chromium projects below (see CHROMIUM_PERMISSIONS).
},
projects: [
// ── Baseline desktop ─────────────────────────────────────────────────
{
name: 'chromium-desktop',
// 09-mobile/ specs only run on the chromium-mobile project below — they
// require `hasTouch: true` and a phone viewport.
testIgnore: ['**/09-mobile/**'],
use: {
...devices['Desktop Chrome'],
permissions: CHROMIUM_PERMISSIONS,
launchOptions: {
args: [
'--use-fake-ui-for-media-stream',
'--use-fake-device-for-media-stream',
// A video file would be loaded with --use-file-for-fake-video-capture
// — we ship a tiny .y4m in fixtures/media/ as a follow-up.
],
},
},
},
// ── Phase 3 mobile gestures ──────────────────────────────────────────
// Runs ONLY the 09-mobile/ specs. Uses the Pixel 7 device descriptor so
// hasTouch + isMobile + mobile viewport are all set, then dispatches
// pointer events via page.mouse / locator helpers. Real touch events
// come from `page.touchscreen` when needed.
{
name: 'chromium-mobile',
testMatch: ['**/09-mobile/**/*.spec.ts'],
use: { ...devices['Pixel 7'], permissions: CHROMIUM_PERMISSIONS },
},
// ── Mobile UA smoke matrix (runs only @smoke specs in CI) ────────────
{
name: 'chromium-pixel7',
use: { ...devices['Pixel 7'], permissions: CHROMIUM_PERMISSIONS },
grep: /@smoke/,
},
{
name: 'chromium-galaxy-s22',
use: {
...devices['Galaxy S9+'], // Playwright doesn't ship S22 yet — S9+ is the closest Samsung descriptor.
viewport: { width: 360, height: 780 },
userAgent:
'Mozilla/5.0 (Linux; Android 14; SM-S911B) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Mobile Safari/537.36',
permissions: CHROMIUM_PERMISSIONS,
},
grep: /@smoke/,
},
{
name: 'samsung-internet',
use: {
...devices['Galaxy S9+'],
viewport: { width: 360, height: 780 },
userAgent:
'Mozilla/5.0 (Linux; Android 14; SM-S911B) AppleWebKit/537.36 (KHTML, like Gecko) SamsungBrowser/24.0 Chrome/124.0.0.0 Mobile Safari/537.36',
permissions: CHROMIUM_PERMISSIONS,
},
grep: /@smoke/,
},
{
name: 'edge-android',
use: {
...devices['Pixel 7'],
userAgent:
'Mozilla/5.0 (Linux; Android 14; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Mobile Safari/537.36 EdgA/124.0.0.0',
permissions: CHROMIUM_PERMISSIONS,
},
grep: /@smoke/,
},
{
name: 'chrome-ios',
use: {
...devices['iPhone 14 Pro'],
userAgent:
'Mozilla/5.0 (iPhone; CPU iPhone OS 17_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/124.0.0.0 Mobile/15E148 Safari/604.1',
},
grep: /@smoke/,
},
{
name: 'webkit-iphone',
use: { ...devices['iPhone 14 Pro'] },
// The PRIMARY user of this app is a wedding guest opening a QR link in iOS Safari. Gating
// that on `@smoke` — which exists on exactly two specs — meant the entire iOS guarantee was
// one happy path and one join test. Every other UA here is a secondary browser and a smoke
// check is proportionate; WebKit is not. Give it the core journeys the guest actually walks:
// join/recover, upload, and browse the feed.
testMatch: ['**/__smoke/**', '**/01-auth/**', '**/02-upload/**', '**/03-feed/**'],
},
{
name: 'firefox-android',
use: {
...devices['Pixel 7'],
defaultBrowserType: 'firefox',
// Firefox rejects `isMobile` (Chromium-only). Keep the phone viewport +
// Android UA for coverage, but drop the unsupported flag.
isMobile: false,
userAgent:
'Mozilla/5.0 (Android 14; Mobile; rv:124.0) Gecko/124.0 Firefox/124.0',
},
grep: /@smoke/,
},
{
name: 'firefox-desktop',
use: { ...devices['Desktop Firefox'] },
grep: /@smoke/,
},
],
});