import { randomBytes } from "node:crypto"; import path from "node:path"; import { defineConfig, devices } from "@playwright/test"; const authFile = path.join(__dirname, ".playwright", ".auth", "user.json"); const TEST_WEB_PORT = 2349; // Fresh Postgres database per e2e run. The create-db script (chained before // the API in the webServer command) CREATEs it and cleans up stale databases // from previous runs. The API auto-migrates the empty database at boot. const E2E_PG_BASE_URL = process.env.E2E_PG_BASE_URL || "postgres://snapotter:snapotter@localhost:5432/snapotter"; const e2eDbName = `snapotter_e2e_${process.pid}_${randomBytes(4).toString("hex")}`; const e2eDatabaseUrl = (() => { const url = new URL(E2E_PG_BASE_URL); url.pathname = `/${e2eDbName}`; return url.toString(); })(); // Specs that mutate global server state (settings, users, roles, API keys) // or assert on global lists/timing. These run in the chromium-serial project // with --workers=1; everything else parallelizes safely. const SERIAL_SPECS = /gui-settings-|settings\.spec|rbac|security|people|api\.spec|state-bleed|full-session|gui-file-carry|i18n|theme|gui-performance/; // Screenshot-comparison specs. Separate project because baselines are // platform-specific: they run locally (darwin baselines) and via the // update-visual-baselines workflow, but not in the nightly linux run until // linux baselines are committed. const VISUAL_SPECS = /visual-regression|gui-visual-/; // Device-emulated specs (real touch, UA, DPR). Tagged @mobile or @tablet // and routed to the dedicated device projects below. const DEVICE_SPECS = /device-mobile|device-tablet|device-visual|device-a11y/; export default defineConfig({ testDir: "./tests/e2e", timeout: 30_000, expect: { timeout: 10_000, toHaveScreenshot: { maxDiffPixelRatio: 0.01, animations: "disabled", caret: "hide", }, }, // Platform-suffixed baselines: darwin baselines serve local runs on macOS, // linux baselines (generated by the update-visual-baselines workflow) serve CI. snapshotPathTemplate: "{testDir}/__screenshots__/{testFilePath}/{arg}-{platform}{ext}", fullyParallel: false, retries: process.env.CI ? 1 : 0, // Files run across workers; tests within a file stay ordered. The serial // bucket is pinned to --workers=1 by its run command. Default is 2: the // dev-mode webServers saturate beyond that and 30s-timeout tests start // flaking. Raise via PW_WORKERS on stronger setups. workers: process.env.PW_WORKERS ? Number(process.env.PW_WORKERS) : 2, reporter: "html", use: { baseURL: `http://localhost:${TEST_WEB_PORT}`, screenshot: "only-on-failure", trace: "retain-on-failure", }, projects: [ { name: "setup", testMatch: /auth\.setup\.ts/, }, { name: "chromium", use: { ...devices["Desktop Chrome"], storageState: authFile, }, testIgnore: [SERIAL_SPECS, VISUAL_SPECS, DEVICE_SPECS], dependencies: ["setup"], }, { name: "chromium-serial", use: { ...devices["Desktop Chrome"], storageState: authFile, }, testMatch: SERIAL_SPECS, dependencies: ["setup"], }, { name: "chromium-visual", use: { ...devices["Desktop Chrome"], storageState: authFile, }, testMatch: VISUAL_SPECS, dependencies: ["setup"], }, { name: "firefox", use: { ...devices["Desktop Firefox"], storageState: authFile, }, testMatch: /gui-cross-browser\.spec\.ts/, dependencies: ["setup"], }, { name: "webkit", use: { ...devices["Desktop Safari"], storageState: authFile, }, testMatch: /gui-cross-browser\.spec\.ts/, dependencies: ["setup"], }, // ---- Real device-emulated projects (touch, UA, DPR, engine) ---- { name: "mobile-chromium", use: { ...devices["Pixel 7"], storageState: authFile, }, testMatch: DEVICE_SPECS, grep: /@mobile/, dependencies: ["setup"], }, { name: "mobile-webkit", use: { ...devices["iPhone 14"], storageState: authFile, }, testMatch: DEVICE_SPECS, grep: /@mobile/, dependencies: ["setup"], }, { name: "tablet-webkit", use: { ...devices["iPad (gen 7)"], storageState: authFile, }, testMatch: DEVICE_SPECS, grep: /@tablet/, dependencies: ["setup"], }, { name: "tablet-chromium", use: { ...devices["Galaxy Tab S9"], storageState: authFile, }, testMatch: DEVICE_SPECS, grep: /@tablet/, dependencies: ["setup"], }, ], webServer: [ { command: `node tests/e2e-pg-create-db.cjs ${e2eDbName} && pnpm --filter @snapotter/api dev`, port: 13490, reuseExistingServer: !process.env.CI, env: { AUTH_ENABLED: "true", DEFAULT_USERNAME: "admin", DEFAULT_PASSWORD: "admin", RATE_LIMIT_PER_MIN: "50000", SKIP_MUST_CHANGE_PASSWORD: "true", ANALYTICS_ENABLED: "false", DATABASE_URL: e2eDatabaseUrl, REDIS_URL: process.env.REDIS_URL ?? "redis://localhost:6379", BULLMQ_PREFIX: e2eDbName, // The in-repo docker/feature-manifest.json makes the API think it is // inside Docker and try to mkdir /data; point it somewhere writable. DATA_DIR: path.join(__dirname, "test-results", ".e2e-data"), }, timeout: 30_000, }, { // Production build + static preview: the dev server's on-demand // transform saturates under parallel workers and flakes 30s-timeout // tests. The build adds ~40s once per run and removes that whole class. command: "pnpm --filter @snapotter/web build && pnpm --filter @snapotter/web preview", port: TEST_WEB_PORT, reuseExistingServer: !process.env.CI, env: { PORT: String(TEST_WEB_PORT), VITE_API_URL: "http://localhost:13490", }, timeout: 240_000, }, ], }); export { authFile };