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