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(); }); });