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>
83 lines
3.8 KiB
Markdown
83 lines
3.8 KiB
Markdown
# 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**:
|
|
|
|
```ts
|
|
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.
|