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({ canvas: { enabled: false }, panels: { controls: { sections: [ { controls: { footer: { actions: [ { icon: "download", label: "Download image", value: "download", variant: "default", }, ], target: "panel.actions", type: "panelActions", }, }, title: "Export", }, ], title: "Generation Controls", }, }, }); 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({ canvas: { enabled: true }, panels: { controls: { sections: [ { actionGroup: "secondary", controls: { footer: { actions: [ { label: "Export PNG", value: "export-png", variant: "default", }, ], target: "panel.actions", type: "panelActions", }, }, }, ], title: "Controls", }, }, }); 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("renders runtime settings transfer as a headerless body section instead of sticky footer actions", () => { const schema = defineToolcraft({ canvas: { enabled: true, size: { height: 720, unit: "px", width: 1280 } }, panels: { controls: { sections: [ { controls: { prompt: { defaultValue: "Initial prompt", label: "Prompt", target: "generation.prompt", type: "text", }, }, title: "Prompt", }, { actionGroup: "secondary", controls: { footer: { actions: [ { label: "Export PNG", value: "export.png", variant: "default", }, ], target: "panel.actions", type: "panelActions", }, }, }, ], title: "Generation Controls", }, }, settingsTransfer: true, }); const { container } = renderControlsPanelWithSchema(schema); const scrollContent = container.querySelector( '[data-slot="toolcraft-panel-content"]', ); const stickyFooter = container.querySelector( '[data-slot="toolcraft-panel-sticky-actions"]', ); const firstSection = container.querySelector("section"); const firstInlineGroup = firstSection?.querySelector( '[data-control-layout="inline"][data-control-layout-columns="2"]', ); const transferButtons = screen .getAllByRole("button", { name: /Settings$/ }) .map((button) => button.textContent); expect(transferButtons).toEqual(["Export Settings", "Import Settings"]); expect(schema.panels.controls?.sections[0]?.title).toBe("Setup"); expect( [...container.querySelectorAll('[data-slot="panel-title"]')].map( (title) => title.textContent, ), ).not.toContain("Setup"); expect(firstSection?.querySelector('[data-slot="panel-title"]')).toBeNull(); expect(firstSection?.querySelector("[data-control-section-collapse-button]")).toBeNull(); expect(firstSection?.querySelector('[data-slot="control-section-header"]')).toBeNull(); expect(firstSection?.querySelector('[data-slot="panel-section-collapsible-body"]')).toBeNull(); expect(firstSection?.getAttribute("data-collapsed")).toBeNull(); expect(firstSection?.className).toContain("py-0"); expect(firstSection?.className).toContain( "[&>[data-control-list]]:pt-3", ); expect(firstSection?.querySelector("[data-control-list]")?.className).toContain("pt-2"); expect(firstSection?.querySelector("[data-control-list]")?.className).toContain("pb-6"); expect(firstInlineGroup?.textContent).toContain("Canvas width"); expect(firstInlineGroup?.textContent).toContain("Canvas height"); expect(firstSection?.textContent).toContain("Aspect ratio"); expect(firstSection?.textContent).toContain("Canvas width"); expect(firstSection?.textContent).toContain("Canvas height"); expect(firstSection?.textContent).toContain("Export Settings"); expect(firstSection?.textContent).toContain("Import Settings"); expect((firstSection?.textContent ?? "").indexOf("Export Settings")).toBeLessThan( (firstSection?.textContent ?? "").indexOf("Import Settings"), ); expect((firstSection?.textContent ?? "").indexOf("Import Settings")).toBeLessThan( (firstSection?.textContent ?? "").indexOf("Aspect ratio"), ); expect((firstSection?.textContent ?? "").indexOf("Aspect ratio")).toBeLessThan( (firstSection?.textContent ?? "").indexOf("Canvas width"), ); expect(scrollContent?.textContent).toContain("Import Settings"); expect(scrollContent?.textContent).toContain("Export Settings"); expect(stickyFooter?.textContent).toContain("Export PNG"); expect(stickyFooter?.textContent).not.toContain("Import Settings"); }); it("keeps panelActions in the sticky footer even when the schema omits actionGroup", () => { const schema = defineToolcraft({ canvas: { enabled: false }, panels: { controls: { sections: [ { controls: { footer: { 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: { defaultValue: "Initial prompt", label: "Prompt", target: "generation.prompt", type: "text", }, }, title: "Export", }, ], title: "Generation Controls", }, }, }); 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({ canvas: { enabled: false }, panels: { controls: { sections: [ { actionGroup: "secondary", controls: { footer: { actions: [ { icon: "download-simple", label: "Export PNG", value: "export-png", variant: "default", }, ], target: "panel.actions", type: "panelActions", }, }, }, ], title: "Generation Controls", }, }, }); 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({ canvas: { enabled: false }, panels: { controls: { sections: [ { controls: { footer: { actions: [ { label: "Copy PNG", value: "copy", variant: "secondary", }, { label: "Export PNG", value: "export", variant: "default", }, ], target: "panel.actions", type: "panelActions", }, }, }, ], title: "Generation Controls", }, }, }); 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({ canvas: { enabled: false }, panels: { controls: { sections: [ { controls: { footer: { 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", }, }, }); renderControlsPanelWithSchema(schema); const exportPngButton = screen.getByRole("button", { name: "Export PNG" }); expect(exportPngButton.className).toContain("col-span-2"); }); });