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