import { cleanup, fireEvent, render, screen } from "@testing-library/react";
import { afterEach, describe, expect, it, vi } from "vitest";
import { defineToolcraft } from "../../schema/define-toolcraft";
import type { ToolcraftCanvasFrame } from "../../state/canvas-frame";
import { ToolcraftRoot } from "../app-shell/toolcraft-root";
import { useToolcraftDispatch } from "../app-shell/use-toolcraft";
import {
ToolcraftProductSceneBoundsBoundary,
ToolcraftProductSceneSurface,
useToolcraftProductSceneFrame,
} from "./product-scene-surface";
const schema = defineToolcraft({
canvas: {
enabled: true,
size: { height: 200, unit: "px", width: 300 },
sizing: { mode: "editable-output" },
},
panels: {},
persistence: { storage: "none" },
});
afterEach(() => {
cleanup();
});
function FrameProbe(): React.JSX.Element {
const frame = useToolcraftProductSceneFrame();
return ;
}
function renderSurface(
frame: ToolcraftCanvasFrame,
boundsProvider = vi.fn(() => [
{ height: 50, width: 80, x: -40, y: -25 },
{ height: 20, width: 30, x: 30, y: 15 },
]),
) {
return {
boundsProvider,
...render(
,
),
};
}
describe("ToolcraftProductSceneSurface", () => {
it("keeps the finite frame canonical without invoking product bounds", () => {
const { boundsProvider, container } = renderSurface({
kind: "finite",
size: { height: 200, unit: "px", width: 300 },
});
expect(boundsProvider).not.toHaveBeenCalled();
expect(screen.getByTestId("frame").textContent).toBe(
JSON.stringify({
kind: "finite",
rect: { height: 200, width: 300, x: 0, y: 0 },
}),
);
expect(
container.querySelector("[data-toolcraft-product-scene]"),
).toBeNull();
});
it("positions one infinite product surface from the exact provider union", () => {
const { boundsProvider, container } = renderSurface({ kind: "infinite" });
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: "infinite",
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.each([
{
expectedKind: "empty",
provider: () => [],
},
{
expectedKind: "unavailable",
provider: () => [
{ height: 20, width: Number.NaN, x: 0, y: 0 },
],
},
{
expectedKind: "unavailable",
provider: () => {
throw new Error("broken provider");
},
},
])(
"reports $expectedKind without falling back to finite geometry",
({ expectedKind, provider }) => {
const { container } = renderSurface(
{ kind: "infinite" },
vi.fn(provider),
);
const productScene = container.querySelector(
"[data-toolcraft-product-scene]",
);
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 }),
);
},
);
it("fails directly when the frame hook escapes the product scene", () => {
expect(() => render()).toThrow(
"useToolcraftProductSceneFrame must be used inside Toolcraft product scene content",
);
});
});