import { cleanup, fireEvent, screen, waitFor } from "@testing-library/react"; import type * as React from "react"; import { afterEach, describe, expect, it, vi } from "vitest"; import type { ToolcraftExternalStore } from "../../state/toolcraft-external-store"; import { useToolcraftStore } from "../app-shell/toolcraft-store-context"; import { CanvasStateProbe, mockCanvasRect, renderCanvasShell, } from "./canvas-shell-test-utils"; afterEach(() => { cleanup(); vi.restoreAllMocks(); }); function CaptureStore({ capture, }: { capture: (store: ToolcraftExternalStore) => void; }): null { capture(useToolcraftStore()); return null; } function createGestureEvent( type: "gesturechange" | "gestureend" | "gesturestart", properties: { clientX?: number; clientY?: number; scale?: number } = {}, ): Event { const event = new Event(type, { bubbles: true, cancelable: true }); Object.defineProperties( event, Object.fromEntries( Object.entries(properties).map(([key, value]) => [ key, { configurable: true, value }, ]), ), ); return event; } describe("CanvasShell pinch interactions", () => { it("keeps a native browser gesture pinch inside the canvas viewport", async () => { let store: ToolcraftExternalStore | undefined; renderCanvasShell({ afterCanvas: ( <> { store = value; }} /> ), frameProps: { "data-slot": "toolcraft-runtime-app", } as React.HTMLAttributes, }); const canvas = screen.getByRole("application", { name: "Canvas viewport" }); const world = canvas.querySelector("[data-toolcraft-canvas-world]") as HTMLElement; mockCanvasRect(canvas); const gestureStart = createGestureEvent("gesturestart", { clientX: 600, clientY: 300, scale: 1, }); const gestureChange = createGestureEvent("gesturechange", { clientX: 600, clientY: 300, scale: 1.5, }); expect(canvas.dispatchEvent(gestureStart)).toBe(false); expect(gestureStart.defaultPrevented).toBe(true); expect(canvas.dispatchEvent(gestureChange)).toBe(false); expect(gestureChange.defaultPrevented).toBe(true); expect(store?.getState().canvas).toMatchObject({ offset: { x: -100, y: 0 }, zoom: 150, }); expect(store?.getCommittedState().canvas.zoom).toBe(100); await waitFor(() => { expect(world.style.transform).toContain("translate(-100px"); expect(world.style.transform).toContain("scale(1.5)"); }); const gestureEnd = createGestureEvent("gestureend"); expect(canvas.dispatchEvent(gestureEnd)).toBe(false); expect(gestureEnd.defaultPrevented).toBe(true); expect(store?.getCommittedState().canvas).toMatchObject({ offset: { x: -100, y: 0 }, zoom: 150, }); }); it("zooms and pans the canvas from a two-touch pinch gesture", () => { let store: ToolcraftExternalStore | undefined; renderCanvasShell({ afterCanvas: ( <> { store = value; }} /> ), children: (
event.stopPropagation()} /> ), }); const canvas = screen.getByRole("application", { name: "Canvas viewport" }); const touchSurface = screen.getByTestId("touch-owned-product"); mockCanvasRect(canvas); fireEvent.pointerDown(touchSurface, { button: 0, clientX: 300, clientY: 300, pointerId: 1, pointerType: "touch", }); fireEvent.pointerDown(touchSurface, { button: 0, clientX: 500, clientY: 300, pointerId: 2, pointerType: "touch", }); fireEvent.pointerMove(touchSurface, { clientX: 700, clientY: 300, pointerId: 2, pointerType: "touch", }); expect(store?.getState().canvas).toMatchObject({ offset: { x: 100, y: 0 }, zoom: 200, }); expect(store?.getCommittedState().canvas).toMatchObject({ offset: { x: 0, y: 0 }, zoom: 100, }); fireEvent.pointerUp(touchSurface, { pointerId: 2, pointerType: "touch" }); fireEvent.pointerUp(touchSurface, { pointerId: 1, pointerType: "touch" }); expect(screen.getByTestId("canvas-offset").textContent).toBe("100,0"); expect(screen.getByTestId("canvas-zoom").textContent).toBe("200"); expect(store?.getCommittedState().canvas).toMatchObject({ offset: { x: 100, y: 0 }, zoom: 200, }); }); });