import { act, cleanup, fireEvent, render, screen, } from "@testing-library/react"; import * as React from "react"; import { afterEach, describe, expect, it, vi } from "vitest"; import { timelineModule } from "../../../modules/built-ins/timeline/timeline-module"; import { mediaSourceModule } from "../../../modules/built-ins/media-source/media-source-module"; import { defineToolcraft } from "../../../schema/define-toolcraft"; import { getToolcraftCanvasSizeTargetDimension, toolcraftRuntimeOwnedTargets, toolcraftTimelinePanelExtendedTarget, toolcraftTimelinePanelVisibleTarget, } from "../../../schema/runtime-targets"; import type { ToolcraftExternalStore } from "../../../state/toolcraft-external-store"; import { imageMediaAsset } from "../../../state/reducer-media-test-utils"; import { ToolcraftRoot } from "../../app-shell/toolcraft-root"; import { useToolcraftStore } from "../../app-shell/toolcraft-store-context"; import { ControlsPanel } from "../controls-panel"; import { ToolcraftDefaultsAuthoringProvider, type ToolcraftDefaultsAuthoring } from "../../app-shell/toolcraft-defaults-authoring"; import type { ToolcraftAppDefaults } from "../../../schema/app-defaults"; const counters = vi.hoisted(() => ({ commits: new Map(), executions: new Map(), })); function increment(counter: Map, id: string): void { counter.set(id, (counter.get(id) ?? 0) + 1); } function CommitProbe({ id }: { id: string }): null { React.useLayoutEffect(() => { increment(counters.commits, id); }); return null; } vi.mock( "../renderers/controls-panel-basic-renderers", async (importOriginal) => { const actual = await importOriginal< typeof import("../renderers/controls-panel-basic-renderers") >(); return { ...actual, renderBasicControl: ( args: Parameters[0], ): React.ReactNode => { increment(counters.executions, args.id); return ( <> {actual.renderBasicControl(args)} ); }, }; }, ); vi.mock( "../renderers/controls-panel-media-renderer", async (importOriginal) => { const actual = await importOriginal< typeof import("../renderers/controls-panel-media-renderer") >(); return { ...actual, renderFileDropControl: ( args: Parameters[0], ): React.ReactNode => { increment(counters.executions, args.id); return ( <> {actual.renderFileDropControl(args)} ); }, }; }, ); function CaptureStore({ capture, }: { capture: (store: ToolcraftExternalStore) => void; }): null { capture(useToolcraftStore()); return null; } function resetCounters(): void { counters.commits.clear(); counters.executions.clear(); } function count(id: string): { commits: number; executions: number } { return { commits: counters.commits.get(id) ?? 0, executions: counters.executions.get(id) ?? 0, }; } function renderPanel( schema: ReturnType, authoring: ToolcraftDefaultsAuthoring | null = null, ): ToolcraftExternalStore { let store: ToolcraftExternalStore | undefined; render( { store = value; }} /> , ); if (!store) { throw new Error("Expected Toolcraft store to be captured."); } resetCounters(); return store; } const canvasWidthTarget = toolcraftRuntimeOwnedTargets.find( (target) => getToolcraftCanvasSizeTargetDimension(target) === "width", ); if (!canvasWidthTarget) { throw new Error("Expected a runtime canvas width target."); } afterEach(() => { cleanup(); resetCounters(); window.localStorage.clear(); }); describe("ControlsPanel dependency isolation", () => { it("updates canvas size and timeline runtime-target controls independently", () => { const schema = defineToolcraft({ base: { identity: { id: "runtime-test", title: "Toolcraft App" }, canvas: { enabled: true, size: { height: 180, unit: "px", width: 320 }, sizing: { mode: "editable-output" }, }, panels: { controls: { sections: [ { id: "test-section-1", controls: { timelineVisible: { applicability: { mode: "always" as const }, defaultValue: true, label: "Timeline visible", target: toolcraftTimelinePanelVisibleTarget, type: "switch", }, }, title: "Panel", }, ], title: "Controls", }, }, }, modules: [ timelineModule({ mode: "playback", }), ], }); const store = renderPanel(schema); act(() => { store.dispatch({ size: { height: 180, unit: "px", width: 640 }, type: "canvas.setSize", }); }); expect(count("canvasWidth")).toEqual({ commits: 1, executions: 1 }); expect(count("timelineVisible")).toEqual({ commits: 0, executions: 0 }); expect(count("timelineExtended")).toEqual({ commits: 0, executions: 0 }); resetCounters(); act(() => { store.dispatch({ hidden: true, panelId: "timeline", type: "panels.setHidden", }); }); expect(count("timelineVisible")).toEqual({ commits: 1, executions: 1 }); expect(count("canvasWidth")).toEqual({ commits: 0, executions: 0 }); expect(count("timelineExtended")).toEqual({ commits: 0, executions: 0 }); resetCounters(); act(() => { store.dispatch({ target: toolcraftTimelinePanelExtendedTarget, type: "controls.setValue", value: true, }); }); expect(count("timelineExtended")).toEqual({ commits: 1, executions: 1 }); expect(count("canvasWidth")).toEqual({ commits: 0, executions: 0 }); expect(count("timelineVisible")).toEqual({ commits: 0, executions: 0 }); expect(store.getState().canvas.size.width).toBe(640); expect(canvasWidthTarget).toBe( schema.panels.controls?.sections.find(({ id }) => id === "runtime.setup")?.controls.canvasWidth?.target, ); }); it("updates a media renderer for media assets and canvas size only", () => { const schema = defineToolcraft({ base: { identity: { id: "runtime-test", title: "Toolcraft App" }, canvas: { enabled: true }, panels: { controls: { sections: [ { id: "test-section-2", controls: { source: { applicability: { mode: "always" as const }, label: "Source", target: "source.media", type: "fileDrop", }, amount: { applicability: { mode: "always" as const }, defaultValue: 1, label: "Amount", max: 10, min: 0, target: "effect.amount", type: "slider", }, }, title: "Source", }, ], title: "Controls", }, }, }, modules: [mediaSourceModule()], }); const sourceTarget = schema.panels.controls?.sections.find( (section) => section.controls.source !== undefined, )?.controls.source?.target; const amountTarget = schema.panels.controls?.sections.find( (section) => section.controls.amount !== undefined, )?.controls.amount?.target; const store = renderPanel(schema); if (!sourceTarget || !amountTarget) { throw new Error("Expected source and amount targets."); } act(() => { store.dispatch({ target: amountTarget, type: "controls.setValue", value: 2, }); }); expect(count("amount")).toEqual({ commits: 1, executions: 1 }); expect(count("source")).toEqual({ commits: 0, executions: 0 }); resetCounters(); act(() => { store.dispatch({ assets: [ { policy: "prepared-source", asset: { sourceSize: { height: 100, unit: "px", width: 100 }, assetKind: "image", fileName: "source.png", lifecycle: "ready", mimeType: "image/png", position: { x: 0, y: 0 }, resourceRef: "media:image:source", sourceTarget, }, }, ], type: "media.importBatch", }); }); expect(count("source")).toEqual({ commits: 1, executions: 1 }); expect(count("amount")).toEqual({ commits: 0, executions: 0 }); resetCounters(); act(() => { store.dispatch({ size: { height: 360, unit: "px", width: 640 }, type: "canvas.setSize", }); }); expect(count("source")).toEqual({ commits: 1, executions: 1 }); expect(count("amount")).toEqual({ commits: 0, executions: 0 }); }); it("reads the current live state when Save State as Default is invoked", async () => { const schema = defineToolcraft({ base: { identity: { id: "runtime-test", title: "Toolcraft App" }, canvas: { enabled: false }, panels: { controls: { sections: [ { id: "test-section-3", controls: { prompt: { applicability: { mode: "always" as const }, defaultValue: "Initial", label: "Prompt", target: "generation.prompt", type: "text", }, }, title: "Prompt", }, ], title: "Controls", }, }, settingsTransfer: true, }, modules: [], }); const promptTarget = schema.panels.controls?.sections.find( (section) => section.title === "Prompt", )?.controls.prompt?.target; const save = vi.fn(async (_defaults: ToolcraftAppDefaults) => {}); const store = renderPanel(schema, { save }); if (!promptTarget) { throw new Error("Expected prompt target."); } act(() => { store.dispatch({ target: promptTarget, type: "controls.setValue", value: "Current", }); }); await act(async () => { fireEvent.click(screen.getByRole("button", { name: "Save State as Default" })); }); expect(save).toHaveBeenCalledTimes(1); const saved = save.mock.calls[0]?.[0]; expect(saved?.version === 2 && saved.state.values[promptTarget]).toBe("Current"); }); });