import { beforeEach, describe, expect, it, vi } from 'vitest';
import { MegaMenu } from './mega-menu';
import {
MEGA_MENU_CLOSE_EVENT,
MEGA_MENU_OPEN_EVENT,
type MegaMenuOpenEvent,
} from './mega-menu.types';
const MARKUP = `
`;
function setup(): HTMLElement {
document.body.innerHTML = MARKUP;
return document.body.firstElementChild as HTMLElement;
}
function trigger(root: HTMLElement, value: string): HTMLElement {
return root.querySelector(`[data-c42-mega-menu-trigger][data-value="${value}"]`)!;
}
function item(root: HTMLElement, value: string): HTMLElement {
return trigger(root, value).closest('[data-c42-mega-menu-item]')!;
}
describe('MegaMenu', () => {
beforeEach(() => {
document.body.innerHTML = '';
});
it('wires ARIA and hides panels on init', () => {
const root = setup();
new MegaMenu(root);
const t = trigger(root, 'products');
expect(t.getAttribute('aria-haspopup')).toBe('true');
expect(t.getAttribute('aria-expanded')).toBe('false');
expect(t.getAttribute('aria-controls')).toBeTruthy();
expect(item(root, 'products').dataset.state).toBe('closed');
});
it('does not wire panel ARIA on plain-link triggers', () => {
const root = setup();
new MegaMenu(root);
expect(trigger(root, 'pricing').hasAttribute('aria-haspopup')).toBe(false);
});
it('toggles a panel on trigger click', () => {
const root = setup();
const menu = new MegaMenu(root);
trigger(root, 'products').click();
expect(menu.openValue).toBe('products');
expect(trigger(root, 'products').getAttribute('aria-expanded')).toBe('true');
trigger(root, 'products').click();
expect(menu.openValue).toBeNull();
});
it('opens on hover when openOnHover is enabled', () => {
const root = setup();
const menu = new MegaMenu(root, { openDelay: 0 });
item(root, 'products').dispatchEvent(new MouseEvent('mouseenter', { bubbles: true }));
expect(menu.openValue).toBe('products');
});
it('closes the previous panel when another opens', () => {
const root = setup();
const menu = new MegaMenu(root);
menu.open('products');
menu.open('solutions');
expect(menu.openValue).toBe('solutions');
expect(item(root, 'products').dataset.state).toBe('closed');
});
it('moves focus between triggers with arrow keys', () => {
const root = setup();
new MegaMenu(root);
const products = trigger(root, 'products');
products.focus();
products.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowRight', bubbles: true }));
expect(document.activeElement).toBe(trigger(root, 'solutions'));
trigger(root, 'solutions').dispatchEvent(
new KeyboardEvent('keydown', { key: 'ArrowLeft', bubbles: true }),
);
expect(document.activeElement).toBe(products);
});
it('wraps focus with Home and End', () => {
const root = setup();
new MegaMenu(root);
trigger(root, 'products').focus();
trigger(root, 'products').dispatchEvent(
new KeyboardEvent('keydown', { key: 'End', bubbles: true }),
);
expect(document.activeElement).toBe(trigger(root, 'pricing'));
});
it('opens the panel and focuses its first item on ArrowDown', () => {
const root = setup();
const menu = new MegaMenu(root);
const products = trigger(root, 'products');
products.focus();
products.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true }));
expect(menu.openValue).toBe('products');
expect(document.activeElement).toBe(item(root, 'products').querySelector('a'));
});
it('closes on Escape and returns focus to the trigger', () => {
const root = setup();
const menu = new MegaMenu(root);
menu.open('products');
root.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }));
expect(menu.openValue).toBeNull();
expect(document.activeElement).toBe(trigger(root, 'products'));
});
it('closes on outside pointerdown', () => {
const root = setup();
const menu = new MegaMenu(root);
menu.open('products');
document.dispatchEvent(new MouseEvent('pointerdown', { bubbles: true }));
expect(menu.openValue).toBeNull();
});
it('emits open and close events', () => {
const root = setup();
const menu = new MegaMenu(root);
const onOpen = vi.fn();
const onClose = vi.fn();
menu.on(MEGA_MENU_OPEN_EVENT, onOpen);
menu.on(MEGA_MENU_CLOSE_EVENT, onClose);
menu.open('products');
menu.closeAll();
expect(onOpen.mock.calls[0][0].detail).toEqual({ value: 'products' });
expect(onClose).toHaveBeenCalledOnce();
});
it('removes listeners on destroy', () => {
const root = setup();
const menu = new MegaMenu(root);
menu.destroy();
trigger(root, 'products').click();
expect(menu.openValue).toBeNull();
});
});