import { defineConfig, devices } from '@playwright/test'; /** * End-to-end suite, driving the portal and the backoffice in a real browser. * * Both frontends are in one project on purpose: the flows that matter cross * them — an applicant files, an officer decides, the applicant collects the * certificate — and per-app projects cannot express that. * * Nothing here reads the shared `.env` files. A developer's own stack is * usually already running against `ema_db` on the default ports, and * `dev/start.sh` rewrites those files on every run; a suite that read them * would point at whichever stack was started last. Instead the servers below * are launched with explicit environment, on ports of their own, against a * database of their own. */ const API_PORT = Number(process.env.E2E_API_PORT ?? 3011); const PORTAL_PORT = Number(process.env.E2E_PORTAL_PORT ?? 4302); const BACKOFFICE_PORT = Number(process.env.E2E_BACKOFFICE_PORT ?? 4303); export const E2E = { apiUrl: `http://localhost:${API_PORT}/api`, portalUrl: `http://localhost:${PORTAL_PORT}`, backofficeUrl: `http://localhost:${BACKOFFICE_PORT}`, database: process.env.E2E_DB_NAME ?? 'ema_e2e', /** * Where the API's own output is teed. * * The one-time code an applicant types at signup exists in exactly one * readable place: this log. `iam.user_verifications` stores it argon2-hashed, * and the notification rows it is delivered through carry an empty body — so * the log line is not a convenience here, it is the only source. */ apiLog: process.env.E2E_API_LOG ?? '/tmp/ema-e2e-api.log', }; const emaapi = '../../../emaapi/apps/server/emaapi'; /** Env the API is started with. Explicit, so no `.env` can redirect it. */ const apiEnv = { ...process.env, PORT: String(API_PORT), DATABASE_NAME: E2E.database, // The API applies migrations on boot, which is what bootstraps a fresh E2E // database — but it must never be the thing that decides what the schema is. MIGRATIONS_RUN: 'false', NODE_ENV: 'development', FRONTEND_URLS: `${E2E.portalUrl},${E2E.backofficeUrl}`, PUBLIC_VERIFY_URL: `${E2E.portalUrl}/verify`, PORTAL_BASE_URL: E2E.portalUrl, PAYMENT_AUTO_CONFIRM: 'true', ALLOW_PAYMENT_BYPASS: 'true', }; export default defineConfig({ testDir: './src', // Flows share one database, and an officer queue is global state. Serial // execution costs wall-clock and buys determinism. workers: 1, fullyParallel: false, forbidOnly: !!process.env.CI, retries: process.env.CI ? 1 : 0, timeout: 90_000, expect: { timeout: 15_000 }, reporter: [['list'], ['html', { outputFolder: '../../dist/e2e-report', open: 'never' }]], use: { baseURL: E2E.portalUrl, // Kept only for failures — a trace per passing test is noise nobody reads. trace: 'retain-on-failure', screenshot: 'only-on-failure', video: 'retain-on-failure', actionTimeout: 15_000, }, projects: [ { name: 'chromium', use: { ...devices['Desktop Chrome'] } }, ], webServer: [ { name: 'api', // Teed rather than piped to Playwright's own stdout, because the OTP // helper has to read it back. command: `sh -c 'node dist/main.js 2>&1 | tee ${E2E.apiLog}'`, cwd: emaapi, env: apiEnv, url: `${E2E.apiUrl}/license-types`, // 401 is the healthy answer: the route exists and demands a session. ignoreHTTPSErrors: true, reuseExistingServer: !process.env.CI, timeout: 120_000, stdout: 'pipe', stderr: 'pipe', }, { name: 'portal', command: `npx vite build --config apps/portal/vite.config.mts --mode e2e && npx vite preview --config apps/portal/vite.config.mts --mode e2e --port ${PORTAL_PORT} --strictPort`, cwd: '../..', url: E2E.portalUrl, reuseExistingServer: !process.env.CI, timeout: 180_000, }, { name: 'backoffice', command: `npx vite build --config apps/backoffice/vite.config.mts --mode e2e && npx vite preview --config apps/backoffice/vite.config.mts --mode e2e --port ${BACKOFFICE_PORT} --strictPort`, cwd: '../..', url: E2E.backofficeUrl, reuseExistingServer: !process.env.CI, timeout: 180_000, }, ], });