import { defineToolcraftCustomControlType } from "../../../schema/custom-control-type"; import { act, 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 { ToolcraftExternalStore } from "../../../state/toolcraft-external-store"; import { ToolcraftRoot } from "../../app-shell/toolcraft-root"; import { useToolcraftStore } from "../../app-shell/toolcraft-store-context"; import type { ToolcraftCustomControlRendererProps } from "../control-renderers"; import { ControlsPanel } from "../controls-panel"; const extensionsSchema = defineToolcraft({ base: { identity: { id: "runtime-test", title: "Toolcraft App" }, canvas: { enabled: true }, panels: { controls: { sections: [ { id: "test-section-1", controls: { title: { applicability: { mode: "always" as const }, defaultValue: "Built in", target: "document.title", type: "text", }, live: { applicability: { mode: "always" as const }, defaultValue: 10, label: "Live", target: "document.live", type: defineToolcraftCustomControlType("liveCustom"), }, export: { applicability: { mode: "always" as const }, actions: [{ label: "Export", value: "export" }], target: "panel.actions", type: "panelActions", }, }, title: "Extensions", }, ], title: "Controls", }, }, }, modules: [], }); function CaptureStore({ capture, }: { capture: (store: ToolcraftExternalStore) => void; }): null { capture(useToolcraftStore()); return null; } afterEach(() => { cleanup(); vi.restoreAllMocks(); }); describe("ControlsPanel extension live state", () => { it("keeps state but renders no custom UI when its renderer is missing", () => { let store: ToolcraftExternalStore | undefined; render( { store = value; }} /> , ); expect(store?.getState().values["document.live"]).toBe(10); expect(screen.queryByText("Live")).toBeNull(); expect(screen.getByDisplayValue("Built in")).toBeTruthy(); }); it("does not allow a JavaScript renderer map to replace a built-in", () => { const override = vi.fn(() => Forbidden override); // Deliberate JavaScript boundary: TypeScript rejects this map in the compiler fixture. const panel = Reflect.apply(React.createElement, React, [ ControlsPanel, { controlRenderers: { text: override }, framed: false, }, ]); render({panel}); expect(override).not.toHaveBeenCalled(); expect(screen.getByDisplayValue("Built in")).toBeTruthy(); }); it("keeps built-in structure cold while a custom renderer receives effective state", () => { const renderCustom = vi.fn( ({ state, value }: ToolcraftCustomControlRendererProps) => ( {state.timeline.currentTimeSeconds}:{state.canvas.offset.x}: {state.canvas.offset.y}:{state.canvas.zoom}:{String(value)} ), ); let store: ToolcraftExternalStore | undefined; render( { store = value; }} /> , ); const builtInInput = screen.getByDisplayValue("Built in"); const initialCustomRenders = renderCustom.mock.calls.length; act(() => { store?.dispatchTransient({ currentTimeSeconds: 3, type: "timeline.setCurrentTime", }); store?.dispatchTransient({ offset: { x: 24, y: -12 }, type: "canvas.setViewport", zoom: 150, }); }); expect(screen.getByDisplayValue("Built in")).toBe(builtInInput); expect(screen.getByTestId("live-custom-state").textContent).toBe( "3:24:-12:150:10", ); expect(renderCustom.mock.calls.length).toBeGreaterThan( initialCustomRenders, ); }); it("reads effective state when an app action starts", () => { const onPanelAction = vi.fn(); let store: ToolcraftExternalStore | undefined; render( { store = value; }} /> , ); act(() => { store?.dispatchTransient({ currentTimeSeconds: 2.5, type: "timeline.setCurrentTime", }); store?.dispatchTransient({ offset: { x: 18, y: -9 }, type: "canvas.setViewport", zoom: 125, }); }); fireEvent.click(screen.getByRole("button", { name: "Export" })); const actionState = onPanelAction.mock.calls[0]?.[0].state; expect(actionState.timeline.currentTimeSeconds).toBe(2.5); expect(actionState.canvas).toMatchObject({ offset: { x: 18, y: -9 }, zoom: 125, }); }); });