import { beforeEach, describe, expect, it, vi } from 'vitest';
import { Accordion } from './accordion';
import { ACCORDION_CHANGE_EVENT, type AccordionChangeEvent } from './accordion.types';
const MARKUP = `
`;
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');
});
});