import { cleanup, render, screen } from "@testing-library/react"; import { afterEach, describe, expect, it } from "vitest"; import { imageExportModule } from "../../modules/built-ins/image-export/image-export-module"; import { canvasEditingModule } from "../../modules/built-ins/canvas-editing/canvas-editing-module"; import { model3dModule } from "../../modules/built-ins/model-3d/model-3d-module"; import { spatialViewModule } from "../../modules/built-ins/spatial-view/spatial-view-module"; import { svgExportModule } from "../../modules/built-ins/svg-export/svg-export-module"; import { videoExportModule } from "../../modules/built-ins/video-export/video-export-module"; import { composeToolcraftApp } from "./compose-toolcraft-app"; import { createModelBase, createModelOnlyBase, createSpatialBase, pipelineRegistration, PRODUCT_SCENE, rasterRenderer, resolveProduct, unsafeToolcraftAppPorts, vectorRenderer, } from "./compose-toolcraft-app.test-support"; import type { ToolcraftAppPorts } from "./toolcraft-app-ports"; import { resolveToolcraftProductSceneRequirement } from "./product-scene-requirement"; import { ToolcraftApp } from "./toolcraft-app"; import { useToolcraftModelPresentationMode } from "../model-rendering/model-presentation-mode"; afterEach(() => { cleanup(); window.localStorage.clear(); }); const INVALID_RUNTIME_PORT_MESSAGE = 'Toolcraft app port "modelPresentation" is invalid; runtime presentation is the omission/default and only custom presentation may be authored.'; function createForgedRuntimePorts(): ToolcraftAppPorts { return unsafeToolcraftAppPorts({ modelPresentation: { mode: "runtime" }, }); } describe("composeToolcraftApp", () => { it("normalizes neutral ports to the current runtime defaults", () => { const product = resolveProduct(); const composition = composeToolcraftApp(product, {}); expect(composition).toEqual({ modelPresentation: { mode: "runtime" }, renderDefaultCanvasMedia: true, schema: product, }); expect(Object.isFrozen(composition)).toBe(true); expect(Object.isFrozen(composition.modelPresentation)).toBe(true); if (false) { // @ts-expect-error A returned composition is immutable by contract. composition.schema = product; // @ts-expect-error Optional composition fields are immutable too. composition.renderDefaultCanvasMedia = false; } }); it("shares one raster renderer across image and video consumers", () => { const product = resolveProduct([imageExportModule(), videoExportModule()]); const composition = composeToolcraftApp(product, { scene: { canvasContent: PRODUCT_SCENE, rasterFrameRenderer: rasterRenderer, }, }); expect(composition.exportRenderer).toEqual(rasterRenderer); expect( product.modulePlan.portRequirements.find( ({ id }) => id === "scene.rasterFrameRenderer", ), ).toMatchObject({ consumers: ["image-export", "video-export"] }); }); it("requires vector output for SVG even when the scene is runtime-only", () => { expect(() => composeToolcraftApp(resolveProduct([svgExportModule()]), {}), ).toThrow( 'Toolcraft integration port "scene.vectorFrameRenderer" is missing; applicability "always" requires it for consumers: svg-export. Expected public field "ports.scene.vectorFrameRenderer".', ); }); it("requires raster output only for canonical product-scene ownership", () => { expect(() => composeToolcraftApp(resolveProduct([imageExportModule()]), {}), ).not.toThrow(); expect(() => composeToolcraftApp(resolveProduct([imageExportModule()]), { scene: { canvasContent: PRODUCT_SCENE }, }), ).toThrow(/scene\.rasterFrameRenderer.*productSceneRequired is true/); expect(() => composeToolcraftApp(resolveProduct([imageExportModule()]), { scene: { rasterFrameRenderer: rasterRenderer }, }), ).toThrow(/scene\.rasterFrameRenderer.*provided but unused/); }); it("requires canvas content for editing and procedural spatial view", () => { expect(() => composeToolcraftApp(resolveProduct(), { scene: { canvasContent: PRODUCT_SCENE }, }), ).not.toThrow(); expect(() => composeToolcraftApp(resolveProduct([canvasEditingModule()]), {}), ).toThrow(/scene\.canvasContent.*canvas-editing/); expect(() => composeToolcraftApp( resolveProduct([spatialViewModule()], createSpatialBase()), {}, ), ).toThrow( /scene\.canvasContent.*spatialProductSceneRequired is true.*spatial-view/, ); }); it("keeps runtime-presented Model 3D plus Spatial View port-free", () => { expect(() => composeToolcraftApp( resolveProduct( [model3dModule(), spatialViewModule()], createModelBase(), ), {}, ), ).not.toThrow(); }); it("derives every product-scene consumer from one pure helper", () => { expect( resolveToolcraftProductSceneRequirement({ canvasContent: PRODUCT_SCENE, modelPresentation: { mode: "runtime" }, rendererPipelineRegistration: undefined, }), ).toEqual({ independentlyRequired: false, productSceneRequired: true, suppressedModelTargets: [], }); expect( resolveToolcraftProductSceneRequirement({ canvasContent: undefined, modelPresentation: { mode: "runtime" }, rendererPipelineRegistration: pipelineRegistration, }), ).toEqual({ independentlyRequired: true, productSceneRequired: true, suppressedModelTargets: [], }); expect( resolveToolcraftProductSceneRequirement({ canvasContent: undefined, modelPresentation: { consumers: [{ id: "model", sourceTarget: "source.model" }], mode: "custom", }, rendererPipelineRegistration: undefined, }), ).toEqual({ independentlyRequired: true, productSceneRequired: true, suppressedModelTargets: ["source.model"], }); }); it("canonicalizes custom model presentation before returning", () => { const requested = { consumers: [{ id: "model", sourceTarget: "source.model" }], mode: "custom" as const, }; const composition = composeToolcraftApp( resolveProduct( [model3dModule({ presentation: "custom" })], createModelOnlyBase(), ), { modelPresentation: requested }, ); expect(composition.modelPresentation).toEqual(requested); expect(composition.modelPresentation).not.toBe(requested); expect(Object.isFrozen(composition.modelPresentation)).toBe(true); if (composition.modelPresentation?.mode === "custom") { expect(Object.isFrozen(composition.modelPresentation.consumers)).toBe( true, ); } }); it("rejects a forged runtime presentation port for a neutral plan", () => { expect(() => composeToolcraftApp(resolveProduct(), createForgedRuntimePorts()), ).toThrow(INVALID_RUNTIME_PORT_MESSAGE); if (false) { const invalidPorts: ToolcraftAppPorts = { // @ts-expect-error Runtime presentation is a default, not an authored port. modelPresentation: { mode: "runtime" }, }; void invalidPorts; } }); it("rejects a forged runtime presentation port for runtime Model 3D", () => { expect(() => composeToolcraftApp( resolveProduct([model3dModule()], createModelOnlyBase()), createForgedRuntimePorts(), ), ).toThrow(INVALID_RUNTIME_PORT_MESSAGE); }); it("rejects a forged runtime presentation port for custom Model 3D", () => { const product = resolveProduct( [model3dModule({ presentation: "custom" })], createModelOnlyBase(), ); expect(() => composeToolcraftApp(product, createForgedRuntimePorts()), ).toThrow(INVALID_RUNTIME_PORT_MESSAGE); }); it("requires a non-empty custom model presentation", () => { const product = resolveProduct( [model3dModule({ presentation: "custom" })], createModelOnlyBase(), ); expect(() => composeToolcraftApp(product, { modelPresentation: { consumers: [], mode: "custom" }, }), ).toThrow(/custom model presentation.*at least one consumer/iu); }); it("captures a model-presentation getter exactly once", () => { const requested = { consumers: [{ id: "model", sourceTarget: "source.model" }], mode: "custom" as const, }; let reads = 0; const forgedPorts = unsafeToolcraftAppPorts( Object.defineProperty({}, "modelPresentation", { get: () => { reads += 1; return reads === 1 ? requested : { mode: "runtime" }; }, }), ); const composition = composeToolcraftApp( resolveProduct( [model3dModule({ presentation: "custom" })], createModelOnlyBase(), ), forgedPorts, ); expect(reads).toBe(1); expect(composition.modelPresentation).toEqual(requested); }); it("keeps custom model suppression independent in Spatial View", () => { const product = resolveProduct( [model3dModule({ presentation: "custom" }), spatialViewModule()], createModelBase(), ); const modelPresentation = { consumers: [{ id: "model", sourceTarget: "source.model" }], mode: "custom" as const, }; expect(() => composeToolcraftApp(product, {})).toThrow( 'Toolcraft app ports are invalid:\n- Toolcraft integration port "modelPresentation" is missing; applicability "always" requires it for consumers: model-3d. Expected public field "ports.modelPresentation".\n- Toolcraft integration port "scene.canvasContent" is missing; applicability "spatial-product-scene" requires it because spatialProductSceneRequired is true for consumers: spatial-view. Expected public field "ports.scene.canvasContent".', ); expect(() => composeToolcraftApp(product, { modelPresentation })).toThrow( /scene\.canvasContent.*spatialProductSceneRequired is true.*spatial-view/, ); expect(() => composeToolcraftApp(product, { modelPresentation, scene: { canvasContent: PRODUCT_SCENE }, }), ).not.toThrow(); }); it("rejects unused gated ports and names the expected public field", () => { expect(() => composeToolcraftApp(resolveProduct(), { scene: { vectorFrameRenderer: vectorRenderer }, }), ).toThrow( /scene\.vectorFrameRenderer.*provided but unused.*ports\.scene\.vectorFrameRenderer/, ); }); it("keeps constructor and defensive host export diagnostics identical", () => { const product = resolveProduct([imageExportModule()]); const invalidRenderer = { ...rasterRenderer, baseFileName: " " }; let constructorMessage = ""; try { composeToolcraftApp(product, { scene: { canvasContent: PRODUCT_SCENE, rasterFrameRenderer: invalidRenderer, }, }); } catch (error) { constructorMessage = (error as Error).message; } expect(constructorMessage).not.toBe(""); expect(() => render( , ), ).toThrow(constructorMessage); }); it("preserves one canonical model-presentation identity through the host", () => { let observedPresentation: | ReturnType | undefined; function PresentationProbe() { observedPresentation = useToolcraftModelPresentationMode(); return
; } const composition = composeToolcraftApp( resolveProduct( [model3dModule({ presentation: "custom" })], createModelOnlyBase(), ), { modelPresentation: { consumers: [{ id: "model", sourceTarget: "source.model" }], mode: "custom", }, scene: { canvasContent: , }, }, ); render(); expect(screen.getByTestId("presentation-probe")).toBeTruthy(); expect(observedPresentation).toBe(composition.modelPresentation); }); });