import { cleanup, render, screen } from "@testing-library/react";
import { readFileSync } from "node:fs";
import { join } from "node:path";
import type { ReactNode } from "react";
import { afterEach, describe, expect, expectTypeOf, it } from "vitest";
import { imageExportModule } from "../../modules/built-ins/image-export/image-export-module";
import { svgExportModule } from "../../modules/built-ins/svg-export/svg-export-module";
import { composeToolcraftApp } from "./compose-toolcraft-app";
import {
pipelineRegistration,
PRODUCT_SCENE,
rasterRenderer,
resolveProduct,
vectorRenderer,
} from "./compose-toolcraft-app.test-support";
import {
getToolcraftProvidedIntegrationPortIds,
type ToolcraftAppPorts,
} from "./toolcraft-app-ports";
import { ToolcraftApp } from "./toolcraft-app";
afterEach(() => {
cleanup();
window.localStorage.clear();
});
describe("composeToolcraftApp port snapshots", () => {
it("freezes named port domains without mutating caller aliases", () => {
const sourceRasterRenderer = {
baseFileName: "source-raster",
renderFrame: () => undefined,
};
const sourceVectorRenderer = {
baseFileName: "source-vector",
renderFrame: () => undefined,
};
const controlRenderers = { custom: () => null };
const initialCanvasNode = (
);
const lateCanvasNode = ;
const nestedCanvasContent = [initialCanvasNode];
const canvasContent = [nestedCanvasContent];
const scene: NonNullable = {
canvasContent,
rasterFrameRenderer: sourceRasterRenderer,
renderDefaultCanvasMedia: false,
vectorFrameRenderer: sourceVectorRenderer,
};
const ports: ToolcraftAppPorts = {
controls: { renderers: controlRenderers },
scene,
};
const composition = composeToolcraftApp(
resolveProduct([imageExportModule(), svgExportModule()]),
ports,
);
const before = JSON.stringify({
raster: composition.exportRenderer?.baseFileName,
renderDefault: composition.renderDefaultCanvasMedia,
rendererKeys: Object.keys(composition.controlRenderers ?? {}),
vector: composition.svgExportRenderer?.baseFileName,
});
(scene as { renderDefaultCanvasMedia?: boolean }).renderDefaultCanvasMedia =
true;
sourceRasterRenderer.baseFileName = "changed-raster";
sourceVectorRenderer.baseFileName = "changed-vector";
(controlRenderers as Record null>).later = () => null;
nestedCanvasContent.push(lateCanvasNode);
expect(
JSON.stringify({
raster: composition.exportRenderer?.baseFileName,
renderDefault: composition.renderDefaultCanvasMedia,
rendererKeys: Object.keys(composition.controlRenderers ?? {}),
vector: composition.svgExportRenderer?.baseFileName,
}),
).toBe(before);
expect(Object.isFrozen(ports)).toBe(false);
expect(Object.isFrozen(scene)).toBe(false);
expect(Object.isFrozen(composition.exportRenderer)).toBe(true);
expect(Object.isFrozen(composition.svgExportRenderer)).toBe(true);
expect(Object.isFrozen(composition.controlRenderers)).toBe(true);
expect(Object.isFrozen(composition.canvasContent)).toBe(true);
if (!Array.isArray(composition.canvasContent)) {
throw new Error("Expected a snapshotted canvas child array.");
}
expect(composition.canvasContent).not.toBe(canvasContent);
expect(Object.isFrozen(composition.canvasContent[0])).toBe(true);
const nestedSnapshot = composition.canvasContent[0];
if (!Array.isArray(nestedSnapshot)) {
throw new Error("Expected a nested snapshotted canvas child array.");
}
expect(nestedSnapshot[0]).toBe(initialCanvasNode);
render();
expect(screen.getByTestId("initial-snapshot-node")).toBeTruthy();
expect(screen.queryByTestId("late-snapshot-node")).toBeNull();
});
it("keeps a closed port surface and an actually immutable exact id set", () => {
expectTypeOf().toEqualTypeOf<
"actions" | "controls" | "modelPresentation" | "renderer" | "scene"
>();
expectTypeOf>().toEqualTypeOf<
| "canvasContent"
| "infiniteCanvasContent"
| "rasterFrameRenderer"
| "renderDefaultCanvasMedia"
| "sceneBoundsProvider"
| "vectorFrameRenderer"
>();
expectTypeOf<
keyof NonNullable
>().toEqualTypeOf<"renderers">();
expect(
readFileSync(
join(process.cwd(), "src/react/app-shell/toolcraft-app.tsx"),
"utf8",
),
).not.toContain('id: "test-section-1"');
const provided = getToolcraftProvidedIntegrationPortIds({
actions: { onPanelAction: () => undefined },
controls: { renderers: { custom: () => null } },
modelPresentation: {
consumers: [{ id: "model", sourceTarget: "source.model" }],
mode: "custom",
},
renderer: { pipelineRegistration },
scene: {
canvasContent: PRODUCT_SCENE,
infiniteCanvasContent: ,
rasterFrameRenderer: rasterRenderer,
renderDefaultCanvasMedia: false,
sceneBoundsProvider: () => [],
vectorFrameRenderer: vectorRenderer,
},
});
expect([...provided]).toEqual([
"modelPresentation",
"scene.canvasContent",
"scene.rasterFrameRenderer",
"scene.vectorFrameRenderer",
]);
expect(Object.isFrozen(provided)).toBe(true);
expect("add" in provided).toBe(false);
const cyclicChildren: ReactNode[] = [];
cyclicChildren.push(cyclicChildren);
expect(() =>
composeToolcraftApp(resolveProduct(), {
scene: { canvasContent: cyclicChildren },
}),
).toThrow("Toolcraft app ReactNode containers cannot be cyclic.");
});
it("snapshots mutable ReactNode iterables and rejects iterable cycles", () => {
const initialEnvironment = ;
const lateEnvironment = ;
const mutableEnvironment = new Set([initialEnvironment]);
const composition = composeToolcraftApp(resolveProduct(), {
scene: { infiniteCanvasContent: mutableEnvironment },
});
mutableEnvironment.add(lateEnvironment);
expect(composition.infiniteCanvasContent).toEqual([initialEnvironment]);
expect(Object.isFrozen(composition.infiniteCanvasContent)).toBe(true);
const cyclicEnvironment = new Set();
cyclicEnvironment.add(cyclicEnvironment);
expect(() =>
composeToolcraftApp(resolveProduct(), {
scene: { infiniteCanvasContent: cyclicEnvironment },
}),
).toThrow("Toolcraft app ReactNode containers cannot be cyclic.");
});
it("maps every named port domain to its existing host consumer", () => {
const infiniteEnvironment = ;
const lateInfiniteEnvironment = (
);
const infiniteCanvasContent = [infiniteEnvironment];
const onPanelAction = () => undefined;
const sceneBoundsProvider = () => [];
const customControlRenderer = () => null;
const composition = composeToolcraftApp(
resolveProduct([imageExportModule(), svgExportModule()]),
{
actions: { onPanelAction },
controls: { renderers: { custom: customControlRenderer } },
renderer: { pipelineRegistration },
scene: {
canvasContent: PRODUCT_SCENE,
infiniteCanvasContent,
rasterFrameRenderer: rasterRenderer,
renderDefaultCanvasMedia: false,
sceneBoundsProvider,
vectorFrameRenderer: vectorRenderer,
},
},
);
infiniteCanvasContent.push(lateInfiniteEnvironment);
render();
expect(screen.getByTestId("composed-product-scene")).toBeTruthy();
expect(composition.controlRenderers?.custom).toBe(customControlRenderer);
expect(composition.exportRenderer).toEqual(rasterRenderer);
expect(composition.infiniteCanvasContent).not.toBe(infiniteCanvasContent);
expect(Object.isFrozen(composition.infiniteCanvasContent)).toBe(true);
if (!Array.isArray(composition.infiniteCanvasContent)) {
throw new Error("Expected a snapshotted Infinity child array.");
}
expect(composition.infiniteCanvasContent).toEqual([infiniteEnvironment]);
expect(composition.onPanelAction).toBe(onPanelAction);
expect(composition.renderDefaultCanvasMedia).toBe(false);
expect(composition.rendererPipelineRegistration).toBe(pipelineRegistration);
expect(composition.sceneBoundsProvider).toBe(sceneBoundsProvider);
expect(composition.svgExportRenderer).toEqual(vectorRenderer);
});
});