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