Files
ehoandClaude Opus 5 f3e5745145 fix: the wizards' seed input never arrived (RD-39)
All three wizard containers read `this.seed()` in the constructor. Angular
binds component inputs after the constructor runs, so the value was always the
`initial` default, `seeded !== initial` was always false, and every mount took
the `draftSync.resume()` branch. The `seed` input was dead code.

The two single-step forms built on the same idiom read the input inside the
microtask and work correctly. That contrast is the diagnosis.

Impact: 21 seeded wizard stories rendered step 1 instead of the state they
asked for. Storybook is this repo's UI test surface, so the states with no
other coverage were exactly the ones not rendering — Submitting, Submitted,
Failed, Ingediend, Mislukt. The a11y runner checks that whatever rendered is
accessible, never that the right thing rendered, so nothing caught it.
Production was unaffected: no route binds `seed`.

Read the input inside the microtask, matching the two forms. Turn the spec's
old `componentInstance.dispatch(...)` workaround into a real regression test
through `componentRef.setInput('seed', ...)`.

Verified: with the intake fix reverted the two spec cases fail; with it, 319
pass. `npm run ci --full` is green, and the newly rendered markup produced no
axe violations. A browser check of seven seeded stories across all three
wizards asserts text only reachable from a seed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-07 11:53:03 +02:00

3.8 KiB

RD-39 — The wizards' seed input never arrived

Status: done Source: found while planning the machine-wiring migration (ADR-0007 arc)

Why

All three wizard containers read their seed input in the constructor:

const seeded = this.seed(); // always the `initial` default
queueMicrotask(() =>
  seeded !== initial ? this.dispatch({ tag: 'Seed', state: seeded }) : this.draftSync.resume(),
);

Angular binds component inputs after the constructor runs. So seeded was always the initial default, seeded !== initial was always false, and every mount took the draftSync.resume() branch. The seed input was dead code.

The two single-step forms built on the same idiom prove the diagnosis by contrast. They read the input inside the microtask, and they work:

change-request-form.component.ts:191  queueMicrotask(() => this.dispatch({ tag: 'Seed', state: this.seed() }));
besluit-form.component.ts:176         queueMicrotask(() => this.dispatch({ tag: 'Seed', state: this.seed() }));

Impact

Every seeded wizard story rendered step 1 instead of the state it asked for — 21 stories across the three wizards. Storybook is this repo's UI test surface (CLAUDE.md "Testing"), so the states that had no other coverage were exactly the ones silently not rendering: Submitting, Submitted, Failed, Ingediend, Mislukt.

Nothing caught it. .storybook-ssp/test-runner.ts runs axe only — it checks that whatever rendered is accessible, never that the right thing rendered. intake-wizard.component.spec.ts had worked around it by calling componentInstance.dispatch(...) instead of setting the input, which is the shape of a test written against a broken input path.

Production was unaffected: no route binds seed, so the resume branch was always the correct one there.

Decisions

  1. Read seed() inside the microtask, matching the two forms. Six lines across three files. The if/else replaces the ternary because the branches are statements, not values.
  2. Keep the microtask. It is what defers the dispatch past input binding. The larger fix — start(seed) on an application store called from ngOnInit — belongs to the ADR-0007 migration, not here. This ticket makes the existing seam correct; it does not move it.
  3. Turn the workaround into the regression test. intake-wizard.component.spec.ts now mounts through componentRef.setInput('seed', …) via a mountSeeded helper, and a new honours the seed input case asserts the machine state directly. Both fail on the old code, which is the point.
  4. Do not touch draftSync.enabled. enabled: () => this.seed() === initial reads the input lazily inside a lambda called from an effect, so it was already correct. Changing the input to | null is part of ADR-0007.

Verification performed

  • npx ng test ssp with the intake fix reverted: 2 failed (both new spec cases). With the fix: 319 passed. That differential is the proof.
  • npm run ci --full green, including 112 storybook a11y tests. The newly rendered markup (<app-document-upload>, error alerts, <app-confirmation>) produced no axe violations — the predicted a11y fallout did not happen.
  • Browser check against the built Storybook, seven seeded stories across all three wizards, each asserted to contain text only reachable from its seed (Netwerkfout for Failed, referentienummer for Ingediend, Documenten for step 3), plus a negative control that step 1 does not show step 3's title. All seven pass.

Trap for the next person

Failed, Submitted, Ingediend and Mislukt are unreachable without a network submit, so they can only be produced by a seed. If a future change breaks the input path again, those four stories silently fall back to step 1 and axe still passes. The spec added here is the guard; keep it.