Files
cozsweet-frontend-nextjs/src/app/private-zone/components/__tests__/private-album-gallery.test.tsx
T

155 lines
4.4 KiB
TypeScript

/* @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<HTMLElement>("[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<HTMLButtonElement>('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(
<PrivateAlbumGallery
album={album}
imageIndex={imageIndex}
onClose={onClose}
onImageIndexChange={onImageIndexChange}
/>,
);
});
}
function getViewport(): HTMLDivElement {
const viewport = container.querySelector<HTMLDivElement>(
'[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));
}