import { describe, expect, it } from 'vitest' import { SECTIONS_ORIGINS, parsePageSections, resolveSections, sectionsOriginOf } from '../page-spec-sections.js' import type { UiDesignOverlay } from '../ui-design-overlay.js' type F = { name: string; section?: string } const fields: F[] = [ { name: 'Name' }, { name: 'ContractType' }, { name: 'HireDate' }, { name: 'Notes' }, ] describe('parsePageSections', () => { it('parses valid sections and reports invalid entries without throwing', () => { const { sections, rejected } = parsePageSections([ { key: 'identity', fields: ['name'] }, { key: 'BAD KEY', fields: ['x'] }, { key: 'contract', labelKey: 'form.section.contract', columns: 2, description: 'd' }, ]) expect(sections.map(s => s.key)).toEqual(['identity', 'contract']) expect(rejected).toHaveLength(1) expect(rejected[0]!.index).toBe(1) expect(rejected[0]!.issues.join(' ')).toMatch(/key/) }) it('returns empty on a non-array payload', () => { expect(parsePageSections(undefined)).toEqual({ sections: [], rejected: [] }) expect(parsePageSections('nope')).toEqual({ sections: [], rejected: [] }) }) }) describe('resolveSections — membership seeding', () => { it('seeds field.section from sections[].fields (camel-matched, Pascal field names)', () => { const { fields: out } = resolveSections(fields, { sections: [ { key: 'identity', fields: ['name'] }, { key: 'contract', fields: ['contractType', 'hireDate'] }, ], }, undefined) expect(out.map(f => f.section)).toEqual(['identity', 'contract', 'contract', undefined]) }) it('never touches an explicit field.section', () => { const { fields: out } = resolveSections( [{ name: 'Name', section: 'custom' }, { name: 'HireDate' }], { sections: [{ key: 'identity', fields: ['name', 'hireDate'] }] }, undefined, ) expect(out[0]!.section).toBe('custom') expect(out[1]!.section).toBe('identity') }) it('sections[].fields wins over a tabs[] seed for the same field', () => { const { fields: out } = resolveSections(fields, { sections: [{ key: 'contract', fields: ['contractType'] }], tabs: [{ key: 'info', fields: ['contractType', 'name'] }], }, undefined) expect(out.find(f => f.name === 'ContractType')!.section).toBe('contract') expect(out.find(f => f.name === 'Name')!.section).toBe('info') }) it('keeps the legacy tabs[]-only seeding when no sections[] exist', () => { const { fields: out, order } = resolveSections(fields, { tabs: [{ key: 'info', fields: ['name'] }, { key: 'dates', fields: ['hireDate'] }], }, undefined) expect(out.find(f => f.name === 'Name')!.section).toBe('info') expect(out.find(f => f.name === 'HireDate')!.section).toBe('dates') expect(order).toEqual([]) // tabs seed membership but carry no section metadata }) it('is a no-op on a pagespec without sections or tabs', () => { const { fields: out, order, rejected } = resolveSections(fields, undefined, undefined) expect(out).toEqual(fields) expect(order).toEqual([]) expect(rejected).toEqual([]) }) }) describe('resolveSections — metadata order', () => { const pageSpec = { sections: [ { key: 'identity', label: 'Identité', fields: ['name'] }, { key: 'contract', labelKey: 'form.section.contract', columns: 2, fields: ['contractType'] }, ], } it('returns the pagespec array order with its metadata', () => { const { order } = resolveSections(fields, pageSpec, undefined) expect(order.map(s => s.key)).toEqual(['identity', 'contract']) expect(order[0]).toMatchObject({ key: 'identity', label: 'Identité' }) expect(order[1]).toMatchObject({ key: 'contract', labelKey: 'form.section.contract', columns: 2 }) }) it('uiDesign.sections wins for order AND per-key metadata; unmentioned spec sections follow', () => { const overlay: UiDesignOverlay = { sections: [{ key: 'contract', columns: 3, description: 'from overlay' }], } const { order } = resolveSections(fields, pageSpec, overlay) expect(order.map(s => s.key)).toEqual(['contract', 'identity']) expect(order[0]).toMatchObject({ key: 'contract', columns: 3, description: 'from overlay' }) // spec metadata still fills what the overlay does not override expect(order[0]!.labelKey).toBe('form.section.contract') expect(order[1]).toMatchObject({ key: 'identity', label: 'Identité' }) }) it('deduplicates repeated keys, keeping the first occurrence', () => { const { order } = resolveSections(fields, { sections: [ { key: 'identity', fields: ['name'], label: 'First' }, { key: 'identity', fields: ['notes'], label: 'Second' }, ], }, undefined) expect(order).toHaveLength(1) expect(order[0]).toMatchObject({ key: 'identity', label: 'First' }) }) it('keeps order entries whose fields resolve elsewhere (renderer skips them)', () => { const { order } = resolveSections( [{ name: 'Name', section: 'custom' }], { sections: [{ key: 'identity', fields: ['name'] }] }, undefined, ) expect(order.map(s => s.key)).toEqual(['identity']) }) }) describe('sectionsOriginOf — the provenance accessor', () => { it('round-trips authored/derived and returns undefined for absent/garbage', () => { expect(sectionsOriginOf({ sectionsOrigin: 'authored' })).toBe('authored') expect(sectionsOriginOf({ sectionsOrigin: 'derived' })).toBe('derived') expect(sectionsOriginOf({})).toBeUndefined() expect(sectionsOriginOf({ sectionsOrigin: 'invented' })).toBeUndefined() expect(sectionsOriginOf(null)).toBeUndefined() expect(sectionsOriginOf('nope')).toBeUndefined() expect(SECTIONS_ORIGINS).toEqual(['authored', 'derived']) }) })