import { cleanup, render, screen } from "@testing-library/react"; import { afterEach, beforeAll, describe, expect, it, vi } from "vitest"; import { ToolcraftApp } from "../react/app-shell/toolcraft-app"; import { installToolcraftAppTestWindow } from "../react/app-shell/toolcraft-app-test-utils"; import type { ToolcraftProductBase } from "../schema/product-base"; import { resolveToolcraftProductDefinition } from "../schema/resolve-toolcraft-product-definition"; import { canvasEditingModule } from "./built-ins/canvas-editing/canvas-editing-module"; import { layersModule } from "./built-ins/layers/layers-module"; import { mediaSourceModule } from "./built-ins/media-source/media-source-module"; import { model3dModule } from "./built-ins/model-3d/model-3d-module"; import { spatialViewModule } from "./built-ins/spatial-view/spatial-view-module"; import type { ToolcraftProductIntegrationPortId } from "./contract/integration-port"; import type { ToolcraftProductModuleDefinition } from "./contract/module-definition"; import { getToolcraftModulePortErrors } from "./resolution/validate-module-ports"; import { permutations } from "./testing/module-contribution-test-fixtures"; import { createProjectBBaseSchema, freezeProjectBFixtureRecursively, type ProjectBBaseOptions, } from "./testing/project-b-schema-fixture-test-support"; beforeAll(() => { installToolcraftAppTestWindow(); vi.spyOn(HTMLCanvasElement.prototype, "getContext").mockReturnValue(null); }); afterEach(() => { cleanup(); window.localStorage.clear(); }); function expectRecursivelyFrozen(value: unknown): void { if (value === null || typeof value !== "object") return; expect(Object.isFrozen(value)).toBe(true); for (const child of Object.values(value)) { expectRecursivelyFrozen(child); } } const projectBModules = Object.freeze([ layersModule(), model3dModule(), canvasEditingModule(), spatialViewModule(), ]); function createProjectBFixture(base = createProjectBBaseSchema()) { return resolveToolcraftProductDefinition({ base, modules: projectBModules }); } describe("Project B current-schema vertical slice", () => { it("materializes all scene modules through defineToolcraft and the real ToolcraftApp", async () => { const base = createProjectBBaseSchema(); freezeProjectBFixtureRecursively(base); const before = JSON.stringify(base); const fixture = createProjectBFixture(base); const schema = fixture; const { modulePlan } = fixture; expect(modulePlan.capabilities).toEqual([ { capabilityId: "canvas.editing", moduleId: "canvas-editing" }, { capabilityId: "layers.management", moduleId: "layers" }, { capabilityId: "media.source", moduleId: "media-source" }, { capabilityId: "model.3d", moduleId: "model-3d" }, { capabilityId: "spatial.view", moduleId: "spatial-view" }, ]); expect(modulePlan.modules).toEqual([ { id: "canvas-editing", origin: "explicit", provides: ["canvas.editing"], requestedBy: [], requires: [], }, { id: "layers", origin: "explicit", provides: ["layers.management"], requestedBy: [], requires: [], }, { id: "media-source", origin: "default-provider", provides: ["media.source"], requestedBy: ["model-3d"], requires: [], }, { id: "model-3d", origin: "explicit", provides: ["model.3d"], requestedBy: [], requires: ["media.source"], }, { id: "spatial-view", origin: "explicit", provides: ["spatial.view"], requestedBy: [], requires: [], }, ]); expect(modulePlan.portRequirements).toEqual([ { applicability: "always", consumers: ["canvas-editing", "spatial-view"], id: "scene.canvasContent", }, ]); expect(schema.panels.layers).toBe(true); expect(schema.persistence).toEqual({ additionalValueTargets: ["product.seed"], include: ["canvas", "layers", "media", "panels", "values"], key: "toolcraft:project-b-scene:state:v2", storage: "localStorage", version: 2, }); expect(schema.assembly.surfaces.panels.layers?.enabled).toBe(true); expect(schema.assembly.capabilities).toEqual( expect.arrayContaining([ "canvas.draggable", "canvas.upload", "layers.panel", ]), ); expect(schema.assembly.commands).toEqual( expect.arrayContaining([ "layers.select", "media.importBatch", "media.transform", ]), ); for (const internalField of [ "canvasBehaviors", "mediaPolicy", "modules", "plan", "portRequirements", ]) { expect(schema).not.toHaveProperty(internalField); } expect(JSON.stringify(base)).toBe(before); expectRecursivelyFrozen(base); expectRecursivelyFrozen(modulePlan); expect(Object.isFrozen(fixture)).toBe(true); expectRecursivelyFrozen(projectBModules); const provided = new Set([ "scene.canvasContent", ] satisfies readonly ToolcraftProductIntegrationPortId[]); expect( getToolcraftModulePortErrors({ plan: modulePlan, productSceneRequired: true, provided, spatialProductSceneRequired: false, }), ).toEqual([]); for (const missing of provided) { expect( getToolcraftModulePortErrors({ plan: modulePlan, productSceneRequired: true, provided: new Set([...provided].filter((id) => id !== missing)), spatialProductSceneRequired: false, }), ).toHaveLength(1); } expect( getToolcraftModulePortErrors({ plan: modulePlan, productSceneRequired: true, provided: new Set([...provided, "modelPresentation"]), spatialProductSceneRequired: false, }), ).toEqual([ 'Toolcraft integration port "modelPresentation" is provided but unused; no resolved module consumes it. Public field "ports.modelPresentation" is not applicable.', ]); const { container } = render( } modelPresentation={{ mode: "runtime" }} schema={schema} />, ); expect(screen.getByRole("listbox", { name: "Layers" })).toBeTruthy(); expect(container.querySelector('[data-panel-type="layers"]')).toBeTruthy(); expect(screen.getByLabelText("Click to upload a file")).toBeTruthy(); expect(screen.getByLabelText("Click to upload a model")).toBeTruthy(); expect( container.querySelector( '[data-canvas-model-prewarm-target="source.model"]', ), ).toBeTruthy(); expect(await screen.findByLabelText("3D orientation gizmo")).toBeTruthy(); }); it("accepts a default model asset as the sole canonical model source", () => { const fixture = createProjectBFixture( createProjectBBaseSchema({ defaultModel: true, modelSource: false }), ); expect(fixture.media.defaultAssets).toEqual([ expect.objectContaining({ assetKind: "model", fileName: "fixture.glb" }), ]); }); it("derives module persistence requirements idempotently", () => { const base = createProjectBBaseSchema(); if (base.persistence?.storage !== "localStorage") { throw new Error("Expected localStorage fixture persistence."); } const before = JSON.stringify(base); const schema = createProjectBFixture(base); expect(schema.persistence.storage).toBe("localStorage"); expect( schema.persistence.storage === "localStorage" ? schema.persistence.include.filter((slice) => slice === "layers") : [], ).toHaveLength(1); expect( schema.persistence.storage === "localStorage" ? schema.persistence.include.filter((slice) => slice === "media") : [], ).toHaveLength(1); expect(JSON.stringify(base)).toBe(before); }); }); function expectFixtureError( base: ToolcraftProductBase, modules: readonly ToolcraftProductModuleDefinition[], diagnostics: readonly string[], ): void { const before = JSON.stringify(base); freezeProjectBFixtureRecursively(base); expect(() => resolveToolcraftProductDefinition({ base, modules }), ).toThrowError( `Toolcraft product definition validation failed:\n- ${[...diagnostics].sort().join("\n- ")}`, ); expect(JSON.stringify(base)).toBe(before); expectRecursivelyFrozen(base); } describe("Project B current-schema correspondence validation", () => { it("rejects a manually authored Layers surface owned by module contributions", () => { expectFixtureError( createProjectBBaseSchema({ layers: true }), projectBModules, [ "Toolcraft product base panels.layers is module-owned and cannot be authored directly.", ], ); }); it.each([ ["canvas upload", { canvasUpload: true, imageSource: false }], ["fileDrop", { canvasUpload: false, imageSource: true }], [ "default asset", { canvasUpload: false, defaultImage: true, imageSource: false }, ], ] satisfies readonly [string, ProjectBBaseOptions][])( "rejects a media-capable base from %s without media.source policy", (_label, options) => { expectFixtureError( createProjectBBaseSchema({ ...options, modelSource: false, orientation: false, }), [layersModule()], [ 'Product media facts require resolved capability "media.source" and media policy "source-workflow".', ], ); }, ); it("rejects model.3d without a canonical model source", () => { expectFixtureError( createProjectBBaseSchema({ modelSource: false, orientation: false }), [model3dModule()], [ 'Resolved capability "model.3d" requires a canonical model source: an assetKind "model" fileDrop or media.defaultAssets model entry.', ], ); }); it("rejects editing behavior without an enabled core canvas", () => { expectFixtureError( createProjectBBaseSchema({ canvasEnabled: false, canvasUpload: false, imageSource: false, modelSource: false, orientation: false, }), [canvasEditingModule()], ['Resolved canvas behavior "editing" requires base canvas.enabled true.'], ); }); it("rejects spatial-view behavior without core canvas and orientationGizmo", () => { expectFixtureError( createProjectBBaseSchema({ canvasEnabled: false, canvasUpload: false, imageSource: false, modelSource: false, orientation: false, }), [spatialViewModule()], [ 'Resolved capability "spatial.view" requires an orientationGizmo control.', 'Resolved canvas behavior "spatial-view" requires base canvas.enabled true.', ], ); }); it("rejects storage none with exact resolved Layers and Media Source slices", () => { const base = createProjectBBaseSchema({ modelSource: false, orientation: false, persistence: "none", }); freezeProjectBFixtureRecursively(base); expect(() => resolveToolcraftProductDefinition({ base, modules: [layersModule(), mediaSourceModule()], }), ).toThrowError( 'Toolcraft product persistence storage "none" conflicts with resolved persistence slices: layers, media.', ); }); it("returns byte-identical sorted diagnostics for every module order", () => { const base = createProjectBBaseSchema({ canvasEnabled: false, canvasUpload: false, layers: true, modelSource: false, orientation: false, persistence: "none", }); freezeProjectBFixtureRecursively(base); const expected = [ "Toolcraft product base panels.layers is module-owned and cannot be authored directly.", 'Resolved canvas behavior "editing" requires base canvas.enabled true.', 'Resolved capability "spatial.view" requires an orientationGizmo control.', 'Resolved canvas behavior "spatial-view" requires base canvas.enabled true.', 'Resolved capability "model.3d" requires a canonical model source: an assetKind "model" fileDrop or media.defaultAssets model entry.', ].sort(); for (const order of permutations(projectBModules)) { expect(() => resolveToolcraftProductDefinition({ base, modules: order }), ).toThrowError( `Toolcraft product definition validation failed:\n- ${expected.join("\n- ")}`, ); } }); });