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