149 lines
5.4 KiB
TypeScript
149 lines
5.4 KiB
TypeScript
/**
|
||
* @vitest-environment jsdom
|
||
*/
|
||
import { readFileSync } from 'node:fs';
|
||
import { dirname, join } from 'node:path';
|
||
import { fileURLToPath } from 'node:url';
|
||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||
|
||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||
const SCRIPT_PATH = join(__dirname, '../../static/assets/js/mobile-menu-helper.js');
|
||
|
||
function setViewportWidth(width: number) {
|
||
Object.defineProperty(window, 'innerWidth', {
|
||
value: width,
|
||
writable: true,
|
||
configurable: true,
|
||
});
|
||
}
|
||
|
||
function createNavDOM() {
|
||
const nav = document.createElement('nav');
|
||
nav.id = 'nav';
|
||
|
||
const checkbox = document.createElement('input');
|
||
checkbox.type = 'checkbox';
|
||
checkbox.id = 'nav-toggle';
|
||
checkbox.className = 'nav-toggle-input';
|
||
nav.appendChild(checkbox);
|
||
|
||
const header = document.createElement('div');
|
||
header.className = 'mobile-nav-header';
|
||
const toggleButton = document.createElement('button');
|
||
toggleButton.type = 'button';
|
||
toggleButton.id = 'nav-toggle-button';
|
||
toggleButton.setAttribute('aria-controls', 'nav-menu');
|
||
toggleButton.setAttribute('aria-expanded', 'false');
|
||
const label = document.createElement('label');
|
||
label.id = 'nav-toggle-label';
|
||
label.htmlFor = 'nav-toggle';
|
||
label.className = 'nav-toggle';
|
||
toggleButton.appendChild(label);
|
||
header.appendChild(toggleButton);
|
||
nav.appendChild(header);
|
||
|
||
const menu = document.createElement('div');
|
||
menu.id = 'nav-menu';
|
||
menu.className = 'nav-menu container';
|
||
const list = document.createElement('ul');
|
||
list.className = 'nav-list';
|
||
const item = document.createElement('li');
|
||
item.className = 'nav-item';
|
||
const link = document.createElement('a');
|
||
link.href = '#test';
|
||
link.className = 'nav-link';
|
||
link.textContent = 'Test';
|
||
item.appendChild(link);
|
||
list.appendChild(item);
|
||
menu.appendChild(list);
|
||
nav.appendChild(menu);
|
||
|
||
document.body.appendChild(nav);
|
||
return { nav, menu, toggleButton, label, checkbox, item, link };
|
||
}
|
||
|
||
describe('mobile-menu-helper.js', () => {
|
||
let dom: ReturnType<typeof createNavDOM>;
|
||
|
||
beforeEach(() => {
|
||
setViewportWidth(400); // mobile
|
||
dom = createNavDOM();
|
||
const code = readFileSync(SCRIPT_PATH, 'utf8');
|
||
|
||
eval(code);
|
||
document.dispatchEvent(new Event('DOMContentLoaded'));
|
||
});
|
||
|
||
afterEach(() => {
|
||
dom.nav.remove();
|
||
vi.restoreAllMocks();
|
||
});
|
||
|
||
it('sets aria-hidden on menu when viewport is mobile and menu is closed', () => {
|
||
expect(dom.menu.getAttribute('aria-hidden')).toBe('true');
|
||
expect(dom.toggleButton.getAttribute('aria-expanded')).toBe('false');
|
||
});
|
||
|
||
it('sets inert and tabindex="-1" on links when menu is closed on mobile', () => {
|
||
expect(dom.menu.hasAttribute('inert')).toBe(true);
|
||
expect(dom.link.getAttribute('tabindex')).toBe('-1');
|
||
});
|
||
|
||
it('sets aria-hidden to false when menu is open on mobile', () => {
|
||
dom.checkbox.checked = true;
|
||
dom.checkbox.dispatchEvent(new Event('change', { bubbles: true }));
|
||
|
||
expect(dom.menu.getAttribute('aria-hidden')).toBe('false');
|
||
expect(dom.toggleButton.getAttribute('aria-expanded')).toBe('true');
|
||
});
|
||
|
||
it('removes inert and link tabindex when menu is open on mobile', () => {
|
||
dom.checkbox.checked = true;
|
||
dom.checkbox.dispatchEvent(new Event('change', { bubbles: true }));
|
||
|
||
expect(dom.menu.hasAttribute('inert')).toBe(false);
|
||
expect(dom.link.hasAttribute('tabindex')).toBe(false);
|
||
});
|
||
|
||
it('removes aria-hidden and inert from menu when viewport is desktop', () => {
|
||
setViewportWidth(1024);
|
||
window.dispatchEvent(new Event('resize'));
|
||
|
||
expect(dom.menu.hasAttribute('aria-hidden')).toBe(false);
|
||
expect(dom.menu.hasAttribute('inert')).toBe(false);
|
||
expect(dom.link.hasAttribute('tabindex')).toBe(false);
|
||
});
|
||
|
||
it('adds aria-hidden and inert when resizing from desktop to mobile', () => {
|
||
setViewportWidth(1024);
|
||
window.dispatchEvent(new Event('resize'));
|
||
expect(dom.menu.hasAttribute('aria-hidden')).toBe(false);
|
||
expect(dom.menu.hasAttribute('inert')).toBe(false);
|
||
|
||
setViewportWidth(400);
|
||
window.dispatchEvent(new Event('resize'));
|
||
expect(dom.menu.getAttribute('aria-hidden')).toBe('true');
|
||
expect(dom.menu.hasAttribute('inert')).toBe(true);
|
||
expect(dom.link.getAttribute('tabindex')).toBe('-1');
|
||
});
|
||
|
||
it('closes menu and syncs aria when a menu item is clicked', () => {
|
||
dom.checkbox.checked = true;
|
||
dom.checkbox.dispatchEvent(new Event('change', { bubbles: true }));
|
||
expect(dom.toggleButton.getAttribute('aria-expanded')).toBe('true');
|
||
|
||
dom.item.click();
|
||
|
||
expect(dom.checkbox.checked).toBe(false);
|
||
expect(dom.toggleButton.getAttribute('aria-expanded')).toBe('false');
|
||
expect(dom.menu.getAttribute('aria-hidden')).toBe('true');
|
||
expect(dom.menu.hasAttribute('inert')).toBe(true);
|
||
expect(dom.link.getAttribute('tabindex')).toBe('-1');
|
||
});
|
||
|
||
// Keyboard open (Enter/Space on toggle button) is not asserted here: jsdom’s KeyboardEvent
|
||
// often does not set e.key, so the keydown handler may not run. Opening and sync are covered
|
||
// by “sets aria-hidden to false when menu is open” and “removes inert and link tabindex when
|
||
// menu is open”; keyboard open can be covered in e2e.
|
||
});
|