Skip to content

Testing

The CPSV Editor's automated test suite runs on Vitest, and 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, the network-boundary utilities, every tab component, and three end-to-end journeys driven against a live stack.

The testing roadmap that ran from P0 to P7 is complete: every phase has landed, and the per-file 80% branch floor is enforced natively by the runner.

Figures on this page are measured, not estimated

The unit figures below were re-measured against v2026.09.7 on 26 September 2026 at a7fe76f on acc โ€” the same tree as 7d154ba on main โ€” in the working checkout after a clean npm run deps:check, so the installed dependencies matched the lockfile, under Node 24.14.1. .nvmrc now names 24.20.0, which is what CI runs. The full run with coverage reported 62 files and 763 tests, all passing, in 73 s (Vitest's own duration), with coverage unchanged from v2026.09.6. The per-file and per-script counts are unchanged too: the same 62 files, the same totals. The three end-to-end journeys were not re-run for this release. They were last run on 19 September 2026 against v2026.09.6, locally against a live stack โ€” this editor's dev server, the Linked Data Explorer backend on :3001 and Operaton on :8081 โ€” and 3 passed in 14.0 s, with Playwright 1.62.1. Rerun the commands in Running the tests to reproduce them.

At a glance: 62 files ยท 763 tests ยท all passing ยท 73 s for a full run with coverage on this measurement, plus 3 end-to-end journeys run separately.

Coverage: 90.10% statements ยท 88.23% branches ยท 78.71% functions ยท 90.76% lines, with every file at or above the 80% branch floor.

Measure after npm ci, never after npm install

npm install has reported "up to date" here while node_modules still held the old versions of a just-bumped dependency โ€” npm had written the new ones into its hidden lockfile without replacing the package directories. A run against that tree reports a green suite measured on the wrong instrument. CI is immune because it installs with npm ci on a clean runner; a local run is not.


Running the tests

All tests run on Vitest. react-scripts and Create React App are gone as of v2026.09.1 โ€” see The Vite migration. Run everything from the repository root after npm ci.

Command Scope Suites Tests
npm run test:ci Everything, once, with coverage 62 763
npm test Everything, interactive watch mode 62 763
npm run test:generator TTL generator regression tests 8 119
npm run test:roundtrip TTL round-trip tests (P1) 1 9
npm run test:p2 Pure-logic utilities (P2) 8 191
npm run test:p3 State hooks (P3) 3 33
npm run test:p4 Network-touching utilities (P4) 2 42
npm run test:p5 Tab components, PreviewPanel, PublishDialog (P5) 23 181
npm run test:p6 DMNTab's full lifecycle (P6) 6 98
npm run test:e2e Playwright journeys (P7) โ€” needs a live stack 3 3

Each scoped script has a :watch counterpart. The phase scripts overlap deliberately โ€” a file can belong to more than one phase โ€” so their counts do not sum to 763.

End-to-end journeys need a live stack

test:e2e drives the real application against a real backend and a real engine, with nothing mocked between the browser and Operaton. It is not wired into CI: it needs the Linked Data Explorer backend and an Operaton engine, neither of which exists on a runner.

Command What it does
npm run test:e2e Headless, and opens the HTML report when it finishes
npm run test:e2e:headed A real browser window, actions slowed to 400 ms
npm run test:e2e:ui Playwright UI mode

A global setup checks both services first and names what is missing, because the failure otherwise arrives disguised: a connection refused surfaced as an amber validation banner, or as a deploy that quietly never enabled the Evaluate button, with the runner then reporting a timeout on a button several steps from the cause. The backend URL is read from .env.development rather than hardcoded, so the preflight checks the same host the application will call.

UI mode waits for you to press play

An idle UI-mode window with an empty trace pane reads as a hang. It is not: the runner discovers the tests and then waits, so no dev server starts and nothing reaches the backend until play is pressed.

# 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. --coverage lives on test:ci and not on npm test, so a watch run reports no coverage figures.

Linting and formatting

Not tests, but part of the same pre-push gate:

Command What it does
npm run lint eslint . โ€” the flat config lints src/**, e2e/**, the root config files and scripts/**/*.{js,mjs}
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 deps:check, then npm run lint, then npm run check-format

deps:check runs first since v2026.09.5: a clone whose install has fallen behind the lockfile stops there with npm ci named, instead of failing lint or the format check on the wrong tool versions. npm start runs the same check before Vite starts. It also warns when the machine's npm is older than 11.10, which ignores the 14-day cooldown in .npmrc without a word.

The hooks do not run the tests

pre-push gates on the install, lint and formatting only, so nothing client-side stops a push that breaks the suite โ€” run npm run test:ci yourself before pushing. The deploy pipelines do run it, and Build and deploy ACC is a required check on acc, so a pull request that fails the suite cannot be merged there; 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.

Since v2026.09.2 formatting is also checked in CI, in the audit job rather than the deploy workflows. Those skip changes confined to docs/**, .claude/** and **/*.md, so a check placed there would never see markdown โ€” precisely what drifts, since lint-staged only formats src/** and package.json on commit.

CI

Seven workflows run in this repository. Every job runs on ubuntu-24.04.

Workflow Job Runs
Deploy ACC (orange-beach) changes, then Build and deploy ACC changes decides whether a pull request touches anything outside docs/**, .claude/** and **/*.md; then Node from .nvmrc โ†’ npm ci โ†’ npm run lint โ†’ npm run test:ci โ†’ npm run build โ†’ upload dist/ to acceptance
Deploy PROD (white-sky) Build and deploy PROD The same sequence without changes, deploying production
Supply-chain audit (zizmor.yml) audit On Node 24.20.0: zizmor 1.29.0, renovate-config-validator --strict, Lockfile matches package.json (npm ci --dry-run --ignore-scripts), npm run check-format, and npm run check-supply-chain
Semgrep (semgrep.yml) scan Semgrep Code and Supply Chain โ€” see Dependency Scanning
Close preview environments Close ACC staging environment, Close PROD staging environment Deletes a pull request's Static Web Apps preview when it closes โ€” no tests, but see below
Dependency audit (dependency-audit.yml) dependency-audit Daily at 05:17 UTC: npm audit --package-lock-only on both acc and main, failing on a high or critical production advisory โ€” no tests
Release SBOM (sbom.yml) release-sbom On a push to main: writes the CycloneDX SBOM and checks the released version has one committed โ€” no tests

Preview environments close from a workflow with no path filter (v2026.09.6). The close jobs used to live in the deploy workflows, whose paths-ignore applies to the close event too, so a pull request that changed only documentation never started the workflow holding its close job, and its preview kept running. Four previews on ACC and four on PROD had been left that way, each serving a public URL with old code and holding one of the ten slots the Static Web Apps Standard plan allows. The close jobs now trigger on every pull-request close for acc and main; closing an environment that was never created succeeds and does nothing. GitHub starts no workflow at all for a pull request with a merge conflict, so the release procedure also runs npm run check-previews, which lists orphaned previews and prints the exact delete command without running it.

The two deploy workflows run lint and then the full suite, then build the bundle on the runner, and a failure anywhere blocks the deploy. The deploy action only uploads dist/ (skip_app_build: true), so the tree the tests ran against is the tree that ships, on the Node .nvmrc names. Until 20 August 2026 the workflows ran nothing of ours at all: they went from checkout straight to the Static Web Apps action, which built inside its own container.

Documentation-only changes get no test run and no preview environment โ€” if you changed only Markdown and expected a green tick from the suite, that is why. On a pull request into acc, the changes job decides this and Build and deploy ACC is skipped, which reports as a pass; a failed lookup builds in full. Pushes, and pull requests into main, filter at the trigger with paths-ignore. The audit workflow goes the opposite way: it has no branches filter and runs on every pull request, including one based on another feature branch. While it had one, a stacked pull request accumulated no audit and GitHub reported it as clean with zero checks, then blocked it permanently once the base was retargeted to acc.

The acc supply-chain gate ruleset makes this enforcement rather than reporting: acc requires a pull request, and audit, scan and Build and deploy ACC are required status checks. A workflow that runs but cannot block is advice โ€” and requiring the checks without also requiring a pull request would still let a direct push past them. There are no bypass actors, so this applies to releases and to the repository owner alike. main requires no status checks. 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 ACC reasons about the code. A pull request into acc needs both, and scan, to be green before it can merge โ€” and because the deploy job skips a documentation-only pull request while the audit does not, such a pull request is in practice gated by audit and scan alone.


Test inventory

Test files are colocated with the source they cover (foo.js โ†’ foo.test.js). Counts are per file, as reported by Vitest.

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.sections.test.js 29 Pure unit The skeleton the others sit in: generate() assembles a dozen optional sections, each behind its own condition โ€” service identifier and sector, organisation logo, legal resource, temporal rules
src/utils/ttlGenerator.uris.test.js 26 Pure unit The URI and date helpers every emitter shares โ€” legal-resource and ruleset URIs, cprmvValidFrom, rules-derived dates, CPRMV rule subject URIs. A wrong one does not throw; it publishes a graph that parses and points at the wrong resources
src/utils/ttlGenerator.entities.test.js 23 Pure unit Parameters, cost, output and the vendor service โ€” above all that an empty field produces no triple, never a triple with an empty literal
src/utils/ttlGenerator.dmn.test.js 11 Pure unit The DMN section for a model uploaded in this editor โ€” deployment, test and validation history, each optional and each its own predicate
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/ttlGenerator.citationStub.test.js 8 Pure unit + the real export The typed stub minted for a cprmv:isBasedOn citation โ€” and no stub for a rule the same document already publishes, which asserted a second, contradictory cprmv:id. Half the file regenerates the real SZW normenbrief export and checks no subject carries two ids
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 31 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 โ€” plus the two guards: prototype path segments are neither read nor written, and a nested-quantifier or over-long transform pattern throws
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 15 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 9 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 9 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 38 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 4 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

src/utils/problem.test.js (7 tests, v2026.09.6) belongs with these, though no phase script names it: it pins getProblemDetail, which reads the backend's RFC 9457 detail first and still understands the older error.message and bare-string error shapes. src/App.lazyTabs.test.jsx (5 tests) covers the lazy tabs, including the DSO deep link that used to leave the DMN tab highlighted over an empty panel.

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.jsx 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

Tab components and dialogs (P5)

Twenty-three files, 181 tests. Deliberately thin and even: every component renders with realistic props, and one representative interaction proves the controlled-component contract. No component is left at zero, and src/components moved from 3.9% to 41% statements in this phase alone.

These tests found a latent crash in IKnowMappingTab, which reads mappingConfig.mappings unguarded inside an expression short-circuited by an empty configuration name โ€” so a config without that key renders fine and throws the moment someone types a name.

They also pinned something that was invisible: VendorTab switches on the vendor URI rather than a capability flag, so iKnow gets integration tooling, Blueriq gets the contact form, and every other vendor gets an "under development" placeholder. Adding a vendor to the concept list gives it a dropdown entry and nothing else.

DMNTab lifecycle (P6)

Six files, 96 tests, covering validate โ†’ deploy โ†’ evaluate. The suite walks the real lifecycle rather than testing each call in isolation, because the interface enforces that order: the Evaluate button stays disabled until a deployment has succeeded.

Two findings worth keeping. The unreachable-backend path is a deliberate third outcome โ€” not valid, not invalid, but skipped โ€” and is now pinned as one. And handleDeployDMN's opening guard is unreachable: the Deploy button renders inside the branch that requires an uploaded file, so the error it raises can never fire.

End-to-end journeys (P7)

Three Playwright specs, run against a live stack rather than mocks. Measured on 11 September 2026 against the acceptance deployment, with the preflight confirming its backend and the Operaton engine before anything was driven:

E2E_BASE_URL=https://acc.cpsv-editor.open-regels.nl \
E2E_BACKEND_URL=https://acc.backend.linkeddata.open-regels.nl \
E2E_OPERATON_URL=https://operaton.open-regels.nl \
npx playwright test --reporter=list
#   3 passed (9.3s) โ€” authoring 3.1 s, normbedragen 3.2 s, round-trip 2.0 s

--reporter=list rather than npm run test:e2e, which opens the HTML report when it finishes โ€” fine at a desk, a hang in anything scripted.

Spec What it proves
e2e/authoring-journey.spec.js The application can create a service: empty form โ†’ real deploy โ†’ real evaluation โ†’ exported TTL
e2e/round-trip-journey.spec.js It can read an existing one back: import a full TTL, swap its decision model, deploy, evaluate, download and check what was written
e2e/normbedragen-journey.spec.js One deployment answers repeatedly: a chained DRD deployed once, evaluated with four request bodies that differ only in the peildatum, then exported. Added in v2026.09.3 with the 2026-H2 bijstandsnormen

E2E_BASE_URL points the journeys at a deployed build โ€” and at production's engine

Since v2026.09.3 E2E_BASE_URL drives the journeys against an already-deployed app instead of a local dev server, and drops the webServer block so none is started. The journeys are not read-only: each clicks Deploy to Operaton and leaves a deployment behind, and the fixtures use real decision keys. .env.acceptance and .env.production name the same engine, https://operaton.open-regels.nl, so a run against acceptance deploys a real version of those keys into the engine production evaluates against โ€” and Operaton versions a duplicate key rather than rejecting it, so a consumer that evaluates by key then answers from the test fixture. Only a local stack, .env.development's localhost:8081, is free of that.

The app also calls the backend it was built against: E2E_BACKEND_URL redirects the preflight probe, not the application. Driving a deployed build means driving its stack.

Both assert values rather than status codes โ€” zorgtoeslag 1150 and a specific annotation string โ€” because a decision table that stopped matching would still answer 200 with an empty result set. That assertion was verified to bite by setting the expected value wrong and watching the test fail. The export assertion reads the downloaded file rather than trusting the filename, since a filename check passes on an empty file.

Fixtures live in e2e-fixtures/ with a manifest recording each one's provenance, so editing an example cannot silently change what a test asserts.

The authoring journey found a defect no unit test could reach: below 1600px the preview panel, mounted fixed and 500px wide, covers the header controls โ€” at 1280px, opening the preview leaves no way to close it. The suite runs at 1600ร—900 so the journey is not blocked, and the threshold is documented rather than dodged.

Coverage

Measured with npm run test:ci against v2026.09.7 on 26 September 2026, in a checkout whose install npm run deps:check confirmed against the lockfile. The totals are identical to v2026.09.6's.

Overall: 90.10% statements ยท 88.23% branches ยท 78.71% functions ยท 90.76% lines โ€” against 54.75% / 40.88% / 38.68% / 55.57% at v2026.09.0. The jump is P5, P6 and the branch-floor work landing together; v2026.09.3's two parser guards moved src/utils a little further.

Area Statements Branches Functions Lines
src/hooks 100% 93.44% 100% 100%
src/utils 94.80% 88.06% 97.98% 95.62%
src/config 92.15% 82.35% 75.00% 91.83%
src/components 84.41% 88.65% 85.71% 87.50%
src/components/tabs 84.46% 92.18% 65.93% 84.67%
src (App, index) 84.56% 82.22% 58.62% 85.63%

The rows are the runner's own per-directory figures, so src/components counts only the files directly in that folder โ€” the dialogs and panels โ€” and the tabs have a row of their own. That split is where the functions gap is: the tabs are well branched and thinly exercised as whole handlers.

src/components was at 4.05% branches before the P5 and P6 work landed.

Every file clears the 80% branch floor

The floor is enforced natively โ€” thresholds: { branches: 80, perFile: true } โ€” so this is a gate rather than a report. See The Coverage Floor for how the three repositories reached it.

DMNTab.jsx, the largest file in the repository, is the best-covered component at 98.32% branches, from 45.73% at v2026.09.1.

Two files sit one branch above the floor

ConceptsTab.jsx 80.55% and ChangelogTab.jsx 80.70%. Thresholds pass at >= 80, and the ratchet that used to absorb a slip is gone, so a single added ?. or || default in either turns CI red on an otherwise unrelated change. useDsoImport.js was the third until v2026.09.6, when the deep-link fix's tests took it from 80.39% to 92.15%.

What the branch column does not see

A branch floor steps straight over branch-free code. ConceptsTab.jsx reads 80.55% on branches but 71.62% statements and 63.33% functions; App.jsx reads 83.33% / 72.44% / 55.55%. The uncovered code there is largely whole handlers no test calls. That asymmetry is why a functions floor is a separate decision rather than a free companion setting.


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.js has eight separate test files โ€” sections, uris, entities, dmn, cellGrounding, versionTarget, dateAxis, citationStub โ€” 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> and test:<phase>:watch in package.json, passing Vitest the file-name filters of the files it covers, mirroring the existing test:p2 / test:p3 / test:p4 entries.
  • 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 fetch mock, not an integration target.
  • Update the counts on this page when a phase lands, and rerun npm run test:ci to get real figures rather than estimating.

The Vite migration

Create React App is gone as of v2026.09.1, migrated in four phases, each independently revertable, in an order chosen so a working test suite exists at every point. react-scripts provided both the build and the test runner, so swapping the build first would have removed the regression net and the thing being tested at the same moment โ€” leaving no way to tell a migration defect from a configuration defect.

Phase What landed What it proved
1 Vitest alongside Jest Both runners reported the same 16 suites and 257 tests
2 Vite builds alongside CRA Same eleven public assets, main chunks within a kilobyte
3 The atomic cutover build/ โ†’ dist/; npm audit 52 โ†’ 10; builds ~30 s โ†’ under 2 s
4 The shims removed 45 jest call sites migrated, not the 43 the plan counted

Two things the plan did not cover would have deployed green and broken: Vite only exposes VITE_-prefixed variables, so renaming the env call sites alone would have left both environments talking to localhost; and react-scripts was where ESLint itself came from, so removing it would have broken the lint step that runs before the tests.


Roadmap

The P0โ€“P7 roadmap is complete. Every phase has landed:

Phase Scope Landed
P0 Smoke test v2026.07.0
P1 TTL round-trip against real fixtures v2026.07.0
P2 Pure-logic utilities v2026.07.0
P3 State hooks v2026.07.0
P4 Network-boundary utilities v2026.07.0
P5 Tab components, PreviewPanel, PublishDialog v2026.09.1
P6 DMNTab's validate โ†’ deploy โ†’ evaluate lifecycle v2026.09.1
P7 Playwright journeys against a live stack v2026.09.1

The DOM-heavy phases were sequenced deliberately after the migration, so that bundler-sensitive work โ€” Tailwind/CSS processing, the JSX transform, process.env versus import.meta.env โ€” was written once against the final toolchain instead of twice.

What remains

Not phases, but the honest remaining edges:

  • Seven unreachable branches in DMNTab.jsx, all guards the UI cannot reach โ€” if (!uploadedFile) under a button that only renders once a file exists, and three of the same shape. Chasing them would mean testing through the component's internals; the file documents them as defensive dead code.
  • Two files one branch above the floor, with no ratchet left to absorb a regression.
  • A functions floor, which is a separate decision needing its own measurement โ€” App.jsx sits at 55.55% functions against 81.48% branches.

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.