import { ToolcraftSceneExportError } from "../../../export/export-frame"; import { defaultToolcraftRuntimeSceneVisibility } from "../../../scene"; import type { ToolcraftActionSchema } from "../../../schema/types"; import { act, defineToolcraft, fireEvent, renderControlsPanelWithSchema, screen, vi, } from "../testing/controls-panel-test-utils"; import { describe, expect, it } from "vitest"; function createExportSchema( actions: readonly ToolcraftActionSchema[] = [ { label: "Export PNG", value: "export", variant: "default" }, ], ) { return defineToolcraft({ canvas: { enabled: true, size: { height: 180, unit: "px", width: 320 } }, panels: { controls: { sections: [ { actionGroup: "secondary", controls: { footer: { actions, target: "panel.actions", type: "panelActions", }, }, }, ], title: "Controls", }, }, }); } describe("ControlsPanel scene export actions", () => { it("shows only feedback reported by the latest panel action", async () => { const schema = createExportSchema([ { label: "First export", value: "first", variant: "secondary" }, { label: "Second export", value: "second", variant: "default" }, ]); const reporters: Array<(feedback: { code: string; message: string }) => void> = []; renderControlsPanelWithSchema(schema, { onPanelAction: ({ reportFeedback }) => { reporters.push(reportFeedback); }, }); fireEvent.click(screen.getByRole("button", { name: "First export" })); fireEvent.click(screen.getByRole("button", { name: "Second export" })); act(() => reporters[0]?.({ code: "stale", message: "Stale failure" })); expect(screen.queryByRole("alert")).toBeNull(); act(() => { reporters[1]?.({ code: "empty-scene", message: "Nothing to export" }); }); const alert = await screen.findByRole("alert"); expect(alert.textContent).toBe("Nothing to export"); expect(alert.getAttribute("data-panel-action-feedback-code")).toBe("empty-scene"); }); it("presents a missing required bounds provider as typed visible feedback", async () => { const onPanelAction = vi.fn(); renderControlsPanelWithSchema( createExportSchema([ { command: "controls.apply", label: "Export PNG", role: "export-image", value: "export", variant: "default", }, ]), { sceneExport: { exportRenderer: { baseFileName: "scene", renderFrame: vi.fn(), }, productSceneRequired: true, visibility: defaultToolcraftRuntimeSceneVisibility, }, onPanelAction, }, { canvas: { mode: "infinite" } }, ); fireEvent.click(screen.getByRole("button", { name: "Export PNG" })); const alert = await screen.findByRole("alert"); expect(alert.getAttribute("data-panel-action-feedback-code")).toBe( "scene-bounds-unavailable", ); expect(alert.textContent).toContain("does not provide export bounds"); expect(onPanelAction).not.toHaveBeenCalled(); }); it("presents rejected typed export failures as visible feedback", async () => { renderControlsPanelWithSchema(createExportSchema(), { onPanelAction: async () => { throw new ToolcraftSceneExportError({ code: "scene-export-too-large", message: "The cropped scene is too large to export.", ok: false, }); }, }); fireEvent.click(screen.getByRole("button", { name: "Export PNG" })); const alert = await screen.findByRole("alert"); expect(alert.getAttribute("data-panel-action-feedback-code")).toBe( "scene-export-too-large", ); expect(alert.textContent).toBe("The cropped scene is too large to export."); }); it("routes typed SVG export through runtime and never through onPanelAction", async () => { const onPanelAction = vi.fn(); const renderFrame = vi.fn(); renderControlsPanelWithSchema( createExportSchema([ { label: "Export SVG", role: "export-svg", value: "export.svg", variant: "default", }, ]), { onPanelAction, sceneExport: { productSceneRequired: false, svgExportRenderer: { baseFileName: "scene", renderFrame, }, visibility: defaultToolcraftRuntimeSceneVisibility, }, }, ); const exportButton = screen.getByRole("button", { name: "Export SVG" }); expect( exportButton.querySelector('[data-icon-name="upload-simple"]'), ).toBeTruthy(); fireEvent.click(exportButton); const alert = await screen.findByRole("alert"); expect(renderFrame).toHaveBeenCalledOnce(); expect(alert.getAttribute("data-panel-action-feedback-code")).toBe( "svg-content-empty", ); expect(onPanelAction).not.toHaveBeenCalled(); }); });