import { describe, expect, it, vi } from "vitest"; import { createToolcraftRuntimeSceneVisibility } from "../scene"; import { defineToolcraft } from "../schema/define-toolcraft"; import { createToolcraftState } from "../state/create-template-state"; import type { ToolcraftArtifactDownloadRequest, ToolcraftArtifactDownloadResult, } from "./artifact-download"; import { exportToolcraftSvgArtifact } from "./svg-artifact-export"; import { TOOLCRAFT_SVG_NAMESPACE } from "./svg-policy"; const visibility = createToolcraftRuntimeSceneVisibility({ renderDefaultImages: true, suppressedModelTargets: [], }); function createState(mode: "finite" | "infinite" = "finite") { return createToolcraftState( defineToolcraft({ base: { identity: { id: "runtime-test", title: "Toolcraft App" }, canvas: { enabled: true }, panels: {}, }, modules: [], }), { canvas: { mode } }, ); } describe("exportToolcraftSvgArtifact", () => { it("renders current vectors, downloads SVG, and reports monotonic progress", async () => { const progress: number[] = []; const downloadArtifact = vi.fn( ( _request: ToolcraftArtifactDownloadRequest, ): ToolcraftArtifactDownloadResult => ({ fileName: "column-graph.svg", }), ); const state = createState(); const renderFrame = vi.fn(({ container, frame, state: frameState }) => { expect(frameState).not.toBe(state); expect(frame).toEqual({ height: 1080, width: 1920, x: -960, y: -540, }); const rect = container.ownerDocument.createElementNS( TOOLCRAFT_SVG_NAMESPACE, "rect", ); rect.setAttribute("width", String(frame.width)); rect.setAttribute("height", String(frame.height)); rect.setAttribute("data-column-graph-background", "true"); container.append(rect); }); const result = await exportToolcraftSvgArtifact({ boundsProvider: undefined, downloadArtifact, rendererPipeline: null, signal: new AbortController().signal, reportProgress: (value) => progress.push(value), state, svgExportRenderer: { baseFileName: "column-graph", renderFrame, }, visibility, }); expect(progress).toEqual([0.1, 0.65, 0.9, 1]); expect(result).toMatchObject({ extension: ".svg", mediaType: "image/svg+xml", vectorElementCount: 1, }); expect(result.byteLength).toBeGreaterThan(0); expect(downloadArtifact).toHaveBeenCalledOnce(); const request = downloadArtifact.mock.calls[0]?.[0]; expect(request?.extension).toBe(".svg"); expect(request?.rawBaseFileName).toBe("column-graph"); expect(request?.blob.type).toBe("image/svg+xml;charset=utf-8"); await expect(request?.blob.text()).resolves.toContain( 'data-column-graph-background="true"', ); await expect(request?.blob.text()).resolves.toContain( 'viewBox="-960 -540 1920 1080"', ); expect(renderFrame).toHaveBeenCalledOnce(); }); it("normalizes product renderer failures", async () => { await expect( exportToolcraftSvgArtifact({ boundsProvider: undefined, downloadArtifact: vi.fn(), rendererPipeline: null, signal: new AbortController().signal, reportProgress: vi.fn(), state: createState(), svgExportRenderer: { baseFileName: "broken", renderFrame: () => { throw new Error("renderer exploded"); }, }, visibility, }), ).rejects.toMatchObject({ feedback: { code: "svg-render-failed" }, }); }); it("requires bounds for a renderer-present infinite SVG export", async () => { await expect( exportToolcraftSvgArtifact({ boundsProvider: undefined, downloadArtifact: vi.fn(), rendererPipeline: null, signal: new AbortController().signal, reportProgress: vi.fn(), state: createState("infinite"), svgExportRenderer: { baseFileName: "scene", renderFrame: vi.fn(), }, visibility, }), ).rejects.toMatchObject({ feedback: { code: "scene-bounds-unavailable" }, }); }); it.each([ () => { throw new Error("provider failed"); }, () => [{ height: 10, width: Number.NaN, x: 0, y: 0 }], ])( "rejects a throwing or invalid product bounds provider", async (boundsProvider) => { await expect( exportToolcraftSvgArtifact({ boundsProvider, downloadArtifact: vi.fn(), rendererPipeline: null, signal: new AbortController().signal, reportProgress: vi.fn(), state: createState(), svgExportRenderer: { baseFileName: "broken-bounds", renderFrame: vi.fn(), }, visibility, }), ).rejects.toMatchObject({ feedback: { code: "scene-bounds-unavailable" }, }); }, ); });