RD-20 asserted `git grep -l "toWizardErrors"` would find 5 files, but its own Steps list regenerates `behaviour-spec.mdx`, and the generator publishes every `describe` title. Naming a spec after the function it tests puts the name in the generated document too, so the honest count is 6. The agent refused to rename the describe block to satisfy the number, which is the correct response and matches the precedent from RD-14. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
8.6 KiB
RD-20 — wizard-errors.ts + spec, adopted by all three wizards
Status: done Source: PLAN.md 3c
Why
Each of the three wizards flattens its per-field error record into the shell's
WizardError[] summary, and each writes the flattening itself. Two of the three are the same
six lines with a different key type. The third does the same job plus a nested group.
The loop is pure, it has no spec, and it lives in a component — so the one part of the wizard that decides what the user sees in the error summary is the one part no test covers.
Extract one pure helper beside naarStapLabel, which lives in that folder for exactly this
reason.
Read first
libs/shared/src/layout/wizard-shell/wizard-shell.component.ts:19-27—naarStapLabeland theWizardErrorinterface. The new file sits beside this one.- The three call sites, in the order they get easier:
herregistratie-wizard.component.ts:253-259intake-wizard.component.ts:361-368registratie-wizard.component.ts:468-479— the one with the nested group.
apps/ssp/src/app/registratie/domain/registratie-wizard.machine.ts:69—Errors, declared as aninterface. Decision 3 is about this line.
Decisions (pre-made, don't relitigate)
-
One helper, two parameters, both used today:
/** Flatten a machine's per-field error record into the shell's summary list. Values that are not a non-empty string are skipped, so a nested group (the registratie wizard's `antwoorden`) is appended by the caller with its own `idPrefix` rather than special-cased here. */ export function toWizardErrors(errors: Record<string, unknown>, idPrefix = ''): WizardError[] { const out: WizardError[] = []; for (const [k, v] of Object.entries(errors)) { if (typeof v === 'string' && v) out.push({ id: idPrefix + k, message: v }); } return out; }Record<string, unknown>plus thetypeofguard narrowsvtostringwith no cast, which keeps the file inside the repo'sany-free rule. Do not reach for a generic key type; the three machines key their errors differently and the helper does not care. -
New file
libs/shared/src/layout/wizard-shell/wizard-errors.ts, plus its spec. It takes theWizardErrortype from the component file with a type-only import:import type { WizardError } from './wizard-shell.component';import typeis erased at compile time, so the helper stays pure and its spec needs no TestBed and pulls in no Angular at run time. Moving theWizardErrorinterface into the new file would be tidier on paper and would touch four more files for no behaviour; not worth it. -
registratie-wizard.machine.ts:69changes frominterface Errorstotype Errors. This is required, not cosmetic. TypeScript gives an implicit index signature to a type alias but not to an interface, soErrorsas an interface is not assignable toRecord<string, unknown>and the call site will not compile. The other two machines already declare their error maps as type aliases (Partial<Record<…, string>>), so this also makes the three consistent. NothingextendsorimplementsErrors— verified. -
The three call sites become:
Wizard Body herregistratie toWizardErrors(this.editing()?.errors ?? {})intake toWizardErrors(this.answering()?.errors ?? {})registratie [...toWizardErrors(e), ...toWizardErrors(e.antwoorden ?? {}, 'vraag-')]The registratie site keeps its
const e = this.invullen()?.errors ?? {};line. Thevraag-prefix and the skipping of non-string values together replace itsif (k !== 'antwoorden' && typeof v === 'string' && v)filter. -
Do not touch the three
shellStatusswitches. The tags genuinely differ per machine and an exhaustive switch is the house style. This ticket is about the error list only. -
No story. The helper is a pure function. Its spec is the test;
wizard-shell.stories.tsalready renders the error summary.
Files
libs/shared/src/layout/wizard-shell/wizard-errors.ts(new)libs/shared/src/layout/wizard-shell/wizard-errors.spec.ts(new)apps/ssp/src/app/herregistratie/ui/herregistratie-wizard/herregistratie-wizard.component.tsapps/ssp/src/app/herregistratie/ui/intake-wizard/intake-wizard.component.tsapps/ssp/src/app/registratie/ui/registratie-wizard/registratie-wizard.component.tsapps/ssp/src/app/registratie/domain/registratie-wizard.machine.ts(decision 3, one line)libs/shared/docs/behaviour-spec.mdx(regenerated, never hand-edited)
Steps
- Write
wizard-errors.tsper decisions 1 and 2. - Write
wizard-errors.spec.ts. Cover: a flat record; an empty record; a record whose values areundefinedor''(both skipped); theidPrefix; and a record holding a nested object value (skipped, which is what lets decision 4's registratie case work). - Apply decision 3 — one word,
interface→type, and the{stays. - Convert the three call sites per decision 4.
- Run
npm run gen:behaviour-spec— the new spec titles otherwise fail the drift check. git add -A, then run the acceptance commands.- Update this ticket's
Status:todoneand the README's RD-20 row todone. - Commit all of it together.
Acceptance criteria
Measured against the tree before handover.
git grep -c "export function toWizardErrors" -- libs/shared/src/layout/wizard-shell/wizard-errors.ts # MUST be 1
git grep -l "toWizardErrors" -- apps libs | wc -l # is 0 -> MUST be 6
Corrected after the ticket ran: 6, not the 5 first written. The sixth file is
libs/shared/docs/behaviour-spec.mdx. Step 5 regenerates it, and the generator publishes every
describe title — so describe('toWizardErrors', …) puts the name in the generated document.
The helper, its spec, the three wizards, and the generated page.
The hand-rolled flattening is gone from all three:
git grep -c "filter((k) => e\[k\])" -- apps | awk -F: '{s+=$NF} END {print s+0}' # is 2 -> MUST be 0
git grep -c "k !== 'antwoorden'" -- apps | awk -F: '{s+=$NF} END {print s+0}' # is 1 -> MUST be 0
Decision 3 landed:
git grep -c "export interface Errors" -- apps/ssp/src/app/registratie/domain/registratie-wizard.machine.ts # is 1 -> MUST be 0
git grep -c "export type Errors" -- apps/ssp/src/app/registratie/domain/registratie-wizard.machine.ts # MUST be 1
npm run ci # exits 0
Verification
npm run ci is enough. --full is not required: no story changes, and the new file is
under libs/shared/src/layout/, not libs/shared/src/ui/. Regenerating behaviour-spec.mdx
does not trigger --full on its own — RD-17 set that precedent.
npm run lint inside the gate is what proves the three /* eslint-disable max-lines */
directives are still needed. reportUnusedDisableDirectives is error, so a directive that
stops being necessary fails the build.
Out of scope
- Splitting any wizard into step components. RD-22 (intake) and RD-23 (registratie) own that, and this ticket makes both smaller first.
- The
shellStatusswitches (decision 5). change-request-formandbesluit-form. Neither builds aWizardError[]; they render field errors directly. There is nothing to share.
Risks
- The
interface→typechange is load-bearing (decision 3). Skipping it produces "Index signature for type 'string' is missing in type 'Errors'" at the registratie call site, and the tempting wrong fix is to widen the helper's parameter toobject, which forces ananyand fails lint. - PLAN says this ticket deletes an
eslint-disable max-linesfromherregistratie-wizard.component.ts. That is stale — there is no such directive. Earlier tickets already brought the file under the budget: it measures ~248 effective lines against a limit of 250. The two disables that do exist (intake, registratie) stay; RD-22 and RD-23 remove them. Both files are far above 250 (~362 and ~574 effective), so this ticket's saving cannot make either directive unused. behaviour-spec.mdxdrift from the new spec titles. Rungen:behaviour-specin the same commit, and never edit that file by hand.- Keep
import type, not a value import (decision 2). A value import of the component into the helper drags Angular into a pure module and its spec.