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>
This commit is contained in:
+7
-4
@@ -263,10 +263,13 @@ export class HerregistratieWizardComponent {
|
||||
constructor() {
|
||||
// An explicit seed (stories/tests) wins; otherwise resume the backend draft
|
||||
// (`?aanvraag=<id>`) or start fresh. Persistence is the draftSync controller's job.
|
||||
const seeded = this.seed();
|
||||
queueMicrotask(() =>
|
||||
seeded !== initial ? this.dispatch({ tag: 'Seed', state: seeded }) : this.draftSync.resume(),
|
||||
);
|
||||
// Read `seed()` INSIDE the microtask: Angular binds inputs after the constructor
|
||||
// runs, so an eager read here always returns the `initial` default.
|
||||
queueMicrotask(() => {
|
||||
const seeded = this.seed();
|
||||
if (seeded !== initial) this.dispatch({ tag: 'Seed', state: seeded });
|
||||
else void this.draftSync.resume();
|
||||
});
|
||||
}
|
||||
|
||||
/** Reset the wizard to a fresh, empty start. */
|
||||
|
||||
@@ -26,14 +26,30 @@ const buitenlandJa: IntakeState = {
|
||||
scholingThreshold: 1000,
|
||||
};
|
||||
|
||||
/** Mount with a seed and let the constructor's microtask apply it. */
|
||||
async function mountSeeded(state: IntakeState) {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [provideHttpClient(), provideApiClient()],
|
||||
});
|
||||
const fixture = TestBed.createComponent(IntakeWizardComponent);
|
||||
fixture.componentRef.setInput('seed', state);
|
||||
await Promise.resolve(); // the seed is applied in a queueMicrotask
|
||||
fixture.detectChanges();
|
||||
return fixture;
|
||||
}
|
||||
|
||||
describe('IntakeWizardComponent', () => {
|
||||
it('renders each field group as its own grey <fieldset>', () => {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [provideHttpClient(), provideApiClient()],
|
||||
});
|
||||
const fixture = TestBed.createComponent(IntakeWizardComponent);
|
||||
fixture.componentInstance.dispatch({ tag: 'Seed', state: buitenlandJa });
|
||||
fixture.detectChanges();
|
||||
// Regression: the constructor must read `seed()` INSIDE its microtask. Angular binds
|
||||
// inputs after the constructor runs, so an eager read silently yields the `initial`
|
||||
// default and every seeded story renders step 1 instead of the state it asked for.
|
||||
it('honours the seed input', async () => {
|
||||
const fixture = await mountSeeded(buitenlandJa);
|
||||
|
||||
expect(fixture.componentInstance.state()).toEqual(buitenlandJa);
|
||||
});
|
||||
|
||||
it('renders each field group as its own grey <fieldset>', async () => {
|
||||
const fixture = await mountSeeded(buitenlandJa);
|
||||
|
||||
const fieldsets: HTMLElement[] = Array.from(
|
||||
fixture.nativeElement.querySelectorAll('form.form-horizontal fieldset'),
|
||||
|
||||
@@ -212,10 +212,13 @@ export class IntakeWizardComponent {
|
||||
constructor() {
|
||||
// An explicit seed (stories/tests) wins; otherwise resume the backend draft
|
||||
// (`?aanvraag=<id>`) or start fresh. Persistence is the draftSync controller's job.
|
||||
const seeded = this.seed();
|
||||
queueMicrotask(() =>
|
||||
seeded !== initial ? this.dispatch({ tag: 'Seed', state: seeded }) : this.draftSync.resume(),
|
||||
);
|
||||
// Read `seed()` INSIDE the microtask: Angular binds inputs after the constructor
|
||||
// runs, so an eager read here always returns the `initial` default.
|
||||
queueMicrotask(() => {
|
||||
const seeded = this.seed();
|
||||
if (seeded !== initial) this.dispatch({ tag: 'Seed', state: seeded });
|
||||
else void this.draftSync.resume();
|
||||
});
|
||||
// Apply the server-owned threshold into machine state as it arrives. Track
|
||||
// only the policy value; untrack the dispatch (it reads the state signal
|
||||
// internally, which would otherwise make this effect loop on its own write).
|
||||
|
||||
@@ -233,10 +233,13 @@ export class RegistratieWizardComponent {
|
||||
constructor() {
|
||||
// An explicit seed (stories/tests) wins; otherwise resume from the backend draft
|
||||
// (`?aanvraag=<id>`), or start fresh. Persistence is the draftSync controller's job.
|
||||
const seeded = this.seed();
|
||||
queueMicrotask(() =>
|
||||
seeded !== initial ? this.dispatch({ tag: 'Seed', state: seeded }) : this.draftSync.resume(),
|
||||
);
|
||||
// Read `seed()` INSIDE the microtask: Angular binds inputs after the constructor
|
||||
// runs, so an eager read here always returns the `initial` default.
|
||||
queueMicrotask(() => {
|
||||
const seeded = this.seed();
|
||||
if (seeded !== initial) this.dispatch({ tag: 'Seed', state: seeded });
|
||||
else void this.draftSync.resume();
|
||||
});
|
||||
// Prefill the address from the BRP lookup as it arrives. Track only the facade's
|
||||
// parsed prefill signal; untrack the dispatch (it reads the state signal, which
|
||||
// would otherwise make this effect loop on its own write). Don't clobber
|
||||
|
||||
Reference in New Issue
Block a user