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();
});
});