/** * screens.test.ts — screen.md → screen registry parsing. */ import { describe, it, expect } from 'vitest' import { parseScreenFile, labelToKey, effectiveMode, hasFormScreen, resolveListTarget, } from '../screens.js' const META = { file: 'CRM/CLIENTS/clients/screen.md', module: 'CLIENTS', section: 'clients' } const DETAIL_SCREEN = ` # Écrans — CRM / CLIENTS / clients ### SCR-CRM-CLIENTS-CLIENTS-DETAIL-001 — Fiche client (SmartForm) - **Entité** : Client (ENT-001) - **Permission** : \`crm.clients.read\` - **Cas d'usage liés** : UC-CRM-CLIENTS-CLIENTS-001 - **Mode** : detail - **Onglet « Informations »** : code (text, readonly), name (text) - **Onglet lié « Factures »** : entité Invoice, FK clientId, affichage table → SCR-CRM-CLIENTS-INVOICES-LIST-001 (\`crm.invoices.read\`) - **Onglet lié « Adresses »** : entité Address, FK clientId - **Onglet lié « Contacts »** : entité Contact, FK clientId → SCR-CRM-CLIENTS-CONTACTS-LIST-001 ` describe('parseScreenFile', () => { it('parses the heading (code, title, SmartComponent type)', () => { const { screens } = parseScreenFile(DETAIL_SCREEN, META) expect(screens).toHaveLength(1) expect(screens[0]).toMatchObject({ code: 'SCR-CRM-CLIENTS-CLIENTS-DETAIL-001', title: 'Fiche client', screenType: 'SmartForm', module: 'CLIENTS', section: 'clients', }) }) it('parses the Entité / Permission / Mode bullets', () => { const { screens } = parseScreenFile(DETAIL_SCREEN, META) expect(screens[0]).toMatchObject({ entity: 'Client', entityCode: 'ENT-001', permission: 'crm.clients.read', mode: 'detail', }) }) it('parses the full Onglet lié grammar (affichage + target + permission)', () => { const { screens } = parseScreenFile(DETAIL_SCREEN, META) expect(screens[0].declaredTabs[0]).toMatchObject({ label: 'Factures', key: 'factures', relatedEntity: 'Invoice', relationFk: 'clientId', displayMode: 'table', targetScreen: 'SCR-CRM-CLIENTS-INVOICES-LIST-001', permission: 'crm.invoices.read', }) }) it('defaults affichage to table and leaves target/permission undefined', () => { const { screens } = parseScreenFile(DETAIL_SCREEN, META) expect(screens[0].declaredTabs[1]).toMatchObject({ label: 'Adresses', relatedEntity: 'Address', relationFk: 'clientId', displayMode: 'table', targetScreen: undefined, permission: undefined, }) }) it('parses a target without a permission', () => { const { screens } = parseScreenFile(DETAIL_SCREEN, META) expect(screens[0].declaredTabs[2]).toMatchObject({ targetScreen: 'SCR-CRM-CLIENTS-CONTACTS-LIST-001', permission: undefined, }) }) it('parses the Sans onglets liés marker', () => { const content = `### SCR-X-Y-Z-001 — Fiche (SmartForm) - **Entité** : Widget - **Mode** : detail - **Sans onglets liés** : entité feuille, aucune collection liée pertinente ` const { screens } = parseScreenFile(content, META) expect(screens[0].noRelatedTabsReason).toBe( 'entité feuille, aucune collection liée pertinente', ) expect(screens[0].declaredTabs).toHaveLength(0) }) it('splits multiple screens in one file', () => { const content = `### SCR-A-001 — Liste (SmartListView) - **Entité** : Invoice ### SCR-A-002 — Fiche (SmartForm) - **Entité** : Invoice - **Mode** : edit ` const { screens } = parseScreenFile(content, META) expect(screens.map((s) => s.code)).toEqual(['SCR-A-001', 'SCR-A-002']) expect(screens[0].screenType).toBe('SmartListView') expect(screens[1].mode).toBe('edit') }) it('warns on a malformed Onglet lié bullet instead of dropping it silently', () => { const content = `### SCR-A-001 — Fiche (SmartForm) - **Entité** : Client - **Mode** : detail - **Onglet lié « Factures »** : Invoice via clientId ` const { screens, warnings } = parseScreenFile(content, META) expect(screens[0].declaredTabs).toHaveLength(0) expect(warnings).toHaveLength(1) expect(warnings[0]).toContain('did not match the grammar') }) }) describe('labelToKey', () => { it('kebabs a multi-word label', () => { expect(labelToKey('Adresses de facturation')).toBe('adresses-de-facturation') }) it('folds diacritics to schema-valid keys', () => { expect(labelToKey('Écritures comptables')).toBe('ecritures-comptables') }) it('strips punctuation', () => { expect(labelToKey("Litiges (en cours)")).toBe('litiges-en-cours') }) }) describe('effectiveMode', () => { it('defaults a SmartForm without a Mode bullet to edit', () => { const { screens } = parseScreenFile( `### SCR-A-001 — Fiche (SmartForm) - **Entité** : Client `, META, ) expect(effectiveMode(screens[0])).toBe('edit') }) }) describe('resolveListTarget', () => { const listIn = (module: string, code: string, type = 'SmartListView', entity = 'Invoice') => { const { screens } = parseScreenFile( `### ${code} — Liste (${type}) - **Entité** : ${entity} `, { file: `CRM/${module}/x/screen.md`, module, section: 'x' }, ) return screens[0] } it('prefers the related entity module, others become alternatives', () => { const screens = [ listIn('BILLING', 'SCR-B-001'), listIn('CLIENTS', 'SCR-C-001'), ] const res = resolveListTarget(screens, 'Invoice', 'CLIENTS') expect(res.resolved).toBe(true) expect(res.code).toBe('SCR-C-001') expect(res.alternatives).toEqual(['SCR-B-001']) }) it('prefers SmartListView over SmartCard', () => { const screens = [ listIn('CLIENTS', 'SCR-CARD-001', 'SmartCard'), listIn('CLIENTS', 'SCR-LIST-001', 'SmartListView'), ] const res = resolveListTarget(screens, 'Invoice', 'CLIENTS') expect(res.code).toBe('SCR-LIST-001') expect(res.screenType).toBe('SmartListView') }) it('falls back to SmartCard when no SmartListView exists', () => { const screens = [listIn('CLIENTS', 'SCR-CARD-001', 'SmartCard')] const res = resolveListTarget(screens, 'Invoice', 'CLIENTS') expect(res.resolved).toBe(true) expect(res.screenType).toBe('SmartCard') }) it('is unresolved when no list screen is bound to the entity', () => { const screens = [listIn('CLIENTS', 'SCR-C-001', 'SmartListView', 'Contact')] const res = resolveListTarget(screens, 'Invoice', 'CLIENTS') expect(res.resolved).toBe(false) expect(res.alternatives).toEqual([]) }) }) describe('fieldTabLabels — the inner « Onglet » bullets (shared-bar budget input)', () => { it('parses the field-tab labels in declaration order, never counting « Onglet lié »', () => { const { screens } = parseScreenFile(DETAIL_SCREEN, META) // ONE inner tab despite three « Onglet lié » bullets in the same block. expect(screens[0].fieldTabLabels).toEqual(['Informations']) }) it('returns an empty list on a flat screen', () => { const flat = `### SCR-FLAT-001 — Fiche plate (SmartForm) - **Entité** : Client (ENT-001) - **Mode** : detail ` const { screens } = parseScreenFile(flat, META) expect(screens[0].fieldTabLabels).toEqual([]) }) it('collects several inner tabs', () => { const tabbed = `### SCR-TABBED-001 — Fiche employé (SmartForm) - **Entité** : Employee (ENT-003) - **Mode** : edit - **Onglet « Information »** : firstName, lastName - **Onglet « Emploi »** : position, hireDate - **Onglet « Rémunération »** : salary ` const { screens } = parseScreenFile(tabbed, META) expect(screens[0].fieldTabLabels).toEqual(['Information', 'Emploi', 'Rémunération']) }) }) describe('hasFormScreen — the inert-satellite predicate', () => { it('true for any SmartForm bound to the entity (create, edit or detail mode)', () => { const create = `### SCR-INV-CREATE-001 — Nouvelle facture (SmartForm) - **Entité** : Invoice (ENT-002) - **Mode** : create ` const { screens } = parseScreenFile(create, META) expect(hasFormScreen(screens, 'Invoice')).toBe(true) expect(hasFormScreen(screens, 'Client')).toBe(false) }) it('false when only non-form screens are bound to the entity', () => { const list = `### SCR-INV-LIST-001 — Liste des factures (SmartListView) - **Entité** : Invoice (ENT-002) ` const { screens } = parseScreenFile(list, META) expect(hasFormScreen(screens, 'Invoice')).toBe(false) }) }) describe('« Onglet lié » — the app token', () => { const withBullet = (bullet: string) => parseScreenFile( ['### SCR-CRM-CLIENTS-DETAIL-001 — Fiche (SmartForm)', '- **Entité** : Client (ENT-001)', '- **Mode** : detail', bullet].join('\n'), { file: 'CRM/CLIENTS/clients/screen.md', app: 'CRM', module: 'CLIENTS', section: 'clients' }, ).screens[0] it('reads the application the related entity lives in, lower-cased', () => { const screen = withBullet('- **Onglet lié « Factures »** : entité Invoice, FK clientId, app FACTURATION, affichage table') expect(screen.declaredTabs[0].relatedApp).toBe('facturation') expect(screen.declaredTabs[0].displayMode).toBe('table') }) it('stays optional — a bullet without it is unchanged', () => { const screen = withBullet('- **Onglet lié « Factures »** : entité Invoice, FK clientId, affichage cards → SCR-X (`crm.invoices.read`)') expect(screen.declaredTabs[0].relatedApp).toBeUndefined() expect(screen.declaredTabs[0].displayMode).toBe('cards') expect(screen.declaredTabs[0].targetScreen).toBe('SCR-X') expect(screen.declaredTabs[0].permission).toBe('crm.invoices.read') }) it('keeps every later token addressable when the app token is present', () => { const screen = withBullet('- **Onglet lié « Factures »** : entité Invoice, FK clientId, app FACTURATION, affichage summary → SCR-Y (`facturation.factures.read`)') const tab = screen.declaredTabs[0] expect(tab.relatedApp).toBe('facturation') expect(tab.displayMode).toBe('summary') expect(tab.targetScreen).toBe('SCR-Y') expect(tab.permission).toBe('facturation.factures.read') }) })