import { ToolcraftSceneExportError } from "../../../export/export-frame"; import type { ToolcraftProductSvgExportFrameContext } from "../../../export/product-svg-export-renderer"; import { imageExportModule } from "../../../modules/built-ins/image-export/image-export-module"; import { svgExportModule } from "../../../modules/built-ins/svg-export/svg-export-module"; import type { ToolcraftProductModuleDefinition } from "../../../modules/contract/module-definition"; 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" }, ], modules: readonly ToolcraftProductModuleDefinition[] = [], ) { return defineToolcraft({ base: { identity: { id: "runtime-test", title: "Toolcraft App" }, canvas: { enabled: true, size: { height: 180, unit: "px", width: 320 } }, panels: { controls: { sections: actions.length === 0 ? [] : [ { id: "test-section-2", actionGroup: "secondary", controls: { footer: { applicability: { mode: "always" as const }, actions, target: "panel.actions", type: "panelActions", }, }, }, ], title: "Controls", }, }, }, modules, }); } 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([], [imageExportModule()]), { sceneExport: { exportRenderer: { baseFileName: "scene", renderFrame: vi.fn(), }, 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("derives the finite SVG product frame from its renderer and bypasses onPanelAction", async () => { const onPanelAction = vi.fn(); const renderFrame = vi.fn( ({ frame }: ToolcraftProductSvgExportFrameContext) => { expect(frame).toEqual({ height: 180, width: 320, x: -160, y: -90 }); }, ); renderControlsPanelWithSchema(createExportSchema([], [svgExportModule()]), { onPanelAction, sceneExport: { 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(); }); });