Skip to content

PA-demo suite

packages/pa-demo โ€” the public, mock-only PA-Cockpit demo. Vitest + jsdom, plus a Playwright suite that is the only one in this repository wired into CI.

Figures on this page are measured, not estimated

19 files ยท 104 tests, all passing, measured with npm run test:serial --workspace=@ronl/pa-demo on 29 August 2026 against acc at 1e7fb19 (v2026.08.33). Coverage 91.30 % statements ยท 86.95 % branches ยท 85.00 % functions ยท 91.66 % lines.

At a glance:

Runner Vitest 4 + jsdom, coverage via v8
Files / tests 19 / 104
Wall time ~27 s serial
Playwright 11 tests in e2e/plato-demo.spec.ts, runs in CI

Why this suite is shaped the way it is

Nearly every guarantee the demo makes is a negative assertion:

  • no backend origin in the Content-Security-Policy
  • no auth library or API URL in the built bundle
  • no dropped section reachable from the rail or the command palette
  • no inherited storage key able to flip the demo out of mock mode

Negative assertions are exactly the ones that pass vacuously when wrong. A test asserting the absence of something is green both when the guard works and when the test is looking in the wrong place. That shaped the plan the demo was built from: five of its twelve steps write no production code at all, and instead name the thing to break, the failure message to expect, and the restore.

That discipline caught real holes, twice over:

  • The mock-lock test only ever proved the test environment file. Vitest's mode is always test, so nothing read the production or acceptance environment files. Deleting a mock flag from either would have left every check green. env-files.test.ts now asserts across all four environment files that the three mock flags are true and the API URL absent โ€” verified red by deleting a flag from the production file and confirming the failure names that file and that flag.
  • The network-isolation guard compared hostnames only, so it could not catch a same-origin backend-shaped request โ€” which is exactly what the agenda fetch issues when the API URL is unset, since the resulting path resolves relative to the demo's own origin.

Test inventory

The no-Live guarantees

File Covers
src/staticwebapp-csp.test.ts The shipped CSP really carries connect-src 'self'
scripts/check-bundle.test.ts The build-time bundle gate โ€” its forbidden list, and that it fails rather than warns
src/mock-lock.test.ts Mock mode is written before mount, so an inherited flag from another Open Regels app on the same origin cannot win
src/env-files.test.ts All four environment files, not just the one the runner happens to load

The bundle gate's forbidden list is adapted from the public site's rather than copied: it targets the auth library and the two backend origins, not the bare product name, which this bundle ships legitimately in its role tables and its visible role bar. The first real build caught a genuine finding.

The gate silently no-opped on Windows until v2026.08.28

Both bundle-check scripts guarded their entry point by comparing a module URL against the process argument. On Windows that argument is a drive-letter path with backslashes, so the comparison never held: the gate exited zero having checked nothing, and the build passed. Both scripts run as the last step of every build for two public, unauthenticated sites. On Windows, none of it had ever been checked.

Section curation

File Covers
src/demo/allowed-modes.test.ts The deny-by-default allow-list โ€” 21 static ids plus the dossiers sentinel, reconciled one-to-one against the real mode config's 26 rail items
src/demo/DemoSectionRouter.test.tsx Dropped and unmatched ids render nothing rather than falling through to a placeholder

Deny-by-default is only deny-by-default while both lists stay exhaustive

A section added to the cockpit later and named in neither list is filtered out of the rail and the palette โ€” which looks exactly like the policy working, rather than like a gap. That is the failure mode to watch for when the cockpit grows.

Roles and the host adapter

File Covers
src/demo/DemoRoleContext.test.tsx Role state, including a StrictMode double-invocation regression
src/demo/RollenRechten.test.tsx The role selector and the capability table
src/demo/pa-cockpit-host.test.ts ยท .auth.test.ts The host adapter, pinned against the frontend's equivalent
src/demo/shims/keycloak.test.ts ยท tenant.test.ts The synthetic auth and tenant shims
src/demo/Profiel.test.tsx That the tenant row names the right kind of organisation

The host adapter test pins the two places the demo's adapter correctly differs from the frontend's, rather than asserting a false equivalence. The Profiel test overrides the shim to a municipality tenant for one render and confirms the label follows, so the coverage is not merely correct by coincidence for the single tenant the shim ships.

Presentation and build

File Covers
src/brand-colours.test.ts The five brand colours across all four files that declare them
src/demo/demo-overrides.test.ts The override rules, including the suppressed live toggle
src/demo/changelog/*.test.* The demo's own changelog panel and data
scripts/social-card-origin.test.ts The build plugin rewrites the card's absolute URLs per origin
src/scaffold.test.ts Package wiring

Both the brand-colour and class-coverage guards strip CSS comments before matching. Without that, a colour or class named only inside a comment would count as defined โ€” and a later task in the same plan added prose comments naming real selectors, which would have opened exactly that hole.


The Playwright suite

packages/pa-demo/e2e/plato-demo.spec.ts โ€” 11 tests, Chromium only.

This is the one Playwright suite in the repository that runs in CI, as a blocking step of azure-pa-demo-acc.yml, before the build. It is the only proof of two of the four no-Live layers: that the live toggle is actually hidden in a real browser's cascade, and that the page issues no network request at all. A source-text assertion that the suppressing CSS rule exists cannot show either.

It needs no backend, database or Keycloak. Playwright starts its own dev server and that is the whole environment โ€” which is why this suite could be wired into CI when the frontend's, needing a five-service stack, could not.

npm run test:e2e --workspace=@ronl/pa-demo

# against a deployed environment instead of a local dev server
E2E_BASE_URL=https://acc.plato.open-regels.nl \
  npm run test:e2e --workspace=@ronl/pa-demo

Two assertions carry the weight: that switching role actually changes what Dossierbeheer permits, and that the page issues no request to any backend โ€” the latter proven load-bearing by a red probe with the agenda mock disabled.

Selectors were read off the running app, not taken from the brief

The task brief held several wrong assumptions: rail items are buttons rather than links, two expected test ids do not exist, and a created dossier survives in-app navigation but not a reload. Read the app.

A guard that hardcodes localhost fails against a real deployment

The backend-request guard originally treated localhost as the only same-origin host, which is correct only when Playwright serves the app itself. Run against a live deployment, the app's own document, bundle and stylesheet became off-host by that check, producing two false failures. It now compares against the configured base URL's own origin and fails loudly if that is missing, rather than silently reverting to the old assumption.


Coverage

Area Stmts Branch Funcs Lines
All files 91.30 86.95 85.00 91.66
src/demo 95.91 84.61 100 97.72
src/demo/changelog 100 87.50 100 100
src/demo/shims 75.00 100 44.44 75.00
src 75.00 100 50.00 75.00

The uncovered remainder is almost entirely shims that deliberately return nothing: the dock stand-in and the session-expiry warning both render null by design, because the real components pull in chat machinery and session handling that a public page must not have. They depress the function percentage without representing a gap.