mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
157 lines
5.3 KiB
TypeScript
157 lines
5.3 KiB
TypeScript
import { defineConfig, devices } from "@playwright/test";
|
|
import * as path from "node:path";
|
|
|
|
import { PERSONAS } from "./fixtures/personas";
|
|
|
|
/**
|
|
* Playwright end-to-end suite for the HR and Finance modules.
|
|
*
|
|
* Scope and doctrine are set by docs/hr-finance-ui-e2e-matrix.md (Phase 0,
|
|
* approved 2026-08-24): every scenario asserts the DOM, the network response
|
|
* AND the database row, and fails if the three disagree.
|
|
*
|
|
* ── Isolation ────────────────────────────────────────────────────────────────
|
|
* Runs against `smart_office_e2e` on its OWN ports, so a run can never disturb
|
|
* the dev stack a human is using, nor write into `smart_office_prod` (a
|
|
* production replica). Both are load-bearing:
|
|
*
|
|
* | service | dev | e2e |
|
|
* | hr-api | 3005 | 3105 |
|
|
* | finance-api | 3004 | 3104 |
|
|
* | hr-web | 5185 | 5285 |
|
|
* | finance-web | 5186 | 5286 |
|
|
*
|
|
* `DB_NAME`, `PORT` and `CORS_ORIGINS` all override the app's own `.env`
|
|
* (verified), and Vite reads `PORT`/`VITE_*` from the process environment — so
|
|
* no config file is edited to run the suite.
|
|
*
|
|
* CORS_ORIGINS is not optional: without it the API rejects the browser at the
|
|
* preflight and login fails with a bare "Network Error" that names nothing.
|
|
*/
|
|
const HR_API = process.env.HR_API_URL ?? "http://localhost:3105";
|
|
const FINANCE_API = process.env.FINANCE_API_URL ?? "http://localhost:3104";
|
|
const HR_WEB = process.env.HR_WEB_URL ?? "http://localhost:5285";
|
|
const FINANCE_WEB = process.env.FINANCE_WEB_URL ?? "http://localhost:5286";
|
|
const DB_NAME = process.env.DB_NAME ?? "smart_office_e2e";
|
|
|
|
const STORAGE = path.join(__dirname, "fixtures", "storage");
|
|
|
|
const webEnv = {
|
|
GITHUB_PACKAGE_TOKEN: process.env.GITHUB_PACKAGE_TOKEN ?? "dummy",
|
|
};
|
|
|
|
export default defineConfig({
|
|
testDir: path.join(__dirname, "specs"),
|
|
// Shared seeded database, and several scenarios move state that cannot be
|
|
// moved back (a posted journal, an approved payroll run). Serialize, as the
|
|
// passenger suite does, so assertions stay deterministic.
|
|
fullyParallel: false,
|
|
workers: 1,
|
|
retries: 0,
|
|
timeout: 60_000,
|
|
expect: { timeout: 10_000 },
|
|
globalSetup: path.join(__dirname, "global-setup.ts"),
|
|
reporter: [
|
|
["list"],
|
|
[
|
|
"html",
|
|
{
|
|
outputFolder: path.join(__dirname, "..", "e2e-hr-finance-report"),
|
|
open: "never",
|
|
},
|
|
],
|
|
],
|
|
|
|
webServer: [
|
|
{
|
|
command: "pnpm --filter @edr/hr-api start",
|
|
url: `${HR_API}/api-docs`,
|
|
timeout: 180_000,
|
|
reuseExistingServer: true,
|
|
env: { ...webEnv, PORT: "3105", DB_NAME, CORS_ORIGINS: HR_WEB },
|
|
},
|
|
{
|
|
command: "pnpm --filter @edr/finance-api start",
|
|
url: `${FINANCE_API}/api-docs`,
|
|
timeout: 180_000,
|
|
reuseExistingServer: true,
|
|
env: { ...webEnv, PORT: "3104", DB_NAME, CORS_ORIGINS: FINANCE_WEB },
|
|
},
|
|
{
|
|
command: "pnpm --filter @edr/hr-web dev",
|
|
url: HR_WEB,
|
|
timeout: 120_000,
|
|
reuseExistingServer: true,
|
|
env: {
|
|
...webEnv,
|
|
PORT: "5285",
|
|
VITE_HR_API_URL: HR_API,
|
|
// hr-api serves POST /api/v1/auth/login itself (it embeds IamModule).
|
|
// The app's own .env points login at passenger-api :4000 on the belief
|
|
// that it does not — that belief is wrong, and the suite does not
|
|
// inherit the coupling.
|
|
VITE_AUTH_API_URL: HR_API,
|
|
VITE_AUTH_BASE_PATH: "/api/v1",
|
|
VITE_CLIENT_APP: "",
|
|
},
|
|
},
|
|
{
|
|
command: "pnpm --filter @edr/finance-web dev",
|
|
url: FINANCE_WEB,
|
|
timeout: 120_000,
|
|
reuseExistingServer: true,
|
|
env: {
|
|
...webEnv,
|
|
PORT: "5286",
|
|
VITE_FINANCE_API_URL: FINANCE_API,
|
|
VITE_AUTH_API_URL: FINANCE_API,
|
|
VITE_AUTH_BASE_PATH: "/api/v1",
|
|
VITE_CLIENT_APP: "",
|
|
},
|
|
},
|
|
],
|
|
|
|
use: {
|
|
trace: "retain-on-failure",
|
|
screenshot: "only-on-failure",
|
|
actionTimeout: 15_000,
|
|
launchOptions: { slowMo: Number(process.env.SLOWMO ?? 0) },
|
|
},
|
|
|
|
/**
|
|
* ONE PROJECT PER APP, not per persona.
|
|
*
|
|
* Per-persona projects were the first design and were wrong: a project's
|
|
* `testMatch` selects files, so every HR spec ran once under each of the four
|
|
* HR personas. A gating scenario written for `hr-employee` then also ran as
|
|
* `hr-manager` and failed — not because the gate was broken, but because it
|
|
* was asserted against the wrong role. Worse, the inverse would pass silently.
|
|
*
|
|
* The persona is a property of the SCENARIO, so each `describe` declares it:
|
|
*
|
|
* test.use({ storageState: storageFor("hr-employee") })
|
|
*
|
|
* The project only supplies what is genuinely per-app: the baseURL.
|
|
*/
|
|
projects: [
|
|
{
|
|
name: "hr",
|
|
testMatch: /specs\/hr\/.*\.spec\.ts/,
|
|
use: { ...devices["Desktop Chrome"], baseURL: HR_WEB },
|
|
},
|
|
{
|
|
name: "finance",
|
|
testMatch: /specs\/finance\/.*\.spec\.ts/,
|
|
use: { ...devices["Desktop Chrome"], baseURL: FINANCE_WEB },
|
|
},
|
|
],
|
|
});
|
|
|
|
/** Path to a persona's storageState — every describe block names its own. */
|
|
export const storageFor = (personaKey: string): string => {
|
|
if (!PERSONAS.some((p) => p.key === personaKey)) {
|
|
throw new Error(`Unknown persona "${personaKey}"`);
|
|
}
|
|
return path.join(STORAGE, `${personaKey}.json`);
|
|
};
|