import { describe, it, expect } from 'vitest'; import { given } from '@shared/testing/machine'; import { reduce, IntakeState } from './intake.machine'; import { givenIntake } from './intake.testing'; /** * Behaviour-level journeys: each test replays the exact `IntakeMsg` sequence a * real user (or the server, for `SetPolicy`) would send, and asserts only on * the reachable end state — never a hand-assembled `IntakeState` literal. No * `$localize` copy is asserted, only tags/fields/error PRESENCE. */ describe('intake acceptance journeys', () => { it('high uren, no buitenland werk: no scholing question, straight through to Submitted', () => { const s = givenIntake( { tag: 'SetAnswer', key: 'buitenlandGewerkt', value: 'nee' }, { tag: 'Next' }, { tag: 'SetAnswer', key: 'uren', value: '1200' }, { tag: 'Next' }, { tag: 'Submit' }, { tag: 'SubmitConfirmed' }, ); expect(s.tag).toBe('Submitted'); expect(s.tag === 'Submitted' && s.data).toEqual({ werktBuitenland: false, land: undefined, buitenlandseUren: undefined, uren: 1200, aanvullendeScholing: undefined, punten: undefined, }); }); it('low uren requires the scholing question, and punten only once scholing is followed', () => { // Leaving the werk step without answering the scholing question is blocked. const blocked = givenIntake( { tag: 'SetAnswer', key: 'buitenlandGewerkt', value: 'nee' }, { tag: 'Next' }, { tag: 'SetAnswer', key: 'uren', value: '500' }, { tag: 'Next' }, ); expect(blocked.tag).toBe('Answering'); expect(blocked.tag === 'Answering' && blocked.cursor).toBe(1); // still on 'werk' expect(blocked.tag === 'Answering' && blocked.errors.scholingGevolgd).toBeTruthy(); // Answering scholing but not yet punten is still blocked. const stillBlocked = given(reduce, blocked)( { tag: 'SetAnswer', key: 'scholingGevolgd', value: 'ja' }, { tag: 'Next' }, ); expect(stillBlocked.tag).toBe('Answering'); expect(stillBlocked.tag === 'Answering' && stillBlocked.cursor).toBe(1); expect(stillBlocked.tag === 'Answering' && stillBlocked.errors.punten).toBeTruthy(); // Supplying punten unblocks: reach review, submit, fail, retry, succeed. const submitting = given(reduce, stillBlocked)( { tag: 'SetAnswer', key: 'punten', value: '150' }, { tag: 'Next' }, { tag: 'Submit' }, ); expect(submitting.tag).toBe('Submitting'); const failed = reduce(submitting, { tag: 'SubmitFailed', error: 'netwerkfout' }); expect(failed.tag).toBe('Failed'); const retried = reduce(failed, { tag: 'Retry' }); expect(retried.tag).toBe('Submitting'); const done = reduce(retried, { tag: 'SubmitConfirmed' }); expect(done.tag).toBe('Submitted'); expect(done.tag === 'Submitted' && done.data).toEqual({ werktBuitenland: false, land: undefined, buitenlandseUren: undefined, uren: 500, aanvullendeScholing: true, punten: 150, }); }); it('buitenland gewerkt requires land and hours abroad before advancing', () => { // Given a user who says they worked abroad. // When they try to advance without a country... const blocked = givenIntake( { tag: 'SetAnswer', key: 'buitenlandGewerkt', value: 'ja' }, { tag: 'Next' }, ); // Then they stay on the same step, blocked by a missing 'land' answer. expect(blocked.tag).toBe('Answering'); expect(blocked.tag === 'Answering' && blocked.cursor).toBe(0); expect(blocked.tag === 'Answering' && blocked.errors.land).toBeTruthy(); // When land and hours abroad are supplied, and the rest of the journey answered... const reviewing = given(reduce, blocked)( { tag: 'SetAnswer', key: 'land', value: 'Duitsland' }, { tag: 'SetAnswer', key: 'buitenlandseUren', value: '300' }, { tag: 'Next' }, // buitenland step now valid -> werk { tag: 'SetAnswer', key: 'uren', value: '1200' }, { tag: 'Next' }, // werk step valid, uren high enough to skip scholing -> review ); // Then the journey advances all the way to review. expect(reviewing.tag).toBe('Answering'); expect(reviewing.tag === 'Answering' && reviewing.cursor).toBe(2); // review }); it('gaNaarStap corrects an earlier answer without losing later ones', () => { // Given a journey that reached review with a foreign-work answer. const reviewing = givenIntake( { tag: 'SetAnswer', key: 'buitenlandGewerkt', value: 'ja' }, { tag: 'Next' }, { tag: 'SetAnswer', key: 'land', value: 'Duitsland' }, { tag: 'SetAnswer', key: 'buitenlandseUren', value: '300' }, { tag: 'Next' }, { tag: 'SetAnswer', key: 'uren', value: '1200' }, { tag: 'Next' }, ); // When gaNaarStap jumps back to correct the country... const corrected: IntakeState = given(reduce, reviewing)( { tag: 'GaNaarStap', cursor: 0 }, { tag: 'SetAnswer', key: 'land', value: 'België' }, { tag: 'Next' }, // buitenland step re-validated { tag: 'Next' }, // werk step re-validated (earlier 'uren' answer preserved) ); // Then the correction lands back on review with the new answer, and the later // 'uren' answer is preserved rather than lost. expect(corrected.tag).toBe('Answering'); expect(corrected.tag === 'Answering' && corrected.cursor).toBe(2); expect(corrected.tag === 'Answering' && corrected.answers.land).toBe('België'); // A forward jump (would skip validation) is refused — a true no-op. const noForwardJump = reduce(corrected, { tag: 'GaNaarStap', cursor: 2 }); expect(noForwardJump).toBe(corrected); // And the corrected journey still submits successfully with the corrected data. const done = given(reduce, corrected)({ tag: 'Submit' }, { tag: 'SubmitConfirmed' }); expect(done.tag).toBe('Submitted'); expect(done.tag === 'Submitted' && done.data).toEqual({ werktBuitenland: true, land: 'België', buitenlandseUren: 300, uren: 1200, aanvullendeScholing: undefined, punten: undefined, }); }); it('SetPolicy (server-owned threshold) can turn an already-answered uren into one that now requires scholing', () => { const atWerkStep = givenIntake( { tag: 'SetAnswer', key: 'buitenlandGewerkt', value: 'nee' }, { tag: 'Next' }, { tag: 'SetAnswer', key: 'uren', value: '1200' }, ); // With the default threshold (1000), 1200 hours needs no scholing question. const acceptedWithDefault = reduce(atWerkStep, { tag: 'Next' }); expect(acceptedWithDefault.tag).toBe('Answering'); expect(acceptedWithDefault.tag === 'Answering' && acceptedWithDefault.cursor).toBe(2); // The server raises the threshold above 1200 -> the same answer now requires it. const raised = reduce(atWerkStep, { tag: 'SetPolicy', scholingThreshold: 1500 }); const blockedByNewPolicy = reduce(raised, { tag: 'Next' }); expect(blockedByNewPolicy.tag).toBe('Answering'); expect(blockedByNewPolicy.tag === 'Answering' && blockedByNewPolicy.cursor).toBe(1); expect( blockedByNewPolicy.tag === 'Answering' && blockedByNewPolicy.errors.scholingGevolgd, ).toBeTruthy(); }); });