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