Skip to content

Testing

Four suites, each answering a different question.

Suite Command Question
pytest uv run pytest Does the pipeline behave correctly?
Vitest npm test Do the frontend helpers behave correctly?
Playwright smoke npm run test:e2e Does the whole product path work?
Screenshots npm run screenshots Do the documentation images still match the app?

npm run check additionally runs the i18n catalog checks (i18n:check, i18n:usage) — see Translations.

Backend (pytest)

uv run pytest
uv run pytest --cov=backend/src --cov-report=term-missing
uv run pytest backend/tests/unit/test_grounding.py -v

Layout:

  • tests/unit/ — recognizers; grounding (multiple occurrences, not found, normalized fallback); chunk boundaries; overlap resolution; right-to-left application; consistent tags; override precedence; the source-unchanged invariant; safe-logger rejection; PDF export; vision OCR; evaluation metrics.
  • tests/integration/ — the API through FastAPI's TestClient, plus test_llm_e2e.py driving the full pipeline against tests/fake_llm.py, an in-process fake OpenAI-compatible server.
  • tests/files/ — fixtures. Synthetic only, each headed SYNTHETIC TEST DATA – NO REAL PATIENT INFORMATION.

conftest.py sets ENV_PATH to a nonexistent file before importing any backend module, so a developer's .env — which may point at real endpoints — can never be picked up by a test run. Preserve that when you add fixtures.

What deserves a test

The invariants, above all: the source text is never modified; every span verifies against the source; a right-to-left application keeps earlier offsets valid; an enabled-but-unavailable detector fails the request; the safe logger drops content fields; an unverifiable export is refused.

Umlauts, ß, combining characters, and non-breaking spaces belong in any test that touches offsets — they are where offset bugs actually live.

Frontend (Vitest + jsdom)

npm test
npm run test:watch

Specs sit next to the code they cover as *.test.ts, under frontend/utils/, frontend/composables/, frontend/services/, frontend/stores/. Config is vitest.config.ts at the repository root, kept separate from frontend/vite.config.ts so the test toolchain never reaches the production bundle. Specs import from vitest explicitly — there are no ambient globals.

Current coverage is the pure helpers (textSegments, policy, errors, entityLabels, notices), the composables (including locale switching), the API-payload helpers, and the settings store. Component-level coverage is not set up.

Three specs are load-bearing rather than routine: policy.test.ts pins the frontend mirror of the backend default policy, textSegments.test.ts pins code-point-correct segmentation, and notices.test.ts pins the fallback from a backend warning code to the backend's own English text.

End to end (Playwright)

npm run test:e2e
npm run test:e2e:ui           # interactive
E2E_PORT=3100 npm run test:e2e  # when something else already owns port 3000

The harness boots a deterministic fake OpenAI-compatible server (e2e/support/fake-llm.mjs), the real backend with ENV_PATH=backend/.env.e2e (DETECTORS=rules,llm), and the Vite dev server. Only the model is faked, so grounding, chunking, and the re-check run for real. Nothing is persisted, so there is no state to reset between runs.

e2e/tests/workflow.spec.ts covers: pasted text → detection → override → undo; the export menu and a text download; a PDF upload → redacted-PDF preview → PDF export; rejection of an unsupported file; the language switch (including that it survives a reload); the output language of the placeholders (and that switching the interface afterwards leaves them alone); and the status header.

Both Playwright configs pin the browser locale to de-DE, because the app otherwise follows the browser language and the assertions are written against the German UI.

Details, including how to teach the fake model about a new fixture, are in e2e/README.md.

Documentation screenshots

npm run screenshots

Walks the same path with a fixed viewport, retina scale, and light theme, and rewrites every PNG under docs/assets/screenshots/. Re-run it after any UI change that affects a documented screen, and review the image diff before committing — these ship in the public docs.

Captures past the core path are wrapped in a helper that logs SKIPPED <name> and continues, so one drifted selector costs one image rather than the run. Read the output: a missing image is a bug, not a decision.

The screenshot project runs with channel: 'chromium' because the default headless shell has no PDF viewer and would capture the redacted-PDF panel blank.

Evaluation is not a test

Evaluation measures anonymization quality on annotated documents. It is not part of CI and never will be: it needs real annotated data, which does not belong in this repository. Run it deliberately, record the numbers, and cite them when someone asks how well the tool works.