import { cleanup, fireEvent, screen } from "@testing-library/react";
import * as React from "react";
import { afterEach, describe, expect, it, vi } from "vitest";
import {
createBackgroundSchema,
createIntrinsicMediaSchema,
createSchema,
createUploadDefaultSchema,
renderCanvasShell,
} from "./canvas-shell-test-utils";
import { useToolcraftProductSceneFrame } from "./product-scene-surface";
import { useToolcraftDispatch } from "../app-shell/use-toolcraft";
afterEach(() => {
cleanup();
window.localStorage.clear();
});
describe("CanvasShell rendering", () => {
it("uses product bounds as the live infinite preview frame", () => {
function ProductFrameProbe(): React.JSX.Element {
const frame = useToolcraftProductSceneFrame();
return (
);
}
const { container } = renderCanvasShell({
children: ,
initialState: {
canvas: {
mode: "infinite",
offset: { x: 0, y: 0 },
size: { height: 200, unit: "px", width: 300 },
zoom: 100,
},
},
productSceneBoundsProvider: () => [
{ height: 260, width: 420, x: -210, y: -130 },
],
});
const productScene = container.querySelector(
"[data-toolcraft-product-scene]",
);
expect(productScene?.style.height).toBe("260px");
expect(productScene?.style.left).toBe("-210px");
expect(productScene?.style.top).toBe("-130px");
expect(productScene?.style.width).toBe("420px");
expect(screen.getByTestId("product-frame").textContent).toBe(
JSON.stringify({
kind: "ready",
rect: { height: 260, width: 420, x: -210, y: -130 },
}),
);
});
it("renders the editable canvas at the schema size", () => {
const { container } = renderCanvasShell();
const editableCanvas = container.querySelector(
"[data-toolcraft-canvas-surface]",
);
expect(editableCanvas?.dataset.toolcraftCanvasMode).toBe("finite");
expect(editableCanvas?.style.width).toBe("300px");
expect(editableCanvas?.style.height).toBe("200px");
});
it("renders infinite mode without an editable-artboard boundary", () => {
const { container } = renderCanvasShell({
children: ,
initialState: {
canvas: {
mode: "infinite",
offset: { x: 0, y: 0 },
size: { height: 200, unit: "px", width: 300 },
zoom: 100,
},
},
});
const surface = container.querySelector(
"[data-toolcraft-canvas-surface]",
);
const sceneOrigin = container.querySelector(
"[data-toolcraft-scene-origin]",
);
expect(surface).toBeTruthy();
expect(surface?.dataset.toolcraftCanvasMode).toBe("infinite");
expect(surface?.style.width).toBe("0px");
expect(surface?.style.height).toBe("0px");
expect(surface?.className).toContain("overflow-visible");
expect(sceneOrigin).toBeTruthy();
expect(
container.querySelector("[data-toolcraft-editable-canvas]"),
).toBeNull();
expect(
container.querySelector(
"[data-slot='toolcraft-runtime-canvas']",
)?.style.backgroundColor,
).toBe("");
});
it("mounts product viewport content only in Infinity behind the transformed world", () => {
const finite = renderCanvasShell({
infiniteCanvasContent: ,
});
expect(
finite.container.querySelector(
"[data-toolcraft-infinite-canvas-content]",
),
).toBeNull();
finite.unmount();
const { container } = renderCanvasShell({
children: ,
infiniteCanvasContent: ,
initialState: {
canvas: {
mode: "infinite",
offset: { x: 42, y: -18 },
size: { height: 200, unit: "px", width: 300 },
zoom: 75,
},
},
});
const backdrop = container.querySelector(
"[data-toolcraft-infinite-canvas-content]",
);
const world = container.querySelector(
"[data-toolcraft-canvas-world]",
);
expect(backdrop).toBeTruthy();
expect(backdrop?.className).toContain("pointer-events-none");
expect(backdrop?.className).toContain("absolute");
expect(backdrop?.className).toContain("inset-0");
expect(backdrop?.style.transform).toBe("");
expect(backdrop?.nextElementSibling).toBe(world);
expect(screen.getByTestId("infinite-viewport-content")).toBeTruthy();
expect(screen.getByTestId("bounded-product-content")).toBeTruthy();
});
it("fills the complete infinite viewport with the selected Background color", () => {
const { container } = renderCanvasShell({
initialState: {
canvas: {
mode: "infinite",
offset: { x: 0, y: 0 },
size: { height: 200, unit: "px", width: 300 },
zoom: 100,
},
values: {
"appearance.background": "#D4CECA",
"export.includeBackground": true,
},
},
schema: createBackgroundSchema(),
});
const viewport = container.querySelector(
"[data-slot='toolcraft-runtime-canvas']",
);
expect(viewport?.dataset.toolcraftInfiniteBackgroundColor).toBe("#D4CECA");
expect(viewport?.style.backgroundColor).toBe("rgb(212, 206, 202)");
expect(
container.querySelector("[data-toolcraft-editable-canvas]"),
).toBeNull();
});
it("rejects structured color ingress before filling the infinite viewport", () => {
const { container } = renderCanvasShell({
initialState: {
canvas: {
mode: "infinite",
offset: { x: 0, y: 0 },
size: { height: 200, unit: "px", width: 300 },
zoom: 100,
},
values: {
"appearance.background": { hex: "#CB9672" },
"export.includeBackground": true,
},
},
schema: createBackgroundSchema(),
});
const viewport = container.querySelector(
"[data-slot='toolcraft-runtime-canvas']",
);
expect(viewport?.dataset.toolcraftInfiniteBackgroundColor).toBe("#D4CECA");
expect(viewport?.style.backgroundColor).toBe("rgb(212, 206, 202)");
});
it("does not render a fixed editable canvas for explicit intrinsic media sizing", () => {
const { container } = renderCanvasShell({
schema: createIntrinsicMediaSchema(),
});
expect(
container.querySelector("[data-toolcraft-editable-canvas]"),
).toBeNull();
});
it("renders the canvas slot inside the editable canvas when custom app content is provided", () => {
const { container } = renderCanvasShell({
children: ,
schema: createUploadDefaultSchema(),
});
const editableCanvas = container.querySelector(
"[data-toolcraft-editable-canvas]",
);
const sceneOrigin = container.querySelector(
"[data-toolcraft-scene-origin]",
);
expect(editableCanvas).toBeTruthy();
expect(sceneOrigin?.parentElement).toBe(editableCanvas);
expect(
sceneOrigin?.contains(screen.getByTestId("custom-canvas-renderer")),
).toBe(true);
expect(screen.getByTestId("custom-canvas-renderer")).toBeTruthy();
});
it("keeps one world, surface, scene origin, and product subtree when Infinity rerenders", () => {
const probeMount = vi.fn();
function StatefulProbe(): React.JSX.Element {
const [mountNumber] = React.useState(() => {
probeMount();
return probeMount.mock.calls.length;
});
return ;
}
function EnableInfinity(): React.JSX.Element {
const dispatch = useToolcraftDispatch();
return (
);
}
const { container } = renderCanvasShell({
afterCanvas: ,
children: ,
initialState: {
canvas: {
mode: "finite",
offset: { x: 12, y: -8 },
size: { height: 200, unit: "px", width: 300 },
zoom: 150,
},
},
schema: createSchema(),
});
const finiteWorld = container.querySelector(
"[data-toolcraft-canvas-world]",
);
const finiteSurface = container.querySelector(
"[data-toolcraft-canvas-surface]",
);
const finiteSceneOrigin = container.querySelector(
"[data-toolcraft-scene-origin]",
);
const finiteProbe = screen.getByTestId("stateful-probe");
expect(finiteWorld?.style.transform).toBe(
"translate(12px, -8px) scale(1.5)",
);
expect(finiteSurface?.style.left).toBe("-150px");
expect(finiteSurface?.style.top).toBe("-100px");
expect(finiteSceneOrigin?.style.left).toBe("150px");
expect(finiteSceneOrigin?.style.top).toBe("100px");
expect(finiteProbe.textContent).toBe("1");
fireEvent.click(screen.getByRole("button", { name: "Enable Infinity" }));
const infiniteWorld = container.querySelector(
"[data-toolcraft-canvas-world]",
);
const infiniteSurface = container.querySelector(
"[data-toolcraft-canvas-surface]",
);
const infiniteSceneOrigin = container.querySelector(
"[data-toolcraft-scene-origin]",
);
const infiniteProbe = screen.getByTestId("stateful-probe");
expect(infiniteWorld).toBe(finiteWorld);
expect(infiniteSurface).toBe(finiteSurface);
expect(infiniteSceneOrigin).toBe(finiteSceneOrigin);
expect(infiniteProbe).toBe(finiteProbe);
expect(infiniteWorld?.style.transform).toBe(
"translate(12px, -8px) scale(1.5)",
);
expect(infiniteSurface?.style.left).toBe("0px");
expect(infiniteSurface?.style.top).toBe("0px");
expect(infiniteSurface?.style.width).toBe("0px");
expect(infiniteSurface?.style.height).toBe("0px");
expect(infiniteSurface?.className).toContain("overflow-visible");
expect(infiniteSceneOrigin?.style.left).toBe("0px");
expect(infiniteSceneOrigin?.style.top).toBe("0px");
expect(infiniteProbe.textContent).toBe("1");
expect(probeMount).toHaveBeenCalledTimes(1);
});
it("does not render decorative canvas background dots", () => {
const { container } = renderCanvasShell();
const pattern = container.querySelector(
"[data-toolcraft-canvas-grid]",
);
const world = container.querySelector(
"[data-toolcraft-canvas-world]",
);
expect(world).toBeTruthy();
expect(pattern).toBeNull();
});
it("moves and scales the canvas world without decorative background dots", () => {
const { container } = renderCanvasShell({
initialState: {
canvas: {
offset: { x: 12, y: -8 },
size: { height: 200, unit: "px", width: 300 },
zoom: 150,
},
},
schema: createSchema(),
});
const pattern = container.querySelector(
"[data-toolcraft-canvas-grid]",
);
const world = container.querySelector(
"[data-toolcraft-canvas-world]",
);
expect(pattern).toBeNull();
expect(world?.style.transform).toBe("translate(12px, -8px) scale(1.5)");
});
it("keeps minimum zoom world transform without decorative background dots", () => {
const { container } = renderCanvasShell({
initialState: {
canvas: {
offset: { x: 3200, y: -2400 },
size: { height: 200, unit: "px", width: 300 },
zoom: 25,
},
},
schema: createSchema(),
});
const pattern = container.querySelector(
"[data-toolcraft-canvas-grid]",
);
const world = container.querySelector(
"[data-toolcraft-canvas-world]",
);
expect(pattern).toBeNull();
expect(world?.style.transform).toBe(
"translate(3200px, -2400px) scale(0.25)",
);
});
});