/** * scaffold-component — the « données confidentielles » badge (lib/ba-sensitivity). * * ≥ confidentiel → one badge in the header actions cluster of list / detail / * form (theme tokens, data-testid), the sensitivity.* i18n floor in the 4 * locales. Absent / interne / public → zero trace (the existing generate.test * suite is the byte-level net). */ import { describe, it, expect } from 'vitest' import { generate } from '../generate.js' import type { ScaffoldComponentInput } from '../types.js' function fixture(overrides: Partial = {}): ScaffoldComponentInput { return { module: 'paie', appCode: 'client', entity: 'Payslip', section: 'fiches', views: ['list', 'detail', 'form'], fields: [ { name: 'reference', type: 'string', required: true }, { name: 'amount', type: 'number', required: true }, ], projectPath: '/web', ...overrides, } } function pages(spec: ScaffoldComponentInput): Record<'list' | 'detail' | 'form', string> { const files = generate(spec) return { list: files.find((f) => f.path.endsWith('PayslipsListPage.tsx'))!.content, detail: files.find((f) => f.path.endsWith('PayslipDetailPage.tsx'))!.content, form: files.find((f) => f.path.endsWith('PayslipFormPage.tsx'))!.content, } } function bundles(spec: ScaffoldComponentInput): Record> { const out: Record> = {} const files = generate(spec) for (const locale of ['fr', 'en', 'it', 'de']) { const bundle = files.find((f) => f.path.replace(/\\/g, '/').includes(`/i18n/locales/${locale}/`))! out[locale] = JSON.parse(bundle.content) as Record } return out } const I18N = { 'list.title': 'x' } const pageSpecOf = (view: 'list' | 'detail' | 'form', extra: Record = {}): ScaffoldComponentInput['pageSpec'] => ({ screenCode: `SCR-PAIE-FICHES-${view.toUpperCase()}-001`, module: 'paie', appCode: 'client', section: 'fiches', entity: 'Payslip', view, filePath: `src/pages/client/paie/fiches/Payslip${view === 'list' ? 's' : ''}${view[0]!.toUpperCase()}${view.slice(1)}Page.tsx`, permission: 'paie.fiches.read', actions: [], i18nKeys: { fr: I18N, en: I18N, it: I18N, de: I18N }, needsRefinement: false, specHash: 'y'.repeat(64), ...extra, }) as ScaffoldComponentInput['pageSpec'] const SENSITIVE = { level: 'confidentiel' as const, categories: ['financier' as const, 'rh' as const] } describe('scaffold-component — sensitivity badge', () => { it('renders the badge in the header cluster of list, detail and form (testid, theme tokens, t() keys)', () => { for (const [view, content] of Object.entries(pages(fixture({ sensitivity: SENSITIVE })))) { expect(content, view).toContain('data-testid="sensitivity-badge"') expect(content, view).toContain("t('payslip.sensitivity.badge')") expect(content, view).toContain("title={t('payslip.sensitivity.hint')}") expect(content, view).toContain('bg-[var(--warning-bg)] border border-[var(--warning-border)] text-[var(--warning-text)]') // One badge per page — the form (no header actions here) carries it on create AND edit through the one cluster. expect((content.match(/sensitivity-badge/g) ?? []).length, view).toBe(1) } }) it('the pagespec key (stamped by derive-sensitivity-specs) is read like the legacy input flag', () => { const viaPagespec = generate(fixture({ views: ['list'], pageSpec: pageSpecOf('list', { sensitivity: SENSITIVE }) })) expect(viaPagespec.find((f) => f.path.endsWith('PayslipsListPage.tsx'))!.content).toContain('data-testid="sensitivity-badge"') const interne = generate(fixture({ views: ['list'], pageSpec: pageSpecOf('list') })) expect(interne.find((f) => f.path.endsWith('PayslipsListPage.tsx'))!.content).not.toContain('sensitivity-badge') }) it('emits the sensitivity.* floor in the 4 locales — badge text per level, hint carrying the categories', () => { const b = bundles(fixture({ sensitivity: SENSITIVE })) const leaf = (loc: string): Record => ((b[loc]!['payslip'] as Record)['sensitivity'] as Record) expect(Object.keys(b).sort()).toEqual(['de', 'en', 'fr', 'it']) expect(leaf('fr')).toEqual({ badge: 'Données confidentielles', hint: 'Données confidentielles (financier, rh) — Accès soumis au besoin d’en connaître' }) expect(leaf('en').badge).toBe('Confidential data') expect(leaf('it').badge).toBe('Dati riservati') expect(leaf('de').badge).toBe('Vertrauliche Daten') const restricted = bundles(fixture({ sensitivity: { level: 'restreint', categories: ['sante'] } })) expect(((restricted['fr']!['payslip'] as Record)['sensitivity'] as Record).badge).toBe('Données restreintes') }) it('absent, interne or public → zero trace in the pages and no sensitivity key in the bundles', () => { for (const spec of [fixture(), fixture({ sensitivity: { level: 'interne', categories: [] } }), fixture({ sensitivity: { level: 'public', categories: [] } })]) { for (const content of Object.values(pages(spec))) { expect(content).not.toContain('sensitivity') } for (const bundle of Object.values(bundles(spec))) { expect((bundle['payslip'] as Record)['sensitivity']).toBeUndefined() } } // Byte-identical: the public / interne variants equal the untagged output. const base = generate(fixture()).map((f) => [f.path, f.content]) expect(generate(fixture({ sensitivity: { level: 'public', categories: [] } })).map((f) => [f.path, f.content])).toEqual(base) }) })