E2E Testing
The end-to-end suite lives in web/e2e/ and drives the real admin UI with
Playwright against the real backend stack — no API
mocks anywhere. Playwright starts its own Next.js dev server on port 3010
(webServer block in web/playwright.config.ts); the app’s /api/* proxy
forwards to the backend at http://localhost:8000, exactly as in development.
Prerequisites
Section titled “Prerequisites”-
The dev stack is running at
http://localhost:8000:Terminal window docker compose up -d --waitWith the default
APP_ENV=development, the one-shotdb-initservice runs migrations + seed automatically, so a fresh database comes up with the deterministic accounts below. Re-runningupon a populated database is a no-op (seed-if-empty). -
Seeded accounts (compose defaults, override via
INITIAL_*_PASSWORD):Account Password Role admin@example.comadmin123adminuser@example.comuser123user(no admin permissions) -
2FA disabled baseline:
admin@example.commust log in WITHOUT a 2FA challenge. The 2FA spec enrols and restores admin itself, and the auth setup self-heals residue from interrupted runs by clearing 2FA directly in the database (e2e/support/totp.ts), so this normally needs no manual action. -
Node dependencies:
cd web && npm ci. No extra packages are needed — even TOTP codes are computed by reusing the OTPHP library inside the backend container (docker exec whity_frankenphp …), so the suite expects the compose container names (whity_frankenphp,whity_postgres).
Projects and roles
Section titled “Projects and roles”Authentication is handled at the project level: the setup project logs in
once per role through the real UI and saves the browser storage state under
web/e2e/.auth/ (gitignored); the authenticated projects load that state, so
specs start already logged in without one slow UI login per test.
| Project | Auth state | Picks up |
|---|---|---|
setup |
— (performs the logins) | e2e/support/auth.setup.ts |
authflow |
none (from scratch) | auth.spec.ts, auth-bugs.spec.ts, auth-transitions.spec.ts, demo.spec.ts |
user |
user@example.com |
regular-user.spec.ts |
admin |
admin@example.com |
navigation, roles, users, ous-tenants, ous-hub, stats, settings-2fa, profile specs |
matrix-admin |
admin@example.com |
every matrix-*.spec.ts |
matrix-user |
user@example.com |
every matrix-*.spec.ts |
matrix-delegate |
delegate@example.com |
every matrix-*.spec.ts |
The third role: a delegation-granted user
Section titled “The third role: a delegation-granted user”delegate@example.com (password delegate123, overridable via
E2E_DELEGATE_EMAIL / E2E_DELEGATE_PASSWORD) is not seeded. The auth
setup provisions it idempotently through the admin API:
ensureUser— find the account by email or create it with roleuser, so its role grants nothing beyond the regular user;ensureDelegation— ensure a live delegation from admin of the permissions inDELEGATED_PERMISSIONS(e2e/support/constants.ts):relations:read,audit:read,hello:view.
Everything the delegate can do beyond the plain user therefore comes from a
delegation, which is the access path the matrix exercises. All three
permissions are held by the seeded admin grantor on a fresh database (core
migrations grant relations:read/audit:read; the bundled HelloWorld plugin
migration grants hello:view), satisfying the delegation API’s
subset-of-own-permissions invariant in every environment, including CI.
Observed contrast (live-verified):
| Endpoint | admin | user | delegate |
|---|---|---|---|
GET /api/frontend/features |
all features | [] |
only hello-greetings |
GET /api/relations |
200 | 403 | 200 |
GET /api/audit-logs |
200 | 403 | 200 |
GET /api/delegations |
200 | 403 | 403 |
GET /api/users |
200 | 403 | 403 |
Note /api/navigation now requires authentication and is per-caller
RBAC-filtered — each role sees only the links its permissions allow (mirroring
/api/frontend/features), and an unauthenticated request gets 401. This landed
in WC-175 (#191). Access is also still enforced at the data layer, so specs
assert on page content (data vs the “Access denied” card), not on the sidebar.
The matrix pattern — contract for spec authors
Section titled “The matrix pattern — contract for spec authors”A matrix spec is written once and runs three times, because the three
matrix-* projects share the testMatch e2e/matrix-*.spec.ts. The spec learns
its current role from the role fixture ('admin' | 'user' | 'delegate',
derived from the project name’s matrix- suffix) and branches its
expectations on it — same journey, role-dependent outcome. roleSession is
the role-agnostic counterpart of adminPage/userPage: it lands the
already-authenticated page on the dashboard and exposes the AppShell page
object.
// web/e2e/matrix-relations.spec.ts — runs under all three matrix-* projectsimport { test, expect } from './support/fixtures';
test('relations page access', async ({ roleSession, role, page }) => { await roleSession.shell.clickNav('Family Relations'); if (role === 'user') { await expect(page.getByRole('heading', { name: 'Access denied' })).toBeVisible(); } else { // admin holds relations:read via its role, delegate via a delegation await expect(page.getByRole('heading', { name: 'Access denied' })).toHaveCount(0); }});matrix-smoke.spec.ts is the reference implementation; it pins the pattern
itself (each project authenticated as the right account).
Running locally
Section titled “Running locally”cd webnpm run test:e2e # the full suite (all projects)npm run test:e2e:ui # Playwright UI modenpx playwright test --project=authflow # one project (setup runs if depended on)npx playwright test --project=matrix-delegate # one role of the matrixnpx playwright test e2e/users.spec.ts # one spec filenpx playwright show-report # open the last HTML reportreuseExistingServer keeps an already-running dev server on :3010 alive
between runs; set E2E_PORT / E2E_BASE_URL to point elsewhere.
Continuous integration
Section titled “Continuous integration”.github/workflows/e2e.yml runs the full suite on every push and pull request
to main, with a 30-minute job timeout and a per-ref concurrency group
(e2e-${{ github.ref }}) so a newer push cancels the in-flight run:
- provisions PHP 8.4 + Composer on the runner and runs
composer installat the repo root — the compose file bind-mounts the checkout, and thedb-initmigrate + seed (and the suite’s in-container TOTP helper) need the gitignoredvendor/to exist inside the containers; docker compose -f docker-compose.yml up -d --wait— buildswhity-core:devand bootstraps migrate + seed viadb-init;- polls
GET /api/healthuntil the API answers (120 s budget); npm ci+npx playwright install chromium --with-depsinweb/;npx playwright test(the config starts the Next dev server itself);- on failure, uploads
playwright-report/andtest-results/(traces, screenshots) as theplaywright-artifactsartifact.
CI runs from a fresh database every time, so it has no dev residue (no Announcements plugin data, no leftover test entities). Specs must pass in both worlds — assert only on what the seed plus the suite’s own setup guarantee.
Flakiness policy
Section titled “Flakiness policy”workers: 1— the suite mutates one shared database; serialised writes keep runs deterministic and re-runnable.retries: 1(locally and in CI) withtrace: 'on-first-retry'and screenshots on failure — a retry must be able to succeed, so every spec has to be self-arranging (set up its own preconditions; never depend on a previous test’s side effects).- Web-first assertions (
expect(locator)…) over manual waits; transient toasts are asserted immediately after the triggering action.
Data hygiene rules
Section titled “Data hygiene rules”- Never mutate the seeded accounts (
admin@example.com,user@example.com) or the seeded tenants/roles. Specs that need an entity create a throwaway one named withuniqueSuffix()and delete it best-effort afterwards. - The delegate account is provisioned, not seeded — treat it like the seeded accounts: matrix specs may use its session, but must not change its password, role, or delegations.
- The 2FA spec is the only one allowed to enrol an account (admin), and it restores the baseline in teardown; the auth setup additionally hard-resets admin 2FA via the database before logging in, so interrupted runs can never wedge the suite.
See also
Section titled “See also”- Development Workflow — where the E2E gate sits in the verify step
- Installation — bringing the dev stack up
- Plugin Development — the HelloWorld feature the plugin-screen specs assert on