import { describe, test, expect } from 'vitest'; import { Tabs } from './Tabs'; const toHtml = (Tabs as any).toHtml; const tabs = [ { label: 'Overview', content: 'Overview content' }, { label: 'Features', content: 'Features content' }, { label: 'Support', content: 'Support content' }, ]; describe('Tabs.toHtml accessibility (F1.2)', () => { test('tab button container has role="tablist"', () => { const { html } = toHtml({ tabs }, ''); expect(html).toMatch(/role="tablist"/); }); test('each tab button has role="tab", aria-selected, aria-controls', () => { const { html } = toHtml({ tabs }, ''); const buttonMatches = html.match(/]*role="tab"[^>]*>/g) || []; expect(buttonMatches.length).toBe(3); expect(html).toMatch(/aria-selected="true"/); expect(html).toMatch(/aria-selected="false"/); expect(html).toMatch(/role="tab"[^>]*aria-controls="[^"]+"/); }); test('each panel has role="tabpanel" and aria-labelledby matching a tab id', () => { const { html } = toHtml({ tabs }, ''); const panelMatches = html.match(/role="tabpanel"/g) || []; expect(panelMatches.length).toBe(3); // aria-controls on the first tab button should point at an id that // actually exists as a panel's id. const controlsMatch = html.match(/role="tab"[^>]*aria-controls="([^"]+)"/); expect(controlsMatch).toBeTruthy(); const controlledId = controlsMatch![1]; expect(html).toContain(`id="${controlledId}"`); }); test('ids linking tab<->panel are deterministic (stable across repeated calls, no randomness)', () => { const { html: html1 } = toHtml({ tabs }, ''); const { html: html2 } = toHtml({ tabs }, ''); const id1 = html1.match(/role="tab"[^>]*aria-controls="([^"]+)"/)![1]; const id2 = html2.match(/role="tab"[^>]*aria-controls="([^"]+)"/)![1]; expect(id1).toBe(id2); }); test('arrow-key navigation is wired in the inline script', () => { const { html } = toHtml({ tabs }, ''); expect(html).toMatch(/ArrowRight/); expect(html).toMatch(/ArrowLeft/); }); }); describe('Tabs.toHtml deterministic + unique ids (thread node id, resolves id-collision finding)', () => { test('same node id -> identical output across calls (deterministic, no Math.random)', () => { const { html: html1 } = toHtml({ tabs }, '', 'node-tabs1'); const { html: html2 } = toHtml({ tabs }, '', 'node-tabs1'); expect(html1).toBe(html2); }); test('two instances with IDENTICAL default tab content but different node ids do not collide', () => { const { html: html1 } = toHtml({ tabs }, '', 'node-tabs1'); const { html: html2 } = toHtml({ tabs }, '', 'node-tabs2'); const id1 = html1.match(/role="tab"[^>]*aria-controls="([^"]+)"/)![1]; const id2 = html2.match(/role="tab"[^>]*aria-controls="([^"]+)"/)![1]; expect(id1).not.toBe(id2); }); test('aria-controls still matches an existing panel id after the node-id change (internal consistency preserved)', () => { const { html } = toHtml({ tabs }, '', 'node-tabs1'); const controlsMatch = html.match(/role="tab"[^>]*aria-controls="([^"]+)"/); expect(controlsMatch).toBeTruthy(); expect(html).toContain(`id="${controlsMatch![1]}"`); }); test('no nodeId (legacy 2-arg call): still deterministic across repeated calls, not random', () => { const { html: html1 } = toHtml({ tabs }, ''); const { html: html2 } = toHtml({ tabs }, ''); expect(html1).toBe(html2); }); }); describe('Tabs.craft.props includes the box-model/animation/visibility rollout props', () => { test('animation, animationDelay, and all 3 hideOn* flags are declared (blank/false defaults)', () => { const props = (Tabs as any).craft.props; expect(props).toHaveProperty('animation', ''); expect(props).toHaveProperty('animationDelay', ''); expect(props).toHaveProperty('hideOnDesktop', false); expect(props).toHaveProperty('hideOnTablet', false); expect(props).toHaveProperty('hideOnMobile', false); }); });