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>
171 lines
7.3 KiB
TypeScript
171 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/,
|
|
},
|
|
],
|
|
});
|