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>
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
- Read
seed()inside the microtask, matching the two forms. Six lines across three files. Theif/elsereplaces the ternary because the branches are statements, not values. - Keep the microtask. It is what defers the dispatch past input binding.
The larger fix —
start(seed)on an application store called fromngOnInit— belongs to the ADR-0007 migration, not here. This ticket makes the existing seam correct; it does not move it. - Turn the workaround into the regression test.
intake-wizard.component.spec.tsnow mounts throughcomponentRef.setInput('seed', …)via amountSeededhelper, and a newhonours the seed inputcase asserts the machine state directly. Both fail on the old code, which is the point. - Do not touch
draftSync.enabled.enabled: () => this.seed() === initialreads the input lazily inside a lambda called from an effect, so it was already correct. Changing the input to| nullis part of ADR-0007.
Verification performed
npx ng test sspwith the intake fix reverted: 2 failed (both new spec cases). With the fix: 319 passed. That differential is the proof.npm run ci --fullgreen, 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
(
NetwerkfoutforFailed,referentienummerforIngediend,Documentenfor 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.