import { cleanup, fireEvent, render, screen } from "@testing-library/react";
import * as React from "react";
import { afterEach, describe, expect, it, vi } from "vitest";
import { defineToolcraft } from "../../schema/define-toolcraft";
import type { ToolcraftProductSceneBoundsProvider } from "../../scene";
import type { ToolcraftCanvasFrame } from "../../state/canvas-frame";
import { ToolcraftRoot } from "../app-shell/toolcraft-root";
import { useToolcraftDispatch } from "../app-shell/use-toolcraft";
import { useToolcraftCanvasFrame } from "./use-toolcraft-canvas-frame";
import {
ToolcraftProductSceneBoundsBoundary,
ToolcraftProductSceneSurface,
useToolcraftProductSceneFrame,
} from "./product-scene-surface";
const schema = defineToolcraft({
base: {
identity: { id: "runtime-test", title: "Toolcraft App" },
canvas: {
enabled: true,
size: { height: 200, unit: "px", width: 300 },
sizing: { mode: "editable-output" },
},
panels: {},
persistence: { storage: "none" },
},
modules: [],
});
afterEach(() => {
cleanup();
});
function FrameProbe(): React.JSX.Element {
const frame = useToolcraftProductSceneFrame();
return ;
}
function renderSurface(
frame: ToolcraftCanvasFrame,
boundsProvider?: ToolcraftProductSceneBoundsProvider,
) {
return {
boundsProvider,
...render(
,
),
};
}
describe("ToolcraftProductSceneSurface", () => {
it("uses the centered finite artboard when no provider is declared", () => {
const { container } = renderSurface({
kind: "finite",
size: { height: 200, unit: "px", width: 300 },
});
expect(screen.getByTestId("frame").textContent).toBe(
JSON.stringify({
kind: "ready",
rect: { height: 200, width: 300, x: -150, y: -100 },
}),
);
const productScene = container.querySelector(
"[data-toolcraft-product-scene]",
);
expect(productScene?.dataset.toolcraftProductSceneStatus).toBe("ready");
expect(productScene?.style.cssText).toBe(
"height: 200px; left: -150px; top: -100px; width: 300px;",
);
});
it("uses one provider frame and host across finite, infinite, and finite", () => {
const probeMount = vi.fn();
const boundsProvider = vi.fn(() => [
{ height: 128, width: 192, x: -96, y: -64 },
]);
function StatefulProbe(): React.JSX.Element {
const productFrame = useToolcraftProductSceneFrame();
const [mountNumber] = React.useState(() => {
probeMount();
return probeMount.mock.calls.length;
});
return (
);
}
function ModeControls(): React.JSX.Element {
const dispatch = useToolcraftDispatch();
const setMode = (infinite: boolean) =>
dispatch({
target: "canvas.infinity",
type: "controls.setValue",
value: infinite,
});
return (
<>
>
);
}
function SurfaceHarness(): React.JSX.Element {
const frame = useToolcraftCanvasFrame();
return (
);
}
const view = render(
,
);
const finiteHost = view.container.querySelector(
"[data-toolcraft-product-scene]",
);
const finiteProbe = screen.getByTestId("stateful-product-probe");
const exactStyle =
"height: 128px; left: -96px; top: -64px; width: 192px;";
expect(finiteHost?.style.cssText).toBe(exactStyle);
expect(finiteProbe.textContent).toBe(
`1:${JSON.stringify({
kind: "ready",
rect: { height: 128, width: 192, x: -96, y: -64 },
})}`,
);
expect(boundsProvider).toHaveBeenCalledTimes(1);
fireEvent.click(screen.getByRole("button", { name: "Enable Infinity" }));
expect(
view.container.querySelector("[data-toolcraft-product-scene]"),
).toBe(finiteHost);
expect(screen.getByTestId("stateful-product-probe")).toBe(finiteProbe);
expect(finiteHost?.style.cssText).toBe(exactStyle);
expect(finiteProbe.textContent).toContain('1:{"kind":"ready"');
fireEvent.click(screen.getByRole("button", { name: "Disable Infinity" }));
expect(
view.container.querySelector("[data-toolcraft-product-scene]"),
).toBe(finiteHost);
expect(screen.getByTestId("stateful-product-probe")).toBe(finiteProbe);
expect(finiteHost?.style.cssText).toBe(exactStyle);
expect(probeMount).toHaveBeenCalledTimes(1);
expect(
boundsProvider.mock.calls.map(
([context]) => context.state.canvas.mode,
),
).toEqual(["finite", "infinite", "finite"]);
});
it("positions one infinite product surface from the exact provider union", () => {
const boundsProvider = vi.fn(() => [
{ height: 50, width: 80, x: -40, y: -25 },
{ height: 20, width: 30, x: 30, y: 15 },
]);
const { container } = renderSurface({ kind: "infinite" }, boundsProvider);
const productScene = container.querySelector(
"[data-toolcraft-product-scene]",
);
expect(boundsProvider).toHaveBeenCalledTimes(1);
expect(productScene?.dataset.toolcraftProductSceneStatus).toBe("ready");
expect(productScene?.style.cssText).toContain("height: 60px");
expect(productScene?.style.cssText).toContain("left: -40px");
expect(productScene?.style.cssText).toContain("top: -25px");
expect(productScene?.style.cssText).toContain("width: 100px");
expect(screen.getByTestId("frame").textContent).toBe(
JSON.stringify({
kind: "ready",
rect: { height: 60, width: 100, x: -40, y: -25 },
}),
);
});
it("keeps hook consumers stable when state changes without changing bounds", () => {
const renderProbe = vi.fn();
function StableFrameProbe(): React.JSX.Element {
renderProbe();
const dispatch = useToolcraftDispatch();
useToolcraftProductSceneFrame();
return (
);
}
const boundsProvider = vi.fn(() => [
{ height: 60, width: 100, x: -40, y: -25 },
]);
render(
,
);
expect(renderProbe).toHaveBeenCalledTimes(1);
fireEvent.click(screen.getByRole("button", { name: "Pan" }));
expect(boundsProvider).toHaveBeenCalledTimes(2);
expect(renderProbe).toHaveBeenCalledTimes(1);
});
it("keeps the resolved product scene stable across local parent rerenders", () => {
const boundsProvider = vi.fn(() => [
{ height: 60, width: 100, x: -40, y: -25 },
]);
const probeMount = vi.fn();
function StatefulProbe(): React.JSX.Element {
const [mountNumber] = React.useState(() => {
probeMount();
return probeMount.mock.calls.length;
});
return ;
}
function LocalRerenderParent(): React.JSX.Element {
const frame = useToolcraftCanvasFrame();
const [, rerender] = React.useReducer((count: number) => count + 1, 0);
return (
<>
>
);
}
const { container } = render(
,
);
const productHost = container.querySelector(
"[data-toolcraft-product-scene]",
);
const probe = screen.getByTestId("local-rerender-probe");
expect(boundsProvider).toHaveBeenCalledTimes(1);
fireEvent.click(screen.getByRole("button", { name: "Rerender parent" }));
expect(boundsProvider).toHaveBeenCalledTimes(1);
expect(
container.querySelector("[data-toolcraft-product-scene]"),
).toBe(productHost);
expect(screen.getByTestId("local-rerender-probe")).toBe(probe);
expect(screen.getByTestId("local-rerender-probe").textContent).toBe("1");
expect(probeMount).toHaveBeenCalledTimes(1);
});
it.each([
{
expectedKind: "empty",
provider: () => [],
},
{
expectedKind: "unavailable",
provider: () => [{ height: 20, width: Number.NaN, x: 0, y: 0 }],
},
{
expectedKind: "unavailable",
provider: () => {
throw new Error("broken provider");
},
},
])(
"clears a ready host when declared bounds become $expectedKind",
({ expectedKind, provider }) => {
const readyProvider = vi.fn(() => [
{ height: 128, width: 192, x: -96, y: -64 },
]);
const renderTree = (
boundsProvider: ToolcraftProductSceneBoundsProvider,
) => (
);
const view = render(renderTree(readyProvider));
const productScene = view.container.querySelector(
"[data-toolcraft-product-scene]",
);
expect(productScene?.style.width).toBe("192px");
expect(productScene?.style.height).toBe("128px");
view.rerender(renderTree(vi.fn(provider)));
expect(productScene?.dataset.toolcraftProductSceneStatus).toBe(
expectedKind,
);
expect(productScene?.style.width).toBe("");
expect(productScene?.style.height).toBe("");
expect(screen.getByTestId("frame").textContent).toBe(
JSON.stringify({ kind: expectedKind, rect: null }),
);
expect(
view.container.querySelector("[data-toolcraft-product-scene]"),
).toBe(productScene);
},
);
it("fails directly when the frame hook escapes the product scene", () => {
expect(() => render()).toThrow(
"useToolcraftProductSceneFrame must be used inside Toolcraft product scene content",
);
});
});