import { describe, it, expect } from 'vitest'; import { defineFragment, compileFragment, normalizeToV1 } from './defineFragment.js'; import type { FragmentDefinition, FragmentDefinitionV2, FragmentVariant, } from './types.js'; // --------------------------------------------------------------------------- // Helpers — minimal component stubs // --------------------------------------------------------------------------- function StubComponent(_props: { children?: string }) { return null; } const renderStub = () => null; function makeV1(): FragmentDefinition<{ children?: string }> { return { component: StubComponent, meta: { name: 'Stub', description: 'A stub', category: 'test', status: 'stable' }, usage: { when: ['Testing v1'], whenNot: ['Production'], }, props: { children: { type: 'node', description: 'Content', required: false }, }, variants: [ { name: 'Default', description: 'Default variant', render: renderStub }, ], ai: { compositionPattern: 'simple', }, contract: { propsSummary: ['children: node'], }, _generated: { source: 'manual', confidence: 1, }, }; } function makeV2(): FragmentDefinitionV2<{ children?: string }> { return { component: StubComponent, meta: { name: 'Stub', description: 'A stub', category: 'test', status: 'stable' }, guidance: { when: ['Testing v2'], whenNot: ['Production'], }, props: { children: { type: 'node', description: 'Content', required: false }, }, examples: [ { name: 'Default', description: 'Default example', render: renderStub }, ], composition: { pattern: 'compound', subComponents: ['Header', 'Body'], requiredChildren: ['Body'], commonPatterns: [''], }, contract: { propsSummary: ['children: node'], }, _provenance: { source: 'scan', confidence: 0.85, autoFields: ['props', 'composition'], humanFields: ['guidance', 'examples'], }, }; } // --------------------------------------------------------------------------- // defineFragment — v1 shape // --------------------------------------------------------------------------- describe('defineFragment — v1 shape', () => { it('accepts and returns v1 definition unchanged', () => { const v1 = makeV1(); const result = defineFragment(v1); expect(result).toBe(v1); expect(result.usage.when).toEqual(['Testing v1']); expect(result.variants[0].name).toBe('Default'); expect(result.ai?.compositionPattern).toBe('simple'); }); it('throws on invalid v1 definition', () => { const invalid = { component: StubComponent, meta: { name: '', description: 'bad', category: 'test' }, // empty name usage: { when: [], whenNot: [] }, props: {}, variants: [], } as unknown as FragmentDefinition; expect(() => defineFragment(invalid)).toThrow('Invalid fragment definition'); }); }); // --------------------------------------------------------------------------- // defineFragment — v2 shape // --------------------------------------------------------------------------- describe('defineFragment — v2 shape', () => { it('accepts and returns v2 definition unchanged', () => { const v2 = makeV2(); const result = defineFragment(v2); expect(result).toBe(v2); expect((result as FragmentDefinitionV2).guidance.when).toEqual(['Testing v2']); expect((result as FragmentDefinitionV2).examples[0].name).toBe('Default'); expect((result as FragmentDefinitionV2).composition?.pattern).toBe('compound'); }); it('validates v2 field names correctly', () => { const v2 = makeV2(); // Should not throw expect(() => defineFragment(v2)).not.toThrow(); }); it('throws on invalid v2 definition', () => { const invalid = { component: StubComponent, meta: { name: 'Test', description: 'test', category: 'test' }, guidance: { when: 'not-an-array', whenNot: [] }, // wrong type props: {}, examples: [], } as unknown as FragmentDefinitionV2; expect(() => defineFragment(invalid)).toThrow('Invalid fragment definition'); }); it('accepts wrapper composition pattern', () => { const v2 = makeV2(); v2.composition = { pattern: 'wrapper' }; expect(() => defineFragment(v2)).not.toThrow(); }); it('accepts scan provenance source', () => { const v2 = makeV2(); v2._provenance = { source: 'scan', confidence: 0.9 }; expect(() => defineFragment(v2)).not.toThrow(); }); }); // --------------------------------------------------------------------------- // normalizeToV1 // --------------------------------------------------------------------------- describe('normalizeToV1', () => { it('maps guidance → usage', () => { const v2 = makeV2(); const v1 = normalizeToV1(v2); expect(v1.usage).toBe(v2.guidance); expect(v1.usage.when).toEqual(['Testing v2']); }); it('maps examples → variants', () => { const v2 = makeV2(); const v1 = normalizeToV1(v2); expect(v1.variants).toBe(v2.examples); expect(v1.variants[0].name).toBe('Default'); }); it('maps composition → ai', () => { const v2 = makeV2(); const v1 = normalizeToV1(v2); expect(v1.ai).toEqual({ compositionPattern: 'compound', subComponents: ['Header', 'Body'], requiredChildren: ['Body'], commonPatterns: [''], }); }); it('maps _provenance → _generated (narrows scan → ai)', () => { const v2 = makeV2(); const v1 = normalizeToV1(v2); expect(v1._generated).toEqual({ source: 'ai', // scan → ai (v1 doesn't have scan) sourceFile: undefined, confidence: 0.85, timestamp: undefined, }); }); it('preserves storybook source as-is', () => { const v2 = makeV2(); v2._provenance = { source: 'storybook', sourceFile: 'Button.stories.tsx' }; const v1 = normalizeToV1(v2); expect(v1._generated!.source).toBe('storybook'); expect(v1._generated!.sourceFile).toBe('Button.stories.tsx'); }); it('handles undefined composition gracefully', () => { const v2 = makeV2(); v2.composition = undefined; const v1 = normalizeToV1(v2); expect(v1.ai).toBeUndefined(); }); it('handles undefined _provenance gracefully', () => { const v2 = makeV2(); v2._provenance = undefined; const v1 = normalizeToV1(v2); expect(v1._generated).toBeUndefined(); }); it('preserves contract and relations', () => { const v2 = makeV2(); v2.relations = [{ component: 'Other', relationship: 'sibling', note: 'test' }]; const v1 = normalizeToV1(v2); expect(v1.contract).toBe(v2.contract); expect(v1.relations).toBe(v2.relations); }); }); // --------------------------------------------------------------------------- // compileFragment — v2 support // --------------------------------------------------------------------------- describe('compileFragment — v2 support', () => { it('compiles v1 definition', () => { const v1 = makeV1(); const compiled = compileFragment(v1, 'test/Stub.fragment.tsx'); expect(compiled.meta.name).toBe('Stub'); expect(compiled.usage.when).toEqual(['Testing v1']); expect(compiled.variants[0].name).toBe('Default'); expect(compiled.ai?.compositionPattern).toBe('simple'); }); it('compiles v2 definition (normalizes to v1 output)', () => { const v2 = makeV2(); const compiled = compileFragment(v2, 'test/Stub.fragment.tsx'); expect(compiled.meta.name).toBe('Stub'); expect(compiled.usage.when).toEqual(['Testing v2']); expect(compiled.variants[0].name).toBe('Default'); expect(compiled.ai?.compositionPattern).toBe('compound'); expect(compiled.ai?.subComponents).toEqual(['Header', 'Body']); expect(compiled._generated?.source).toBe('ai'); // scan normalized to ai }); it('compiles v2 without optional fields', () => { const v2 = makeV2(); v2.composition = undefined; v2._provenance = undefined; v2.contract = undefined; const compiled = compileFragment(v2, 'test/Stub.fragment.tsx'); expect(compiled.ai).toBeUndefined(); expect(compiled._generated).toBeUndefined(); expect(compiled.contract).toBeUndefined(); }); });