import { cleanup, createEvent, fireEvent, screen } from "@testing-library/react"; import { afterEach, describe, expect, it, vi } from "vitest"; import { defineToolcraft } from "../../schema/define-toolcraft"; import { CanvasStateProbe, renderCanvasShell } from "./canvas-shell-test-utils"; afterEach(() => { cleanup(); window.localStorage.clear(); vi.restoreAllMocks(); }); function pressSpace(target: Document | HTMLElement = document) { const event = createEvent.keyDown(target, { key: " ", code: "Space" }); fireEvent(target, event); return event; } function drag(canvas: HTMLElement) { fireEvent.pointerDown(canvas, { button: 0, clientX: 10, clientY: 20, pointerId: 1, pointerType: "mouse" }); fireEvent.pointerMove(canvas, { clientX: 40, clientY: 70, pointerId: 1, pointerType: "mouse" }); } describe("Canvas Space hand tool", () => { it("requires Space for primary mouse pan and changes ready/dragging/idle cursors", () => { renderCanvasShell({ afterCanvas: }); const canvas = screen.getByRole("application", { name: "Canvas viewport" }); expect(canvas.dataset.canvasPanState).toBe("idle"); drag(canvas); fireEvent.pointerUp(canvas, { pointerId: 1 }); expect(screen.getByTestId("canvas-offset").textContent).toBe("0,0"); expect(pressSpace().defaultPrevented).toBe(true); expect(canvas.dataset.canvasPanState).toBe("ready"); drag(canvas); expect(canvas.dataset.canvasPanState).toBe("dragging"); fireEvent.keyUp(document, { key: " ", code: "Space" }); expect(canvas.dataset.canvasPanState).toBe("dragging"); fireEvent.pointerUp(canvas, { pointerId: 1 }); expect(canvas.dataset.canvasPanState).toBe("idle"); expect(screen.getByTestId("canvas-offset").textContent).toBe("30,50"); }); it("captures Space drag before product interaction and returns to the hand on release", () => { const productPointerDown = vi.fn((event: React.PointerEvent) => event.stopPropagation()); renderCanvasShell({ children:
, afterCanvas: , }); const canvas = screen.getByRole("application", { name: "Canvas viewport" }); const product = screen.getByTestId("product"); drag(product); fireEvent.pointerUp(product, { pointerId: 1 }); expect(productPointerDown).toHaveBeenCalledTimes(1); expect(screen.getByTestId("canvas-offset").textContent).toBe("0,0"); pressSpace(); drag(product); fireEvent.pointerUp(canvas, { pointerId: 1 }); expect(productPointerDown).toHaveBeenCalledTimes(1); expect(screen.getByTestId("canvas-offset").textContent).toBe("30,50"); expect(canvas.dataset.canvasPanState).toBe("ready"); }); it.each(["blur", "pointercancel", "lostpointercapture"])("ends a pan safely on %s", (ending) => { renderCanvasShell({ afterCanvas: }); const canvas = screen.getByRole("application", { name: "Canvas viewport" }); pressSpace(); drag(canvas); if (ending === "blur") fireEvent(window, new Event("blur")); else fireEvent(canvas, new PointerEvent(ending, { bubbles: true, pointerId: 1 })); expect(canvas.dataset.canvasPanState).toBe(ending === "blur" ? "idle" : "ready"); expect(screen.getByTestId("canvas-offset").textContent).toBe("30,50"); fireEvent.pointerMove(canvas, { pointerId: 1, clientX: 100, clientY: 100 }); expect(screen.getByTestId("canvas-offset").textContent).toBe("30,50"); }); it("preserves Space in text editors and panel buttons", () => { renderCanvasShell({ afterCanvas: <>
, }); const canvas = screen.getByRole("application", { name: "Canvas viewport" }); for (const element of [screen.getByLabelText("Name"), screen.getByRole("button", { name: "Panel action" }), screen.getByTestId("editor")]) { expect(pressSpace(element).defaultPrevented).toBe(false); expect(canvas.dataset.canvasPanState).toBe("idle"); } }); it("does not enable the hand when canvas dragging is disabled", () => { const schema = defineToolcraft({ base: { identity: { id: "fixed", title: "Fixed" }, canvas: { enabled: true, draggable: false }, panels: {} }, modules: [] }); renderCanvasShell({ schema, afterCanvas: }); const canvas = screen.getByRole("application", { name: "Canvas viewport" }); expect(pressSpace().defaultPrevented).toBe(false); drag(canvas); fireEvent.pointerUp(canvas, { pointerId: 1 }); expect(canvas.dataset.canvasPanState).toBe("idle"); expect(screen.getByTestId("canvas-offset").textContent).toBe("0,0"); }); });