Files
emaui/apps/e2e/playwright.config.ts
2026-08-03 12:49:48 +03:00

116 lines
4.1 KiB
TypeScript

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,
},
],
});