import { describe, it, expect } from 'vitest'; import { generate } from '../generate.js'; import type { ScaffoldUiPrimitivesInput } from '../types.js'; function fixture(overrides: Partial = {}): ScaffoldUiPrimitivesInput { return { projectPath: '/tmp/web', appCode: 'crm', force: false, ...overrides }; } const PATH = 'src/components/ui/SectionCard.tsx'; function sectionCard(): { path: string; content: string; strategy: string } { const f = generate(fixture()).find((x) => x.path === PATH); if (!f) throw new Error(`missing generated file ${PATH}`); return f; } describe('scaffold-ui-primitives / SectionCard — owned section card (regression guard)', () => { it('is emitted (so every sectioned Form/DetailPage import resolves) with overwrite strategy + header', () => { const f = sectionCard(); expect(f.strategy).toBe('overwrite'); expect(f.content).toMatch(/AUTO-GENERATED/); expect(f.content).toMatch(/export function SectionCard\(/); expect(f.content).toMatch(/export interface SectionCardProps/); }); it('exposes the controlled read-first contract (editable/editing/onToggleEdit + labels)', () => { const c = sectionCard().content; expect(c).toMatch(/editable\?: boolean/); expect(c).toMatch(/editing\?: boolean/); expect(c).toMatch(/onToggleEdit\?: \(\) => void/); expect(c).toMatch(/editLabel\?: string/); expect(c).toMatch(/doneLabel\?: string/); expect(c).toMatch(/sectionKey\?: string/); }); it('carries the driver testid contract: section-edit- in read mode, section-done- while editing', () => { const c = sectionCard().content; expect(c).toMatch(/section-done-\$\{sectionKey\}/); expect(c).toMatch(/section-edit-\$\{sectionKey\}/); }); it('draws the accent border + halo only while editing, token-styled', () => { const c = sectionCard().content; expect(c).toMatch(/var\(--color-accent-600\)/); expect(c).toMatch(/shadow-\[var\(--shadow-overlay\)\]/); expect(c).toMatch(/var\(--radius-card\)/); expect(c).toMatch(/var\(--border-color\)/); expect(c).toMatch(/var\(--bg-card\)/); }); it('styles the toggle with the theme helper classes (.btn .btn-secondary — ui-polish R3)', () => { expect(sectionCard().content).toMatch(/btn btn-secondary/); }); it('uses zero hardcoded Tailwind palettes, zero dark: prefix, zero hex in className', () => { const c = sectionCard().content; const FORBIDDEN = [ 'slate', 'gray', 'zinc', 'neutral', 'stone', 'red', 'orange', 'amber', 'yellow', 'lime', 'green', 'emerald', 'teal', 'cyan', 'sky', 'blue', 'indigo', 'violet', 'purple', 'fuchsia', 'pink', 'rose', ]; for (const palette of FORBIDDEN) { const re = new RegExp(`\\b(?:bg|text|border|ring|placeholder|from|to|via|divide|outline)-${palette}-\\d{2,3}\\b`); expect(c.match(re), `Hardcoded Tailwind color (${palette})`).toBeNull(); } expect(c).not.toMatch(/\bdark:/); for (const block of c.match(/className=[`"']([^`"']*)[`"']/g) ?? []) { expect(block, `Hex literal in className: ${block}`).not.toMatch(/#[0-9a-fA-F]{3,8}/); } }); it('stays i18n-agnostic (controlled labels — no useTranslation inside the primitive)', () => { expect(sectionCard().content).not.toMatch(/useTranslation/); }); });