/* @vitest-environment jsdom */ import { act } from "react"; import { createRoot, type Root } from "react-dom/client"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { PrivateAlbumSchema } from "@/data/schemas/private-zone"; import { PrivateAlbumGallery } from "../private-album-gallery"; const album = PrivateAlbumSchema.parse({ albumId: "album-1", title: "Private afternoon", imageCount: 5, images: [ { url: "/images/private-zone/photo-0.png", locked: false, index: 0 }, { url: "", locked: false, index: 1 }, { url: "/images/private-zone/locked.png", locked: true, index: 2 }, { url: "/images/private-zone/photo-3.png", locked: false, index: 3 }, { url: "/images/private-zone/photo-4.png", locked: false, index: 4 }, ], locked: false, unlocked: true, lockDetail: { locked: false }, }); describe("PrivateAlbumGallery", () => { let container: HTMLDivElement; let root: Root; let onClose: () => void; let onImageIndexChange: (index: number) => void; beforeEach(() => { (globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }) .IS_REACT_ACT_ENVIRONMENT = true; Object.defineProperties(HTMLElement.prototype, { setPointerCapture: { configurable: true, value: vi.fn(), }, hasPointerCapture: { configurable: true, value: vi.fn(() => true), }, releasePointerCapture: { configurable: true, value: vi.fn(), }, }); onClose = vi.fn(); onImageIndexChange = vi.fn(); container = document.createElement("div"); document.body.append(container); root = createRoot(container); }); afterEach(() => { act(() => root.unmount()); container.remove(); }); it("follows a drag and switches to the next available source index", () => { renderGallery(0); const viewport = getViewport(); Object.defineProperty(viewport, "clientWidth", { configurable: true, value: 390, }); dispatchPointer(viewport, "pointerdown", 300); dispatchPointer(viewport, "pointermove", 200); expect(viewport.querySelector("[aria-hidden=false]")?.style.getPropertyValue("--gallery-drag-offset")).toBe("-100px"); dispatchPointer(viewport, "pointerup", 200); expect(onImageIndexChange).toHaveBeenCalledWith(3); expect(container.textContent).toContain("2 / 3"); }); it("snaps back on pointer cancellation without changing images", () => { renderGallery(3); const viewport = getViewport(); Object.defineProperty(viewport, "clientWidth", { configurable: true, value: 390, }); dispatchPointer(viewport, "pointerdown", 300); dispatchPointer(viewport, "pointermove", 200); dispatchPointer(viewport, "pointercancel", 200); expect(onImageIndexChange).not.toHaveBeenCalled(); expect(container.textContent).toContain("2 / 3"); }); it("uses the same available indexes for controls and keyboard navigation", () => { renderGallery(3); act(() => { container .querySelector('button[aria-label="Next photo"]') ?.click(); }); expect(onImageIndexChange).toHaveBeenLastCalledWith(4); act(() => { document.dispatchEvent( new KeyboardEvent("keydown", { key: "ArrowLeft", bubbles: true }), ); }); expect(onImageIndexChange).toHaveBeenLastCalledWith(3); act(() => { document.dispatchEvent( new KeyboardEvent("keydown", { key: "Escape", bubbles: true }), ); }); expect(onClose).toHaveBeenCalledOnce(); }); function renderGallery(imageIndex: number): void { act(() => { root.render( , ); }); } function getViewport(): HTMLDivElement { const viewport = container.querySelector( '[data-testid="private-album-gallery-viewport"]', ); if (!viewport) throw new Error("Missing Gallery viewport"); return viewport; } }); function dispatchPointer( target: HTMLElement, type: "pointerdown" | "pointermove" | "pointerup" | "pointercancel", clientX: number, ): void { const event = new MouseEvent(type, { bubbles: true, button: 0, clientX, }); Object.defineProperty(event, "pointerId", { value: 1 }); act(() => target.dispatchEvent(event)); }