test: close illegal-state escape hatches in spec type-safety (WP-71)
ESLint blanket-exempted every *.spec.ts from the any ban, and no gate type-checked spec files at all (ng test is transpile-only), so a wrong cast in a test could never fail the build. 76 `as any` + 12 `as Extract<>` state-narrowing casts in the three biggest wizard specs read one variant's fields off a whole-union value: if the reducer returned the wrong variant, the assertion silently read undefined instead of failing. expectTag(state, tag) (libs/shared/src/testing/expect-tag.ts) asserts and narrows in one call, replacing every one of those casts. Removes the spec-file any exemption, adds `npm run typecheck` (tsc --noEmit over each project's tsconfig.spec.json) to CI, and forbids production code from importing libs/shared/src/testing via dependency-cruiser. Backend: AanvraagBuilder now models ZaakUrl (closing the last post-Build() mutation) and guards AtStep; null-forgiving `!` on endpoint assertions replaced with Assert.NotNull so a null DTO fails by name, not NullReferenceException. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { ok, err } from '@shared/kernel/fp';
|
||||
import { given } from '@shared/testing/machine';
|
||||
import { expectTag } from '@shared/testing/expect-tag';
|
||||
import {
|
||||
initial,
|
||||
next,
|
||||
@@ -43,45 +44,56 @@ const toStep3 = (uren: string, punten: string, jaren = '5'): WizardState =>
|
||||
describe('wizard.machine', () => {
|
||||
it('next advances only when step 1 parses', () => {
|
||||
expect(next(initial).tag).toBe('Editing'); // empty uren -> stays, with error
|
||||
expect((next(initial) as any).errors.uren).toBeTruthy();
|
||||
expect((next(toStep1('4160')) as any).step).toBe(2);
|
||||
expect(expectTag(next(initial), 'Editing').errors.uren).toBeTruthy();
|
||||
expect(expectTag(next(toStep1('4160')), 'Editing').step).toBe(2);
|
||||
});
|
||||
|
||||
it('next advances step 2 → 3 only when punten parses', () => {
|
||||
expect((next(toStep2('4160', 'x')) as any).step).toBe(2); // invalid punten -> stays
|
||||
expect((next(toStep2('4160', 'x')) as any).errors.punten).toBeTruthy();
|
||||
expect((next(toStep2('4160', '200')) as any).step).toBe(3);
|
||||
expect(expectTag(next(toStep2('4160', 'x')), 'Editing').step).toBe(2); // invalid punten -> stays
|
||||
expect(expectTag(next(toStep2('4160', 'x')), 'Editing').errors.punten).toBeTruthy();
|
||||
expect(expectTag(next(toStep2('4160', '200')), 'Editing').step).toBe(3);
|
||||
});
|
||||
|
||||
it('submit reaches Submitting ONLY from step 3 with fully valid data', () => {
|
||||
expect(submit(toStep2('4160', '200')).tag).toBe('Editing'); // not on step 3 -> no Submitting
|
||||
expect(submit(toStep3('4160', 'x')).tag).toBe('Editing'); // invalid punten
|
||||
const good = submit(toStep3('4160', '200'));
|
||||
expect(good.tag).toBe('Submitting');
|
||||
expect((good as any).data).toEqual({ uren: 4160, jaren: 5, punten: 200, documents: [] });
|
||||
const good = expectTag(submit(toStep3('4160', '200')), 'Submitting');
|
||||
expect(good.data).toEqual({ uren: 4160, jaren: 5, punten: 200, documents: [] });
|
||||
});
|
||||
|
||||
it('next requires BOTH step-1 fields (uren and jaren)', () => {
|
||||
expect((next(toStep1('4160', '')) as any).errors.jaren).toBeTruthy(); // jaren empty -> stays
|
||||
expect((next(toStep1('4160', '')) as any).step).toBe(1);
|
||||
expect((next(toStep1('4160', '5')) as any).step).toBe(2); // both valid -> advance
|
||||
expect(expectTag(next(toStep1('4160', '')), 'Editing').errors.jaren).toBeTruthy(); // jaren empty -> stays
|
||||
expect(expectTag(next(toStep1('4160', '')), 'Editing').step).toBe(1);
|
||||
expect(expectTag(next(toStep1('4160', '5')), 'Editing').step).toBe(2); // both valid -> advance
|
||||
});
|
||||
|
||||
it('back steps down one (3 → 2 → 1) and is a no-op from step 1', () => {
|
||||
expect(back(initial)).toBe(initial); // step 1, nothing to go back to
|
||||
expect((back(toStep3('1', '2')) as any).step).toBe(2);
|
||||
expect((back(toStep2('1', '2')) as any).step).toBe(1);
|
||||
expect(expectTag(back(toStep3('1', '2')), 'Editing').step).toBe(2);
|
||||
expect(expectTag(back(toStep2('1', '2')), 'Editing').step).toBe(1);
|
||||
expect(resolve(initial, ok(undefined))).toBe(initial); // not Submitting
|
||||
});
|
||||
|
||||
it('resolve maps Submitting to Submitted / Failed', () => {
|
||||
it('resolve maps a successful Submitting to Submitted', () => {
|
||||
// Given a wizard mid-submit.
|
||||
const submitting = submit(toStep3('4160', '200'));
|
||||
|
||||
// When the submission resolves ok...
|
||||
// Then the wizard reaches Submitted.
|
||||
expect(resolve(submitting, ok(undefined)).tag).toBe('Submitted');
|
||||
});
|
||||
|
||||
it('resolve maps a failing Submitting to Failed', () => {
|
||||
// Given a wizard mid-submit.
|
||||
const submitting = submit(toStep3('4160', '200'));
|
||||
|
||||
// When the submission resolves with an error...
|
||||
// Then the wizard reaches Failed.
|
||||
expect(resolve(submitting, err('boom')).tag).toBe('Failed');
|
||||
});
|
||||
|
||||
it('gaNaarStap jumps back to an earlier step, clearing errors', () => {
|
||||
expect((gaNaarStap(toStep3('4160', '200'), 1) as any).step).toBe(1);
|
||||
expect(expectTag(gaNaarStap(toStep3('4160', '200'), 1), 'Editing').step).toBe(1);
|
||||
});
|
||||
|
||||
it('gaNaarStap ignores a same/forward jump and jumps outside Editing', () => {
|
||||
@@ -125,14 +137,16 @@ describe('reduce (message-driven)', () => {
|
||||
});
|
||||
s = reduce(s, { tag: 'Submit' });
|
||||
expect(s.tag).toBe('Editing');
|
||||
expect((s as any).errors.documenten).toBeTruthy();
|
||||
expect(expectTag(s, 'Editing').errors.documenten).toBeTruthy();
|
||||
s = reduce(s, {
|
||||
tag: 'Upload',
|
||||
msg: { type: 'DeliveryChannelChanged', categoryId: 'bewijs', channel: 'post' },
|
||||
});
|
||||
s = reduce(s, { tag: 'Submit' });
|
||||
expect(s.tag).toBe('Submitting');
|
||||
expect((s as any).data.documents).toEqual([{ categoryId: 'bewijs', channel: 'post' }]);
|
||||
expect(expectTag(s, 'Submitting').data.documents).toEqual([
|
||||
{ categoryId: 'bewijs', channel: 'post' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('SubmitFailed then Retry returns to Submitting with the same data', () => {
|
||||
@@ -143,7 +157,12 @@ describe('reduce (message-driven)', () => {
|
||||
expect(s.tag).toBe('Failed');
|
||||
s = reduce(s, { tag: 'Retry' });
|
||||
expect(s.tag).toBe('Submitting');
|
||||
expect((s as any).data).toEqual({ uren: 4160, jaren: 5, punten: 200, documents: [] });
|
||||
expect(expectTag(s, 'Submitting').data).toEqual({
|
||||
uren: 4160,
|
||||
jaren: 5,
|
||||
punten: 200,
|
||||
documents: [],
|
||||
});
|
||||
});
|
||||
|
||||
it('Seed mounts an arbitrary state', () => {
|
||||
|
||||
Reference in New Issue
Block a user