Files
atomic-design-poc/libs/shared/docs/behaviour-spec.mdx
T
ehoandClaude Opus 5 e63db509ef refactor(shared): extract uploadOutcome from the XHR load closure (RB-27)
UploadAdapter.xhrUpload built new XMLHttpRequest() directly and put the
actual decisions inside its load listener: 2xx-vs-not, JSON.parse of the
body with a fallback, and ProblemDetails mapping via parseError. None of
it was reachable without stubbing the XHR global, so it had no spec
(TE-005; file LH 5/64, BRH 3/57).

Extract uploadOutcome(status, responseText): Result<string, {
documentId }>, a pure function next to genericError/parseError. It holds
the 2xx check, the JSON.parse-with-fallback, and the ProblemDetails
mapping. The load listener is now a two-line dispatch into it.

Abort-vs-error disambiguation stays where it is: it decides whether a
response exists at all, before uploadOutcome would even run, and the
proposed signature has no field for "aborted". It is already a one-line
ternary with no DOM-only logic to extract.

Add upload.adapter.spec.ts: plain describe/it, no DOM, no XHR stub,
covering a 2xx success, a 2xx unparseable body, a non-2xx ProblemDetails
body, a non-2xx non-ProblemDetails body, and the 200/300 boundary.
Verified red by editing uploadOutcome down to one line (an Edit, not
git checkout): 4 of 5 new specs failed. Re-applied with a second Edit.
Coverage for upload.adapter.ts: LH 5/64 -> 12/65, BRH 3/57 -> 7/59.

Skip TE-005's optional half (moving the currentScenario() branch into
KeepaliveTransport.send()): it needs a second file, upload-shell.
service.ts, and this ticket's own scope fences it to upload.adapter.ts
and its spec. The dev simulator's behaviour is unchanged.

Mark RB-27 implemented in 99-backlog.md and add its implementation note,
including a batch 5 close-out.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 08:52:17 +02:00

1352 lines
46 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{/* GENERATED by `npm run gen:behaviour-spec` (scripts/gen-behaviour-spec.mjs) — do not
edit. Every bullet below is a real `it()` title or backend test method name, extracted
verbatim from the suite. The team rejected Cucumber/Gherkin for BDD scenarios (runtime string
matching undoes the compile-time guarantees WP-70 bought, and needs two frameworks for
.NET+TS) — this page is the replacement: business-readable documentation generated FROM test
names, so it can never drift from what the suite actually asserts. A test name changing (or a
test being added/removed) is the only way this page changes; hand-editing it is pointless,
the next `npm run gen:behaviour-spec` overwrites it. */}
import { Meta } from '@storybook/addon-docs/blocks';
<Meta title="Foundations/Behaviour spec" />
# Behaviour spec
_Generated by `npm run gen:behaviour-spec` — do not hand-edit; the next generation
overwrites this page. See [BDD](?path=/docs/foundations-bdd--docs) for how these names are
written, and [Testing strategy](?path=/docs/foundations-testing-strategy--docs) for what gets
tested where._
Every bullet below is a real test name from the suite — an `it()` title (frontend) or a test
method name (backend), read as a sentence. Nothing here is hand-written prose: this page
**is** the suite, reshaped for a business reader. 497 frontend behaviours across
9 contexts; 261 backend behaviours across 42 test
classes.
## Frontend (by context)
### auth
#### isAuthenticated
- narrows a present principal to Principal
- reports no principal as not authenticated
- narrows a present principal to Principal
- reports no principal as not authenticated
#### parseRollen
- parses a single recognized rol
- is case-insensitive and trims whitespace
- drops unrecognized tokens (the deny-path toggle, e.g. ?rollen=geen)
- returns an empty list for an empty string
#### parseStoredPrincipal
- returns null when nothing is stored
- returns null for a non-JSON string
- returns null when the stored shape is wrong (no naam)
- returns null when kind is not medewerker
- returns null when rollen holds an unrecognized token
- restores a well-shaped stored principal as-is (no BSN to strip)
- returns null when nothing is stored
- returns null for a non-JSON string
- returns null when the stored shape is wrong (no naam)
- G1: a stored bsn is never restored, even if present in the raw value
### behandeling
#### besluit reduce
- SetField updates the draft while editing
- Submit with no besluit chosen stays Editing and reports a field error
- Submit Afwijzen without a toelichting stays Editing and reports a field error
- Submit Goedkeuren with no toelichting moves to Submitting (optional there)
- Submit Afwijzen with a toelichting moves to Submitting with the trimmed value
- SubmitConfirmed maps Submitting to Submitted
- SubmitFailed maps Submitting to Failed with the error
- Retry re-submits a failure
- Reset returns to the initial editing state
#### detailRows
- lists soort/status/referentie/eigenaar/ingediend
- a reden row is present only for Afgewezen and MeerInfoGevraagd
#### parseBeoordelingStatus
- parses each tag with its required fields
- rejects a malformed status
#### parseBeoordelingView
- maps a valid DTO to domain
- rejects a malformed view
- defaults an absent documenten list to empty
#### parseWerkvoorraad
- parses a list and fails fast on a bad element
#### parseWerkvoorraadItem
- parses Ingediend and InBehandeling
- rejects a case whose status is not an open queue tag
- rejects a missing owner, bad type, and non-objects
#### statusLabel
- labels every tag distinctly
#### werkvoorraadRow
- heading is the type, subtitle carries the owner BSN
- status line carries the status label, reference and submit date
- manual review is called out distinctly from an automatic InBehandeling
- a missing submit date leaves no dangling separator
### beheer
#### StamdataStore undo/redo (WP-32)
- records a cell edit, undoes and redoes it
- records addRow and undoes it
- clears history when switching table
#### StamdataStore.download (RB-28)
- does not call the presenter while the two-clause guard blocks (nothing dirty yet)
- does not call the presenter while previewing a date, even with edits
- passes toJson(...)'s exact output and the table id as the filename (success path)
#### activeOn (valid-time, half-open [van, tot))
- includes a row whose window covers the date
- excludes a row before its geldigVan
- excludes on the geldigTot boundary (half-open)
#### changeCounts (diff against the loaded snapshot, by key)
- counts an added key
- counts a removed key
- counts an edited cell
#### parseStamdataTable
- maps schema + rows and turns a null cell into empty text
- falls back to text for an unknown column type
- rejects a response with no columns
#### rowErrors / isValid (format only)
- flags a blank key
- flags a missing geldigVan on a temporal table
- flags geldigTot on or before geldigVan
- passes a well-formed row
#### stamdata-editor reduce
- Loaded snapshots original independently of rows
- RowAdded appends an empty row shaped by the schema
- RowRemoved drops the row at the index
- edit messages are ignored unless loaded
- LoadFailed and Loading transition regardless of prior state
#### toJson (draft → file shape)
- reconstructs an open-ended geldigTot as null and pretty-prints
- coerces a number column
### brief
#### BriefStore action state (Idle | Busy | Failed)
- is Busy synchronously once a transition starts
- settles to Idle on a successful transition
- goes Busy then Failed on a failing transition, surfacing the error
- a subsequent successful transition clears a prior Failed state
#### BriefStore rejection diff
- captures the rejected letter and diffs a subsequent edit against it
#### BriefStore undo/redo history
- starts with nothing to undo
- records an edit and makes it undoable
- undo reverts the edit and enables redo
- redo reapplies the undone edit
- a no-op edit does not clear the redo future
- a new edit clears the redo future
- caps history at 50 snapshots
#### BriefStore.flushPending (CanDeactivate guard / beforeunload)
- flushes a pending debounced edit immediately and clears the pending flag
- is a no-op when no edit is pending
#### BriefStore.load — 404 tolerance (RB-22)
- a 404 drives exactly one reset(), which populates the store
- a second 404 does not drive a second reset()
#### BriefStore.previewLetter
- opens the composed letter via BLOB_PRESENTER on success (RB-28)
- surfaces the error without opening a tab on failure
#### BriefStore.revealBigNummer (PRD-0002 §5c)
- swaps the masked value for the revealed one on success
- keeps the value masked and surfaces the error on failure
#### LetterPreviewAdapter.preview (BIO-012)
- sends no X-Role/X-Subject headers outside isDevMode()
- sends X-Role (and X-Subject when known) under isDevMode()
#### OrgTemplateStore.proefbrief (RB-28)
- opens the rendered proefbrief via BLOB_PRESENTER on success
- surfaces the error without opening a tab on failure
#### RevealBigNummerAdapter.reveal (BIO-006a + BIO-012)
- sends X-Step-Up only when the caller passes stepUp: true
- sends X-Role only under isDevMode()
#### besluitGuidance
- positief: counts inserted passages, no reden needed (positief has no redenen)
- negatief without a reden: flags that a reden must be chosen
- negatief with a reden: no longer flags, and the reason passage is counted
#### brief selectors
- unresolvedPlaceholders returns deduped manual keys only (auto excluded)
- allDiagnostics flattens across sections and blocks
- canSubmit is false when a required section is empty, true otherwise
#### brief.adapter parse boundary
- parses a well-formed view into the domain unions
- rejects a view whose case context is missing or malformed
- parses the org template and drops a null logoDocumentId
- rejects a view whose org template is missing or malformed
- rejects a view whose decisions are missing or malformed
- narrows node variants and rejects unknown ones
- rejects a status DTO missing its required fields
- reads section.locked (default false) and paragraph.list
- rejects a library passage with an unknown scope
- rejects a passage block missing provenance
#### brief.machine reduce
- BriefLoaded moves loading to loaded
- BriefLoadFailed moves loading to failed with the reason
- Seed sets the state directly
- BesluitSelected composes the kern: the besluit passages, in reading order, as frozen local blocks
- BesluitSelected swaps the passages when the selection changes, keeping free text
- BesluitSelected deep-copies content — later library mutation does not leak in
- FreeTextBlockAdded appends an empty free-text block
- BlockContentEdited replaces content and marks a passage block edited
- BlockMovedWithinSection reorders blocks within a section
- BlockRemoved drops a block from a section
- edits to a locked section are no-ops (besluit, free-text, content, remove, move)
- edits are no-ops once submitted (status invariant)
- editing a rejected letter reopens it to draft
- Submitted fires only from draft and only when required sections are filled
- approve fires only from submitted
- reject fires from submitted, carrying comments
- send fires only from approved
- a status transition replaces decisions with the fresh server value
#### diffBlocks
- marks added, removed, changed and unchanged by blockId
- changedBlocks drops unchanged and keeps added/removed/changed
#### errorMessage (TE-002 trust boundary)
- surfaces the ProblemDetails detail when present
- falls back to PREVIEW_FAILED when the body has no detail
- falls back to PREVIEW_FAILED when the body is not JSON
#### inferSelection
- round-trips a positief selection
- round-trips a negatief selection with redenen (in order)
- an empty kern (nothing chosen) infers no besluit
- ignores free-text blocks and unknown passage ids
#### lintPlaceholders
- clean content (auto-resolvable, fillable, current key) yields no diagnostics
- flags an unknown key as an error
- flags a not-fillable key as an error
- flags a deprecated key as a warning
- flags a manual placeholder as unresolved-at-send (warning)
- flags raw braces in text as malformed (paste safety net)
- returns diagnostics in document order across paragraphs/nodes
- severityOf maps each code to its policy
#### org-template.machine
- DraftLoaded moves to loaded with the draft, clean
- LoadFailed carries the reason
- FieldEdited edits the draft and marks dirty
- MarginEdited edits one edge and marks dirty
- DraftSaved clears dirty when the saved draft is the current one
- DraftSaved keeps dirty when an edit landed during the save round-trip
- edits are no-ops in non-loaded states
- a completed logo upload sets logoDocumentId + dirty
- removing the logo clears logoDocumentId + dirty
- DraftLoaded (sub-org switch) keeps the loaded logo category, drops uploads
#### parseOrgTemplateAdminView
- parses a well-formed admin view
- rejects a missing draft
- rejects a missing count field
- rejects a malformed history entry
#### parseRevealed (TE-002 trust boundary)
- accepts a well-formed body
- rejects a bigNummer sent as a number
- rejects a missing bigNummer field
- rejects null and non-object bodies
#### passagesForBesluit
- positief = shared intro + the positief passage, no negatief/reason passages
- negatief without redenen = intro + negatief base, but no reason-specific passages
- negatief with a reden ticked includes that reason-specific passage only
- preserves library order (= reading order)
- never offers non-kern passages
#### proefbriefErrorMessage (TE-002 trust boundary)
- surfaces the ProblemDetails detail when present
- falls back to PROEFBRIEF_FAILED when the body has no detail
- falls back to PROEFBRIEF_FAILED when the body is not JSON
#### redenenFor
- derives reason checkboxes (code + label) from the negatief reason passages
- positief has no reason-specific redenen
### herregistratie
#### IntakeWizardComponent
- renders each field group as its own grey &lt;fieldset&gt;
#### STEPS (fixed) and inline questions
- always has the same three steps
- reveals the buitenland detail questions inline only when worked abroad
- reveals the scholing question only when NL-hours are below the threshold
- uses the (server-owned) threshold passed in, not a hardcoded constant
#### herregistratie hasProgress
- is false for a fresh form
- is true once a field is filled or the user advances
#### intake acceptance journeys
- high uren, no buitenland werk: no scholing question, straight through to Submitted
- low uren requires the scholing question, and punten only once scholing is followed
- buitenland gewerkt requires land and hours abroad before advancing
- gaNaarStap corrects an earlier answer without losing later ones
- raising uren above the threshold after answering scholing drops both fields (WP-69 §6)
- SetPolicy (server-owned threshold) can turn an already-answered uren into one that now requires scholing
#### intake hasProgress
- is false for a fresh questionnaire
- is true once an answer is given or the user advances
#### intake-policy.adapter parse boundary
- parses a well-formed policy
- rejects a missing or non-numeric threshold
#### navigation
- Next is a no-op (sets an error) when the current step is invalid
- Next advances once the step is valid
- editing an answer leaves the cursor fixed (steps never collapse)
- Back never goes below the first step
- gaNaarStap jumps back to an earlier step, clearing errors
- gaNaarStap ignores a same/forward jump and jumps outside Answering
#### reduce (message-driven happy path)
- drives abroad branch end to end
#### reduce (message-driven)
- drives the full happy path via messages
- blocks submit until required documents are satisfied
- SubmitFailed then Retry returns to Submitting with the same data
- Seed mounts an arbitrary state
#### submit
- reaches Submitting ONLY with valid answers
- punten is required only when aanvullende scholing was gevolgd
- low hours requires the scholing answer before submit
- does not require punten for a hidden question (WP-69 §6)
- drops punten when raising uren hides the question (WP-69 §6)
- resolve maps Submitting to Submitted on a successful submit
- resolve maps Submitting to Failed on a failed submit
#### wizard.machine
- next advances only when step 1 parses
- next advances step 2 → 3 only when punten parses
- submit reaches Submitting ONLY from step 3 with fully valid data
- next requires BOTH step-1 fields (uren and jaren)
- back steps down one (3 → 2 → 1) and is a no-op from step 1
- resolve maps a successful Submitting to Submitted
- resolve maps a failing Submitting to Failed
- gaNaarStap jumps back to an earlier step, clearing errors
- gaNaarStap ignores a same/forward jump and jumps outside Editing
### registratie
#### AdminCasesStore
- loads and parses the cross-owner list
- deletes optimistically and confirms via the admin endpoint
- rolls back the removal and surfaces the error when the delete fails
- clears a stale error on the next delete attempt
#### ApplicationsStore
- loads and parses the list
- cancels optimistically and confirms via the DELETE endpoint
- rolls back the removal and surfaces the error when the cancel fails
- clears a stale error on the next cancel attempt
#### STEPS (fixed)
- always has the same three steps
#### adres origin (BRP vs handmatig)
- prefillAdres flags origin brp
- editing a prefilled address field flips origin to handmatig
- typing an address with no BRP prefill yields handmatig
- editing the e-mail field does not change the address origin
- a manually entered address still submits (only manual diploma is gated)
#### big-register.adapter parse boundary
- parses known aantekening types
- rejects an unknown type
#### blockActions
- a Concept can be resumed or cancelled
- an in-behandeling aanvraag only exposes its documents
- ingediend and meer-info-gevraagd behave like in-behandeling
- resolved aanvragen have no actions
#### change-request reduce
- SetField updates the draft while editing
- Submit with an invalid draft stays Editing and reports field errors
- Submit with a valid draft moves to Submitting with parsed (normalised) data
- SubmitConfirmed maps Submitting to Submitted with the referentie
- SubmitFailed maps Submitting to Failed with the error
- Retry re-submits a failure
- Reset returns to the initial editing state
#### createDraftSync
- coalesces rapid snapshot changes into ONE debounced sync of the latest value
- a trailing change after the debounce fires schedules its own sync
#### createDraftSync flushPending (CanDeactivate guard / beforeunload)
- hasPendingSave reflects an armed debounce timer
- flushPending writes the pending draft immediately, before the debounce fires
- flushPending is a no-op when nothing is pending
#### createDraftSync submit
- resolves ok with the server response on success
- folds a rejected submit into a Result error, never throwing
- recovers from a create conflict by adopting the existing Concept (WP-35)
#### createSubmitChangeRequest
- resolves ok with the referentie on success
- folds a rejected call into a Result error, never throwing
- surfaces a ProblemDetails detail message when the server rejects with one
#### detailRows
- lists soort/waarvoor/status/referentie/ingediend, plus reason when rejected
- reference falls back to em dash for a Concept
#### findConcept
- returns the id of the existing Concept of the given type
- returns undefined when the list has no application of the given type
- returns undefined when the matching type is not a Concept
- returns undefined when adapter.list() resolves with an unparsable shape
- returns undefined when adapter.list() rejects
#### hasProgress
- is false for a fresh wizard
- ignores an auto-prefilled BRP address at step 0
- is true once the user advances, picks correspondence/diploma, or is past step 0
#### inline document upload (beroep step)
- routes Upload messages through the upload reducer
- blocks the beroep step until a required category is satisfied
- includes delivery refs in the submitted data
#### kiesDiploma
- derives the beroep from the chosen diploma and flags origin duo
#### loadConcept
- reads the draft off a Concept
- reports a missing draft as null
- reports not-concept when the id has moved past Concept (submitted)
- reports not-concept when the id is unknown or deleted (detail rejects)
#### manual diploma fallback
- KiesHandmatig flags handmatig with the maximal question set and no beroep yet
- requires a declared beroep + all maximal questions before submit
#### navigation
- Next is a no-op (sets errors) when the adres step is invalid
- Next advances once the adres step is valid
- requires a valid e-mail only when the channel is email
- beroep step requires a chosen diploma
- Back never goes below the first step and preserves the draft
- GaNaarStap only jumps backwards
#### parseAanvraagStatus
- parses each tag with its required fields
- rejects a missing status, unknown tag, and wrong-typed fields
#### parseApplicationSummary
- maps a valid DTO to domain
- rejects a bad type and non-objects
#### parseApplications / parseApplicationDetail
- parses a list and fails fast on a bad element
- carries the opaque draft through detail
#### parseBigNummer
- accepts exactly 11 digits, trimming whitespace
- rejects wrong length or non-digits
#### parseBrpAddress (trust boundary)
- accepts a found address
- accepts "geen adres" (gevonden: false) as a valid outcome
- rejects malformed responses
#### parseDashboardView (trust boundary)
- maps a valid response into a DashboardView
- rejects malformed responses instead of trusting them
#### parseDuoLookup (trust boundary)
- maps a valid lookup (diplomas + manual fallback)
- accepts an empty diploma list (forces manual entry)
- rejects malformed responses
#### parseEmail
- accepts a well-formed address and trims it
- rejects malformed addresses
#### parsePostcode
- normalises to "1234 AB" (uppercase, single space, trimmed)
- rejects malformed postcodes
#### parseTelefoonnummer
- accepts a 10-digit number starting 0 and strips formatting
- normalises a +31 prefix to a leading 0
- rejects a too-short number, a non-0 start, and junk
#### parseUren
- accepts non-negative whole numbers, including 0
- rejects empty, negative, and non-integer input
#### policy questions (geldigheidsvragen)
- a diploma with questions blocks Next until they are answered
- validateAll keeps only the answers to the questions that applied
#### reduce (message-driven happy path)
- adres and correspondentie set, Next advances from adres to beroep
- diploma chosen, Next advances from beroep to controle
- Submit moves a complete Invullen draft to Indienen
- SubmitConfirmed moves Indienen to Ingediend
- SubmitFailed moves Indienen to Mislukt
- Retry returns Mislukt to Indienen with the same data
#### registration.policy
- statusLabel echoes the tag
- statusColor is total over the union
- herregistratieDeadline is only set for an active registration
#### submit
- stays in Invullen when the draft is incomplete (no diploma)
- reaches Indienen with a complete, valid draft, carrying its data
- resolve maps Indienen to Ingediend with the referentie
- resolve maps Indienen to Mislukt on a failed submit
#### submittedRow
- heading is the type, subtitle is the purpose
- status line carries the status label, reference and submit date
- manual review adds a note
- rejection adds its reason
- meer-info-gevraagd adds its reason, like a rejection
#### tasksFromProfile
- offers herregistratie when the server says eligible, with the formatted deadline
- offers nothing when the server says not eligible
- surfaces a notice for a suspended registration (independent of eligibility)
- surfaces a notice for a struck-off registration
### shared
#### BackgroundUploadsReturned
- resolves each in-flight upload to complete or failed
#### CategoriesLoadFailed / BackgroundSyncAvailability
- records the error
- flips background sync availability
#### CategoriesLoaded
- defaults every category to digital without clobbering existing choices
- clears a prior categoriesError
- drops uploads + channel choices for categories that disappear
#### DeliveryChannelChanged
- switching to post removes that category uploads
- rejects post for a category that does not allow it
- switching back to digital starts clean
#### FileRejected
- stores a per-category message
#### FileSelected
- queues a new upload and clears any rejection for that category
- ignores selection for an unknown category
- ignores selection for a category set to post-delivery
- single-file category: a new selection replaces the existing upload
- multiple category: selections accumulate
#### PendingSaves registry
- hasPending is true only while some registered owner has a pending write
- unregister removes an owner so it no longer counts
- flushAll flushes only the pending owners
- flushAll awaits every owner and swallows a rejected flush
#### RemoteData combinators
- map only touches Success
- map2 precedence: Failure &gt; Loading &gt; Success
#### UploadShellService.cancel
- calls the transport cancel function for an in-flight upload and forgets it
- is a no-op for a localId with nothing in flight
#### UploadShellService.delete
- dispatches UploadDeleting, then UploadDeleteComplete on success
- dispatches UploadDeleteFailed with the server detail on failure
- falls back to an empty reason when the server sends no detail
#### UploadShellService.pollReturning
- does nothing when there are no uploads to poll
- dispatches BackgroundUploadsReturned for uploads the server reports complete
- does not dispatch when nothing has arrived yet
#### UploadShellService.upload
- dispatches UploadQueued with the transport backgroundSync flag, then sends via the transport
- translates a progress callback into UploadProgress
- translates a resolved transport into UploadComplete
- translates a rejected transport into UploadFailed with the reason
- does not dispatch UploadFailed on a user-initiated abort
#### authGuard
- allows an authenticated user
- redirects an anonymous user to /login
#### capabilityGuard
- waits for /me, then allows an entitled admin
- sends an authenticated-but-unentitled user to /dashboard (not a login loop)
- redirects an anonymous user to /login without waiting for caps
- reads authentication through the port, not an app-local store
#### createDebouncedSave
- flushes after the delay when canSave is true
- does not schedule when canSave is false
- coalesces rapid schedules into a single flush
- flushPending runs the save immediately and clears; no-op when idle
- cancel drops a scheduled save without running it
#### createHistory
- starts empty; undo/redo are no-ops
- records pre-edit snapshots, then undoes and redoes through them
- record() clears the redo stack (no dead redo after a fresh edit)
- caps the stack depth
- clear() empties both stacks
#### createStore
- applies the pure update on dispatch
- dispatch from inside an effect does not self-loop
#### currentRole (dev mechanism)
- lists the three roles
- reads a valid ?role= from the URL and persists it for the tab
- falls back to drafter when nothing is set or the value is invalid
#### currentRole (dev mechanism) outside isDevMode() (production build)
- ignores a ?role= in the URL and returns the default
- never touches sessionStorage
- ignores a role already sitting in sessionStorage from a prior dev session
#### currentSubject (dev mechanism)
- reads a ?subject= from the URL and persists it for the tab
- returns undefined when nothing has ever been set
#### currentSubject (dev mechanism) outside isDevMode() (production build)
- ignores a ?subject= (a BSN) in the URL and returns undefined
- never writes the BSN into sessionStorage
- ignores a subject already sitting in sessionStorage from a prior dev session
#### delete flow (optimistic, revertible)
- UploadDeleteRequested keeps the documentId for revert
- UploadDeleteComplete removes the upload
- UploadDeleteFailed reverts to complete with the original documentId
#### deliveryRefs
- emits documentId for completed digital uploads and channel for post
- omits digital categories with no completed upload
#### flushPendingGuard
- flushes then allows navigation when a write is pending
- allows navigation immediately when nothing is pending
#### formatDatumNl
- formats a Date in long Dutch form
- formats an ISO string the same way
- is empty-safe: undefined, null, and empty string all yield the empty string
- returns empty for an unparseable string rather than "Invalid Date"
#### httpClientFetch
- sends the pending idempotency key as a header for a write, not a fresh one per attempt
- retries a failing GET twice before giving up
- never retries a failing write
#### inFlight
- returns only queued/uploading uploads
#### localeLinks (nl at root, en under /en/)
- an nl route (no prefix) links nl to the bare path, en under /en, marks active
- an en route strips the /en prefix for the nl target (deep path, en active)
- keeps query + hash on both targets
- the root maps nl → / and en → /en/
#### machineRemoteData
- maps loading → Loading
- maps failed → Failure carrying an Error with the reason
- maps loaded → Success carrying the whole loaded state
#### parseBsn (elfproef)
- accepts a valid BSN (passes the elfproef)
- rejects a 9-digit number that fails the elfproef
- rejects wrong length / non-digits / all zeros
#### parseMe (trust boundary)
- parses a known capability list
- parses an empty list (drafter — no capabilities)
- recognizes the admin org-template capability (WP-23)
- recognizes the behandelportal besluit capability (WP-66)
- drops unrecognized capability strings instead of rejecting the response
- rejects malformed responses instead of trusting them
#### pii maskers
- maskBsn keeps the last 3 digits
- maskTail keeps the requested tail length
- masks the whole value when it is not longer than the kept tail
#### planFileSelection
- plans nothing for an unknown category
- rejects the whole batch with reason "multiple" for a single-file category
- does not reject a single file against a single-file category
- rejects one file via rejectReason (wrong type)
- rejects one file via rejectReason (too large)
- plans a FileSelected entry for a file that passes format validation
- judges each file independently and preserves order for a mixed multiple-file category
#### problemDetail
- extracts the detail from an RFC-7807 ProblemDetails
- falls back when there is no detail
#### problemFieldErrors (G4 seam)
- maps a ValidationProblemDetails errors dict to first-message-per-field
- returns &#123;&#125; when there is no errors envelope (the current backend shape)
#### rejectReason
- rejects a disallowed type
- rejects an oversized file
- accepts a valid file
- allows any type when the category lists none
#### rich-text
- emptyBlock is one empty paragraph and reads as empty
- isBlockEmpty is false when any placeholder or non-blank text exists
- placeholderKeysIn walks in document order, keeping duplicates
- deepCopyBlock is an independent value copy (frozen snapshot)
#### rich-text DOM boundary
- round-trips text, marks, placeholders, line breaks and multiple paragraphs
- round-trips an empty paragraph (filler &lt;br&gt; is not a line break)
- renders a placeholder as a non-editable chip carrying its key and label
- reads combined marks in canonical order regardless of nesting
- round-trips bullet and numbered lists mixed with paragraphs
- groups consecutive same-kind list lines into one &lt;ul&gt;/&lt;ol&gt;
- adjacentChip finds a chip next to a collapsed caret so Backspace/Delete can remove it
- marks auto-resolvable vs manual chips with data-auto for styling
#### roleInterceptor
- leaves an unrelated endpoint untouched
#### runResult
- folds a resolved call into ok(value)
- maps a ProblemDetails rejection to err(detail)
- falls back when the rejection has no detail
- mints no Idempotency-Key — the read fold
#### runSubmit
- folds a resolved call into ok(value)
- maps a ProblemDetails rejection to err(detail)
- falls back when the rejection has no detail
- mints exactly one Idempotency-Key for the whole call — the write fold
#### satisfaction helpers
- a post-delivery choice satisfies a required category
- an active upload satisfies a required category
- a failed upload does not satisfy a required category
- requiredCategoriesSatisfied ignores optional categories
#### scenario (dev mechanism)
- reads a valid ?scenario= from the URL and persists it for the tab
- falls back to default when nothing is set or the value is invalid
- setScenario persists the chosen scenario
#### stripDevParams (WP-37)
- removes ?scenario and ?role so the stored dev value wins on reload
- keeps unrelated query params and the path/hash
- is a no-op when neither param is present
#### subjectInterceptor
- stamps X-Subject on an /api/v1/ request once ?subject= has been seen
- keeps stamping later requests on the same tab after the query param is gone (WP-33-style stickiness)
- leaves a non-API request untouched even when a subject is known
- sends no header at all when no subject has ever been seen
#### upload lifecycle messages
- queued → progress → complete
- failed then retried returns to queued
- UploadRemoved drops the upload
#### uploadOutcome
- resolves a 2xx response with a valid JSON body to the document id
- falls back to the generic error when a 2xx body is not valid JSON
- maps a non-2xx ProblemDetails body to its detail
- falls back to the generic error for a non-2xx body without a ProblemDetails detail
- treats status 200-299 as success and everything else as failure
#### withIdempotencyKey / currentIdempotencyKey
- threads the key to every read made inside the wrapped fn
- clears the key once the wrapped fn settles
- falls back to a generated uuid-shaped key when none is pending
### shell
#### redactProfile
- masks the BIG-nummer to its last 3 digits
- redacts the name on both the registration and the person
- redacts every date of birth
- redacts the address
- keeps structural/decision-relevant fields untouched
### showcase
#### highlightTs
- wraps keywords, strings and comments in the styling spans
- escapes HTML metacharacters so the [innerHTML] sink is safe
- treats the whole // tail as one comment (keywords after // are not re-highlighted)
## Backend (by test class)
### AdminCasesTests
- Admin lists every case with its owner
- Non admin is forbidden
- Admin can delete a submitted case
- Deleting a missing case is not found
### ApplicationRuleTests
- AanvraagStatusTag covers the published lifecycle
### ApplicationTests
- Create then list shows a concept with step progress
- Draft sync is readable back from detail
- Submit duo registratie is in behandeling and auto
- Submit handmatig registratie succeeds as manual case
- Submit herregistratie with zero uren is afgewezen
- Submitting twice conflicts
- Creating a second concept of the same type conflicts
- A concept of a different type is allowed
- A new concept is allowed once the previous one is submitted
- Cancel concept removes it
- Cancel submitted aanvraag conflicts
- Applications are scoped to the caller bsn
- Submitting a foreign documentId is rejected and leaves it deletable by its owner
- Draft sync with a foreign documentId is rejected
- AutoApprovable flips to goedgekeurd after the window
- Manual case never auto advances
### AuthzAuditTests
- A denied admin action is recorded
- A reveal attempt is recorded
- An allowed admin action is recorded
- An admin upload delete is recorded
- A feature flag toggle records which flag changed
- A refused brief transition is recorded
- No audit row carries a subjects bsn
- The audit schema carries no pii
### AuthzTests
- Drafter may not approve or reject even when submitted
- Approver may approve and reject a different drafters letter
- Send is not role gated
- Decisions CanEdit true for drafter in editable statuses
- Decisions CanApprove requires submitted status and approver role
- Decisions CanSend requires approved status only
- RoleCapabilities are empty for drafter and the three brief capabilities for approver
- CanRevealBigNummer only for the case drafter behandelaar
- Decisions CanRevealBigNummer is status independent
- CanBeoordelen true for a medewerker with the behandelaar rol
- CanBeoordelen false for a medewerker without it
- CanBeoordelen false for a zorgverlener regardless of role
### BeoordelingIdMismatchTests
- Besluit resolves by referentie when the case id differs from the local aanvraag id
### BeoordelingRuleTests
- Only open statuses are decidable
- Only a non approval requires a toelichting
- A terminal decision refuses any further besluit
- MeerInfoOpvragen is not terminal a further besluit is still legal
### BeoordelingTests
- Detail shows status documents and a masked owner
- Concept and unknown id are not found
- Zorgverlener is forbidden even with admin role
- Medewerker without behandelaar rol is forbidden
- Goedkeuren advances status to Goedgekeurd
- Afwijzen requires a toelichting
- MeerInfoOpvragen is still decidable afterwards
- Already decided case rejects a further besluit
- Concurrent besluiten on the same aanvraag yield exactly one success
- Unknown id 404s
- Zorgverlener is forbidden from deciding
### BesluitLifecycleTests
- A terminal besluit is frozen
- MeerInfoGevraagd can be decided again
- A recorded decision wins over the auto approve computation
- Afwijzen requires a toelichting
### BriefEndpointTests
- Get returns 404 and writes no row when no brief exists for the owner
- SeedBrief creates a draft with expected sections locked and empty
- Get offers only global and arts scoped besluit tagged passages
- Get joins the case context with the BIG nummer masked
- Reveal returns the unmasked BIG nummer for the drafter with step up
- Reveal is forbidden without the step up
- Reveal is forbidden for a role without the capability
- Save is drafter only
- Submit blocks on empty required section
- Submit succeeds when required sections filled
- Drafter cannot approve own letter but a different reviewer can
- Reject returns comments
- Editing a rejected letter reopens it to draft
- Send only from approved
- Decisions on the view mirror the acting principal and live status
- Me returns no capabilities for drafter and the brief set for approver
- Reset recreates a fresh draft with locked prefilled sections
### BriefRuleTests
- A drafter may save a draft or rejected letter
- A drafter may not save a non editable letter
- A non drafter is forbidden to save regardless of status
- Saving a rejected letter reopens it to draft
- Saving a draft letter leaves its status unchanged
- No required sections means nothing to fill
- An optional empty section does not block submission
- A required section with a block is filled
- A required section with no blocks is not filled
- One unfilled required section blocks submission even if others are filled
- A drafter may submit a filled draft
- A drafter may not submit an unfilled draft
- A drafter may not submit a letter that is not a draft
- A non drafter is forbidden to submit even a filled draft
- An approved letter may be sent
- Only an approved letter may be sent
- An approver may decide a submitted letter drafted by someone else
- A drafter may not approve or reject
- An approver may not decide a letter they drafted themselves
- An approver may not decide a letter that is not submitted
- Entitlement is checked before status forbidden wins over conflict
### DiplomaRuleTests
- Profession is derived from program
- English diploma requires dutch proficiency
- Dutch diploma has no policy questions
- Manual diploma gets maximal set
- Manual professions match known programs
### DocumentRuleTests
- Rejects unknown category
- Rejects disallowed type
- Rejects oversized file
- Accepts valid file
- First load has no diploma upload
- Manual diploma needs a diploma upload
- Duo diploma skips diploma upload
- Confirmed dutch proficiency requires taalvaardigheid proof
- Unconfirmed proficiency requires no taalvaardigheid proof
- Find resolves taalvaardigheid for upload validation
### EndpointTests
- DashboardView computes eligibility decision
- Notes returns seeded aantekeningen
- Brp returns address
- Duo lookup carries server decided questions and professions
- IntakePolicy returns scholing threshold
- Change request with valid phone succeeds
- Change request with bad phone is rejected with problem details
- Health endpoint is ok
- Correlation id supplied by the caller is echoed back
- Correlation id is generated when the caller omits it
- Categories are server owned config
- Upload then status reports complete for known localId
- Upload content is served back with its type inline for pdf
- Upload content 404 for unknown document
- Upload rejects wrong type
- User delete succeeds then 404
- User delete blocked with 409 once linked to submission
- Admin delete requires admin role
- Audit log records upload and delete metadata only
### FeatureFlagTests
- The catalog has unique keys
- Get flags returns the catalog
- Toggling is admin only and an unknown key is 404
- Closing inschrijving blocks a registratie then reopening allows it
### HerregistratieRuleTests
- Eligible within window
- Not eligible before window
- Eligible on window boundary
- Suspended is not eligible
### IdempotencyTests
- Replaying the same idempotency key returns the same reference not a new one
- Different idempotency keys are independent submissions
- A caller replaying another callers idempotency key does not get their cached result
- A rejected submission replays the same rejection not a retry
### IntakeRuleTests
- Below threshold with no answer is incomplete
- At the threshold no answer is required
- Niet gevolgd is a complete answer below threshold
- Gevolgd without punten is incomplete
- Gevolgd with zero punten is valid
- Gevolgd with negative punten is refused
### IntakeSubmissionTests
- Below threshold without an answer is rejected and stays a concept
- Below threshold with an answer succeeds
- Above threshold needs no answer
- Punten without gevolgd is rejected
- Herregistratie is unaffected by the intake only gate
- Zero uren is still afgewezen not a 400
### LetterHtmlTests
- Render matches the golden file
- Render resolves the body datum placeholder from the given at not the wall clock
- Render keeps the letterhead date and the body datum in agreement for a historical at
- Every letter prefixed class exists in letter css
### NotificatieTests
- Correct shared secret is accepted and recorded
- Wrong secret is rejected and recorded
- Missing authorization header is rejected
### OpenZaakDocumentSourceTests
- Upload registers an eio in drc and persists its url locally
- Upload falls back to openbaar for a category absent from the confidentialiteit table
- Upload keeps the local document when the category has no configured informatieobjecttype
- Upload keeps the local document and does not throw when drc rejects it
- LinkToZaak posts a zaakinformatieobject per document once a zaak exists
- LinkToZaak makes no zgw call when the local source created no zaak
### OpenZaakIntegrationTests
- Admin cases returns the seeded zaak mapped through real HTTP and JWT
### OpenZaakZaakSourceTests
- Follows pagination maps the internal aanvraag type and sends bearer token
- ListCases throws when a zaak zaaktype has no configured aanvraag type
- ListMyCases filters by the callers bsn
- CreateZaak posts zaak status and rol and maps the result back
- CreateZaak throws when the aanvraag type has no configured zaaktype
- RecordBesluit posts the last statustype with besluit and toelichting
- RecordBesluit creates a resultaat before posting the eindstatus
- RecordBesluit does nothing when the aanvraag has no zaak
- RecordBesluit throws when the aanvraag type has no configured zaaktype
- CreateZaak retries a transient failure and then succeeds
- CreateZaak gives up after three attempts on a persistent transient failure
- CreateZaak does not retry a permanent rejection
### OrgTemplateEndpointTests
- Admin endpoints are admin only
- Publish increments the version
- Publish appends to the version history
- Publish counts the unsent briefs it affects
- Save draft validates margins
- Save draft round trips the edited values
- Rollback copies an old version into the draft without rewriting history
- Sent brief keeps its pinned template after a republish
- Unsent brief follows a republish
- Me returns the orgtemplate capability for admin
- Admin cannot slip into the brief review flow
### OrgTemplateRuleTests
- Accepts a complete draft within the margin bounds
- Rejects a missing organisation name
- Rejects a missing signature name
- Rejects a margin outside the allowed range
- Accepts margins on the boundary
### PhoneFormatContractTests
- A leading plus31 is accepted like the frontends normalised form
- Parentheses around the area code are accepted like the frontend
### PreviewEndpointTests
- Preview of an unsent brief renders live with a watermark
- Preview of a sent brief serves the archive unchanged after a republish
- Proefbrief is admin only
- Proefbrief renders the draft template with a watermark
### ProductionIdentityProviderTests
- Production environment with no real identity provider fails at startup
### ProfessionsTests
- A mapping is absent before its geldigVan
- A mapping is present on and after its geldigVan
- A closed mapping is absent from its geldigTot onwards
- ByProgram is evaluated per call not captured at type load
### RouteInventoryTests
- Every mapped route is authz gated or on the named allow list
- Every gate marker names a known admin wrapper
### StamdataEndpointTests
- Stamdata reads are admin only
- Table list exposes the reflected schema
- Table returns all rows without a peildatum
- Peildatum before the seed windows hides every row
- Unparseable peildatum is 400 not 500
- Unknown table is 404
### StamdataValidationTests
- Every declared reference into stamdata resolves
- Profession map has no blank programs or professions
- Manual professions are non empty and distinct
- Every policy question has an id and wording
- Manual question set has distinct ids
- Every catalog table is valid
### StubIdentityProviderTests
- No headers resolves to the seeded citizen as a drafter
- X role maps to the principal role
- X subject overrides the default bsn
- No headers resolves a zorgverlener kind
- X medewerker resolves a medewerker with the default behandelaar rol
- X medewerker takes precedence over x subject
- Empty x medewerker falls through to the zorgverlener default
- X rollen parses known tokens and drops unknown ones
- X role still applies to a medewerker
- Never returns null even with no headers at all
### SubmissionRuleTests
- Zero hours is rejected
- Worked hours are accepted
- Phone change is validated
### SwaggerGateTests
- Swagger document is served in development
- Swagger document is not served outside development
### UploadAccessTests
- The owner can read the bytes
- Another citizen gets 404 not 403
- A behandelaar can read a linked document
- A medewerker without the behandelaar rol does not
- The document audit trail records a masked actor
- Status reports another citizens localId as unknown
### WerkvoorraadTests
- Behandelaar sees submitted cases in the queue
- Queue excludes concepts
- Zorgverlener is forbidden even with admin role
- Medewerker without behandelaar rol is forbidden
- Me reports the capability only for a behandelaar
### ZgwDiagnosticHandlerTests
- Passes request and response through unchanged
### ZgwDivergenceTests
- Submit with a failing zgw flags the divergence instead of diverging silently
- Submit with a healthy zgw leaves no divergence flag
- A recorded divergence carries no response body and no query string
### ZgwTokenProviderTests
- Mints a three part jwt with the required claims
- Mint with a caller carries that citizen not the static config identity
- Mint with a medewerker caller uses the medewerkerId as user id
- Signature verifies with the shared secret
### ZgwZaakMapperTests
- Maps url identity zaaktype and open status
- Closed zaak maps to goedgekeurd
- Uuid extracts trailing segment ignoring trailing slash