Testing¶
The CPSV Editor's automated test suite covers the pure-logic core the editor
depends on โ TTL generation and parsing, DMN XML handling, validators, the
iKnow import mapping โ plus the state hooks and the network-boundary
utilities. Components and App.js orchestration are largely uncovered by
design; see Roadmap for why, and what comes next.
Figures on this page are measured, not estimated
Every count, command and coverage percentage below was produced by running the suite against v2026.08.3 on 28 August 2026 โ the full run and each scoped script individually. Rerun the commands in Running the tests to reproduce them.
At a glance: 16 suites ยท 257 tests ยท all passing ยท ~13 s for a full
run with coverage. The two dependency updates in v2026.08.2 and v2026.08.3
(@testing-library/react 16.3.2, postcss 8.5.23) moved no count and shifted
coverage by under half a percentage point. Wall-clock time is machine-dependent
โ treat the suite counts as the stable figures, not the seconds.
Running the tests¶
All tests run on Jest via react-scripts test โ this is still a Create React
App project, with a Vite/Vitest migration planned (see Roadmap).
Run everything from the repository root after npm install.
| Command | Scope | Suites | Tests |
|---|---|---|---|
npm run test:ci |
Everything, once, with coverage | 16 | 257 |
npm test |
Everything, interactive watch mode | 16 | 257 |
npm run test:generator |
TTL generator regression tests | 3 | 22 |
npm run test:roundtrip |
TTL round-trip tests (P1) | 1 | 9 |
npm run test:p2 |
Pure-logic utilities (P2) | 6 | 157 |
npm run test:p3 |
State hooks (P3) | 3 | 28 |
npm run test:p4 |
Network-touching utilities (P4) | 2 | 40 |
Each scoped script has a :watch counterpart โ test:generator:watch,
test:roundtrip:watch, test:p2:watch, test:p3:watch, test:p4:watch โ
that runs the same --testPathPattern without --watchAll=false.
# Full suite, non-interactive, with a coverage report
npm run test:ci
# Day-to-day watch mode
npm test
# One layer in isolation
npm run test:p2
npm test is watch mode โ it does not exit
Use npm run test:ci in CI, scripts, or anywhere a process must
terminate. Note also that --coverage lives on test:ci and not on
npm test: in watch mode Jest pins coverage collection to whatever
matched the first run, which on a clean start is nothing, so a watch
run would report 0% across the board no matter what you exercise
afterwards.
Linting and formatting¶
Not tests, but part of the same pre-push gate:
| Command | What it does |
|---|---|
npm run lint |
ESLint over src/**/*.{js,jsx,ts,tsx} |
npm run lint:fix |
The same, applying fixable corrections |
npm run check-format |
Prettier in check mode over the whole tree |
npm run format |
Prettier in write mode over the whole tree |
Git hooks¶
Husky installs two hooks:
| Hook | Runs |
|---|---|
pre-commit |
npx lint-staged |
pre-push |
npm run lint then npm run check-format |
The hooks do not run the tests
pre-push gates on lint and formatting only, so nothing client-side stops
a push that breaks the suite โ run npm run test:ci yourself before
pushing. Since 20 August 2026 the deploy pipelines do run it, and since
v2026.08.2 a branch ruleset means a failing pull request cannot be merged
into acc at all; see CI.
Note that check-format runs across the whole tree, not just staged
files โ a Prettier violation in a Markdown file fails the push just as a
source file would.
CI¶
Three workflows run in this repository.
| Workflow | Job | Runs |
|---|---|---|
azure-static-web-apps-orange-beach-โฆ |
build_and_deploy_job |
npm ci โ npm run lint โ npm run test:ci โ deploy to acceptance |
azure-static-web-apps-white-sky-โฆ |
build_and_deploy_job |
The same sequence, deploying production |
zizmor.yml (Supply-chain audit) |
audit |
zizmor 1.29.0 over the repository โ pin format, token scope, credential persistence |
The two deploy workflows run lint and then the full suite before the deploy
action, and a failure blocks the deploy. Until 20 August 2026 neither ran
anything of ours at all: the Static Web Apps action builds inside its own
container and invokes none of the repository's scripts, so the workflows went
from checkout straight to build-and-deploy. Both now carry an explicit Node
setup, npm ci, lint and test sequence ahead of it.
Since v2026.08.2 the acc supply-chain gate ruleset makes this enforcement
rather than reporting: acc requires a pull request, and audit is a required
status check. A workflow that runs but cannot block is advice โ and requiring
the check without also requiring a pull request would still let a direct push
past it. There are no bypass actors, so this applies to releases and to the
repository owner alike. The mechanics are covered in
Supply-Chain Pinning.
audit does not run the tests, and the deploy job does not run the audit
They are separate gates on the same pull request. audit reasons about the
pipeline's own supply chain; build_and_deploy_job reasons about the code.
A pull request needs both to be green before it can merge.
Test inventory¶
Test files are colocated with the source they cover (foo.js โ
foo.test.js). Counts are per file, as reported by Jest.
Core: TTL generation and parsing¶
| File | Tests | Style | Covers |
|---|---|---|---|
src/parseTTL.roundtrip.test.js |
9 | Real fixtures, no mocks | Parses a real reference export, regenerates TTL from the parsed state, then parses again โ comparing business fields between the two parses rather than diffing against the original file's formatting |
src/utils/ttlGenerator.cellGrounding.test.js |
12 | Pure unit | Per-cell cprmv:Rule emission, concept dedup, nested hasPart for compound cells, and the SHACL-conformance rules โ see Cell-Level Legislative Grounding |
src/utils/ttlGenerator.versionTarget.test.js |
7 | Pure unit | The CPRMV version selector โ namespace and shape differences between the 0.4.1 and 0.3.2 targets |
src/utils/ttlGenerator.dateAxis.test.js |
3 | Pure unit | Rules-derived consolidation dates and duplicate-path rule URIs |
src/utils/ttlHelpers.test.js |
32 | Pure unit | All ten TTL string/URI helpers โ escaping, sanitising (filenames, ruleIdPath, IRIs), formatting. sanitizeIri is checked for idempotence and for leaving structural URI characters (/, :, #, ?) intact |
Round-trip fixtures are the real reference exports in examples/ โ
full-test.ttl, full-test-import-export.ttl, and the DMN-free
organizations/svb/Bepaling-leeftijd-AOW.ttl. dmnData is derived from each
fixture's own hasDmnData/importedDmnBlocks the way importHandler.js
really does it, so the tests exercise a shape the app actually produces.
examples/ronl.ttl is deliberately excluded โ it is the RONL SKOS vocabulary
file, not a CPSV-AP service export, and parseTTLEnhanced correctly throws on
it.
Pure-logic utilities (P2)¶
| File | Tests | Style | Covers |
|---|---|---|---|
src/utils/dmnHelpers.test.js |
54 | Real DOM parsing (jsdom DOMParser) |
Primary-decision-key extraction (root detection, p_* constant skipping, multi-root tie-breaking), rule and cell extraction, validateDMNData, concept generation, evaluateTestCaseExpectation, extractOutputsFromDMN |
src/utils/validators.test.js |
29 | Pure unit | All eight exports: the six per-section validators, the validateForm aggregation across every section including array fields, and the isValidDate helper |
src/utils/iknowParser.test.js |
24 | Real DOM parsing | Both iKnow XML export formats, format auto-detection, the field-map helper, the dot-path value extractor, and applyMapping including filters and transforms |
src/utils/cprmvImport.test.js |
13 | Pure unit | flattenCprmvRules โ sub-clause folding, namespace variants (0.4.1 slash, 0.3.0 contains, legacy flat arrays), multi-entry input, malformed input tolerance, id uniqueness |
src/utils/ronlHelper.test.js |
5 | global.fetch mock |
The two SPARQL functions that query the RONL vocabulary through the shared backend proxy |
State hooks (P3)¶
| File | Tests | Style | Covers |
|---|---|---|---|
src/hooks/useArrayHandlers.test.js |
13 | renderHook + a useState harness |
Array CRUD handlers, the four default-item factories, and the pre-configured wrapper hooks. New ids continue from the highest existing id, not the array length |
src/hooks/useDsoImport.test.js |
8 | renderHook + fetch mock + real window.history |
The DSO โ DMN deep-link import: no-op paths, params stripped before the fetch resolves, error branches, and the full success path prefilling three tabs |
src/hooks/useEditorState.test.js |
7 | renderHook + mocked ronlHelper |
Initial defaults, the mount effects, and clearAllData โ including its documented exception that TriplyDB config is not cleared |
Network-boundary utilities (P4)¶
| File | Tests | Style | Covers |
|---|---|---|---|
src/utils/triplydbHelper.test.js |
37 | global.fetch mock + real File/FormData/Blob |
All ten exports: graph-IRI construction, config validation, the three publish paths (including the @prefix โ PREFIX conversion and INSERT DATA/GRAPH wrapping asserted against the posted body), logo upload, connection testing across every HTTP status branch, and the three localStorage-backed config functions |
src/utils/shaclHelper.test.js |
3 | global.fetch mock |
validateTtl โ success, a parsed-but-invalid backend response, and the distinct unavailable shape on network failure. SHACL validation is advisory and must never block publishing |
These use a plain global.fetch mock rather than msw. Every function here
is a single self-contained fetch call rather than a multi-request flow, so the
lighter approach stays proportionate; msw is worth revisiting if a future
phase needs to model a multi-endpoint flow or share fixtures across many
tests.
Smoke test (P0)¶
| File | Tests | Covers |
|---|---|---|
src/App.test.js |
1 | Renders <App /> and asserts on the real header. Replaces CRA's stock "learn react link" stub, which asserted text this app never rendered and had kept test:ci red since the project was scaffolded |
Coverage¶
Measured with npm run test:ci against v2026.08.3 on 28 August 2026.
Overall: 54.75% statements ยท 40.88% branches ยท 38.68% functions ยท 55.57% lines.
That headline number is dominated by untested UI. The layers the suite actually targets are well covered:
| Area | Statements | Branches | Functions | Lines |
|---|---|---|---|---|
src/hooks |
96.73% | 75.40% | 97.05% | 100% |
src/config |
92.15% | 80.39% | 75.00% | 91.83% |
src/utils |
85.05% | 62.27% | 90.57% | 85.69% |
src/components |
24.44% | 4.05% | 18.18% | 23.80% |
src/components/tabs |
5.74% | 2.72% | 2.46% | 6.03% |
Per module, highest first:
| Module | Statements | Branches |
|---|---|---|
utils/ttlHelpers.js |
100% | 100% |
utils/ronlHelper.js |
100% | 100% |
utils/shaclHelper.js |
100% | 87.50% |
hooks/useEditorState.js |
96.77% | 50.00% |
utils/validators.js |
97.40% | 94.31% |
hooks/useArrayHandlers.js |
97.14% | 75.00% |
utils/triplydbHelper.js |
96.69% | 83.62% |
hooks/useDsoImport.js |
96.42% | 78.43% |
utils/dmnHelpers.js |
90.62% | 81.18% |
utils/cprmvImport.js |
90.54% | 81.81% |
utils/iknowParser.js |
88.95% | 70.10% |
parseTTL.enhanced.js |
81.34% | 71.11% |
utils/ttlGenerator.js |
80.99% | 54.25% |
utils/importHandler.js |
10.34% | 0% |
App.js |
14.16% | 31.87% |
components/tabs/DMNTab.jsx |
8.24% | 3.31% |
The two hand-written files the Due Diligence review flagged
as the biggest risk โ ttlGenerator.js and parseTTL.enhanced.js, neither
backed by an RDF library โ now sit around 80% statement coverage with a
round-trip harness over real fixtures. importHandler.js, App.js and
DMNTab.jsx remain the largest gaps, and are exactly what phases P5 and P6
target.
Defects the tests found¶
Both were discovered by writing the tests, not known beforehand.
cprmv:isBasedOn was silently dropped on re-import¶
A past release renamed the generator's cprmv:extends predicate to
cprmv:isBasedOn, but the read side in parseTTL.enhanced.js was never
updated to match. From that rename until v2026.07.0, every
export-then-reimport of a temporal rule silently lost its
extends/isBasedOn relationship โ no error, just an empty field. The
round-trip test caught it on the first run. The parser now accepts both
spellings: isBasedOn first, extends for historical exports.
flattenCprmvRules minted colliding ids¶
Ids were built as Date.now() plus a sequence counter, and both were reset
on every call. Two calls landing in the same millisecond โ plausible in a fast
import flow โ produced identical ids, which collide as React keys if both
results end up in the same list. A module-level counter now keeps ids unique
for the life of the page regardless of call timing.
Documented behaviour¶
Not a bug, but a coupling worth knowing about before you change either side.
NL-SBB concepts only regenerate when a DMN model is attached in session
state. generateConceptsSection() is gated behind hasDMN() โ that is,
dmnData.isImported && dmnData.importedDmnBlocks, or dmnData.fileName &&
dmnData.content โ and not on concepts.length > 0. End to end this works
correctly, because importHandler.js derives dmnData from the imported
file. But a caller that skips that mapping and calls generateTTL with an
empty dmnData will see concepts silently vanish even though concepts still
has entries. parseTTL.roundtrip.test.js asserts both cases explicitly, so
the coupling is locked in and visible rather than a silent trap. Whether
concepts should be exportable independent of an attached DMN is a product
question, deliberately left open.
Adding tests¶
- Colocate.
foo.jsโfoo.test.js, next to the source. - Split by concern, not by file size.
ttlGenerator.jsalready has three separate test files (dateAxis,versionTarget,cellGrounding) rather than one large one โ easier to review, and easier to see what is covered at a glance. - Add a script pair per phase. Each new phase gets
test:<phase>andtest:<phase>:watchinpackage.json, using a--testPathPatternregex naming the files it covers, mirroring the existingtest:p2/test:p3/test:p4entries. - Mock at the network boundary. There is no local backend to run against โ
the editor depends on the Linked Data Explorer's shared Express backend for
the SPARQL proxy, TriplyDB publishing, and DMN validation, deploy and
evaluate. Anything touching those needs a
fetchmock, not an integration target. - Update the counts on this page when a phase lands, and rerun
npm run test:cito get real figures rather than estimating.
Roadmap¶
Vite/Vitest migration checkpoint โ next. P0โP4 are green, which is the
precondition. The plan is to migrate the bundler (CRA โ Vite, following the
Linked Data Explorer's proven configuration) and re-run the existing suite
under Vitest as the proof the migration regressed nothing. These phases were
written to port cheaply: they are pure logic and hooks, with Jest and Vitest
sharing nearly the same API surface (jest.fn() โ vi.fn() and
jest.mock() โ vi.mock() are close to mechanical renames), and they touch
neither JSX nor CRA-specific tooling.
| Phase | Scope | Notes |
|---|---|---|
| P5 | Tab components, PreviewPanel, PublishDialog |
Written directly under Vite/Vitest after the checkpoint. Critical interactions only โ field entry โ state update โ preview reflects it; publish-workflow step transitions โ not exhaustive branch coverage |
| P6 | components/tabs/DMNTab.jsx |
The largest and most complex file. Mock the backend at the fetch boundary and validate the validate โ deploy โ test โ generate-concepts lifecycle at the interaction level |
| P7 | Playwright E2E smoke tests | Start narrow: the app loads, a minimal service description exports successfully. Expand once there is more automated-testable surface |
The DOM-heavy phases are sequenced deliberately after the migration so that
bundler-sensitive work โ Tailwind/CSS processing, the JSX transform,
process.env versus import.meta.env โ gets written once against the final
toolchain instead of twice.
Deliberately out of scope for now: swapping in an RDF library (the round-trip tests are the data that should decide whether the hand-rolled parser is fragile enough to justify the migration cost); OIDC and production publishing auth, which is a separate security workstream described in Due Diligence; and visual regression and cross-browser matrices, which are worth revisiting once the phases above are stable locally. CI wiring was on this list too, and came off it on 20 August 2026 โ see CI.
An E2E phase here will not resemble the RONL Business API's. There is no Keycloak, no roles and no tenants โ the editor is a single-user authoring tool today, so a login-flow test has nothing to drive until OIDC lands. Publishing to TriplyDB and deploying to Operaton are real side effects against shared infrastructure; prefer a disposable or local target over the shared instances if an E2E test ever needs to deploy for real.