import { UI_CATALOG, getCatalogEntry, catalogByCategory, builtInWidgets, renderCatalogForPrompt, renderDashboardConfig, widgetsNeedingScaffold, } from '../index'; describe('UI_CATALOG', () => { it('has unique ids', () => { const ids = UI_CATALOG.map((e) => e.id); expect(new Set(ids).size).toBe(ids.length); }); it('gives every entry the fields the AI builder + scaffolder need', () => { for (const e of UI_CATALOG) { expect(e.id).toMatch(/^[a-z][a-z0-9-]*$/); expect(['widget', 'view', 'component']).toContain(e.category); expect(e.title.length).toBeGreaterThan(0); expect(e.description.length).toBeGreaterThan(0); expect(e.dataNeeds.length).toBeGreaterThan(0); expect(e.source.export.length).toBeGreaterThan(0); expect(e.source.from.startsWith('@startsimpli/ui')).toBe(true); } }); it('exposes the five template built-in widgets as config-only widgets', () => { const builtIns = builtInWidgets().map((e) => e.id).sort(); expect(builtIns).toEqual(['metrics', 'pipeline', 'quick-create', 'recent', 'type-grid']); for (const e of builtInWidgets()) expect(e.category).toBe('widget'); }); it('built-in widgets ship an example config whose kind matches their id', () => { for (const e of builtInWidgets()) { expect(e.configExample).toBeDefined(); expect(e.configExample!.kind).toBe(e.id); } }); it('flags heavy blocks so the AI can avoid them by default', () => { // The bundle-costly ones (dnd/xlsx/calendar/gantt) must be marked heavy. for (const id of ['kanban-board', 'data-table', 'calendar', 'gantt']) { expect(getCatalogEntry(id)?.heavy).toBe(true); } // The lightweight defaults must NOT be heavy. for (const id of ['metrics', 'quick-create', 'recent', 'type-grid']) { expect(getCatalogEntry(id)?.heavy).toBeFalsy(); } }); it('renderCatalogForPrompt lists every block with its heavy/built-in flags', () => { const text = renderCatalogForPrompt(); for (const e of UI_CATALOG) expect(text).toContain(e.id); expect(text).toContain('⚠ heavy'); expect(text).toContain('built-in'); expect(text).toContain('scaffolded'); }); it('catalogByCategory partitions the catalog', () => { const total = catalogByCategory('widget').length + catalogByCategory('view').length + catalogByCategory('component').length; expect(total).toBe(UI_CATALOG.length); }); }); describe('renderDashboardConfig', () => { it('renders a valid foundry.dashboard.ts for known widgets', () => { const r = renderDashboardConfig([ { kind: 'metrics', title: 'Overview' }, { kind: 'pipeline', type: 'topic', title: 'Pipeline' }, { kind: 'recent', limit: 8 }, ]); expect(r.ok).toBe(true); expect(r.errors).toEqual([]); expect(r.file?.path).toBe('src/foundry.dashboard.ts'); expect(r.file?.content).toContain('export const dashboard: DashboardConfig = ['); expect(r.file?.content).toContain('"kind":"metrics"'); expect(r.file?.content).toContain('"type":"topic"'); expect(r.file?.content).toContain("import type { DashboardConfig } from '@/components/dashboard/config'"); }); it('rejects an unknown widget kind (never writes a config the app cannot render)', () => { const r = renderDashboardConfig([{ kind: 'metrics' }, { kind: 'teleporter' }]); expect(r.ok).toBe(false); expect(r.file).toBeUndefined(); expect(r.errors.join(' ')).toContain('teleporter'); }); it('rejects an empty layout', () => { expect(renderDashboardConfig([]).ok).toBe(false); }); it('flags chosen widgets that are not built into the template', () => { expect(widgetsNeedingScaffold([{ kind: 'metrics' }, { kind: 'kanban-board' }])).toEqual(['kanban-board']); expect(widgetsNeedingScaffold([{ kind: 'metrics' }, { kind: 'recent' }])).toEqual([]); }); });