import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { Gallery } from './gallery'; import { GALLERY_CHANGE_EVENT, type GalleryChangeEvent } from './gallery.types'; const MARKUP = `
`; function setup(): HTMLElement { document.body.innerHTML = MARKUP; return document.body.firstElementChild as HTMLElement; } function items(root: HTMLElement): HTMLElement[] { return Array.from(root.querySelectorAll('[data-c42-gallery-item]')); } function image(root: HTMLElement): HTMLImageElement { return root.querySelector('[data-c42-gallery-image]')!; } describe('Gallery', () => { beforeEach(() => { document.body.innerHTML = ''; }); afterEach(() => { document.body.style.overflow = ''; }); it('throws without lightbox or image', () => { document.body.innerHTML = '
'; const root = document.body.firstElementChild as HTMLElement; expect(() => new Gallery(root)).toThrow(/lightbox and an image/); }); it('wires dialog ARIA and stays hidden on init', () => { const root = setup(); new Gallery(root); const lightbox = root.querySelector('[data-c42-gallery-lightbox]')!; expect(lightbox.getAttribute('role')).toBe('dialog'); expect(lightbox.getAttribute('aria-modal')).toBe('true'); expect(lightbox.hasAttribute('hidden')).toBe(true); }); it('opens at the clicked item and sets the image', () => { const root = setup(); const gallery = new Gallery(root); items(root)[1].click(); expect(gallery.isOpen).toBe(true); expect(gallery.index).toBe(1); expect(image(root).getAttribute('src')).toBe('full-2.jpg'); expect(image(root).getAttribute('alt')).toBe('Two'); expect(document.body.style.overflow).toBe('hidden'); }); it('navigates with next/prev buttons', () => { const root = setup(); const gallery = new Gallery(root); gallery.openAt(0); root.querySelector('[data-c42-gallery-next]')!.click(); expect(gallery.index).toBe(1); root.querySelector('[data-c42-gallery-prev]')!.click(); expect(gallery.index).toBe(0); }); it('loops by default and clamps when loop=false', () => { const root = setup(); const looping = new Gallery(root); looping.openAt(2); looping.next(); expect(looping.index).toBe(0); looping.destroy(); const clamped = new Gallery(root, { loop: false }); clamped.openAt(2); clamped.next(); expect(clamped.index).toBe(2); }); it('navigates with arrow keys and closes on Escape', () => { const root = setup(); const gallery = new Gallery(root); gallery.openAt(0); document.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowRight' })); expect(gallery.index).toBe(1); document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape' })); expect(gallery.isOpen).toBe(false); expect(document.body.style.overflow).toBe(''); }); it('closes on overlay click', () => { const root = setup(); const gallery = new Gallery(root); gallery.openAt(0); root.querySelector('[data-c42-gallery-overlay]')!.click(); expect(gallery.isOpen).toBe(false); }); it('emits change events with index and src', () => { const root = setup(); const gallery = new Gallery(root); const spy = vi.fn(); gallery.on(GALLERY_CHANGE_EVENT, spy); gallery.openAt(0); expect(spy).toHaveBeenCalled(); expect(spy.mock.calls[0][0].detail).toEqual({ index: 0, src: 'full-1.jpg' }); }); it('restores focus on close', () => { const root = setup(); const gallery = new Gallery(root); const opener = items(root)[0]; opener.focus(); gallery.openAt(0); gallery.close(); expect(document.activeElement).toBe(opener); }); });