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/domain/upload.machine'; const template: OrgTemplate = { subOrgId: 'cibg-registers', orgName: 'CIBG', returnAddress: 'Postbus 1\n2500 AA Den Haag', footerContact: 'info@cibg.nl', footerLegal: 'CIBG is onderdeel van VWS', signatureName: 'A. de Vries', signatureRole: 'Hoofd Registratie', signatureClosing: 'Met vriendelijke groet,', margins: { topMm: 25, rightMm: 20, bottomMm: 25, leftMm: 20 }, version: 3, }; const view = (over: Partial = {}): OrgTemplateAdminView => ({ draft: template, publishedVersion: 3, history: [], unsentBriefs: 2, ...over, }); const loaded = (): OrgTemplateState => reduce({ tag: 'Loading' }, { tag: 'DraftLoaded', view: view() }); const logoCategory: DocumentCategory = { categoryId: 'org-logo', label: 'Logo', description: '', required: false, acceptedTypes: ['image/png'], maxSizeMb: 2, multiple: false, allowPostDelivery: false, }; describe('org-template.machine', () => { it('DraftLoaded moves to loaded with the draft, clean', () => { const s = expectTag(loaded(), 'Loaded'); expect(s.draft.orgName).toBe('CIBG'); expect(s.subOrgId).toBe('cibg-registers'); expect(s.unsentBriefs).toBe(2); expect(s.dirty).toBe(false); }); it('LoadFailed carries the reason', () => { const s = reduce({ tag: 'Loading' }, { tag: 'LoadFailed', reason: 'boom' }); expect(s).toEqual({ tag: 'Failed', reason: 'boom' }); }); it('FieldEdited edits the draft and marks dirty', () => { 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 = 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 = 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 = 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 = expectTag(reduce(raced, { tag: 'DraftSaved', savedDraft }), 'Loaded'); expect(s.dirty).toBe(true); }); it('edits are no-ops in non-loaded states', () => { expect( reduce({ tag: 'Loading' }, { tag: 'FieldEdited', field: 'orgName', value: 'x' }), ).toEqual({ tag: 'Loading', }); }); it('a completed logo upload sets logoDocumentId + dirty', () => { const withCat = reduce(loaded(), { tag: 'Upload', msg: { type: 'CategoriesLoaded', categories: [logoCategory] }, }); const selected = reduce(withCat, { tag: 'Upload', msg: { type: 'FileSelected', categoryId: 'org-logo', localId: 'a', fileName: 'l.png', fileSizeMb: 0.1, }, }); 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', () => { const withLogo = reduce(loaded(), { tag: 'Upload', msg: { type: 'UploadComplete', localId: 'a', documentId: 'doc-1' }, }); 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', () => { const withCat = reduce(loaded(), { tag: 'Upload', msg: { type: 'CategoriesLoaded', categories: [logoCategory] }, }); 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'); }); // --- the action lifecycle + publish impact-confirm gate, folded into one union (RD-13) --- it('PublishRequested moves a loaded template to ConfirmingPublish', () => { const s = expectTag(reduce(loaded(), { tag: 'PublishRequested' }), 'Loaded'); expect(s.action).toEqual({ tag: 'ConfirmingPublish' }); }); it('PublishCancelled returns to Idle', () => { const confirming = reduce(loaded(), { tag: 'PublishRequested' }); const s = expectTag(reduce(confirming, { tag: 'PublishCancelled' }), 'Loaded'); expect(s.action).toEqual({ tag: 'Idle' }); }); it('ActionStarted from ConfirmingPublish goes to Busy, so confirming and busy cannot coexist', () => { const confirming = expectTag(reduce(loaded(), { tag: 'PublishRequested' }), 'Loaded'); expect(confirming.action.tag).toBe('ConfirmingPublish'); const s = expectTag(reduce(confirming, { tag: 'ActionStarted' }), 'Loaded'); expect(s.action).toEqual({ tag: 'Busy' }); }); it('ActionFailed carries the error', () => { const busy = reduce(loaded(), { tag: 'ActionStarted' }); const s = expectTag(reduce(busy, { tag: 'ActionFailed', error: 'mislukt' }), 'Loaded'); expect(s.action).toEqual({ tag: 'Failed', error: 'mislukt' }); }); it('DraftLoaded resets a stale action error to Idle', () => { const failed = reduce(loaded(), { tag: 'ActionFailed', error: 'mislukt' }); const s = expectTag(reduce(failed, { tag: 'DraftLoaded', view: view() }), 'Loaded'); expect(s.action).toEqual({ tag: 'Idle' }); }); it('an action message is a no-op when the template is not loaded', () => { expect(reduce({ tag: 'Loading' }, { tag: 'PublishRequested' })).toEqual({ tag: 'Loading' }); expect(reduce({ tag: 'Loading' }, { tag: 'ActionStarted' })).toEqual({ tag: 'Loading' }); expect(reduce({ tag: 'Loading' }, { tag: 'ActionFailed', error: 'x' })).toEqual({ tag: 'Loading', }); }); });