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,4 +1,5 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { expectTag } from '@shared/testing/expect-tag';
|
||||
import { BesluitState, reduce, initial } from './besluit.machine';
|
||||
|
||||
const editingWith = (besluit: string, toelichting = ''): BesluitState => ({
|
||||
@@ -10,26 +11,22 @@ const editingWith = (besluit: string, toelichting = ''): BesluitState => ({
|
||||
describe('besluit reduce', () => {
|
||||
it('SetField updates the draft while editing', () => {
|
||||
const s = reduce(initial, { tag: 'SetField', key: 'besluit', value: 'Goedkeuren' });
|
||||
expect(s.tag).toBe('Editing');
|
||||
expect((s as Extract<BesluitState, { tag: 'Editing' }>).draft.besluit).toBe('Goedkeuren');
|
||||
expect(expectTag(s, 'Editing').draft.besluit).toBe('Goedkeuren');
|
||||
});
|
||||
|
||||
it('Submit with no besluit chosen stays Editing and reports a field error', () => {
|
||||
const s = reduce(editingWith(''), { tag: 'Submit' });
|
||||
expect(s.tag).toBe('Editing');
|
||||
expect((s as Extract<BesluitState, { tag: 'Editing' }>).errors.besluit).toBeTruthy();
|
||||
expect(expectTag(s, 'Editing').errors.besluit).toBeTruthy();
|
||||
});
|
||||
|
||||
it('Submit Afwijzen without a toelichting stays Editing and reports a field error', () => {
|
||||
const s = reduce(editingWith('Afwijzen'), { tag: 'Submit' });
|
||||
expect(s.tag).toBe('Editing');
|
||||
expect((s as Extract<BesluitState, { tag: 'Editing' }>).errors.toelichting).toBeTruthy();
|
||||
expect(expectTag(s, 'Editing').errors.toelichting).toBeTruthy();
|
||||
});
|
||||
|
||||
it('Submit Goedkeuren with no toelichting moves to Submitting (optional there)', () => {
|
||||
const s = reduce(editingWith('Goedkeuren'), { tag: 'Submit' });
|
||||
expect(s.tag).toBe('Submitting');
|
||||
expect((s as Extract<BesluitState, { tag: 'Submitting' }>).data).toEqual({
|
||||
expect(expectTag(s, 'Submitting').data).toEqual({
|
||||
besluit: 'Goedkeuren',
|
||||
toelichting: undefined,
|
||||
});
|
||||
@@ -37,8 +34,7 @@ describe('besluit reduce', () => {
|
||||
|
||||
it('Submit Afwijzen with a toelichting moves to Submitting with the trimmed value', () => {
|
||||
const s = reduce(editingWith('Afwijzen', ' niet erkend '), { tag: 'Submit' });
|
||||
expect(s.tag).toBe('Submitting');
|
||||
expect((s as Extract<BesluitState, { tag: 'Submitting' }>).data).toEqual({
|
||||
expect(expectTag(s, 'Submitting').data).toEqual({
|
||||
besluit: 'Afwijzen',
|
||||
toelichting: 'niet erkend',
|
||||
});
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { expectTag } from '@shared/testing/expect-tag';
|
||||
import { OrgTemplate, OrgTemplateAdminView } from './org-template';
|
||||
import { OrgTemplateState, reduce } from './org-template.machine';
|
||||
import { DocumentCategory } from '@shared/upload/upload.machine';
|
||||
@@ -40,9 +41,7 @@ const logoCategory: DocumentCategory = {
|
||||
|
||||
describe('org-template.machine', () => {
|
||||
it('DraftLoaded moves to loaded with the draft, clean', () => {
|
||||
const s = loaded();
|
||||
expect(s.tag).toBe('loaded');
|
||||
if (s.tag !== 'loaded') return;
|
||||
const s = expectTag(loaded(), 'loaded');
|
||||
expect(s.draft.orgName).toBe('CIBG');
|
||||
expect(s.subOrgId).toBe('cibg-registers');
|
||||
expect(s.unsentBriefs).toBe(2);
|
||||
@@ -55,33 +54,44 @@ describe('org-template.machine', () => {
|
||||
});
|
||||
|
||||
it('FieldEdited edits the draft and marks dirty', () => {
|
||||
const s = reduce(loaded(), { tag: 'FieldEdited', field: 'orgName', value: 'CIBG Nieuw' });
|
||||
expect(s.tag === 'loaded' && s.draft.orgName).toBe('CIBG Nieuw');
|
||||
expect(s.tag === 'loaded' && s.dirty).toBe(true);
|
||||
const s = expectTag(
|
||||
reduce(loaded(), { tag: 'FieldEdited', field: 'orgName', value: 'CIBG Nieuw' }),
|
||||
'loaded',
|
||||
);
|
||||
expect(s.draft.orgName).toBe('CIBG Nieuw');
|
||||
expect(s.dirty).toBe(true);
|
||||
});
|
||||
|
||||
it('MarginEdited edits one edge and marks dirty', () => {
|
||||
const s = reduce(loaded(), { tag: 'MarginEdited', edge: 'topMm', value: 40 });
|
||||
expect(s.tag === 'loaded' && s.draft.margins.topMm).toBe(40);
|
||||
expect(s.tag === 'loaded' && s.draft.margins.leftMm).toBe(20);
|
||||
expect(s.tag === 'loaded' && s.dirty).toBe(true);
|
||||
const s = expectTag(
|
||||
reduce(loaded(), { tag: 'MarginEdited', edge: 'topMm', value: 40 }),
|
||||
'loaded',
|
||||
);
|
||||
expect(s.draft.margins.topMm).toBe(40);
|
||||
expect(s.draft.margins.leftMm).toBe(20);
|
||||
expect(s.dirty).toBe(true);
|
||||
});
|
||||
|
||||
it('DraftSaved clears dirty when the saved draft is the current one', () => {
|
||||
const edited = reduce(loaded(), { tag: 'FieldEdited', field: 'orgName', value: 'X' });
|
||||
const savedDraft = edited.tag === 'loaded' ? edited.draft : template;
|
||||
const s = reduce(edited, { tag: 'DraftSaved', savedDraft });
|
||||
expect(s.tag === 'loaded' && s.dirty).toBe(false);
|
||||
expect(s.tag === 'loaded' && s.draft.orgName).toBe('X');
|
||||
const edited = expectTag(
|
||||
reduce(loaded(), { tag: 'FieldEdited', field: 'orgName', value: 'X' }),
|
||||
'loaded',
|
||||
);
|
||||
const s = expectTag(reduce(edited, { tag: 'DraftSaved', savedDraft: edited.draft }), 'loaded');
|
||||
expect(s.dirty).toBe(false);
|
||||
expect(s.draft.orgName).toBe('X');
|
||||
});
|
||||
|
||||
it('DraftSaved keeps dirty when an edit landed during the save round-trip', () => {
|
||||
const editing = reduce(loaded(), { tag: 'FieldEdited', field: 'orgName', value: 'X' });
|
||||
const savedDraft = editing.tag === 'loaded' ? editing.draft : template;
|
||||
const editing = expectTag(
|
||||
reduce(loaded(), { tag: 'FieldEdited', field: 'orgName', value: 'X' }),
|
||||
'loaded',
|
||||
);
|
||||
const savedDraft = editing.draft;
|
||||
// a further edit changes the draft reference before the save resolves
|
||||
const raced = reduce(editing, { tag: 'FieldEdited', field: 'orgName', value: 'Y' });
|
||||
const s = reduce(raced, { tag: 'DraftSaved', savedDraft });
|
||||
expect(s.tag === 'loaded' && s.dirty).toBe(true);
|
||||
const s = expectTag(reduce(raced, { tag: 'DraftSaved', savedDraft }), 'loaded');
|
||||
expect(s.dirty).toBe(true);
|
||||
});
|
||||
|
||||
it('edits are no-ops in non-loaded states', () => {
|
||||
@@ -107,12 +117,15 @@ describe('org-template.machine', () => {
|
||||
fileSizeMb: 0.1,
|
||||
},
|
||||
});
|
||||
const done = reduce(selected, {
|
||||
tag: 'Upload',
|
||||
msg: { type: 'UploadComplete', localId: 'a', documentId: 'doc-1' },
|
||||
});
|
||||
expect(done.tag === 'loaded' && done.draft.logoDocumentId).toBe('doc-1');
|
||||
expect(done.tag === 'loaded' && done.dirty).toBe(true);
|
||||
const done = expectTag(
|
||||
reduce(selected, {
|
||||
tag: 'Upload',
|
||||
msg: { type: 'UploadComplete', localId: 'a', documentId: 'doc-1' },
|
||||
}),
|
||||
'loaded',
|
||||
);
|
||||
expect(done.draft.logoDocumentId).toBe('doc-1');
|
||||
expect(done.dirty).toBe(true);
|
||||
});
|
||||
|
||||
it('removing the logo clears logoDocumentId + dirty', () => {
|
||||
@@ -120,12 +133,15 @@ describe('org-template.machine', () => {
|
||||
tag: 'Upload',
|
||||
msg: { type: 'UploadComplete', localId: 'a', documentId: 'doc-1' },
|
||||
});
|
||||
const removed = reduce(withLogo, {
|
||||
tag: 'Upload',
|
||||
msg: { type: 'UploadRemoved', localId: 'a' },
|
||||
});
|
||||
expect(removed.tag === 'loaded' && removed.draft.logoDocumentId).toBeUndefined();
|
||||
expect(removed.tag === 'loaded' && removed.dirty).toBe(true);
|
||||
const removed = expectTag(
|
||||
reduce(withLogo, {
|
||||
tag: 'Upload',
|
||||
msg: { type: 'UploadRemoved', localId: 'a' },
|
||||
}),
|
||||
'loaded',
|
||||
);
|
||||
expect(removed.draft.logoDocumentId).toBeUndefined();
|
||||
expect(removed.dirty).toBe(true);
|
||||
});
|
||||
|
||||
it('DraftLoaded (sub-org switch) keeps the loaded logo category, drops uploads', () => {
|
||||
@@ -133,12 +149,15 @@ describe('org-template.machine', () => {
|
||||
tag: 'Upload',
|
||||
msg: { type: 'CategoriesLoaded', categories: [logoCategory] },
|
||||
});
|
||||
const switched = reduce(withCat, {
|
||||
tag: 'DraftLoaded',
|
||||
view: view({ draft: { ...template, subOrgId: 'cibg-vakbekwaamheid' } }),
|
||||
});
|
||||
expect(switched.tag === 'loaded' && switched.upload.categories).toHaveLength(1);
|
||||
expect(switched.tag === 'loaded' && switched.upload.uploads).toHaveLength(0);
|
||||
expect(switched.tag === 'loaded' && switched.subOrgId).toBe('cibg-vakbekwaamheid');
|
||||
const switched = expectTag(
|
||||
reduce(withCat, {
|
||||
tag: 'DraftLoaded',
|
||||
view: view({ draft: { ...template, subOrgId: 'cibg-vakbekwaamheid' } }),
|
||||
}),
|
||||
'loaded',
|
||||
);
|
||||
expect(switched.upload.categories).toHaveLength(1);
|
||||
expect(switched.upload.uploads).toHaveLength(0);
|
||||
expect(switched.subOrgId).toBe('cibg-vakbekwaamheid');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,15 +1,27 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { hasProgress, initial, WizardState } from './herregistratie.machine';
|
||||
import { given } from '@shared/testing/machine';
|
||||
import { expectTag } from '@shared/testing/expect-tag';
|
||||
import { hasProgress, initial, reduce } from './herregistratie.machine';
|
||||
|
||||
const editing = initial as Extract<WizardState, { tag: 'Editing' }>;
|
||||
const wizard = given(reduce, initial);
|
||||
|
||||
describe('herregistratie hasProgress', () => {
|
||||
it('is false for a fresh form', () => {
|
||||
expect(hasProgress(editing)).toBe(false);
|
||||
expect(hasProgress(expectTag(initial, 'Editing'))).toBe(false);
|
||||
});
|
||||
|
||||
it('is true once a field is filled or the user advances', () => {
|
||||
expect(hasProgress({ ...editing, draft: { uren: '40', jaren: '', punten: '' } })).toBe(true);
|
||||
expect(hasProgress({ ...editing, step: 2 })).toBe(true);
|
||||
const filled = expectTag(wizard({ tag: 'SetField', key: 'uren', value: '40' }), 'Editing');
|
||||
expect(hasProgress(filled)).toBe(true);
|
||||
|
||||
const advanced = expectTag(
|
||||
wizard(
|
||||
{ tag: 'SetField', key: 'uren', value: '4160' },
|
||||
{ tag: 'SetField', key: 'jaren', value: '5' },
|
||||
{ tag: 'Next' },
|
||||
),
|
||||
'Editing',
|
||||
);
|
||||
expect(hasProgress(advanced)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -1,15 +1,26 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { hasProgress, initial, IntakeState } from './intake.machine';
|
||||
import { given } from '@shared/testing/machine';
|
||||
import { expectTag } from '@shared/testing/expect-tag';
|
||||
import { hasProgress, initial, reduce } from './intake.machine';
|
||||
|
||||
const answering = initial as Extract<IntakeState, { tag: 'Answering' }>;
|
||||
const intake = given(reduce, initial);
|
||||
|
||||
describe('intake hasProgress', () => {
|
||||
it('is false for a fresh questionnaire', () => {
|
||||
expect(hasProgress(answering)).toBe(false);
|
||||
expect(hasProgress(expectTag(initial, 'Answering'))).toBe(false);
|
||||
});
|
||||
|
||||
it('is true once an answer is given or the user advances', () => {
|
||||
expect(hasProgress({ ...answering, answers: { buitenlandGewerkt: 'ja' } })).toBe(true);
|
||||
expect(hasProgress({ ...answering, cursor: 1 })).toBe(true);
|
||||
const answered = expectTag(
|
||||
intake({ tag: 'SetAnswer', key: 'buitenlandGewerkt', value: 'ja' }),
|
||||
'Answering',
|
||||
);
|
||||
expect(hasProgress(answered)).toBe(true);
|
||||
|
||||
const advanced = expectTag(
|
||||
intake({ tag: 'SetAnswer', key: 'buitenlandGewerkt', value: 'nee' }, { tag: 'Next' }),
|
||||
'Answering',
|
||||
);
|
||||
expect(hasProgress(advanced)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { ok, err } from '@shared/kernel/fp';
|
||||
import { expectTag } from '@shared/testing/expect-tag';
|
||||
import {
|
||||
Answers,
|
||||
initial,
|
||||
@@ -31,9 +32,11 @@ describe('STEPS (fixed) and inline questions', () => {
|
||||
it('reveals the buitenland detail questions inline only when worked abroad', () => {
|
||||
// No new step; instead these fields become required within the buitenland step.
|
||||
expect(next(answering({ buitenlandGewerkt: 'ja' })).tag).toBe('Answering'); // land/uren missing -> blocked
|
||||
expect((next(answering({ buitenlandGewerkt: 'ja' })) as any).errors.land).toBeTruthy();
|
||||
expect(
|
||||
expectTag(next(answering({ buitenlandGewerkt: 'ja' })), 'Answering').errors.land,
|
||||
).toBeTruthy();
|
||||
expect(next(answering({ buitenlandGewerkt: 'nee' })).tag).toBe('Answering'); // valid, advances (cursor moves)
|
||||
expect((next(answering({ buitenlandGewerkt: 'nee' })) as any).cursor).toBe(1);
|
||||
expect(expectTag(next(answering({ buitenlandGewerkt: 'nee' })), 'Answering').cursor).toBe(1);
|
||||
});
|
||||
|
||||
it('reveals the scholing question only when NL-hours are below the threshold', () => {
|
||||
@@ -50,31 +53,33 @@ describe('STEPS (fixed) and inline questions', () => {
|
||||
answering({ buitenlandGewerkt: 'nee', uren: '1500', punten: '200' }, 0, 2000),
|
||||
);
|
||||
expect(lowThreshold.tag).toBe('Answering'); // scholing now required (1500 < 2000), unanswered → blocked
|
||||
expect((lowThreshold as any).errors.scholingGevolgd).toBeTruthy();
|
||||
expect(expectTag(lowThreshold, 'Answering').errors.scholingGevolgd).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('navigation', () => {
|
||||
it('Next is a no-op (sets an error) when the current step is invalid', () => {
|
||||
const s = next(initial); // buitenland unanswered
|
||||
expect(s.tag).toBe('Answering');
|
||||
expect((s as any).cursor).toBe(0);
|
||||
expect((s as any).errors.buitenlandGewerkt).toBeTruthy();
|
||||
const s = expectTag(next(initial), 'Answering'); // buitenland unanswered
|
||||
expect(s.cursor).toBe(0);
|
||||
expect(s.errors.buitenlandGewerkt).toBeTruthy();
|
||||
});
|
||||
|
||||
it('Next advances once the step is valid', () => {
|
||||
const s = next(answering({ buitenlandGewerkt: 'nee' }));
|
||||
expect((s as any).cursor).toBe(1);
|
||||
expect(currentStep(s as any)).toBe('werk');
|
||||
const s = expectTag(next(answering({ buitenlandGewerkt: 'nee' })), 'Answering');
|
||||
expect(s.cursor).toBe(1);
|
||||
expect(currentStep(s)).toBe('werk');
|
||||
});
|
||||
|
||||
it('editing an answer leaves the cursor fixed (steps never collapse)', () => {
|
||||
const edited = reduce(answering({ buitenlandGewerkt: 'ja' }, 1), {
|
||||
tag: 'SetAnswer',
|
||||
key: 'buitenlandGewerkt',
|
||||
value: 'nee',
|
||||
});
|
||||
expect((edited as any).cursor).toBe(1); // cursor untouched; only inline questions change
|
||||
const edited = expectTag(
|
||||
reduce(answering({ buitenlandGewerkt: 'ja' }, 1), {
|
||||
tag: 'SetAnswer',
|
||||
key: 'buitenlandGewerkt',
|
||||
value: 'nee',
|
||||
}),
|
||||
'Answering',
|
||||
);
|
||||
expect(edited.cursor).toBe(1); // cursor untouched; only inline questions change
|
||||
});
|
||||
|
||||
it('Back never goes below the first step', () => {
|
||||
@@ -83,7 +88,7 @@ describe('navigation', () => {
|
||||
|
||||
it('gaNaarStap jumps back to an earlier step, clearing errors', () => {
|
||||
const s = answering({ buitenlandGewerkt: 'nee' }, 2);
|
||||
expect((gaNaarStap(s, 0) as any).cursor).toBe(0);
|
||||
expect(expectTag(gaNaarStap(s, 0), 'Answering').cursor).toBe(0);
|
||||
});
|
||||
|
||||
it('gaNaarStap ignores a same/forward jump and jumps outside Answering', () => {
|
||||
@@ -106,19 +111,18 @@ describe('submit', () => {
|
||||
answering({ buitenlandGewerkt: 'nee', uren: '500', scholingGevolgd: 'ja', punten: 'x' }),
|
||||
).tag,
|
||||
).toBe('Answering');
|
||||
const good = submit(answering(complete));
|
||||
expect(good.tag).toBe('Submitting');
|
||||
expect((good as any).data.uren).toBe(4160);
|
||||
expect((good as any).data.punten).toBeUndefined(); // not collected without scholing
|
||||
const good = expectTag(submit(answering(complete)), 'Submitting');
|
||||
expect(good.data.uren).toBe(4160);
|
||||
expect(good.data.punten).toBeUndefined(); // not collected without scholing
|
||||
});
|
||||
|
||||
it('punten is required only when aanvullende scholing was gevolgd', () => {
|
||||
// scholing = ja but punten missing -> blocked on punten.
|
||||
const missing = submit(
|
||||
answering({ buitenlandGewerkt: 'nee', uren: '500', scholingGevolgd: 'ja' }),
|
||||
const missing = expectTag(
|
||||
submit(answering({ buitenlandGewerkt: 'nee', uren: '500', scholingGevolgd: 'ja' })),
|
||||
'Answering',
|
||||
);
|
||||
expect(missing.tag).toBe('Answering');
|
||||
expect((missing as any).errors.punten).toBeTruthy();
|
||||
expect(missing.errors.punten).toBeTruthy();
|
||||
// scholing = nee -> punten not required, submits without it.
|
||||
expect(
|
||||
submit(answering({ buitenlandGewerkt: 'nee', uren: '500', scholingGevolgd: 'nee' })).tag,
|
||||
@@ -128,12 +132,14 @@ describe('submit', () => {
|
||||
it('low hours requires the scholing answer before submit', () => {
|
||||
const noScholing = submit(answering({ buitenlandGewerkt: 'nee', uren: '500' }));
|
||||
expect(noScholing.tag).toBe('Answering'); // scholing question is required, unanswered
|
||||
const withScholing = submit(
|
||||
answering({ buitenlandGewerkt: 'nee', uren: '500', scholingGevolgd: 'ja', punten: '200' }),
|
||||
const withScholing = expectTag(
|
||||
submit(
|
||||
answering({ buitenlandGewerkt: 'nee', uren: '500', scholingGevolgd: 'ja', punten: '200' }),
|
||||
),
|
||||
'Submitting',
|
||||
);
|
||||
expect(withScholing.tag).toBe('Submitting');
|
||||
expect((withScholing as any).data.aanvullendeScholing).toBe(true);
|
||||
expect((withScholing as any).data.punten).toBe(200);
|
||||
expect(withScholing.data.aanvullendeScholing).toBe(true);
|
||||
expect(withScholing.data.punten).toBe(200);
|
||||
});
|
||||
|
||||
it('resolve maps Submitting to Submitted on a successful submit', () => {
|
||||
@@ -155,12 +161,12 @@ describe('reduce (message-driven happy path)', () => {
|
||||
s = reduce(s, { tag: 'SetAnswer', key: 'land', value: 'België' });
|
||||
s = reduce(s, { tag: 'SetAnswer', key: 'buitenlandseUren', value: '800' });
|
||||
s = reduce(s, { tag: 'Next' });
|
||||
expect(currentStep(s as any)).toBe('werk');
|
||||
expect(currentStep(expectTag(s, 'Answering'))).toBe('werk');
|
||||
// Step 2: werk — uren + punten (no inline scholing, hours are high).
|
||||
s = reduce(s, { tag: 'SetAnswer', key: 'uren', value: '4160' });
|
||||
s = reduce(s, { tag: 'SetAnswer', key: 'punten', value: '200' });
|
||||
s = reduce(s, { tag: 'Next' });
|
||||
expect(currentStep(s as any)).toBe('review');
|
||||
expect(currentStep(expectTag(s, 'Answering'))).toBe('review');
|
||||
s = reduce(s, { tag: 'Submit' });
|
||||
expect(s.tag).toBe('Submitting');
|
||||
s = reduce(s, { tag: 'SubmitConfirmed' });
|
||||
|
||||
@@ -2,9 +2,9 @@ import { describe, it, expect } from 'vitest';
|
||||
import { submittedRow, detailRows, purposeLabel, statusLabel, TYPE_LABELS } from './aanvraag-view';
|
||||
import { Aanvraag } from './aanvraag';
|
||||
|
||||
const base = {
|
||||
const base: Omit<Aanvraag, 'status'> = {
|
||||
id: '1',
|
||||
type: 'herregistratie' as const,
|
||||
type: 'herregistratie',
|
||||
documentIds: [],
|
||||
createdAt: '',
|
||||
updatedAt: '',
|
||||
@@ -13,19 +13,27 @@ const base = {
|
||||
|
||||
describe('submittedRow', () => {
|
||||
it('heading is the type, subtitle is the purpose', () => {
|
||||
// Given a submitted herregistratie aanvraag.
|
||||
// When its row is derived...
|
||||
const row = submittedRow({
|
||||
...base,
|
||||
status: { tag: 'InBehandeling', referentie: 'R1', manual: false },
|
||||
} as Aanvraag);
|
||||
});
|
||||
// Then the heading/subtitle come from the same label functions the view uses —
|
||||
// never a hardcoded Dutch literal here.
|
||||
expect(row.heading).toBe(TYPE_LABELS.herregistratie);
|
||||
expect(row.subtitle).toBe(purposeLabel('herregistratie'));
|
||||
});
|
||||
|
||||
it('status line carries the status label, reference and submit date', () => {
|
||||
// Given an aanvraag InBehandeling, submitted 2024-05-12, referentie R1.
|
||||
// When its row is derived...
|
||||
const row = submittedRow({
|
||||
...base,
|
||||
status: { tag: 'InBehandeling', referentie: 'R1', manual: false },
|
||||
} as Aanvraag);
|
||||
});
|
||||
// Then the status line contains the label (via statusLabel(), not a literal),
|
||||
// the reference, and the formatted submit date.
|
||||
expect(row.status).toContain(
|
||||
statusLabel({ tag: 'InBehandeling', referentie: 'R1', manual: false }),
|
||||
);
|
||||
@@ -33,35 +41,58 @@ describe('submittedRow', () => {
|
||||
expect(row.status).toContain('12 mei 2024');
|
||||
});
|
||||
|
||||
it('manual review adds a note; rejection adds its reason', () => {
|
||||
it('manual review adds a note', () => {
|
||||
// Given an aanvraag InBehandeling with manual review flagged.
|
||||
// When its row is derived...
|
||||
const manual = submittedRow({
|
||||
...base,
|
||||
status: { tag: 'InBehandeling', referentie: 'R1', manual: true },
|
||||
} as Aanvraag);
|
||||
});
|
||||
// Then the status line notes the manual review.
|
||||
expect(manual.status).toContain('handmatig');
|
||||
});
|
||||
|
||||
// `reden` (rejection.status.reden / meerInfo.status.reden) is raw domain data — a
|
||||
// free-text field (`Aanvraag`'s status union types it `string`, not an enum), passed
|
||||
// through `submittedRow` unchanged and unwrapped by `$localize`. There is no
|
||||
// reason-code/tag backing it to assert on instead: the value under test IS the exact
|
||||
// string the Given supplied, so asserting it reappears in the Then is checking
|
||||
// pass-through, not translated copy.
|
||||
it('rejection adds its reason', () => {
|
||||
// Given a rejected aanvraag with a rejection reason.
|
||||
// When its row is derived...
|
||||
const rejected = submittedRow({
|
||||
...base,
|
||||
status: { tag: 'Afgewezen', referentie: 'R2', reden: 'Onvoldoende uren' },
|
||||
} as Aanvraag);
|
||||
});
|
||||
// Then the reason passes through into the status line unchanged.
|
||||
expect(rejected.status).toContain('Onvoldoende uren');
|
||||
});
|
||||
|
||||
it('meer-info-gevraagd adds its reason, like a rejection', () => {
|
||||
// Given an aanvraag with more information requested, with a reason.
|
||||
// When its row is derived...
|
||||
const row = submittedRow({
|
||||
...base,
|
||||
status: { tag: 'MeerInfoGevraagd', referentie: 'R3', reden: 'Diploma ontbreekt' },
|
||||
} as Aanvraag);
|
||||
});
|
||||
// Then the reason passes through into the status line unchanged (see note above).
|
||||
expect(row.status).toContain('Diploma ontbreekt');
|
||||
});
|
||||
});
|
||||
|
||||
describe('detailRows', () => {
|
||||
it('lists soort/waarvoor/status/referentie/ingediend, plus reason when rejected', () => {
|
||||
// Given a rejected aanvraag with a rejection reason.
|
||||
// When its detail rows are derived...
|
||||
const rows = detailRows({
|
||||
...base,
|
||||
status: { tag: 'Afgewezen', referentie: 'R2', reden: 'Onvoldoende uren' },
|
||||
} as Aanvraag);
|
||||
});
|
||||
const values = rows.map((r) => r.value);
|
||||
|
||||
// Then the type label (via TYPE_LABELS, not a literal), the reference, and the
|
||||
// reason (raw pass-through, see note above) all appear, and a reason row is added.
|
||||
expect(values).toContain(TYPE_LABELS.herregistratie);
|
||||
expect(values).toContain('R2');
|
||||
expect(values).toContain('Onvoldoende uren');
|
||||
@@ -69,11 +100,15 @@ describe('detailRows', () => {
|
||||
});
|
||||
|
||||
it('reference falls back to em dash for a Concept', () => {
|
||||
// Given a Concept (not yet submitted, no reference assigned).
|
||||
// When its detail rows are derived...
|
||||
const rows = detailRows({
|
||||
...base,
|
||||
submittedAt: undefined,
|
||||
status: { tag: 'Concept', stepIndex: 0, stepCount: 3 },
|
||||
} as Aanvraag);
|
||||
});
|
||||
|
||||
// Then the reference row falls back to an em dash, and no reason row is added.
|
||||
const ref = rows.find((r) => r.value === '—');
|
||||
expect(ref).toBeTruthy();
|
||||
expect(rows.length).toBe(5);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { given } from '@shared/testing/machine';
|
||||
import { expectTag } from '@shared/testing/expect-tag';
|
||||
import { ChangeRequestState, reduce, initial } from './change-request.machine';
|
||||
|
||||
const givenChangeRequest = given(reduce, initial);
|
||||
@@ -10,25 +11,17 @@ const editingWith = (telefoon: string): ChangeRequestState =>
|
||||
describe('change-request reduce', () => {
|
||||
it('SetField updates the draft while editing', () => {
|
||||
const s = reduce(initial, { tag: 'SetField', key: 'telefoon', value: '0612345678' });
|
||||
expect(s.tag).toBe('Editing');
|
||||
expect((s as Extract<ChangeRequestState, { tag: 'Editing' }>).draft.telefoon).toBe(
|
||||
'0612345678',
|
||||
);
|
||||
expect(expectTag(s, 'Editing').draft.telefoon).toBe('0612345678');
|
||||
});
|
||||
|
||||
it('Submit with an invalid draft stays Editing and reports field errors', () => {
|
||||
const s = reduce(editingWith('nope'), { tag: 'Submit' });
|
||||
expect(s.tag).toBe('Editing');
|
||||
const errors = (s as Extract<ChangeRequestState, { tag: 'Editing' }>).errors;
|
||||
expect(errors.telefoon).toBeTruthy();
|
||||
expect(expectTag(s, 'Editing').errors.telefoon).toBeTruthy();
|
||||
});
|
||||
|
||||
it('Submit with a valid draft moves to Submitting with parsed (normalised) data', () => {
|
||||
const s = reduce(editingWith('06 12 34 56 78'), { tag: 'Submit' });
|
||||
expect(s.tag).toBe('Submitting');
|
||||
expect((s as Extract<ChangeRequestState, { tag: 'Submitting' }>).data.telefoon).toBe(
|
||||
'0612345678',
|
||||
);
|
||||
expect(expectTag(s, 'Submitting').data.telefoon).toBe('0612345678');
|
||||
});
|
||||
|
||||
it('SubmitConfirmed maps Submitting to Submitted with the referentie', () => {
|
||||
|
||||
@@ -1,34 +1,51 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { hasProgress, initial, RegistratieState } from './registratie-wizard.machine';
|
||||
import { given } from '@shared/testing/machine';
|
||||
import { expectTag } from '@shared/testing/expect-tag';
|
||||
import { hasProgress, initial, reduce } from './registratie-wizard.machine';
|
||||
|
||||
const invullen = (over: Partial<Extract<RegistratieState, { tag: 'Invullen' }>>) => ({
|
||||
...(initial as Extract<RegistratieState, { tag: 'Invullen' }>),
|
||||
...over,
|
||||
});
|
||||
const wizard = given(reduce, initial);
|
||||
|
||||
describe('hasProgress', () => {
|
||||
it('is false for a fresh wizard', () => {
|
||||
expect(hasProgress(initial as Extract<RegistratieState, { tag: 'Invullen' }>)).toBe(false);
|
||||
expect(hasProgress(expectTag(initial, 'Invullen'))).toBe(false);
|
||||
});
|
||||
|
||||
it('ignores an auto-prefilled BRP address at step 0', () => {
|
||||
const s = invullen({
|
||||
draft: {
|
||||
const s = expectTag(
|
||||
wizard({
|
||||
tag: 'PrefillAdres',
|
||||
straat: 'Lange Voorhout 9',
|
||||
postcode: '2514 EA',
|
||||
woonplaats: 'Den Haag',
|
||||
adresHerkomst: 'brp',
|
||||
antwoorden: {},
|
||||
},
|
||||
});
|
||||
}),
|
||||
'Invullen',
|
||||
);
|
||||
expect(hasProgress(s)).toBe(false);
|
||||
});
|
||||
|
||||
it('is true once the user advances, picks correspondence/diploma, or is past step 0', () => {
|
||||
expect(hasProgress(invullen({ cursor: 1 }))).toBe(true);
|
||||
expect(hasProgress(invullen({ draft: { correspondentie: 'post', antwoorden: {} } }))).toBe(
|
||||
true,
|
||||
const advanced = expectTag(
|
||||
wizard(
|
||||
{ tag: 'SetField', key: 'straat', value: 'Lange Voorhout 9' },
|
||||
{ tag: 'SetField', key: 'postcode', value: '2514 EA' },
|
||||
{ tag: 'SetField', key: 'woonplaats', value: 'Den Haag' },
|
||||
{ tag: 'SetCorrespondentie', value: 'post' },
|
||||
{ tag: 'Next' },
|
||||
),
|
||||
'Invullen',
|
||||
);
|
||||
expect(hasProgress(invullen({ draft: { diplomaId: 'd1', antwoorden: {} } }))).toBe(true);
|
||||
expect(hasProgress(advanced)).toBe(true);
|
||||
|
||||
const withCorrespondentie = expectTag(
|
||||
wizard({ tag: 'SetCorrespondentie', value: 'post' }),
|
||||
'Invullen',
|
||||
);
|
||||
expect(hasProgress(withCorrespondentie)).toBe(true);
|
||||
|
||||
const withDiploma = expectTag(
|
||||
wizard({ tag: 'KiesDiploma', diplomaId: 'd1', beroep: 'Arts', vraagIds: [] }),
|
||||
'Invullen',
|
||||
);
|
||||
expect(hasProgress(withDiploma)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { ok, err } from '@shared/kernel/fp';
|
||||
import { initialUpload } from '@shared/upload/upload.machine';
|
||||
import { expectTag } from '@shared/testing/expect-tag';
|
||||
import {
|
||||
Draft,
|
||||
RegistratieState,
|
||||
@@ -51,69 +52,75 @@ describe('STEPS (fixed)', () => {
|
||||
|
||||
describe('navigation', () => {
|
||||
it('Next is a no-op (sets errors) when the adres step is invalid', () => {
|
||||
const s = next(initial);
|
||||
const s = expectTag(next(initial), 'Invullen');
|
||||
expect(s.tag).toBe('Invullen');
|
||||
expect((s as any).cursor).toBe(0);
|
||||
expect((s as any).errors.straat).toBeTruthy();
|
||||
expect((s as any).errors.correspondentie).toBeTruthy();
|
||||
expect(s.cursor).toBe(0);
|
||||
expect(s.errors.straat).toBeTruthy();
|
||||
expect(s.errors.correspondentie).toBeTruthy();
|
||||
});
|
||||
|
||||
it('Next advances once the adres step is valid', () => {
|
||||
const s = next(invullen(validAdres));
|
||||
expect((s as any).cursor).toBe(1);
|
||||
expect(currentStep(s as any)).toBe('beroep');
|
||||
const s = expectTag(next(invullen(validAdres)), 'Invullen');
|
||||
expect(s.cursor).toBe(1);
|
||||
expect(currentStep(s)).toBe('beroep');
|
||||
});
|
||||
|
||||
it('requires a valid e-mail only when the channel is email', () => {
|
||||
const bad = next(invullen({ ...validAdres, correspondentie: 'email' }));
|
||||
expect((bad as any).errors.email).toBeTruthy();
|
||||
const good = next(invullen({ ...validAdres, correspondentie: 'email', email: 'a@b.nl' }));
|
||||
expect((good as any).cursor).toBe(1);
|
||||
const bad = expectTag(next(invullen({ ...validAdres, correspondentie: 'email' })), 'Invullen');
|
||||
expect(bad.errors.email).toBeTruthy();
|
||||
const good = expectTag(
|
||||
next(invullen({ ...validAdres, correspondentie: 'email', email: 'a@b.nl' })),
|
||||
'Invullen',
|
||||
);
|
||||
expect(good.cursor).toBe(1);
|
||||
});
|
||||
|
||||
it('beroep step requires a chosen diploma', () => {
|
||||
const noDiploma = next(invullen(validAdres, 1));
|
||||
expect((noDiploma as any).cursor).toBe(1);
|
||||
expect((noDiploma as any).errors.diploma).toBeTruthy();
|
||||
const withDiploma = next(invullen(validDraft, 1));
|
||||
expect((withDiploma as any).cursor).toBe(2);
|
||||
const noDiploma = expectTag(next(invullen(validAdres, 1)), 'Invullen');
|
||||
expect(noDiploma.cursor).toBe(1);
|
||||
expect(noDiploma.errors.diploma).toBeTruthy();
|
||||
const withDiploma = expectTag(next(invullen(validDraft, 1)), 'Invullen');
|
||||
expect(withDiploma.cursor).toBe(2);
|
||||
});
|
||||
|
||||
it('Back never goes below the first step and preserves the draft', () => {
|
||||
expect(back(initial)).toBe(initial);
|
||||
const s = back(invullen(validDraft, 2));
|
||||
expect((s as any).cursor).toBe(1);
|
||||
expect((s as any).draft.beroep).toBe('Arts');
|
||||
const s = expectTag(back(invullen(validDraft, 2)), 'Invullen');
|
||||
expect(s.cursor).toBe(1);
|
||||
expect(s.draft.beroep).toBe('Arts');
|
||||
});
|
||||
|
||||
it('GaNaarStap only jumps backwards', () => {
|
||||
expect((gaNaarStap(invullen(validDraft, 2), 0) as any).cursor).toBe(0);
|
||||
expect((gaNaarStap(invullen(validDraft, 1), 2) as any).cursor).toBe(1); // forward jump rejected
|
||||
expect(expectTag(gaNaarStap(invullen(validDraft, 2), 0), 'Invullen').cursor).toBe(0);
|
||||
expect(expectTag(gaNaarStap(invullen(validDraft, 1), 2), 'Invullen').cursor).toBe(1); // forward jump rejected
|
||||
});
|
||||
});
|
||||
|
||||
describe('adres origin (BRP vs handmatig)', () => {
|
||||
it('prefillAdres flags origin brp', () => {
|
||||
const s = prefillAdres(invullen({}), 'Lange Voorhout 9', '2514 EA', 'Den Haag');
|
||||
expect((s as any).draft.adresHerkomst).toBe('brp');
|
||||
expect((s as any).draft.straat).toBe('Lange Voorhout 9');
|
||||
const s = expectTag(
|
||||
prefillAdres(invullen({}), 'Lange Voorhout 9', '2514 EA', 'Den Haag'),
|
||||
'Invullen',
|
||||
);
|
||||
expect(s.draft.adresHerkomst).toBe('brp');
|
||||
expect(s.draft.straat).toBe('Lange Voorhout 9');
|
||||
});
|
||||
|
||||
it('editing a prefilled address field flips origin to handmatig', () => {
|
||||
const prefilled = prefillAdres(invullen({}), 'Lange Voorhout 9', '2514 EA', 'Den Haag');
|
||||
const edited = setField(prefilled, 'woonplaats', 'Rotterdam');
|
||||
expect((edited as any).draft.adresHerkomst).toBe('handmatig');
|
||||
const edited = expectTag(setField(prefilled, 'woonplaats', 'Rotterdam'), 'Invullen');
|
||||
expect(edited.draft.adresHerkomst).toBe('handmatig');
|
||||
});
|
||||
|
||||
it('typing an address with no BRP prefill yields handmatig', () => {
|
||||
const s = setField(invullen({}), 'straat', 'Kerkstraat 1');
|
||||
expect((s as any).draft.adresHerkomst).toBe('handmatig');
|
||||
const s = expectTag(setField(invullen({}), 'straat', 'Kerkstraat 1'), 'Invullen');
|
||||
expect(s.draft.adresHerkomst).toBe('handmatig');
|
||||
});
|
||||
|
||||
it('editing the e-mail field does not change the address origin', () => {
|
||||
const prefilled = prefillAdres(invullen({}), 'Lange Voorhout 9', '2514 EA', 'Den Haag');
|
||||
const edited = setField(prefilled, 'email', 'a@b.nl');
|
||||
expect((edited as any).draft.adresHerkomst).toBe('brp');
|
||||
const edited = expectTag(setField(prefilled, 'email', 'a@b.nl'), 'Invullen');
|
||||
expect(edited.draft.adresHerkomst).toBe('brp');
|
||||
});
|
||||
|
||||
it('a manually entered address still submits (only manual diploma is gated)', () => {
|
||||
@@ -129,37 +136,36 @@ describe('adres origin (BRP vs handmatig)', () => {
|
||||
diplomaHerkomst: 'duo',
|
||||
}),
|
||||
);
|
||||
expect(s.tag).toBe('Indienen');
|
||||
expect((s as any).data.adresHerkomst).toBe('handmatig');
|
||||
const indienen = expectTag(s, 'Indienen');
|
||||
expect(indienen.data.adresHerkomst).toBe('handmatig');
|
||||
});
|
||||
});
|
||||
|
||||
describe('kiesDiploma', () => {
|
||||
it('derives the beroep from the chosen diploma and flags origin duo', () => {
|
||||
const s = kiesDiploma(invullen({}), 'd9', 'Verpleegkundige', []);
|
||||
expect((s as any).draft.diplomaId).toBe('d9');
|
||||
expect((s as any).draft.beroep).toBe('Verpleegkundige');
|
||||
expect((s as any).draft.diplomaHerkomst).toBe('duo');
|
||||
const s = expectTag(kiesDiploma(invullen({}), 'd9', 'Verpleegkundige', []), 'Invullen');
|
||||
expect(s.draft.diplomaId).toBe('d9');
|
||||
expect(s.draft.beroep).toBe('Verpleegkundige');
|
||||
expect(s.draft.diplomaHerkomst).toBe('duo');
|
||||
});
|
||||
});
|
||||
|
||||
describe('policy questions (geldigheidsvragen)', () => {
|
||||
it('a diploma with questions blocks Next until they are answered', () => {
|
||||
let s = kiesDiploma(invullen(validAdres, 1), 'd2', 'Arts', ['nl-taalvaardigheid']);
|
||||
const blocked = next(s);
|
||||
expect((blocked as any).cursor).toBe(1);
|
||||
expect((blocked as any).errors.antwoorden['nl-taalvaardigheid']).toBeTruthy();
|
||||
const blocked = expectTag(next(s), 'Invullen');
|
||||
expect(blocked.cursor).toBe(1);
|
||||
expect(blocked.errors.antwoorden?.['nl-taalvaardigheid']).toBeTruthy();
|
||||
s = setAntwoord(s, 'nl-taalvaardigheid', 'ja');
|
||||
expect((next(s) as any).cursor).toBe(2);
|
||||
expect(expectTag(next(s), 'Invullen').cursor).toBe(2);
|
||||
});
|
||||
|
||||
it('validateAll keeps only the answers to the questions that applied', () => {
|
||||
let s = kiesDiploma(invullen(validAdres, 2), 'd2', 'Arts', ['nl-taalvaardigheid']);
|
||||
s = setAntwoord(s, 'nl-taalvaardigheid', 'ja');
|
||||
s = setAntwoord(s, 'stale', 'x'); // not in vraagIds
|
||||
const done = submit(s);
|
||||
expect(done.tag).toBe('Indienen');
|
||||
expect((done as any).data.antwoorden).toEqual({ 'nl-taalvaardigheid': 'ja' });
|
||||
const done = expectTag(submit(s), 'Indienen');
|
||||
expect(done.data.antwoorden).toEqual({ 'nl-taalvaardigheid': 'ja' });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -167,10 +173,10 @@ describe('manual diploma fallback', () => {
|
||||
const maxIds = ['nl-taalvaardigheid', 'diploma-erkend', 'toelichting'];
|
||||
|
||||
it('KiesHandmatig flags handmatig with the maximal question set and no beroep yet', () => {
|
||||
const s = kiesHandmatig(invullen(validAdres, 1), maxIds);
|
||||
expect((s as any).draft.diplomaHerkomst).toBe('handmatig');
|
||||
expect((s as any).draft.beroep).toBeUndefined();
|
||||
expect((s as any).draft.vraagIds).toEqual(maxIds);
|
||||
const s = expectTag(kiesHandmatig(invullen(validAdres, 1), maxIds), 'Invullen');
|
||||
expect(s.draft.diplomaHerkomst).toBe('handmatig');
|
||||
expect(s.draft.beroep).toBeUndefined();
|
||||
expect(s.draft.vraagIds).toEqual(maxIds);
|
||||
});
|
||||
|
||||
it('requires a declared beroep + all maximal questions before submit', () => {
|
||||
@@ -179,10 +185,9 @@ describe('manual diploma fallback', () => {
|
||||
s = declareerBeroep(s, 'Fysiotherapeut');
|
||||
expect(submit(s).tag).toBe('Invullen'); // questions unanswered
|
||||
for (const id of maxIds) s = setAntwoord(s, id, 'ja');
|
||||
const done = submit(s);
|
||||
expect(done.tag).toBe('Indienen');
|
||||
expect((done as any).data.diplomaHerkomst).toBe('handmatig');
|
||||
expect((done as any).data.beroep).toBe('Fysiotherapeut');
|
||||
const done = expectTag(submit(s), 'Indienen');
|
||||
expect(done.data.diplomaHerkomst).toBe('handmatig');
|
||||
expect(done.data.beroep).toBe('Fysiotherapeut');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -192,17 +197,18 @@ describe('submit', () => {
|
||||
});
|
||||
|
||||
it('reaches Indienen with a complete, valid draft, carrying its data', () => {
|
||||
const good = submit(invullen(validDraft));
|
||||
expect(good.tag).toBe('Indienen');
|
||||
expect((good as any).data.beroep).toBe('Arts');
|
||||
expect((good as any).data.adres.postcode).toBe('2514 EA');
|
||||
expect((good as any).data.adresHerkomst).toBe('brp');
|
||||
const good = expectTag(submit(invullen(validDraft)), 'Indienen');
|
||||
expect(good.data.beroep).toBe('Arts');
|
||||
expect(good.data.adres.postcode).toBe('2514 EA');
|
||||
expect(good.data.adresHerkomst).toBe('brp');
|
||||
});
|
||||
|
||||
it('resolve maps Indienen to Ingediend with the referentie', () => {
|
||||
const ingediend = resolve(submit(invullen(validDraft)), ok('BIG-2026-001'));
|
||||
expect(ingediend.tag).toBe('Ingediend');
|
||||
expect((ingediend as any).referentie).toBe('BIG-2026-001');
|
||||
const ingediend = expectTag(
|
||||
resolve(submit(invullen(validDraft)), ok('BIG-2026-001')),
|
||||
'Ingediend',
|
||||
);
|
||||
expect(ingediend.referentie).toBe('BIG-2026-001');
|
||||
});
|
||||
|
||||
it('resolve maps Indienen to Mislukt on a failed submit', () => {
|
||||
@@ -211,7 +217,10 @@ describe('submit', () => {
|
||||
});
|
||||
|
||||
describe('reduce (message-driven happy path)', () => {
|
||||
it('drives the full flow via messages', () => {
|
||||
// Each helper replays real messages through the real reducer up to the named
|
||||
// point — no hand-assembled state literal — so each test below Givens its own
|
||||
// starting point independently, one transition at a time.
|
||||
const toBeroepStep = (): RegistratieState => {
|
||||
let s: RegistratieState = initial;
|
||||
s = reduce(s, {
|
||||
tag: 'PrefillAdres',
|
||||
@@ -220,14 +229,52 @@ describe('reduce (message-driven happy path)', () => {
|
||||
woonplaats: 'Den Haag',
|
||||
});
|
||||
s = reduce(s, { tag: 'SetCorrespondentie', value: 'post' });
|
||||
s = reduce(s, { tag: 'Next' });
|
||||
expect(currentStep(s as any)).toBe('beroep');
|
||||
s = reduce(s, { tag: 'KiesDiploma', diplomaId: 'd1', beroep: 'Arts', vraagIds: [] });
|
||||
s = reduce(s, { tag: 'Next' });
|
||||
expect(currentStep(s as any)).toBe('controle');
|
||||
s = reduce(s, { tag: 'Submit' });
|
||||
return reduce(s, { tag: 'Next' });
|
||||
};
|
||||
const toControleStep = (): RegistratieState => {
|
||||
const s = reduce(toBeroepStep(), {
|
||||
tag: 'KiesDiploma',
|
||||
diplomaId: 'd1',
|
||||
beroep: 'Arts',
|
||||
vraagIds: [],
|
||||
});
|
||||
return reduce(s, { tag: 'Next' });
|
||||
};
|
||||
const toIndienen = (): RegistratieState => reduce(toControleStep(), { tag: 'Submit' });
|
||||
|
||||
it('adres and correspondentie set, Next advances from adres to beroep', () => {
|
||||
// Given the initial wizard.
|
||||
// When the adres is prefilled, correspondentie chosen, and Next dispatched...
|
||||
const s = toBeroepStep();
|
||||
|
||||
// Then the wizard advances to the beroep step.
|
||||
expect(currentStep(expectTag(s, 'Invullen'))).toBe('beroep');
|
||||
});
|
||||
|
||||
it('diploma chosen, Next advances from beroep to controle', () => {
|
||||
// Given a wizard on the beroep step.
|
||||
// When a diploma is chosen and Next dispatched...
|
||||
const s = toControleStep();
|
||||
|
||||
// Then the wizard advances to the controle step.
|
||||
expect(currentStep(expectTag(s, 'Invullen'))).toBe('controle');
|
||||
});
|
||||
|
||||
it('Submit moves a complete Invullen draft to Indienen', () => {
|
||||
// Given a wizard on the controle step with a complete, valid draft.
|
||||
// When Submit is dispatched...
|
||||
const s = toIndienen();
|
||||
|
||||
// Then the wizard moves to Indienen.
|
||||
expect(s.tag).toBe('Indienen');
|
||||
s = reduce(s, { tag: 'SubmitConfirmed', referentie: 'BIG-2026-001' });
|
||||
});
|
||||
|
||||
it('SubmitConfirmed moves Indienen to Ingediend', () => {
|
||||
// Given a wizard mid-submit (Indienen).
|
||||
// When SubmitConfirmed arrives with a referentie...
|
||||
const s = reduce(toIndienen(), { tag: 'SubmitConfirmed', referentie: 'BIG-2026-001' });
|
||||
|
||||
// Then the wizard reaches Ingediend.
|
||||
expect(s.tag).toBe('Ingediend');
|
||||
});
|
||||
|
||||
@@ -244,9 +291,8 @@ describe('reduce (message-driven happy path)', () => {
|
||||
tag: 'SubmitFailed',
|
||||
error: 'boom',
|
||||
});
|
||||
const s = reduce(mislukt, { tag: 'Retry' });
|
||||
expect(s.tag).toBe('Indienen');
|
||||
expect((s as any).data.beroep).toBe('Arts');
|
||||
const s = expectTag(reduce(mislukt, { tag: 'Retry' }), 'Indienen');
|
||||
expect(s.data.beroep).toBe('Arts');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -263,11 +309,14 @@ describe('inline document upload (beroep step)', () => {
|
||||
};
|
||||
|
||||
it('routes Upload messages through the upload reducer', () => {
|
||||
const s = reduce(invullen(validDraft), {
|
||||
tag: 'Upload',
|
||||
msg: { type: 'CategoriesLoaded', categories: [cat] },
|
||||
});
|
||||
expect((s as any).upload.categories).toHaveLength(1);
|
||||
const s = expectTag(
|
||||
reduce(invullen(validDraft), {
|
||||
tag: 'Upload',
|
||||
msg: { type: 'CategoriesLoaded', categories: [cat] },
|
||||
}),
|
||||
'Invullen',
|
||||
);
|
||||
expect(s.upload.categories).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('blocks the beroep step until a required category is satisfied', () => {
|
||||
@@ -276,15 +325,17 @@ describe('inline document upload (beroep step)', () => {
|
||||
msg: { type: 'CategoriesLoaded', categories: [cat] },
|
||||
});
|
||||
s = reduce(s, { tag: 'Next' }); // beroep → controle blocked
|
||||
expect(currentStep(s as any)).toBe('beroep');
|
||||
expect((s as any).errors.documenten).toBeTruthy();
|
||||
let invullenState = expectTag(s, 'Invullen');
|
||||
expect(currentStep(invullenState)).toBe('beroep');
|
||||
expect(invullenState.errors.documenten).toBeTruthy();
|
||||
// choosing post delivery satisfies the requirement
|
||||
s = reduce(s, {
|
||||
tag: 'Upload',
|
||||
msg: { type: 'DeliveryChannelChanged', categoryId: 'diploma', channel: 'post' },
|
||||
});
|
||||
s = reduce(s, { tag: 'Next' });
|
||||
expect(currentStep(s as any)).toBe('controle');
|
||||
invullenState = expectTag(s, 'Invullen');
|
||||
expect(currentStep(invullenState)).toBe('controle');
|
||||
});
|
||||
|
||||
it('includes delivery refs in the submitted data', () => {
|
||||
@@ -296,8 +347,7 @@ describe('inline document upload (beroep step)', () => {
|
||||
tag: 'Upload',
|
||||
msg: { type: 'DeliveryChannelChanged', categoryId: 'diploma', channel: 'post' },
|
||||
});
|
||||
const done = submit(s as any);
|
||||
expect(done.tag).toBe('Indienen');
|
||||
expect((done as any).data.documents).toEqual([{ categoryId: 'diploma', channel: 'post' }]);
|
||||
const done = expectTag(submit(s), 'Indienen');
|
||||
expect(done.data.documents).toEqual([{ categoryId: 'diploma', channel: 'post' }]);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user