Files
emaui/apps/e2e
fitse-yotor 659e954306 refactor(theme): share one base theme between backoffice and portal
The two apps had drifted into unrelated themes. The portal's carried a full
type scale, radius scale, shadow ramp and component defaults; the backoffice's
had none of them — 55 lines defining two colour ramps and little else. With
nothing to inherit, its 23 features each invented their own sizing, which is
the real source of the inconsistency the UI reads with.

Promote the portal's structure to `libs/shared` as `baseTheme`, and reduce both
themes to what they should differ on: brand. The backoffice keeps #1e40af and
the portal keeps Coastal Modern — a distinct accent tells an officer which of
the two systems they are in, and the ramps are not interchangeable in contrast.

Both export names are preserved, so no consumer import changes.

Two properties are deliberately held back rather than shared:
- `colors.gray`: the portal's blue-tinted neutrals retint every dimmed label,
  neutral badge and table border. The backoffice adopts them as its own
  reviewed change, not as a side effect of sharing a base.
- `primaryShade.dark`: moves every filled control in dark mode; waits until
  dark mode is verified end to end.

Also fixes a live bug: PageLoader coloured its primary label `navy.9`, which is
defined in neither theme. Mantine drops unresolved colour keys silently, so the
label in a component used by 20 files had been rendering an inherited colour.

Adds a visual-regression harness to make all of this reviewable. It runs
against a static gallery route rather than real pages, so it needs no API,
database or auth — a theme diff cannot be masked by a migration or an expired
token. The portal is the control group: it is pixel-identical across all four
light/dark × desktop/tablet baselines, which is what makes the refactor
provably lossless.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 11:25:26 +03:00
..

End-to-end browser tests

Playwright, driving the portal and the backoffice in a real browser against a real API and a real database.

# from emaui/
npx nx e2e @ema-platform/e2e            # or: npx playwright test --config apps/e2e/playwright.config.ts
npx playwright test --config apps/e2e/playwright.config.ts --headed --debug
npx playwright show-report ../../dist/e2e-report

What it runs against

Nothing shared with your own dev stack. The suite starts its own servers, on its own ports, against its own database:

port notes
API 3011 node dist/main.js with explicit env
Portal 4302 vite build --mode e2e then vite preview
Backoffice 4303 same
Database ema_e2e

This is deliberate. A developer's stack is usually already up on 3000/4200/4201, and dev/start.sh rewrites emaapi/apps/server/emaapi/.env and the apps' .env.local on every run — a suite that read those files would point at whichever stack was started last. The API is launched with DATABASE_NAME, PORT and the payment flags passed directly, and the frontends are built with --mode e2e, which picks up apps/*/.env.e2e.local (higher precedence than .env.local, so your own config is left alone).

vite preview on a production build rather than nx serve, because Nx serialises serve targets per project: with your dev portal already running, a second nx serve @ema-platform/portal waits forever on the first.

First-time setup

The E2E database has to exist and be bootstrapped once:

DB_NAME=ema_e2e ./dev/start.sh      # creates it, migrates, seeds, then starts a stack you can Ctrl-C

Afterwards the suite manages its own servers; you do not need start.sh again. Note that this run leaves the shared .env files pointing at the E2E database and its ports — rerun ./dev/start.sh plain to put them back.

How it gets its data

  • Accounts are created through the UI. Every spec signs up its own applicant with a timestamped email, and deletes it in afterEach (deleteApplicant). Nothing is shared between specs, so specs can run in any order and the second run of the suite behaves exactly like the first.
  • The one-time code is scraped from the API log. There is no local SMS gateway, iam.user_verifications.verification_code is argon2-hashed, and the notification rows it is delivered through carry an empty body — the log line is the only plaintext. The API is therefore started with its output teed to /tmp/ema-e2e-api.log, and support/api-log.ts reads codes written after a recorded offset. The test still types the real code into the real screen. Because the log line names no recipient, matching is by position, which is sound only while the suite runs single-worker — hence workers: 1.
  • Database reads use psql, shelled out from support/db.ts, rather than adding a pg dependency to the frontend workspace for the sake of a few assertions.

Conventions

  • One worker, serial. The officer queue is global state and the log-offset trick needs ordering; determinism is worth the wall-clock.
  • Assertions are on what the user sees — headings, badges, buttons — with database checks only where the point is that something was persisted.
  • No waitForTimeout. Wait for a condition.
  • Traces, screenshots and video are retained for failures only.

Debugging a failure

Playwright writes test-results/<test>/error-context.md with a full accessibility snapshot of the page at the moment it failed — usually enough on its own. Otherwise npx playwright show-trace test-results/<test>/trace.zip.

Status

Implemented: signup → OTP → operations gate → dashboard, and the empty-state dashboard. See the parent prompt (dev/prompts/e2e-browser-tests-prompt.md) for the flows still to be written: catalogue filtering, the full application lifecycle per licence type, officer review, payment bypass, certificate download, expiry, renewal and reminders.