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 ( {JSON.stringify(frame)} ); } 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 {mountNumber}; } 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)", ); }); });