import { beforeEach, describe, expect, it, vi } from 'vitest';
import { Dropdown } from './dropdown';
import { DROPDOWN_OPEN_EVENT } from './dropdown.types';
const MARKUP = `
`;
function setup(): HTMLElement {
document.body.innerHTML = MARKUP;
return document.body.firstElementChild as HTMLElement;
}
function items(root: HTMLElement): HTMLElement[] {
return Array.from(root.querySelectorAll('[data-c42-dropdown-item]'));
}
describe('Dropdown', () => {
beforeEach(() => {
document.body.innerHTML = '';
});
it('wires ARIA roles and hides the menu on init', () => {
const root = setup();
new Dropdown(root);
const trigger = root.querySelector('[data-c42-dropdown-trigger]')!;
const menu = root.querySelector('[data-c42-dropdown-menu]')!;
expect(trigger.getAttribute('aria-haspopup')).toBe('menu');
expect(trigger.getAttribute('aria-expanded')).toBe('false');
expect(trigger.getAttribute('aria-controls')).toBe(menu.id);
expect(menu.getAttribute('role')).toBe('menu');
expect(menu.hasAttribute('hidden')).toBe(true);
expect(items(root)[0].getAttribute('role')).toBe('menuitem');
});
it('toggles open and closed on trigger click', () => {
const root = setup();
const dropdown = new Dropdown(root);
const trigger = root.querySelector('[data-c42-dropdown-trigger]')!;
trigger.click();
expect(dropdown.isOpen).toBe(true);
expect(trigger.getAttribute('aria-expanded')).toBe('true');
trigger.click();
expect(dropdown.isOpen).toBe(false);
});
it('navigates items with arrow keys', () => {
const root = setup();
const dropdown = new Dropdown(root);
dropdown.openMenu();
const menu = root.querySelector('[data-c42-dropdown-menu]')!;
const [first, second] = items(root);
first.focus();
menu.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true }));
expect(document.activeElement).toBe(second);
});
it('closes on Escape and returns focus to the trigger', () => {
const root = setup();
const dropdown = new Dropdown(root);
const trigger = root.querySelector('[data-c42-dropdown-trigger]')!;
dropdown.openMenu();
const menu = root.querySelector('[data-c42-dropdown-menu]')!;
menu.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }));
expect(dropdown.isOpen).toBe(false);
expect(document.activeElement).toBe(trigger);
});
it('closes on outside pointerdown', () => {
const root = setup();
const dropdown = new Dropdown(root);
dropdown.openMenu();
document.dispatchEvent(new MouseEvent('pointerdown', { bubbles: true }));
expect(dropdown.isOpen).toBe(false);
});
it('emits open event', () => {
const root = setup();
const dropdown = new Dropdown(root);
const spy = vi.fn();
dropdown.on(DROPDOWN_OPEN_EVENT, spy);
dropdown.openMenu();
expect(spy).toHaveBeenCalledOnce();
});
it('stops reacting after destroy', () => {
const root = setup();
const dropdown = new Dropdown(root);
dropdown.destroy();
const trigger = root.querySelector('[data-c42-dropdown-trigger]')!;
trigger.click();
expect(dropdown.isOpen).toBe(false);
});
});