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