import { beforeEach, describe, expect, it, vi } from 'vitest'; import { Tabs } from './tabs'; import { TABS_CHANGE_EVENT, type TabsChangeEvent } from './tabs.types'; const MARKUP = `
Panel A
Panel B
Panel C
`; function setup(): HTMLElement { document.body.innerHTML = MARKUP; return document.body.firstElementChild as HTMLElement; } function triggers(root: HTMLElement): HTMLElement[] { return Array.from(root.querySelectorAll('[data-c42-tabs-trigger]')); } function panels(root: HTMLElement): HTMLElement[] { return Array.from(root.querySelectorAll('[data-c42-tabs-panel]')); } describe('Tabs', () => { beforeEach(() => { document.body.innerHTML = ''; }); it('wires ARIA roles and selects the first tab by default', () => { const root = setup(); new Tabs(root); const [first, second] = triggers(root); const [panelA] = panels(root); expect(root.querySelector('[data-c42-tabs-list]')!.getAttribute('role')).toBe('tablist'); expect(first.getAttribute('role')).toBe('tab'); expect(first.getAttribute('aria-selected')).toBe('true'); expect(first.getAttribute('aria-controls')).toBe(panelA.id); expect(first.getAttribute('tabindex')).toBe('0'); expect(second.getAttribute('tabindex')).toBe('-1'); expect(panelA.getAttribute('role')).toBe('tabpanel'); expect(panelA.hasAttribute('hidden')).toBe(false); }); it('honors defaultValue', () => { const root = setup(); new Tabs(root, { defaultValue: 'b' }); const [, second] = triggers(root); expect(second.getAttribute('aria-selected')).toBe('true'); }); it('throws when the list is missing', () => { document.body.innerHTML = '
'; const root = document.body.firstElementChild as HTMLElement; expect(() => new Tabs(root)).toThrow(/data-c42-tabs-list/); }); it('throws when a panel is missing for a tab', () => { document.body.innerHTML = `
`; const root = document.body.firstElementChild as HTMLElement; expect(() => new Tabs(root)).toThrow(/No panel found/); }); it('selects a tab on click and toggles panel visibility', () => { const root = setup(); new Tabs(root); const [, second] = triggers(root); const [panelA, panelB] = panels(root); second.click(); expect(second.getAttribute('aria-selected')).toBe('true'); expect(panelB.hasAttribute('hidden')).toBe(false); expect(panelA.hasAttribute('hidden')).toBe(true); }); it('moves focus and auto-activates with arrow keys', () => { const root = setup(); new Tabs(root); const [first, second] = triggers(root); first.focus(); first.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowRight', bubbles: true })); expect(document.activeElement).toBe(second); expect(second.getAttribute('aria-selected')).toBe('true'); }); it('wraps focus from last to first with ArrowRight', () => { const root = setup(); new Tabs(root); const all = triggers(root); const last = all[all.length - 1]!; last.focus(); last.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowRight', bubbles: true })); expect(document.activeElement).toBe(all[0]); }); it('only moves focus in manual mode until Enter', () => { const root = setup(); new Tabs(root, { activationMode: 'manual' }); const [first, second] = triggers(root); first.focus(); first.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowRight', bubbles: true })); expect(document.activeElement).toBe(second); expect(second.getAttribute('aria-selected')).toBe('false'); second.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true })); expect(second.getAttribute('aria-selected')).toBe('true'); }); it('uses vertical arrow keys when orientation is vertical', () => { const root = setup(); new Tabs(root, { orientation: 'vertical' }); const [first, second] = triggers(root); expect(root.querySelector('[data-c42-tabs-list]')!.getAttribute('aria-orientation')).toBe( 'vertical', ); first.focus(); first.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true })); expect(document.activeElement).toBe(second); }); it('emits change events with the selected value', () => { const root = setup(); const tabs = new Tabs(root); const spy = vi.fn(); tabs.on(TABS_CHANGE_EVENT, spy); triggers(root)[1].click(); expect(spy).toHaveBeenCalledOnce(); expect(spy.mock.calls[0][0].detail.value).toBe('b'); }); it('removes listeners on destroy', () => { const root = setup(); const tabs = new Tabs(root); tabs.destroy(); const [, second] = triggers(root); second.click(); expect(second.getAttribute('aria-selected')).toBe('false'); }); });