import { beforeEach, describe, expect, it, vi } from 'vitest'; import { Accordion } from './accordion'; import { ACCORDION_CHANGE_EVENT, type AccordionChangeEvent } from './accordion.types'; const MARKUP = `
Panel A
Panel B
`; 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-accordion-trigger]')); } describe('Accordion', () => { beforeEach(() => { document.body.innerHTML = ''; }); it('sets ARIA attributes and hides panels on init', () => { const root = setup(); new Accordion(root); const [trigger] = triggers(root); const panel = root.querySelector('[data-c42-accordion-panel]')!; expect(trigger.getAttribute('aria-expanded')).toBe('false'); expect(trigger.getAttribute('aria-controls')).toBe(panel.id); expect(panel.getAttribute('aria-labelledby')).toBe(trigger.id); expect(panel.getAttribute('aria-hidden')).toBe('true'); }); it('throws when an item is missing a trigger or panel', () => { document.body.innerHTML = '
'; const root = document.body.firstElementChild as HTMLElement; expect(() => new Accordion(root)).toThrow(/trigger and a panel/); }); it('opens a panel on trigger click', () => { const root = setup(); new Accordion(root); const [trigger] = triggers(root); trigger.click(); expect(trigger.getAttribute('aria-expanded')).toBe('true'); expect(root.querySelector('[data-c42-accordion-item]')!.getAttribute('data-state')).toBe('open'); }); it('closes other panels in single mode', () => { const root = setup(); new Accordion(root, { multiple: false }); const [first, second] = triggers(root); first.click(); second.click(); expect(first.getAttribute('aria-expanded')).toBe('false'); expect(second.getAttribute('aria-expanded')).toBe('true'); }); it('keeps multiple panels open in multiple mode', () => { const root = setup(); new Accordion(root, { multiple: true }); const [first, second] = triggers(root); first.click(); second.click(); expect(first.getAttribute('aria-expanded')).toBe('true'); expect(second.getAttribute('aria-expanded')).toBe('true'); }); it('honors defaultValue', () => { const root = setup(); new Accordion(root, { defaultValue: 'b' }); const [, second] = triggers(root); expect(second.getAttribute('aria-expanded')).toBe('true'); }); it('respects collapsible=false on the last open panel', () => { const root = setup(); new Accordion(root, { collapsible: false, defaultValue: 'a' }); const [first] = triggers(root); first.click(); expect(first.getAttribute('aria-expanded')).toBe('true'); }); it('emits change events with the open values', () => { const root = setup(); const acc = new Accordion(root, { multiple: true }); const spy = vi.fn(); acc.on(ACCORDION_CHANGE_EVENT, spy); triggers(root)[0].click(); expect(spy).toHaveBeenCalledOnce(); expect(spy.mock.calls[0][0].detail.value).toEqual(['a']); }); it('moves focus with arrow keys', () => { const root = setup(); new Accordion(root); const [first, second] = triggers(root); first.focus(); first.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true })); expect(document.activeElement).toBe(second); }); it('removes listeners on destroy', () => { const root = setup(); const acc = new Accordion(root); acc.destroy(); const [trigger] = triggers(root); trigger.click(); expect(trigger.getAttribute('aria-expanded')).toBe('false'); }); });