import { describe, it, expect } from 'vitest' import { applyDirectivesToPagespec } from '../execute.js' import { validate } from '../validate.js' const PAGESPEC = `# Employee.form — Fiche employé > Screen : SCR-RH-EMPLOYES-LISTE-002 \`\`\`json { "screenCode": "SCR-RH-EMPLOYES-LISTE-002", "entity": "Employee", "view": "form", "fields": [ { "key": "userId", "control": "select-lookup", "required": true }, { "key": "managerId", "control": "select-lookup", "required": false }, { "key": "birthDate", "control": "date-picker", "required": false } ], "needsRefinement": false } \`\`\` ## Description La fiche employé. ` describe('apply-form-directives / applyDirectivesToPagespec', () => { it('writes a uiDesign overlay while preserving every existing machine-block key', () => { const { md, overlay } = applyDirectivesToPagespec(PAGESPEC, { pagespecPath: 'x.md', formLayout: 'two-column', fields: [ { key: 'userId', currentUserFk: true, section: 'identity' }, { key: 'managerId', section: 'organisation' }, { key: 'birthDate', control: 'date-popover', section: 'identity' }, ], order: ['userId', 'birthDate', 'managerId'], }) // Surrounding markdown preserved. expect(md).toMatch(/# Employee\.form/) expect(md).toMatch(/## Description/) // Existing keys untouched. const block = JSON.parse(/```json\s*\n([\s\S]*?)\n```/.exec(md)![1]!) expect(block.screenCode).toBe('SCR-RH-EMPLOYES-LISTE-002') expect(block.fields).toHaveLength(3) expect(block.needsRefinement).toBe(false) // Overlay present and correct. expect(block.uiDesign).toEqual(overlay) expect(overlay.formLayout).toBe('two-column') expect(overlay.fields!.userId).toEqual({ currentUserFk: true, section: 'identity' }) expect(overlay.fields!.managerId).toEqual({ section: 'organisation' }) expect(overlay.fields!.birthDate).toEqual({ control: 'date-popover', section: 'identity' }) expect(overlay.order).toEqual(['userId', 'birthDate', 'managerId']) }) it('merges additively over an existing overlay (idempotent + incremental)', () => { const first = applyDirectivesToPagespec(PAGESPEC, { pagespecPath: 'x.md', fields: [{ key: 'userId', currentUserFk: true }], }).md // A second pass adds a section to userId and a new field without losing currentUserFk. const { overlay } = applyDirectivesToPagespec(first, { pagespecPath: 'x.md', fields: [{ key: 'userId', section: 'identity' }, { key: 'birthDate', control: 'date-popover' }], }) expect(overlay.fields!.userId).toEqual({ currentUserFk: true, section: 'identity' }) expect(overlay.fields!.birthDate).toEqual({ control: 'date-popover' }) }) it('persists a dateBounds judgment, merging over a prior control/section idempotently', () => { // Pass 1: presentation + section. Pass 2: the temporal nature, on the same field. const first = applyDirectivesToPagespec(PAGESPEC, { pagespecPath: 'x.md', fields: [{ key: 'birthDate', control: 'date-popover', section: 'identity' }], }).md const { overlay } = applyDirectivesToPagespec(first, { pagespecPath: 'x.md', fields: [ { key: 'birthDate', dateBounds: 'past' }, { key: 'contractEndDate', dateBounds: 'future' }, ], }) // dateBounds lands without dropping the earlier control/section. expect(overlay.fields!.birthDate).toEqual({ control: 'date-popover', section: 'identity', dateBounds: 'past' }) expect(overlay.fields!.contractEndDate).toEqual({ dateBounds: 'future' }) }) it('persists a fullWidth judgment, merging over prior directives idempotently', () => { const first = applyDirectivesToPagespec(PAGESPEC, { pagespecPath: 'x.md', fields: [{ key: 'userId', currentUserFk: true, section: 'identity' }], }).md const second = applyDirectivesToPagespec(first, { pagespecPath: 'x.md', fields: [ { key: 'userId', fullWidth: true }, { key: 'managerId', fullWidth: false }, ], }) expect(second.overlay.fields!.userId).toEqual({ currentUserFk: true, section: 'identity', fullWidth: true }) expect(second.overlay.fields!.managerId).toEqual({ fullWidth: false }) // Re-applying the same directives changes nothing (idempotent). const third = applyDirectivesToPagespec(second.md, { pagespecPath: 'x.md', fields: [{ key: 'userId', fullWidth: true }], }) expect(third.overlay).toEqual(second.overlay) }) it('throws when the pagespec has no ```json machine block', () => { expect(() => applyDirectivesToPagespec('# no block here', { pagespecPath: 'x.md', fields: [] })) .toThrow(/no ```json machine block/) }) }) describe('apply-form-directives / sections[] sugar', () => { it('normalises sections[] into per-field membership + ordered metadata (no fields in meta)', () => { const { overlay, warnings } = applyDirectivesToPagespec(PAGESPEC, { pagespecPath: 'x.md', fields: [], sections: [ { key: 'identity', label: 'Identité', columns: 2, fields: ['userId', 'birthDate'] }, { key: 'organisation', fields: ['managerId'] }, ], }) expect(overlay.fields!.userId).toEqual({ section: 'identity' }) expect(overlay.fields!.birthDate).toEqual({ section: 'identity' }) expect(overlay.fields!.managerId).toEqual({ section: 'organisation' }) expect(overlay.sections).toEqual([ { key: 'identity', label: 'Identité', columns: 2 }, { key: 'organisation' }, ]) expect(warnings).toEqual([]) }) it('last call owns the order for its keys; unmentioned previous keys append after', () => { const first = applyDirectivesToPagespec(PAGESPEC, { pagespecPath: 'x.md', fields: [], sections: [ { key: 'identity', fields: ['userId'] }, { key: 'organisation', fields: ['managerId'] }, ], }).md const { overlay } = applyDirectivesToPagespec(first, { pagespecPath: 'x.md', fields: [], sections: [{ key: 'organisation', label: 'Organisation', fields: ['managerId', 'birthDate'] }], }) expect(overlay.sections!.map(s => s.key)).toEqual(['organisation', 'identity']) expect(overlay.sections![0]).toEqual({ key: 'organisation', label: 'Organisation' }) // membership re-pointed: birthDate moved into organisation expect(overlay.fields!.birthDate).toEqual({ section: 'organisation' }) expect(overlay.fields!.userId).toEqual({ section: 'identity' }) }) it('warns on a section field key absent from the pagespec fields[]', () => { const { warnings } = applyDirectivesToPagespec(PAGESPEC, { pagespecPath: 'x.md', fields: [], sections: [{ key: 'identity', fields: ['userId', 'ghostField'] }], }) expect(warnings.join(' ')).toMatch(/ghostField.*not declared/) }) it('persists editMode and merges it over an existing overlay', () => { const first = applyDirectivesToPagespec(PAGESPEC, { pagespecPath: 'x.md', fields: [{ key: 'userId', currentUserFk: true }], editMode: 'direct', }) expect(first.overlay.editMode).toBe('direct') const second = applyDirectivesToPagespec(first.md, { pagespecPath: 'x.md', fields: [], editMode: 'read-first', }) expect(second.overlay.editMode).toBe('read-first') expect(second.overlay.fields!.userId).toEqual({ currentUserFk: true }) }) }) describe('apply-form-directives / validate', () => { it('rejects a spec missing pagespecPath', () => { const r = validate({ fields: [] }) expect(r.valid).toBe(false) expect(r.errors.join(' ')).toMatch(/pagespecPath/) }) it('warns (but stays valid) when more than one currentUserFk is set', () => { const r = validate({ pagespecPath: 'x.md', fields: [ { key: 'a', currentUserFk: true }, { key: 'b', currentUserFk: true }, ] }) expect(r.valid).toBe(true) expect(r.warnings.join(' ')).toMatch(/exactly one signed-in-user FK/) }) it('rejects mixing sections[] with per-field section directives in one call', () => { const r = validate({ pagespecPath: 'x.md', fields: [{ key: 'a', section: 'x' }], sections: [{ key: 'identity', fields: ['a', 'b'] }], }) expect(r.valid).toBe(false) expect(r.errors.join(' ')).toMatch(/Do not mix sections\[\]/) }) it('rejects duplicate section keys and a field claimed by two sections', () => { const r = validate({ pagespecPath: 'x.md', fields: [], sections: [ { key: 'identity', fields: ['a', 'b'] }, { key: 'identity', fields: ['c', 'd'] }, { key: 'contract', fields: ['a', 'e'] }, ] }) expect(r.valid).toBe(false) expect(r.errors.join(' ')).toMatch(/Duplicate section keys: identity/) expect(r.errors.join(' ')).toMatch(/Field "a" belongs to two sections/) }) it('warns on a one-field section and on more than 4 sections', () => { const r = validate({ pagespecPath: 'x.md', fields: [], sections: [ { key: 's1', fields: ['a'] }, { key: 's2', fields: ['b', 'c'] }, { key: 's3', fields: ['d', 'e'] }, { key: 's4', fields: ['f', 'g'] }, { key: 's5', fields: ['h', 'i'] }, ] }) expect(r.valid).toBe(true) expect(r.warnings.join(' ')).toMatch(/single field/) expect(r.warnings.join(' ')).toMatch(/caps a form at 2-4/) }) it('rejects an invalid editMode', () => { const r = validate({ pagespecPath: 'x.md', fields: [], editMode: 'weird' }) expect(r.valid).toBe(false) }) }) describe('apply-form-directives / list directives (uiDesign.list — plan UI 2.5)', () => { const LIST_PAGESPEC = PAGESPEC.replace('"view": "form"', '"view": "list"') it('writes uiDesign.list and merges key-by-key over an existing block', () => { const first = applyDirectivesToPagespec(LIST_PAGESPEC, { pagespecPath: 'x.md', fields: [], list: { density: 'compact', viewMode: 'cards' }, }) expect(first.overlay.list).toEqual({ density: 'compact', viewMode: 'cards' }) const { overlay, warnings } = applyDirectivesToPagespec(first.md, { pagespecPath: 'x.md', fields: [], list: { statsOrder: ['open', 'total'], cardFields: { titleKey: 'name' } }, }) expect(overlay.list).toEqual({ density: 'compact', viewMode: 'cards', statsOrder: ['open', 'total'], cardFields: { titleKey: 'name' }, }) expect(warnings.length).toBe(0) }) it('warns when list directives target a non-list pagespec', () => { const { warnings } = applyDirectivesToPagespec(PAGESPEC, { pagespecPath: 'x.md', fields: [], list: { density: 'compact' }, }) expect(warnings.join(' ')).toMatch(/list directives applied to a 'form' pagespec/) }) }) describe('apply-form-directives / lifecycle (rubric §6) — first-order, additive', () => { const LC = { statusField: 'status', phases: [{ key: 'sortie', statuses: ['TERMINATED'], fields: ['birthDate'] }], } it('writes the lifecycle as a FIRST-ORDER key without planting an empty uiDesign', () => { const { md } = applyDirectivesToPagespec(PAGESPEC, { pagespecPath: 'x.md', fields: [], lifecycle: LC, }) const block = JSON.parse(/```json\s*\n([\s\S]*?)\n```/.exec(md)![1]!) as Record expect(block.lifecycle).toEqual(LC) // A lifecycle-only call must not flip Phase 3.1's uiDesign idempotency key. expect(block).not.toHaveProperty('uiDesign') }) it('an existing block only gains NEW phase keys — existing phases + statusField untouched', () => { const first = applyDirectivesToPagespec(PAGESPEC, { pagespecPath: 'x.md', fields: [], lifecycle: LC }) const { md, warnings } = applyDirectivesToPagespec(first.md, { pagespecPath: 'x.md', fields: [], lifecycle: { statusField: 'etat', phases: [ { key: 'sortie', statuses: ['AUTRE'], fields: ['managerId'] }, // existing key → ignored { key: 'validation', statuses: ['VALIDE'], fields: ['managerId'] }, ], }, }) const block = JSON.parse(/```json\s*\n([\s\S]*?)\n```/.exec(md)![1]!) as { lifecycle: { statusField: string; phases: Array<{ key: string; statuses: string[] }> } } expect(block.lifecycle.statusField).toBe('status') expect(block.lifecycle.phases.map(p => p.key)).toEqual(['sortie', 'validation']) expect(block.lifecycle.phases[0]!.statuses).toEqual(['TERMINATED']) expect(warnings.some(w => w.includes("statusField 'status' kept"))).toBe(true) }) it('lifecycle composes with uiDesign directives in one call (both written)', () => { const { md } = applyDirectivesToPagespec(PAGESPEC, { pagespecPath: 'x.md', fields: [{ key: 'userId', currentUserFk: true }], lifecycle: LC, }) const block = JSON.parse(/```json\s*\n([\s\S]*?)\n```/.exec(md)![1]!) as Record expect(block.lifecycle).toEqual(LC) expect(block.uiDesign).toMatchObject({ fields: { userId: { currentUserFk: true } } }) }) it('validate accepts a lifecycle-only spec and rejects a malformed block', () => { expect(validate({ pagespecPath: 'x.md', lifecycle: LC }).valid).toBe(true) const bad = validate({ pagespecPath: 'x.md', lifecycle: { phases: [] } }) expect(bad.valid).toBe(false) }) })