import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { Drawer } from './drawer';
import { DRAWER_CLOSE_EVENT, DRAWER_OPEN_EVENT } from './drawer.types';
const MARKUP = `
`;
function setup(): HTMLElement {
document.body.innerHTML = MARKUP;
return document.body.firstElementChild as HTMLElement;
}
describe('Drawer', () => {
beforeEach(() => {
document.body.innerHTML = '';
});
afterEach(() => {
document.body.style.overflow = '';
});
it('wires dialog ARIA and stays hidden on init', () => {
const root = setup();
new Drawer(root);
const content = root.querySelector('[data-c42-drawer-content]')!;
expect(content.getAttribute('role')).toBe('dialog');
expect(content.getAttribute('aria-modal')).toBe('true');
expect(root.hasAttribute('hidden')).toBe(true);
expect(root.dataset.state).toBe('closed');
});
it('reflects the default and explicit side on data-side', () => {
const root = setup();
new Drawer(root);
expect(root.dataset.side).toBe('right');
const root2 = setup();
new Drawer(root2, { side: 'left' });
expect(root2.dataset.side).toBe('left');
});
it('throws without content', () => {
document.body.innerHTML = '';
const root = document.body.firstElementChild as HTMLElement;
expect(() => new Drawer(root)).toThrow(/content element/);
});
it('opens, locks scroll and focuses first focusable', () => {
const root = setup();
const drawer = new Drawer(root);
drawer.open();
expect(drawer.isOpen).toBe(true);
expect(root.dataset.state).toBe('open');
expect(root.hasAttribute('hidden')).toBe(false);
expect(document.body.style.overflow).toBe('hidden');
expect(document.activeElement).toBe(root.querySelector('[data-c42-drawer-close]'));
});
it('closes and toggles data-state + hidden', () => {
const root = setup();
const drawer = new Drawer(root);
drawer.open();
drawer.close();
expect(drawer.isOpen).toBe(false);
expect(root.dataset.state).toBe('closed');
expect(root.hasAttribute('hidden')).toBe(true);
expect(document.body.style.overflow).toBe('');
});
it('closes on overlay click', () => {
const root = setup();
const drawer = new Drawer(root);
drawer.open();
root.querySelector('[data-c42-drawer-overlay]')!.click();
expect(drawer.isOpen).toBe(false);
});
it('closes on Escape', () => {
const root = setup();
const drawer = new Drawer(root);
drawer.open();
document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape' }));
expect(drawer.isOpen).toBe(false);
});
it('closes via a close button', () => {
const root = setup();
const drawer = new Drawer(root);
drawer.open();
root.querySelector('[data-c42-drawer-close]')!.click();
expect(drawer.isOpen).toBe(false);
});
it('emits open and close events', () => {
const root = setup();
const drawer = new Drawer(root);
const onOpen = vi.fn();
const onClose = vi.fn();
drawer.on(DRAWER_OPEN_EVENT, onOpen);
drawer.on(DRAWER_CLOSE_EVENT, onClose);
drawer.open();
drawer.close();
expect(onOpen).toHaveBeenCalledOnce();
expect(onClose).toHaveBeenCalledOnce();
});
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-drawer]')!;
opener.focus();
const drawer = new Drawer(root);
drawer.open();
drawer.close();
expect(document.activeElement).toBe(opener);
});
it('respects closeOnEscape=false', () => {
const root = setup();
const drawer = new Drawer(root, { closeOnEscape: false });
drawer.open();
document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape' }));
expect(drawer.isOpen).toBe(true);
});
it('supports defaultOpen and is idempotent on destroy', () => {
const root = setup();
const drawer = new Drawer(root, { defaultOpen: true });
expect(drawer.isOpen).toBe(true);
drawer.destroy();
expect(drawer.isOpen).toBe(false);
expect(() => drawer.destroy()).not.toThrow();
});
});