Testing¶
RONL Business API is an npm-workspaces monorepo with five tested packages: the
Express/TypeScript backend on Jest, and four on Vitest β the caseworker
portal (packages/frontend) with React Testing Library, the Public Affairs
cockpit (packages/pa-cockpit), the public cockpit demo
(packages/pa-demo) and the public search site (packages/public-site) with
jsdom. All five run with coverage by default.
Figures on this page are measured, not estimated
Every count and percentage below was produced by running
npm run test:serial against v2026.08.33 on 29 August 2026, on
acc at 1e7fb19. Rerun the commands in
Running the tests to reproduce them.
Two things were not re-run for this release and say so where they appear: the frontend and public-site Playwright suites, and the four live-smoke shell scripts, which remain described from their configuration only. See E2E & live smoke.
At a glance:
| Package | Runner | Files | Tests | Result | Wall timeΒΉ | Statements | Branches | Functions | Lines |
|---|---|---|---|---|---|---|---|---|---|
packages/backend |
Jest + ts-jest | 74 | 1576 | all passing | ~86s | 98.35% | 91.49% | 96.65% | 98.75% |
packages/frontend |
Vitest + RTL | 103 | 842 | all passing | ~276s | 88.07% | 80.09% | 83.53% | 88.96% |
packages/pa-cockpit |
Vitest + RTL | 41 | 368 | all passing | ~92s | 86.16% | 75.55% | 83.46% | 88.53% |
packages/pa-demo |
Vitest + jsdom | 19 | 104 | all passing | ~27s | 91.30% | 86.95% | 85.00% | 91.66% |
packages/public-site |
Vitest + jsdom | 28 | 140 | all passing | ~108s | 86.82% | 70.39% | 87.63% | 88.76% |
265 files Β· 3030 tests, plus one performance spec run separately β 3031 in total. See Coverage for what those percentages mean and where the remaining gaps are.
The frontend did not shrink β the cockpit moved
The frontend reads 103 files / 842 tests here against 133 / 1155 at
v2026.08.23. Nothing was deleted: 41 files and 368 tests moved into
packages/pa-cockpit when the cockpit was extracted into a package. Taken
together the pair grew by 11 files and 55 tests. See
PA-Cockpit package.
ΒΉ These are serial wall times, measured with test:serial, and are much
longer than a parallel run β the frontend suite measures ~61s parallel on an
idle machine. Treat them as an order of magnitude rather than a figure to match,
and compare like with like.
Where to look¶
| Page | Covers |
|---|---|
| Coverage | Headline and per-area coverage for all five packages, and why the last two decimals are noise |
| Backend suite | The 74 files and 1576 tests in packages/backend, by area |
| Public site suite | The 28 files and 140 tests in packages/public-site, plus its own Playwright suite |
| PA-demo suite | The 19 files and 104 tests in packages/pa-demo, and the one Playwright suite that runs in CI |
| Caseworker Β· PA cockpit Β· Infra-board Β· Woo-dashboard | The frontend and cockpit suites, split the way the product is β one page per board |
| E2E & live smoke | The Playwright suites, what they need running, and the four cross-app shell scripts |
| Writing tests | Conventions for adding tests here, and the traps that have already cost time |
The four board pages do not add up to the frontend total, by design
They account for 841 of the 1155 frontend tests. The other 314 are not board-specific and so have no board page to live on:
- 223 in
src/services(211),App.test.tsx(5),src/hooks(4) andsrc/test(3) - 49 in shared widgets reused across boards β
ProcessStartFormViewer,TimeLine,DecisionViewer,LoginChoice,AltchaWidget,SessionExpiryWarning,PersonalDataPanel - 42 in pages that belong to no board β
AuthCallback,Dashboard,ChangelogPanel,LoginChoice
Note also that components/CaseworkerDashboard/ is counted under
Caseworker but is the shared section-component
library, reused across three of the four V2 dashboards. Its 185 tests
protect more than that one board.
Running the tests¶
Run from the repository root after npm install (node_modules must already
be installed in every workspace).
| Command | Scope | Files | Tests |
|---|---|---|---|
npm test |
Every workspace with a test script (see below) |
265 | 3030 |
npm run test:serial |
The same, without file parallelism | 265 | 3030 |
npm test --workspace=@ronl/backend |
Backend only (Jest, coverage on by default) | 74 | 1576 |
npm test --workspace=@ronl/frontend |
Frontend only (Vitest, coverage on by default) | 103 | 842 |
npm test --workspace=@ronl/pa-cockpit |
The cockpit package | 41 | 368 |
npm test --workspace=@ronl/pa-demo |
The public demo | 19 | 104 |
npm test --workspace=@ronl/public-site |
Public site only (Vitest, coverage on by default) | 28 | 140 |
npm run test:perf --workspace=@ronl/frontend |
The wall-clock budget, run without file parallelism | 1 | 1 |
# Everything
npm test
# Everything, serially β see the warning below
npm run test:serial
# One workspace
npm test --workspace=@ronl/backend
npm test --workspace=@ronl/pa-cockpit
# Single file / pattern
npx jest --config packages/backend/jest.config.js --no-coverage --testPathPattern=rules
npx vitest run --config packages/frontend/vite.config.ts session
npx vitest run --config packages/public-site/vite.config.ts SectionIndex
Reach for test:serial, not for a flag
This repository runs two test runners behind one command shape β the
backend on Jest, the other four on Vitest. Any serial flag you reach for is
right in four places and wrong in the fifth: --no-file-parallelism is
Vitest's, --runInBand is Jest's, and Jest rejects the Vitest flags
outright. Every workspace defines a test:serial script for exactly this
reason, so the runner's identity stops being something the caller has to
know.
Note that a parallel-run failure is not a finding until it fails
serially. The flakiness chased through August turned out to be timeouts at
Vitest's 5000ms default under machine load, not a defect in the code under
test; testTimeout is now 20s in all four Vitest workspaces.
What npm test at the root actually runs. The root script is
npm run test --workspaces --if-present, fanning out over every package under
packages/*. Five of the six have a test script β @ronl/backend,
@ronl/frontend, @ronl/pa-cockpit, @ronl/pa-demo, @ronl/public-site β and
--if-present silently skips the sixth, @ronl/shared, which has no test
script at all (only build, prepare, clean, type-check). That is
expected, not a gap: shared is a types-only package with nothing to unit test.
Clear the Jest cache before trusting a green backend run
ts-jest caches type diagnostics per file, so a warm cache can skip
re-checking a file that no longer compiles and report the suite green. That
is not hypothetical: nine backend test files were latently broken for
weeks β every top-level declaration landing in the global scope, colliding
across files β while npm test passed locally every time. The first CI run
on a cold runner failed immediately.
Which files fail also varies between runs, because it depends on the order the type program reaches them. See Writing tests.
The performance budget¶
simEngine.ts carries a real budget: run(cfg) must process the default
3,150-application population in under 250ms. Its source comment is emphatic
that if the assertion ever fails the threshold must not be loosened β the
intended remedy is a web worker, not a bigger number.
The problem was never the threshold. A single performance.now() call measures
the machine as much as the engine: on a contended host the assertion was
observed at 302ms, then 837ms, then 1297ms across three consecutive runs, and
inside a full npm test β where Vitest saturates every core with 133 parallel
test files β even the fastest of three CPU-time samples came out at 468ms,
against ~100ms in isolation. Wiring the CI test gate would have made that a
permanently red pipeline.
So the budget moved rather than moved up:
- The assertion lives in
simEngine.perf.test.ts, still asserting< 250ms, now with a warm-up run and the fastest of three samples so a JIT pause or a stray GC cannot decide it. vite.config.tsexcludessrc/**/*.perf.test.tsfrom the default run.vitest.perf.config.tsmirrors it β same plugins, aliases and setup, but the perf specs are the only thing included, andfileParallelismis off. It spreads and overrides the base test block rather than usingmergeConfig, which concatenates arrays and would have kept the baseexclude, hiding the perf specs from their own run.npm run test:perfruns it, and both frontend workflows run that as their own blocking CI step.
ChangelogPanel.test.tsx was the other casualty of the same contention: its
15-second timeout sufficed in isolation but not inside a full run, where it was
observed taking 22s. changelog-data.ts renders every real version entry, so
the test is genuinely slow rather than unreliable β and a timeout exists to
catch a hang, not to assert a speed. It was raised to 60s, which costs no
coverage; the perf spec remains the one place a real budget is asserted.
Linting, formatting, git hooks, and CI¶
| Command | What it does | Result (measured) |
|---|---|---|
npm run lint |
npm run lint --workspaces --if-present β eslint . in backend, frontend, public-site |
exit 0, no errors |
npm run check-format |
prettier --check "**/*.{ts,tsx,json,md}" --ignore-path .gitignore β one repo-wide glob, not a per-workspace fan-out |
exit 0, no violations |
npm run lint skips @ronl/shared the same way test does β no lint script
there. npm run check-format, by contrast, is not scoped by workspace
scripts at all: it is a single Prettier invocation over the whole tree (minus
.gitignored paths), so it reaches packages/shared too even though shared
defines no format-check script of its own.
Git hooks¶
| Hook | Runs | Scope |
|---|---|---|
pre-commit |
npx lint-staged |
Staged files only |
pre-push |
build @ronl/shared β npm run type-check β npm run lint β npm run check-format |
All workspaces (type-check, lint) / whole tree (check-format) |
The hooks do not run the tests
Neither pre-commit nor pre-push invokes any test script, so nothing
client-side stops a push that breaks a suite β run npm test yourself
before pushing anything nontrivial. Since 20 August 2026 every pipeline
does run them, so a broken push no longer reaches acceptance; the
client-side gap is now a matter of feedback speed rather than of what
ships.
CI¶
Nine workflows under .github/workflows/ on acc β an acc/prod pair per
package, plus the supply-chain audit gate:
| Workflow | Lint | Type-check | Tests | E2E | Perf budget | Build | Deploys? |
|---|---|---|---|---|---|---|---|
azure-backend-acc.yml / -prod.yml |
β | β | β | β | β | β | No β packages and uploads an artifact |
azure-frontend-acc.yml / -prod.yml |
β | β | β | β | β | β | Yes |
azure-pa-demo-acc.yml / -prod.yml |
β | β | β | β (acc only) | β | β | Yes |
azure-publicsite-acc.yml / -prod.yml |
β | β | β | β | β | β | Yes |
zizmor.yml |
β | β | β | β | β | β | No β blocking audit gate |
Every package has a real CI test gate, as of 20 August 2026 β a failing test
blocks the pipeline in all eight Azure workflows. Public-site had one already.
Backend CI now runs Jest alongside its existing lint step; frontend CI previously
ran neither lint nor test, going straight from npm ci to vite build.
azure-pa-demo-acc.yml is the only workflow that runs an end-to-end suite β
the first Playwright in CI anywhere in this repository. See
PA-demo suite.
The backend workflows are the exception to "deploy": they end at Create deployment zip β Upload deployment artifact. Nothing in them calls a deploy action, and there is no post-deployment health check β the artifact is deployed separately, from a developer machine. See CI/CD and the supply-chain gate.
The gap was deliberate while it lasted: gating on coverage that was still climbing was judged theatre, the same call the Linked Data Explorer made and has since reversed for the same reason.
Roadmap¶
E2E CI wiring is done for one of the three Playwright suites. The
PA-demo suite runs as a blocking step of
azure-pa-demo-acc.yml, which was possible because that demo needs no backend,
database or Keycloak β Playwright starts its own dev server and that is the whole
environment.
The other two still run locally only. The frontend suite needs a
webServer-style auto-boot for Keycloak/Postgres/Redis/backend/LDE-backend β
there is no human to start the stack manually in CI β plus a known Node
v24-on-Windows exit crash in globalSetup and an unbounded
local-Operaton-history-growth gap to close first. The public-site suite has no
such blocker and is the obvious next candidate.
Two boards have no end-to-end coverage. Infra-board and Woo-dashboard are well covered by unit tests and have no Playwright specs at all. That is a gap rather than a decision β the PA cockpit work showed exactly which class of defect unit tests cannot see.
Doccle has no live-tested results yet. test-doccle-live.sh exists and is
ready to run, but every run so far has been in DOCCLE_STUB_MODE=true β see
Doccle β Live Testing.
Branch-coverage depth is the natural next target across the frontend and public site: defensive guards and catch-block edges inside already-tested files, not new files to reach.
Deliberately out of scope for now: visual regression and screenshot diffing, a cross-browser matrix (Chromium only in both Playwright suites), and parallel or sharded E2E execution tuning β none are blockers at current suite size.