/** * Unit tests for the declarative `` light-DOM API of * ``. * * Strategy: the `defineWebComponent` call registers a real Shadow-DOM custom * element which requires a full browser environment (Constructable Stylesheets, * shadow roots, etc.) and is therefore not suitable for jsdom unit tests. * Instead we: * 1. Test the exported `parseKcModelElement` helper in isolation — it has no * DOM dependencies beyond `Element`, which jsdom provides perfectly. * 2. Test that the merged list of prop + slotted models combines correctly, * mirroring the pattern used in `prompt-suggestions.declarative.test.tsx`. */ import { describe, it, expect } from 'vitest'; import { parseKcModelElement } from './model-switcher'; import type { ModelOption } from '../types'; // --------------------------------------------------------------------------- // parseKcModelElement — pure helper // --------------------------------------------------------------------------- describe('parseKcModelElement', () => { function makeNode(textContent: string, id?: string, provider?: string): Element { const el = document.createElement('kc-model'); el.textContent = textContent; if (id !== undefined) el.setAttribute('id', id); if (provider !== undefined) el.setAttribute('provider', provider); return el; } it('maps textContent to name', () => { const item = parseKcModelElement(makeNode('GPT-4o', 'gpt-4o')); expect(item.name).toBe('GPT-4o'); }); it('maps the id attribute to id', () => { const item = parseKcModelElement(makeNode('GPT-4o', 'gpt-4o')); expect(item.id).toBe('gpt-4o'); }); it('maps the provider attribute to provider', () => { const item = parseKcModelElement(makeNode('GPT-4o', 'gpt-4o', 'OpenAI')); expect(item.provider).toBe('OpenAI'); }); it('sets provider to undefined when the attribute is absent', () => { const item = parseKcModelElement(makeNode('Claude Sonnet', 'sonnet')); expect(item.provider).toBeUndefined(); }); it('trims leading/trailing whitespace from textContent (name)', () => { const item = parseKcModelElement(makeNode(' Claude Sonnet ', 'sonnet')); expect(item.name).toBe('Claude Sonnet'); }); it('returns empty string for id when id attribute is absent', () => { const item = parseKcModelElement(makeNode('GPT-4o')); expect(item.id).toBe(''); }); it('produces the full ModelOption shape', () => { const item = parseKcModelElement(makeNode('GPT-4o', 'gpt-4o', 'OpenAI')); expect(item).toEqual({ id: 'gpt-4o', name: 'GPT-4o', provider: 'OpenAI', description: undefined, group: undefined }); }); it('maps the description and group attributes', () => { const el = document.createElement('kc-model'); el.textContent = 'GPT-4o'; el.setAttribute('id', 'gpt-4o'); el.setAttribute('description', 'Flagship model'); el.setAttribute('group', 'Legacy models'); const item = parseKcModelElement(el); expect(item.description).toBe('Flagship model'); expect(item.group).toBe('Legacy models'); }); }); // --------------------------------------------------------------------------- // Merge logic: prop models + slotted models combine correctly // --------------------------------------------------------------------------- describe('prop + declarative merge', () => { function makeNode(textContent: string, id: string, provider?: string): Element { const el = document.createElement('kc-model'); el.textContent = textContent; el.setAttribute('id', id); if (provider !== undefined) el.setAttribute('provider', provider); return el; } it('renders prop items before declarative (slotted) items', () => { const propModels: ModelOption[] = [{ id: 'opus', name: 'Claude Opus', provider: 'Anthropic' }]; const slottedModels = [makeNode('GPT-4o', 'gpt-4o', 'OpenAI')].map(parseKcModelElement); const merged = [...propModels, ...slottedModels]; expect(merged).toHaveLength(2); expect(merged[0].id).toBe('opus'); expect(merged[1].id).toBe('gpt-4o'); }); it('works with only declarative children (empty prop array)', () => { const slottedModels = [ makeNode('GPT-4o', 'gpt-4o', 'OpenAI'), makeNode('GPT-4o mini', 'gpt-4o-mini', 'OpenAI'), ].map(parseKcModelElement); const merged = [...[], ...slottedModels]; expect(merged).toHaveLength(2); expect(merged[0]).toMatchObject({ id: 'gpt-4o', name: 'GPT-4o', provider: 'OpenAI' }); expect(merged[1]).toMatchObject({ id: 'gpt-4o-mini', name: 'GPT-4o mini', provider: 'OpenAI' }); }); it('works with only prop models (no declarative children)', () => { const propModels: ModelOption[] = [ { id: 'sonnet', name: 'Claude Sonnet', provider: 'Anthropic' }, { id: 'haiku', name: 'Claude Haiku', provider: 'Anthropic' }, ]; const merged = [...propModels, ...([] as ModelOption[])]; expect(merged).toHaveLength(2); expect(merged[0].id).toBe('sonnet'); expect(merged[1].id).toBe('haiku'); }); it('parseKcModelElement produces items with the correct event payload key (id → modelId)', () => { // Confirm the parsed `id` field is what gets dispatched as `modelId` in kc-model-change. // The element fires: dispatch('kc-model-change', { modelId }) where modelId = item.id. const node = makeNode('GPT-4o', 'gpt-4o', 'OpenAI'); const parsed = parseKcModelElement(node); // The model-switcher component passes item.id as the modelId in the event. expect(parsed.id).toBe('gpt-4o'); // this becomes e.detail.modelId }); });