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 ยท 106 tests, all passing, measured with
npm test --workspace=@ronl/pa-demo on 24 September 2026 against main
at 86af73e (v2026.09.11), after a clean npm ci in a separate clone on
Node 24.14.1 / npm 11.11.0: Duration 19.62s. Coverage 93.47 % statements ยท
95.65 % branches ยท 85.00 % functions ยท 92.85 % lines โ all four
reproduced to the decimal for the fourth release running, which is exactly
what to expect from a package whose src/ tree has not changed since
v2026.09.5: git diff 311d732 86af73e -- packages/pa-demo/ touches nothing
but package.json. Branches stand 8.70 points above v2026.08.33 under the
per-file 80% floor adopted in v2026.09.2 โ a floor that v2026.09.6 turned
from a convention into a configured threshold here
(thresholds: { branches: 80, perFile: true }), and that this run passed
without naming a file.
A repeat measurement reproducing to the decimal is the useful kind of boring: it is the check that the harness, not the package, is what changed when a figure moves elsewhere. The duration is the counter-example in the same run โ 8.57s on 20 September, 19.62s here, for byte-identical source. Coverage percentages are a property of the code; elapsed time is a property of the machine.
At a glance:
| Runner | Vitest 4 + jsdom, coverage via v8 |
| Files / tests | 19 / 106 |
| Wall time | 19.62 s |
| 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.tsnow 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.
Not re-run on 24 September 2026
The spec file was confirmed present and unchanged at 86af73e, but the
suite itself was not executed in this pass either: its last measurement
still stands at 30 August 2026 โ 11 passed, 14.7s. See
E2E & live smoke.
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 โ the acc workflow
only; azure-pa-demo-prod.yml runs the unit tests and skips the E2E step.
Since 19 September 2026 that workflow's job is also a required status
check on acc, listed in the ruleset as Build and Deploy ACC PA Demo, so
this is the one end-to-end suite in the repository that can block a merge. Its
playwright.config.ts is also the only one of the three that is genuinely
cold-startable: it declares a webServer and needs no backend, database or
Keycloak behind it.
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¶
Re-derived on 24 September 2026 from the run's own coverage-summary.json.
| Area | Files | Stmts | Branch | Funcs | Lines |
|---|---|---|---|---|---|
| All files | 16 | 93.47 | 95.65 | 85.00 | 92.85 |
src/demo |
8 | 100 | 100 | 100 | 100 |
src/demo/changelog |
2 | 100 | 87.50 | 100 | 100 |
src/demo/shims |
4 | 75.00 | 100 | 44.44 | 75.00 |
src |
2 | 75.00 | 100 | 50.00 | 75.00 |
The src/demo row on this page was stale, and the package total is what caught it
Through v2026.09.9 this table gave src/demo as 95.91 / 84.61 / 100 /
97.72. Re-derived from the report, all eight files directly in that
directory are at 100 on all four measures โ which is what
Coverage โ pa-demo by area has carried since
20 September, while this table did not. The figures were left over from
before the vendored fork was removed.
The tell was available the whole time: a row below the package's 95.65% branches, in a package whose other three rows are at 100, 87.5 and 100, cannot be reconciled with a 95.65% total. Two tables of the same numbers is a contradiction waiting for the next release โ the same warning Backend suite carries, demonstrated here.
The uncovered remainder is 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. Five files in this package would fail an 80%
functions floor โ App.tsx, PADock.tsx and SessionExpiryWarning.tsx at 0%,
keycloak.ts at 66.66% and tenant.ts at 50% โ and none at all would fail
the 80% branch floor, which is the floor that is actually configured.
Related¶
- PA-Cockpit demo โ the user-facing guide
- PA-Cockpit package โ the package the demo consumes
- PA cockpit โ tests โ the cockpit's own suite
- E2E & live smoke โ the other Playwright suites