import { cleanup, fireEvent, render, screen, waitFor, } from "@testing-library/react"; import { afterEach, describe, expect, it, vi } from "vitest"; import { defineToolcraft } from "../../schema/define-toolcraft"; import type { ToolcraftSourceAssetCoordinator } from "../../source-assets/source-asset-coordinator"; import { ControlsPanel } from "../controls-panel/controls-panel"; import { useToolcraftStore } from "./toolcraft-store-context"; import { ToolcraftRoot } from "./toolcraft-root"; import { ToolcraftSourceAssetCoordinatorContext } from "./toolcraft-source-asset-context"; import { runtimePublicApiSchema } from "./runtime-public-api-test-support"; import { useToolcraft, useToolcraftDispatch, useToolcraftSelector, } from "./use-toolcraft"; const runtimePublicApiPanelActionSchema = defineToolcraft({ canvas: { enabled: true }, panels: { controls: { sections: [ { actionGroup: "secondary", controls: { action: { actions: [ { label: "Resolve media", value: "resolve-media", variant: "default", }, ], target: "panel.actions", type: "panelActions", }, }, }, ], title: "Controls", }, }, }); function createRuntimePublicApiSourceAssetCoordinator( resolveResource?: ToolcraftSourceAssetCoordinator["resolveResource"], ): ToolcraftSourceAssetCoordinator { const idleOperation = Object.freeze({ phase: "idle" as const, target: "source", }); return { cancelTarget: vi.fn(), clearPresentationFeedback: vi.fn(), dispose: vi.fn(async () => undefined), getOperation: () => idleOperation, getPresentation: () => null, hydrateBinaryMedia: vi.fn(async () => undefined), hydrateModels: vi.fn(async () => undefined), importBatch: vi.fn(async () => ({ assetIds: [] as const, kind: "committed" as const, })), repairModel: vi.fn(async () => ({ kind: "cancelled" as const })), reportPresentationFeedback: vi.fn(), ...(resolveResource ? { resolveResource } : {}), subscribe: () => () => undefined, }; } afterEach(cleanup); describe("Toolcraft template runtime public API integration", () => { it("binds the source asset resolver into panel action context", async () => { const bytes = new Uint8Array([11, 22, 33]); const resolveResource = vi.fn< NonNullable >(async () => bytes); const coordinator = createRuntimePublicApiSourceAssetCoordinator( resolveResource, ); const signal = new AbortController().signal; let resolvedBytes: Uint8Array | null | undefined; render( { resolvedBytes = await resolveMediaResource("media:known", { signal, }); }} /> , ); fireEvent.click(screen.getByRole("button", { name: "Resolve media" })); await waitFor(() => expect(resolvedBytes).toEqual(bytes)); expect(resolveResource).toHaveBeenCalledWith("media:known", { signal }); }); it("returns null from panel action media resolution when no resolver exists", async () => { const coordinator = createRuntimePublicApiSourceAssetCoordinator(); const signal = new AbortController().signal; let resolvedBytes: Uint8Array | null | undefined; render( { resolvedBytes = await resolveMediaResource("media:missing", { signal, }); }} /> , ); fireEvent.click(screen.getByRole("button", { name: "Resolve media" })); await waitFor(() => expect(resolvedBytes).toBeNull()); }); it("keeps broad consumers live while isolating selector and dispatch consumers", () => { const renders = { broad: 0, dispatch: 0, playback: 0, value: 0, viewport: 0, }; function BroadProbe() { const { state } = useToolcraft(); renders.broad += 1; return {state.timeline.currentTimeSeconds}; } function DispatchProbe() { const dispatch = useToolcraftDispatch(); renders.dispatch += 1; return ( ); } function ValueProbe() { const value = useToolcraftSelector( (state) => state.values["selectedLayer.opacity"], ); renders.value += 1; return {String(value)}; } function PlaybackProbe() { const currentTimeSeconds = useToolcraftSelector( (state) => state.timeline.currentTimeSeconds, ); renders.playback += 1; return {currentTimeSeconds}; } function ViewportProbe() { const offset = useToolcraftSelector( (state) => ({ ...state.canvas.offset }), (previous, next) => previous.x === next.x && previous.y === next.y, ); renders.viewport += 1; return {offset.x},{offset.y}; } function TransientProbe() { const store = useToolcraftStore(); return (
); } render( , ); expect(renders).toEqual({ broad: 1, dispatch: 1, playback: 1, value: 1, viewport: 1, }); fireEvent.click(screen.getByRole("button", { name: "Set value" })); expect(screen.getByTestId("selected-value").textContent).toBe("50"); expect(renders).toEqual({ broad: 2, dispatch: 1, playback: 1, value: 2, viewport: 1, }); fireEvent.click(screen.getByRole("button", { name: "Set playback time" })); expect(screen.getByTestId("broad-time").textContent).toBe("2"); expect(screen.getByTestId("playback-time").textContent).toBe("2"); expect(renders).toEqual({ broad: 3, dispatch: 1, playback: 2, value: 2, viewport: 1, }); fireEvent.click(screen.getByRole("button", { name: "Set viewport zoom" })); expect(screen.getByTestId("viewport-offset").textContent).toBe("0,0"); expect(renders).toEqual({ broad: 4, dispatch: 1, playback: 2, value: 2, viewport: 1, }); }); });