import { describe, it, expect } from 'vitest' import { generate } from '../generate.js' import type { ScaffoldComponentInput, PageSpecMin } from '../types.js' /** * Responsive list-table contract (the "tableaux responsives + tooltip si le * libellé est tronqué" feature). scaffold-component must: * - render (wraps the local ) instead of , * - keep the code + label columns ALWAYS visible (no minBreakpoint), * - assign md → lg → xl to the remaining columns by declaration order, * - flag every text column truncate:true, * - honour BA overrides (priority:'always', explicit minBreakpoint). */ const I18N = { 'list.title': 'Budgets', 'list.subtitle': 'x', 'list.create': 'x', 'list.search': 'x', 'list.loading': 'x', 'list.empty': 'x', 'list.columns.code': 'Code', 'list.columns.label': 'Libellé', 'list.columns.amount': 'Montant', 'list.columns.category': 'Catégorie', 'list.columns.region': 'Région', 'list.columns.notes': 'Notes', } function pageSpec(columns: PageSpecMin['columns']): PageSpecMin { return { screenCode: 'SCR-BUDGET-LIST', module: 'budgets', appCode: 'testv2', section: 'budgets', entity: 'Budget', view: 'list', filePath: 'src/pages/testv2/budgets/budgets/BudgetsListPage.tsx', permission: 'budgets.budgets.read', actions: [], columns, i18nKeys: { fr: I18N, en: I18N, it: I18N, de: I18N }, needsRefinement: false, specHash: 'a'.repeat(64), } } function gen(columns: PageSpecMin['columns']): string { const input: ScaffoldComponentInput = { module: 'budgets', appCode: 'testv2', entity: 'Budget', section: 'budgets', views: ['list'], fields: [ { name: 'code', type: 'string', required: true }, { name: 'label', type: 'string', required: true }, { name: 'amount', type: 'number', required: true }, { name: 'category', type: 'string', required: false }, { name: 'region', type: 'string', required: false }, { name: 'notes', type: 'string', required: false }, ], projectPath: '/web', pageSpec: pageSpec(columns), } const files = generate(input) const list = files.find((f) => /ListPage\.tsx$/.test(f.path)) if (!list) throw new Error('no list page emitted') return list.content } /** Extracts the emitted column object literal for a given key. The `sortable:` * guard keeps the match off the columnVisibilityDefaults literal (same key + * label shape, no sortable). */ function entryFor(content: string, key: string): string { const re = new RegExp(`\\{ key: '${key}',(?=[^}]*sortable:)[^}]*\\}`) const m = content.match(re) if (!m) throw new Error(`no column entry for key '${key}'`) return m[0] } const DEFAULT_COLUMNS: PageSpecMin['columns'] = [ { key: 'code', labelKey: 'list.columns.code', sortable: true }, { key: 'label', labelKey: 'list.columns.label', sortable: true }, { key: 'amount', labelKey: 'list.columns.amount', sortable: true }, { key: 'category', labelKey: 'list.columns.category', sortable: true }, { key: 'region', labelKey: 'list.columns.region', sortable: true }, ] describe('scaffold-component / responsive list table — wrapper wiring', () => { it('renders and imports it from @/components/ui/ResponsiveDataTable', () => { const c = gen(DEFAULT_COLUMNS) expect(c).toMatch(/import \{ ResponsiveDataTable, type ResponsiveColumn \} from '@\/components\/ui\/ResponsiveDataTable'/) expect(c).toMatch(//) expect(c).toMatch(/const columns: ResponsiveColumn\[\] =/) }) it('no longer imports or renders the raw ', () => { const c = gen(DEFAULT_COLUMNS) // The COMPONENT must not be imported; the `formatCellValue` helper that // lives in the same module legitimately is (the cards branch uses it). expect(c).not.toMatch(/import \{[^}]*DataTable[^}]*\} from '@\/components\/ui\/DataTable'/) expect(c).not.toMatch(/ { it('keeps code + label ALWAYS visible (no minBreakpoint) and truncatable', () => { const c = gen(DEFAULT_COLUMNS) const code = entryFor(c, 'code') const label = entryFor(c, 'label') expect(code).toMatch(/truncate: true/) expect(code).not.toMatch(/minBreakpoint/) expect(label).toMatch(/truncate: true/) expect(label).not.toMatch(/minBreakpoint/) }) it('assigns md → lg → xl to the extra columns by declaration order', () => { const c = gen(DEFAULT_COLUMNS) expect(entryFor(c, 'amount')).toMatch(/minBreakpoint: 'md'/) expect(entryFor(c, 'category')).toMatch(/minBreakpoint: 'lg'/) expect(entryFor(c, 'region')).toMatch(/minBreakpoint: 'xl'/) }) it('caps further extra columns at xl (no breakpoint beyond xl)', () => { const c = gen([...DEFAULT_COLUMNS, { key: 'notes', labelKey: 'list.columns.notes', sortable: false }]) expect(entryFor(c, 'region')).toMatch(/minBreakpoint: 'xl'/) expect(entryFor(c, 'notes')).toMatch(/minBreakpoint: 'xl'/) }) it('flags every text column truncate:true (the actions column has its own render)', () => { const c = gen(DEFAULT_COLUMNS) for (const key of ['code', 'label', 'amount', 'category', 'region']) { expect(entryFor(c, key), key).toMatch(/truncate: true/) } }) }) describe('scaffold-component / responsive list table — BA overrides', () => { it('priority:"always" forces a column to stay visible (no minBreakpoint)', () => { const c = gen([ { key: 'code', labelKey: 'list.columns.code', sortable: true }, { key: 'label', labelKey: 'list.columns.label', sortable: true }, { key: 'amount', labelKey: 'list.columns.amount', sortable: true, priority: 'always' }, { key: 'category', labelKey: 'list.columns.category', sortable: true }, ]) expect(entryFor(c, 'amount')).not.toMatch(/minBreakpoint/) // category is then the FIRST auto-tier extra → md expect(entryFor(c, 'category')).toMatch(/minBreakpoint: 'md'/) }) it('respects an explicit minBreakpoint override', () => { const c = gen([ { key: 'code', labelKey: 'list.columns.code', sortable: true }, { key: 'label', labelKey: 'list.columns.label', sortable: true }, { key: 'amount', labelKey: 'list.columns.amount', sortable: true, minBreakpoint: 'xl' }, ]) expect(entryFor(c, 'amount')).toMatch(/minBreakpoint: 'xl'/) }) }) describe('scaffold-component / responsive list table — fallback identification', () => { it('with no code/label match, keeps the first two columns always visible', () => { const c = gen([ { key: 'firstName', labelKey: 'list.columns.firstName', sortable: true }, { key: 'lastName', labelKey: 'list.columns.lastName', sortable: true }, { key: 'amount', labelKey: 'list.columns.amount', sortable: true }, ]) expect(entryFor(c, 'firstName')).not.toMatch(/minBreakpoint/) expect(entryFor(c, 'lastName')).not.toMatch(/minBreakpoint/) expect(entryFor(c, 'amount')).toMatch(/minBreakpoint: 'md'/) }) }) describe('scaffold-component / responsive list table — date formatting (display settings)', () => { it('honours formatHint "date" with a formatDate render + the package import', () => { const c = gen([ { key: 'code', labelKey: 'list.columns.code', sortable: true }, { key: 'label', labelKey: 'list.columns.label', sortable: true }, { key: 'amount', labelKey: 'list.columns.amount', sortable: true, formatHint: 'date' }, ]) expect(entryFor(c, 'amount')).toMatch(/render: \(item\) => formatDate\(item\.amount\)/) expect(c).toMatch(/import \{ Slot, formatDate \} from '@atlashub\/smartstack'/) }) it('honours formatHint "datetime" with a formatDateTime render', () => { const c = gen([ { key: 'code', labelKey: 'list.columns.code', sortable: true }, { key: 'label', labelKey: 'list.columns.label', sortable: true }, { key: 'amount', labelKey: 'list.columns.amount', sortable: true, formatHint: 'datetime' }, ]) expect(entryFor(c, 'amount')).toMatch(/render: \(item\) => formatDateTime\(item\.amount\)/) expect(c).toMatch(/import \{ Slot, formatDateTime \} from '@atlashub\/smartstack'/) }) it('falls back on the entity field type when the pagespec carries no hint', () => { const input: ScaffoldComponentInput = { module: 'budgets', appCode: 'testv2', entity: 'Budget', section: 'budgets', views: ['list'], fields: [ { name: 'code', type: 'string', required: true }, { name: 'startDate', type: 'date', required: false }, { name: 'closedAt', type: 'datetime', required: false }, ], projectPath: '/web', } const list = generate(input).find((f) => /ListPage\.tsx$/.test(f.path))! expect(entryFor(list.content, 'startDate')).toMatch(/render: \(item\) => formatDate\(item\.startDate\)/) expect(entryFor(list.content, 'closedAt')).toMatch(/render: \(item\) => formatDateTime\(item\.closedAt\)/) expect(list.content).toMatch(/import \{ Slot, formatDate, formatDateTime \} from '@atlashub\/smartstack'/) }) it('leaves plain columns on the String fallback with no datetime import', () => { const c = gen(DEFAULT_COLUMNS) expect(c).not.toMatch(/formatDate/) expect(c).toMatch(/import \{ Slot \} from '@atlashub\/smartstack'/) }) })