// Playwright E2E / visual-regression / a11y harness for BillTracker. // See docs/QA_PLAN.md §4.4 and the B-UI / B14 / B15 batches. // // Setup (one-time): npm install && npx playwright install chromium // Run: npm run test:e2e (headless) // npm run test:e2e:ui (watch/debug) // npm run test:e2e:update (re-baseline screenshots) // // SAFETY: the two `webServer` entries boot the app against a SCRATCH DB // (e2e/setup/prepare-db.js seeds it) on DEDICATED ports (5199 UI / 3099 API), so // E2E never touches your real db/bills.db and never reuses a dev server running // on 5173/3000. `reuseExistingServer` is false for the same reason. const { defineConfig, devices } = require('@playwright/test'); const { scratchDbPath, API_PORT, UI_PORT } = require('./e2e/constants'); const baseURL = `http://localhost:${UI_PORT}`; module.exports = defineConfig({ testDir: './e2e', fullyParallel: true, forbidOnly: !!process.env.CI, retries: process.env.CI ? 2 : 0, workers: process.env.CI ? 1 : undefined, reporter: [['list'], ['html', { open: 'never' }]], use: { baseURL, trace: 'on-first-retry', screenshot: 'only-on-failure', video: 'retain-on-failure', }, // Visual-regression tolerance. Screenshots are OS/font-sensitive, so baselines // are committed per-platform (Playwright suffixes them with the platform). expect: { toHaveScreenshot: { maxDiffPixelRatio: 0.02, animations: 'disabled' }, }, projects: [ // Logs in once and writes STORAGE_STATE; browser projects depend on it. { name: 'setup', testMatch: /auth\.setup\.js/ }, { name: 'chromium-desktop', use: { ...devices['Desktop Chrome'] }, dependencies: ['setup'], testIgnore: [/auth\.setup\.js/, /api\.probe\.spec\.js/, /a11y\.authed\.spec\.js/], }, { name: 'chromium-mobile', use: { ...devices['Pixel 5'] }, dependencies: ['setup'], testIgnore: [/auth\.setup\.js/, /api\.probe\.spec\.js/, /a11y\.authed\.spec\.js/], }, // Find-mode diagnostics (adversarial API probe + authenticated a11y scan). // Own project so they don't run concurrently with / pollute the UI specs, and // so open findings here don't turn the default suite red. Run on demand: // `npm run test:e2e:probe`. Fold a spec into the default projects once its // findings are fixed (it becomes a passing regression guard). { name: 'probe', testMatch: /(api\.probe|a11y\.authed)\.spec\.js/, use: { ...devices['Desktop Chrome'] }, dependencies: ['setup'], }, // Cross-browser (WebAuthn/Select behavior differs) — enable when ready: // { name: 'firefox', use: { ...devices['Desktop Firefox'] }, dependencies: ['setup'], testIgnore: /auth\.setup\.js/ }, // { name: 'webkit', use: { ...devices['Desktop Safari'] }, dependencies: ['setup'], testIgnore: /auth\.setup\.js/ }, ], // Two servers: the API (node) and the Vite UI that proxies /api to it. Both // run against the scratch DB on dedicated ports. Playwright waits for each // `url` to respond before starting tests. webServer: [ { command: 'node server.js', url: `http://localhost:${API_PORT}/api/version`, reuseExistingServer: false, timeout: 120_000, stdout: 'ignore', stderr: 'pipe', env: { DB_PATH: scratchDbPath(), PORT: String(API_PORT), BIND_HOST: '127.0.0.1', }, }, { command: `npm run dev:ui -- --port ${UI_PORT} --strictPort`, url: baseURL, reuseExistingServer: false, timeout: 120_000, stdout: 'ignore', stderr: 'pipe', env: { API_PORT: String(API_PORT), }, }, ], });