155 lines
4.4 KiB
TypeScript
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));
|
|
}
|