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);
});
});