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);
});
});