On this page
End-to-end testing
Every new or re-engineered service ships with an end-to-end test. The test proves the full process works: find the service, sign in, apply with the form and documents, pay, every officer step, the decision, and the issued output document with its seal and QR verification. The same run captures the screenshots for the service's documentation (see Service documentation).
The tests live in the e2e kit, a Playwright project. The kit is kept on the central documentation share, not in a git repository. The course E2E testing and documentation teaches it step by step.
What the kit boots
npm run e2e starts everything on your own machine, runs the journeys and stops it again:
- a fresh PostgreSQL database (
zcm_e2e, dropped and re-created on every run) with PostGIS, built by the backend's ownmigrations and filled by the kit's seed;
- production builds of the citizen portal and the back office;
- the API, with jobs off, a random session secret and a network guard that refuses every outgoing call to a non-local
host;
- the provider test doubles.
Nothing leaves the machine. E-mail is unconfigured and AI is off.
Provider test doubles
The doubles are the backend's own (zcm-backend/test/fixtures/providers/), so the e2e kit, the backend's contract suites and its integration tests share one set. Each is faithful to its provider's contract and has a /__control API the journeys use.
| Double | Stands in for |
|---|---|
| ZamPay (behind ZamConnect) | Invoices, payment, receipts, and receipt callbacks to the instance's own callback address |
| ZamPass | Citizen sign-in (OpenID Connect): discovery, keys, persona picker, PKCE token, logout |
| SMS capture | The SMS provider; every text is kept for the test to read |
| Government gateway | The legacy verification source |
| Register source | An external register's sync API |
| Development PKI | Certificate authority, time-stamp authority, OCSP |
| Signing service | Seals issued permits (PAdES with a time-stamp) |
Never point a journey at real ZamPay, ZamPass, SMS or signing services.
Issued documents are rendered with LibreOffice when it is installed. Without it, a text-only stand-in is used, and documentation made from the stand-in is not published.
Seed profiles
E2E_SEED_PROFILE chooses which installation the run stands for:
| Profile | Modules | Stands for |
|---|---|---|
mihud (default) | estate, rentals, valuation and shop switched on for one ministry | an installation that already runs ministry modules |
neutral | every module off | a new installation, or any other ministry's node |
A service's journey must pass on both. Module journeys switch on the module they need themselves, for the owning ministry only, under maker-checker, so they run on either profile.
Topologies
E2E_TOPOLOGY chooses the shape of the platform:
combined(default): one installation does everything.federated: a national hub and a ministry node, each with its own API, database and ZamPay double. The kitgenerates fresh federation signing keys, registers the node on the hub under maker-checker, and waits until the hub lists the node's services.
Run a journey in the federated topology when its service can live on a node. There the citizen applies, pays and verifies on the hub; the officers work in the node's back office.
E2E_TOPOLOGY=federated npm run e2e -- tests/23-water-permit.spec.js
Commands
Run them in the e2e folder of your workspace.
| Command | What it does |
|---|---|
npm run setup | Once: creates the app worktrees the kit runs against (-- --refresh re-creates them from the base branch) |
npm run e2e | Start the stack, run the journeys, stop the stack. Extra arguments go to Playwright (-- tests/<spec>.js --headed) |
npm run stack | Start only; then run npx playwright test in another terminal |
npm run docs:services | Run the documented services' journeys with capture on and publish their documentation folders |
npm run docs:check | No run: are the published documents complete and did their journeys pass? |
npm run status:services | Regenerate SERVICE-STATUS.md |
npm run share:sync | Upload what is new or newer to the central share; -- --dry-run lists only, -- --pull takes a fresh copy |
For both profiles:
npm run e2e -- tests/<your-service>.spec.js
E2E_SEED_PROFILE=neutral npm run e2e -- tests/<your-service>.spec.js
Reports: .stack/last-summary.txt (one line per test, each failure with its screenshot and trace), npx playwright show-report, and the API log in .stack/logs/.
Writing a journey spec
A journey is a Playwright spec in tests/, numbered after the last one. Its header comment lists each actor and step. A typical shape:
const { test, expect } = require('@playwright/test');
const { cfg, signInCitizen } = require('./support/helpers');
const { serviceDocs } = require('./support/serviceDocs');
const docs = serviceDocs(test, { service: () => myService(), mda: 'EXAMPLE', key: 'example-permit' });
test.describe.serial('Example permit: apply, review, pay, decide, verify', () => {
test('citizen: finds the service and applies', async ({ page }) => {
await signInCitizen(page);
await page.goto(`${cfg.urls.portal}/service/${myService().id}`);
await expect(page.getByRole('heading', { name: 'Example Permit' })).toBeVisible();
await docs.step(page, { actor: 'Citizen', title: 'Find the service',
description: 'The citizen opens the service page and reads what to have ready.' });
// ... fill the form, upload a synthetic PDF, send
});
test('officer: takes it on and reviews it', async ({ page }) => { /* ... */ });
});
Rules that keep journeys useful:
- Drive the real screens. Find elements by role and visible label, as a user would. Wait for what the user would
see; no fixed waits.
- Check the result, not only the screen. Assert the case status, the invoice amount set by the server, the permit
row, the seal. Include the abuse cases that matter to the service, such as an officer approving their own proposal.
- Cover the whole process: apply, pay, every officer step with the role it names, the decision, the download, and
verification of the seal and QR code.
- Use the helpers in
tests/support/: sign-in, the apply flow, back-office steps, arranging a case through theAPI so a later journey still runs when an earlier one fails, and federation helpers that say where a service lives.
- Synthetic data only. Seeded accounts end in
@example.test; names, NRCs (for example000000/00/0) andaddresses are invented. Never type real personal data into a journey.
Screenshots at desktop and 375 px
Playwright runs a desktop project at 1366 × 900 and a mobile project at 375 × 812 (specs whose file name contains mobile). The documentation capture (docs.step) takes a desktop image of every step and, for citizen steps, a 375 px image as well. The portal must work at 375 px with no horizontal scroll; the mobile journey checks this and runs an accessibility scan.
Known failures
A failing test stays failing when the application is wrong. Two mechanisms keep the rest of the run useful: tests/support/known-gaps.js parks a test only while its fix sits on an unmerged branch, and tests/support/workarounds.js (E2E_WORKAROUNDS=all) can hide a known bug so later steps can still be exercised.