import { describe, expect, it } from "vitest"; import { act, defineToolcraft, fireEvent, renderControlsPanel, renderControlsPanelWithSchema, screen, vi, waitFor, } from "../testing/controls-panel-test-utils"; describe("ControlsPanel footer actions and settings transfer", () => { it("routes footer panel actions through the command bus", () => { renderControlsPanel(); fireEvent.change(screen.getByDisplayValue("Initial prompt"), { target: { value: "Draft" }, }); fireEvent.blur(screen.getByDisplayValue("Draft")); expect(screen.getByTestId("prompt-value").textContent).toBe("Draft"); fireEvent.click(screen.getByRole("button", { name: "Reset" })); expect(screen.getByTestId("prompt-value").textContent).toBe( "Initial prompt", ); }); it("routes app-specific footer panel actions through onPanelAction", () => { const handledActions: string[] = []; const schema = defineToolcraft({ base: { identity: { id: "runtime-test", title: "Toolcraft App" }, canvas: { enabled: false }, panels: { controls: { sections: [ { id: "test-section-1", controls: { footer: { applicability: { mode: "always" as const }, actions: [ { icon: "download", label: "Download image", value: "download", variant: "default", }, ], target: "panel.actions", type: "panelActions", }, }, title: "Export", }, ], title: "Generation Controls", }, }, }, modules: [], }); renderControlsPanelWithSchema(schema, { onPanelAction: ({ action, state }) => { handledActions.push( `${action.value}:${state.schema.panels.controls?.title}`, ); }, }); fireEvent.click(screen.getByRole("button", { name: "Download image" })); expect(handledActions).toEqual(["download:Generation Controls"]); }); it("keeps runtime export mechanics out of generic product action context", () => { const schema = defineToolcraft({ base: { identity: { id: "runtime-test", title: "Toolcraft App" }, canvas: { enabled: true }, panels: { controls: { sections: [ { id: "test-section-2", actionGroup: "secondary", controls: { footer: { applicability: { mode: "always" as const }, actions: [ { label: "Export PNG", value: "export-png", variant: "default", }, ], target: "panel.actions", type: "panelActions", }, }, }, ], title: "Controls", }, }, }, modules: [], }); let actionContextKeys: string[] = []; renderControlsPanelWithSchema(schema, { onPanelAction: (context) => { actionContextKeys = Object.keys(context).sort(); }, }); fireEvent.click(screen.getByRole("button", { name: "Export PNG" })); expect(actionContextKeys).not.toContain("renderModelsToCanvas"); expect(actionContextKeys).not.toContain("renderRuntimeSceneToCanvas"); expect(actionContextKeys).not.toContain("resolveSceneExportFrame"); }); it("keeps panelActions in the sticky footer even when the schema omits actionGroup", () => { const schema = defineToolcraft({ base: { identity: { id: "runtime-test", title: "Toolcraft App" }, canvas: { enabled: false }, panels: { controls: { sections: [ { id: "test-section-5", controls: { footer: { applicability: { mode: "always" as const }, actions: [ { command: "controls.reset", label: "Reset", value: "reset", variant: "outline", }, { command: "controls.apply", label: "Apply", value: "apply", variant: "default", }, ], target: "panel.actions", type: "panelActions", }, prompt: { applicability: { mode: "always" as const }, defaultValue: "Initial prompt", label: "Prompt", target: "generation.prompt", type: "text", }, }, title: "Export", }, ], title: "Generation Controls", }, }, }, modules: [], }); const { container } = renderControlsPanelWithSchema(schema); const stickyFooter = container.querySelector( '[data-slot="toolcraft-panel-sticky-actions"]', ); const scrollContent = container.querySelector( '[data-slot="toolcraft-panel-content"]', ); expect(stickyFooter?.textContent).toContain("Reset"); expect(stickyFooter?.textContent).toContain("Apply"); expect(stickyFooter?.className).toContain("before:h-px"); expect(stickyFooter?.className).toContain("var(--accent)"); expect(stickyFooter?.className).toContain( "before:scale-x-[var(--sticky-footer-progress,1)]", ); expect(stickyFooter?.className).toContain("before:opacity-0"); expect(stickyFooter?.className).toContain( "before:transition-[opacity,transform]", ); expect(stickyFooter?.className).toContain( "data-[sticky-footer-active=true]:before:opacity-100", ); expect(stickyFooter?.getAttribute("data-sticky-footer-active")).toBeNull(); expect( stickyFooter?.querySelector("[data-toolcraft-section-actions]"), ).toBeTruthy(); expect(stickyFooter?.querySelector('[data-slot="panel-title"]')).toBeNull(); expect( stickyFooter?.querySelector("[data-control-section-collapse-button]"), ).toBeNull(); expect(stickyFooter?.textContent).not.toContain("Export"); expect(scrollContent?.textContent).toContain("Prompt"); expect(scrollContent?.textContent).not.toContain("ResetApply"); }); it("shows the sticky footer export indicator only while an async panel action is pending", async () => { const schema = defineToolcraft({ base: { identity: { id: "runtime-test", title: "Toolcraft App" }, canvas: { enabled: false }, panels: { controls: { sections: [ { id: "test-section-6", actionGroup: "secondary", controls: { footer: { applicability: { mode: "always" as const }, actions: [ { icon: "download-simple", label: "Export PNG", value: "export-png", variant: "default", }, ], target: "panel.actions", type: "panelActions", }, }, }, ], title: "Generation Controls", }, }, }, modules: [], }); let resolveAction: (() => void) | undefined; const pendingAction = new Promise((resolve) => { resolveAction = resolve; }); let reportActionProgress: ((progress: number) => void) | undefined; const onPanelAction = vi.fn(({ reportProgress }) => { reportActionProgress = reportProgress; return pendingAction; }); const { container } = renderControlsPanelWithSchema(schema, { onPanelAction, }); const stickyFooter = container.querySelector( '[data-slot="toolcraft-panel-sticky-actions"]', ); const exportPngButton = screen.getByRole("button", { name: "Export PNG" }); expect(stickyFooter?.getAttribute("data-sticky-footer-active")).toBeNull(); expect( exportPngButton.querySelector('[data-icon-name="upload-simple"]'), ).toBeTruthy(); fireEvent.click(exportPngButton); expect(onPanelAction).toHaveBeenCalledTimes(1); await waitFor(() => { expect(stickyFooter?.getAttribute("data-sticky-footer-active")).toBe( "true", ); }); expect( stickyFooter?.getAttribute("data-sticky-footer-progress"), ).toBeNull(); act(() => { reportActionProgress?.(0.25); }); await waitFor(() => { expect(stickyFooter?.getAttribute("data-sticky-footer-progress")).toBe( "0.25", ); expect( (stickyFooter as HTMLElement | null)?.style.getPropertyValue( "--sticky-footer-progress", ), ).toBe("0.25"); }); act(() => { reportActionProgress?.(2); }); await waitFor(() => { expect(stickyFooter?.getAttribute("data-sticky-footer-progress")).toBe( "1", ); }); await act(async () => { resolveAction?.(); await pendingAction; }); await waitFor(() => { expect( stickyFooter?.getAttribute("data-sticky-footer-active"), ).toBeNull(); }); }); it("renders secondary footer actions as secondary buttons", () => { const schema = defineToolcraft({ base: { identity: { id: "runtime-test", title: "Toolcraft App" }, canvas: { enabled: false }, panels: { controls: { sections: [ { id: "test-section-7", controls: { footer: { applicability: { mode: "always" as const }, actions: [ { label: "Copy PNG", value: "copy", variant: "secondary", }, { label: "Export PNG", value: "export", variant: "default", }, ], target: "panel.actions", type: "panelActions", }, }, }, ], title: "Generation Controls", }, }, }, modules: [], }); renderControlsPanelWithSchema(schema); expect( screen .getByRole("button", { name: "Copy PNG" }) .getAttribute("data-variant"), ).toBe("secondary"); expect( screen .getByRole("button", { name: "Export PNG" }) .getAttribute("data-variant"), ).toBe("default"); }); it("spans the final odd footer action across the full action row", () => { const schema = defineToolcraft({ base: { identity: { id: "runtime-test", title: "Toolcraft App" }, canvas: { enabled: false }, panels: { controls: { sections: [ { id: "test-section-8", controls: { footer: { applicability: { mode: "always" as const }, actions: [ { label: "Copy PNG", value: "copy", variant: "secondary", }, { label: "Export OG", value: "export-og", variant: "secondary", }, { label: "Export PNG", value: "export", variant: "default", }, ], target: "panel.actions", type: "panelActions", }, }, }, ], title: "Generation Controls", }, }, }, modules: [], }); renderControlsPanelWithSchema(schema); const exportPngButton = screen.getByRole("button", { name: "Export PNG" }); expect(exportPngButton.className).toContain("col-span-2"); }); });