import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { Modal } from './modal';
import { MODAL_CLOSE_EVENT, MODAL_OPEN_EVENT } from './modal.types';
const MARKUP = `
`;
function setup(): HTMLElement {
document.body.innerHTML = MARKUP;
return document.body.firstElementChild as HTMLElement;
}
describe('Modal', () => {
beforeEach(() => {
document.body.innerHTML = '';
});
afterEach(() => {
document.body.style.overflow = '';
});
it('wires dialog ARIA and stays hidden on init', () => {
const root = setup();
new Modal(root);
const content = root.querySelector('[data-c42-modal-content]')!;
expect(content.getAttribute('role')).toBe('dialog');
expect(content.getAttribute('aria-modal')).toBe('true');
expect(root.hasAttribute('hidden')).toBe(true);
});
it('throws without content', () => {
document.body.innerHTML = '';
const root = document.body.firstElementChild as HTMLElement;
expect(() => new Modal(root)).toThrow(/content element/);
});
it('opens, locks scroll and focuses first focusable', () => {
const root = setup();
const modal = new Modal(root);
modal.openModal();
expect(modal.isOpen).toBe(true);
expect(root.dataset.state).toBe('open');
expect(document.body.style.overflow).toBe('hidden');
expect(document.activeElement).toBe(root.querySelector('[data-c42-modal-close]'));
});
it('closes on overlay click and unlocks scroll', () => {
const root = setup();
const modal = new Modal(root);
modal.openModal();
root.querySelector('[data-c42-modal-overlay]')!.click();
expect(modal.isOpen).toBe(false);
expect(document.body.style.overflow).toBe('');
});
it('closes on Escape', () => {
const root = setup();
const modal = new Modal(root);
modal.openModal();
document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape' }));
expect(modal.isOpen).toBe(false);
});
it('closes via a close button', () => {
const root = setup();
const modal = new Modal(root);
modal.openModal();
root.querySelector('[data-c42-modal-close]')!.click();
expect(modal.isOpen).toBe(false);
});
it('restores focus to the previously focused element', () => {
document.body.innerHTML = `${MARKUP}`;
const opener = document.getElementById('opener') as HTMLElement;
const root = document.querySelector('[data-c42-modal]')!;
opener.focus();
const modal = new Modal(root);
modal.openModal();
modal.close();
expect(document.activeElement).toBe(opener);
});
it('respects closeOnEscape=false', () => {
const root = setup();
const modal = new Modal(root, { closeOnEscape: false });
modal.openModal();
document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape' }));
expect(modal.isOpen).toBe(true);
});
it('emits open and close events', () => {
const root = setup();
const modal = new Modal(root);
const onOpen = vi.fn();
const onClose = vi.fn();
modal.on(MODAL_OPEN_EVENT, onOpen);
modal.on(MODAL_CLOSE_EVENT, onClose);
modal.openModal();
modal.close();
expect(onOpen).toHaveBeenCalledOnce();
expect(onClose).toHaveBeenCalledOnce();
});
it('supports defaultOpen', () => {
const root = setup();
const modal = new Modal(root, { defaultOpen: true });
expect(modal.isOpen).toBe(true);
});
});