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");
});
});