import { beforeEach, describe, expect, it, vi } from 'vitest'; import { Nav } from './nav'; import { NAV_COLLAPSE_EVENT, NAV_SELECT_EVENT, NAV_TOGGLE_EVENT, type NavSelectEvent, type NavToggleEvent, } from './nav.types'; const MARKUP = ` `; function setup(markup = MARKUP): HTMLElement { document.body.innerHTML = markup; return document.body.firstElementChild as HTMLElement; } function trigger(root: HTMLElement, value: string): HTMLButtonElement { return root.querySelector(`[data-c42-nav-trigger][data-value="${value}"]`)!; } function group(root: HTMLElement, value: string): HTMLElement { return trigger(root, value).closest('[data-c42-nav-group]')!; } function item(root: HTMLElement, value: string): HTMLElement { return root.querySelector(`[data-c42-nav-item][data-value="${value}"]`)!; } describe('Nav', () => { beforeEach(() => { document.body.innerHTML = ''; }); it('wires aria-controls and starts groups closed', () => { const root = setup(); new Nav(root); const t = trigger(root, 'settings'); expect(t.getAttribute('aria-expanded')).toBe('false'); expect(t.getAttribute('aria-controls')).toBeTruthy(); expect(group(root, 'settings').dataset.state).toBe('closed'); }); it('toggles a group on trigger click', () => { const root = setup(); new Nav(root); trigger(root, 'settings').click(); expect(group(root, 'settings').dataset.state).toBe('open'); expect(trigger(root, 'settings').getAttribute('aria-expanded')).toBe('true'); trigger(root, 'settings').click(); expect(group(root, 'settings').dataset.state).toBe('closed'); }); it('allows multiple open groups by default', () => { const root = setup(); const nav = new Nav(root); nav.openGroup('settings'); nav.openGroup('reports'); expect(group(root, 'settings').dataset.state).toBe('open'); expect(group(root, 'reports').dataset.state).toBe('open'); }); it('closes other groups in accordion mode', () => { const root = setup(); const nav = new Nav(root, { accordion: true }); nav.openGroup('settings'); nav.openGroup('reports'); expect(group(root, 'settings').dataset.state).toBe('closed'); expect(group(root, 'reports').dataset.state).toBe('open'); }); it('marks an item active with aria-current', () => { const root = setup(); const nav = new Nav(root); nav.setActive('profile'); expect(item(root, 'profile').getAttribute('aria-current')).toBe('page'); expect(item(root, 'profile').hasAttribute('data-active')).toBe(true); expect(nav.active).toBe('profile'); }); it('expands the ancestor group of the active item', () => { const root = setup(); new Nav(root, { defaultActive: 'profile' }); expect(group(root, 'settings').dataset.state).toBe('open'); }); it('moves active state when a different item is selected', () => { const root = setup(); const nav = new Nav(root); nav.setActive('profile'); nav.setActive('team'); expect(item(root, 'profile').hasAttribute('data-active')).toBe(false); expect(item(root, 'team').getAttribute('aria-current')).toBe('page'); }); it('emits nav:select on item click', () => { const root = setup(); const nav = new Nav(root); const spy = vi.fn(); nav.on(NAV_SELECT_EVENT, spy); item(root, 'home').click(); expect(spy).toHaveBeenCalledOnce(); expect(spy.mock.calls[0][0].detail).toEqual({ value: 'home' }); }); it('emits nav:toggle on group toggle', () => { const root = setup(); const nav = new Nav(root); const spy = vi.fn(); nav.on(NAV_TOGGLE_EVENT, spy); nav.toggleGroup('settings'); expect(spy.mock.calls[0][0].detail).toEqual({ value: 'settings', open: true }); }); it('closes the focused group on Escape', () => { const root = setup(); const nav = new Nav(root); nav.openGroup('settings'); const submenu = group(root, 'settings').querySelector('[data-c42-nav-submenu]')!; submenu.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true })); expect(group(root, 'settings').dataset.state).toBe('closed'); }); it('toggles the collapsed (mini) state and emits', () => { const root = setup(); const nav = new Nav(root); const spy = vi.fn(); nav.on(NAV_COLLAPSE_EVENT, spy); expect(root.dataset.collapsed).toBe('false'); nav.toggleCollapsed(); expect(root.dataset.collapsed).toBe('true'); expect(root.querySelector('[data-c42-nav-collapse]')!.getAttribute('aria-expanded')).toBe('false'); expect(spy).toHaveBeenCalledOnce(); }); it('can start collapsed', () => { const root = setup(); const nav = new Nav(root, { collapsed: true }); expect(nav.isCollapsed).toBe(true); expect(root.dataset.collapsed).toBe('true'); }); it('removes listeners on destroy', () => { const root = setup(); const nav = new Nav(root); const spy = vi.fn(); nav.on(NAV_TOGGLE_EVENT, spy); nav.destroy(); trigger(root, 'settings').click(); expect(spy).not.toHaveBeenCalled(); }); });