import { describe, it, expect } from 'vitest'; import { generate } from '../generate.js'; import { ScaffoldDashboardPrimitivesInputSchema } from '../types.js'; function files() { return generate(ScaffoldDashboardPrimitivesInputSchema.parse({ projectPath: '/p', appCode: 'demo' })); } function content(path: string): string { const f = files().find((x) => x.path === path); if (!f) throw new Error(`missing generated file: ${path}`); return f.content; } // Saturated Tailwind color classes (and dark: prefix) must NEVER appear — colors // come from tokens (var(--…)) only. `bg-[var(--bg-card)]` is fine (not a match). const FORBIDDEN_PALETTE = /\b(bg|text|border|ring|fill|stroke|from|via|to)-(red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose|gray|slate|zinc|neutral|stone)-\d{2,3}\b/; const DARK_PREFIX = /\bdark:/; const TSX = [ 'src/components/dashboard/KpiCard.tsx', 'src/components/dashboard/ChartCard.tsx', 'src/components/dashboard/ListWidget.tsx', 'src/components/dashboard/DashboardGrid.tsx', 'src/components/dashboard/WidgetRenderer.tsx', ]; describe('scaffold-dashboard-primitives — emitted files', () => { it('emits the 8 primitives + 4 locale files with correct strategies', () => { const paths = files().map((f) => `${f.path} [${f.strategy}]`); expect(paths).toEqual([ 'src/components/dashboard/types.ts [overwrite]', 'src/components/dashboard/useDatavizPalette.ts [overwrite]', 'src/components/dashboard/KpiCard.tsx [overwrite]', 'src/components/dashboard/ChartCard.tsx [overwrite]', 'src/components/dashboard/Sparkline.tsx [overwrite]', 'src/components/dashboard/ListWidget.tsx [overwrite]', 'src/components/dashboard/DashboardGrid.tsx [overwrite]', 'src/components/dashboard/WidgetRenderer.tsx [overwrite]', 'src/i18n/locales/fr/common.json [deep-merge-json]', 'src/i18n/locales/en/common.json [deep-merge-json]', 'src/i18n/locales/it/common.json [deep-merge-json]', 'src/i18n/locales/de/common.json [deep-merge-json]', ]); }); it('every .tsx carries the AUTO-GENERATED @customised header', () => { for (const p of TSX) expect(content(p), p).toContain('@customised'); }); }); describe('theme compliance — tokens only, no hardcoded palette', () => { it('no saturated Tailwind color class and no dark: prefix in any primitive', () => { for (const p of [...TSX, 'src/components/dashboard/types.ts', 'src/components/dashboard/useDatavizPalette.ts']) { expect(FORBIDDEN_PALETTE.test(content(p)), `palette in ${p}`).toBe(false); expect(DARK_PREFIX.test(content(p)), `dark: in ${p}`).toBe(false); } }); it('KpiCard reads the --kpi-* tokens', () => { const c = content('src/components/dashboard/KpiCard.tsx'); expect(c).toContain('var(--kpi-value)'); expect(c).toContain('var(--kpi-label)'); expect(c).toContain('var(--kpi-trend-up)'); expect(c).toContain('var(--kpi-trend-down)'); }); it('ChartCard pulls categorical colors from the palette hook + reads --chart-* tokens', () => { const c = content('src/components/dashboard/ChartCard.tsx'); expect(c).toContain("import { useDatavizPalette } from './useDatavizPalette'"); expect(c).toContain('palette[i % palette.length]'); expect(c).toContain('var(--chart-grid)'); expect(c).toContain('var(--chart-axis)'); expect(c).toContain('var(--chart-tooltip-bg)'); }); it('useDatavizPalette resolves the platform palette first, then the scaffolded one', () => { const c = content('src/components/dashboard/useDatavizPalette.ts'); expect(c).toContain('getComputedStyle(document.documentElement)'); // 1. the live palette the SDK theme runtime writes from the UI configuration expect(c).toContain("'--dataviz-cat-' + i"); // 2. the palette scaffold-theme bakes into index.css, 3. the accent ramp expect(c).toContain("'--dataviz-' + i"); expect(c).toContain("'--color-accent-' + shade"); // a live theme switch writes INLINE styles on — observing `class` alone misses it expect(c).toContain("attributeFilter: ['class', 'style']"); }); }); describe('the shared contract', () => { it('types.ts defines DashboardWidget + WidgetResult + the 6 widget types', () => { const c = content('src/components/dashboard/types.ts'); expect(c).toContain('interface DashboardWidget'); expect(c).toContain('interface WidgetResult'); for (const t of ['kpi', 'counter', 'chart-line', 'chart-bar', 'chart-pie', 'list']) { expect(c, t).toContain(`'${t}'`); } }); it('WidgetRenderer maps all 6 types and degrades unknown to a placeholder', () => { const c = content('src/components/dashboard/WidgetRenderer.tsx'); expect(c).toContain(' { it('each locale carries a parseable dashboard namespace', () => { for (const loc of ['fr', 'en', 'it', 'de']) { const json = JSON.parse(content(`src/i18n/locales/${loc}/common.json`)); expect(json.dashboard, loc).toBeDefined(); expect(typeof json.dashboard.noData, loc).toBe('string'); expect(typeof json.dashboard.unknownWidget, loc).toBe('string'); } }); });