import { resolveToolcraftPanelSurfaceContributions } from "./contributions/panel-surface-contributions"; import { snapshotToolcraftModuleData } from "./contract/module-data"; import * as React from "react"; import { render, screen } from "@testing-library/react"; import { describe, expect, it } from "vitest"; import { resolveToolcraftModuleDependencies } from "./resolution/resolve-module-dependencies"; import { resolveToolcraftArtifactSettingsContributions } from "./contributions/artifact-settings-contributions"; import { assertUniqueToolcraftCommandOwners } from "./contract/command-handlers"; import { createToolcraftPersistenceCodec } from "./contract/persistence-codec"; import { resolveToolcraftModuleBindings } from "./contract/module-bindings"; import { normalizeToolcraftSettingsContribution } from "./contract/control-section-contribution"; import { gridSettingsModule } from "./testing/extensibility/settings-module"; import { notesCommands, notesModule, notesPersistence, NotesSurface, reduceNotes } from "./testing/extensibility/notes-module"; import { imageExportModule } from "./built-ins/image-export/image-export-module"; describe("module extension mechanisms", () => { it("resolves and materializes unrelated settings with arbitrary fields and existing control types", () => { const definition = gridSettingsModule(24); const resolved = resolveToolcraftModuleDependencies([definition], []); expect(resolved.modules.map(module => module.id)).toEqual(["grid-settings"]); const imageSettings = imageExportModule().contributions.filter(contribution => contribution.kind === "control-section"); const sections = resolveToolcraftArtifactSettingsContributions([...imageSettings, ...definition.contributions]); expect(sections.map(section => section.id)).toEqual(["runtime.grid-settings", "runtime.image-export"]); expect(sections[0]?.controls.spacing).toMatchObject({ type: "slider", defaultValue: 24, target: "grid.spacing" }); expect(Object.keys(sections[0]!.controls)).toEqual(["spacing", "color"]); expect(Object.isFrozen(sections[0]!.controls.spacing)).toBe(true); expect(() => gridSettingsModule(65)).toThrow("Grid spacing"); }); it("checks settings targets/layout and rejects executable declarations before materialization", () => { const contribution = gridSettingsModule().contributions[0]!; expect(() => normalizeToolcraftSettingsContribution({ ...contribution, section: { ...contribution.section, controls: { ...contribution.section.controls, duplicate: contribution.section.controls.spacing! }, } })).toThrow("Duplicate control target"); expect(() => normalizeToolcraftSettingsContribution({ ...contribution, section: { ...contribution.section, layoutGroups: [{ controls: ["missing"], layout: "inline" }], } })).toThrow("Unknown layout control"); expect(() => snapshotToolcraftModuleData({ callback: () => { } })).toThrow("serializable data"); let reads = 0; expect(() => snapshotToolcraftModuleData({ get value() { reads += 1; return 1; } })).toThrow("accessors"); expect(reads).toBe(0); }); it("binds a new panel, applies its typed command once and restores its payload through the shared codec", () => { const definition = notesModule(); const resolution = resolveToolcraftModuleDependencies([definition], []); expect(resolveToolcraftPanelSurfaceContributions(definition.contributions)).toEqual({ notes: { enabled: true } }); const state = reduceNotes({ notes: [], revision: 0 }, { type: "notes.replace", notes: ["First", "Second"] }); expect(state.revision).toBe(1); const saved = JSON.stringify(notesPersistence.write(state, undefined, new Set(["notes"]))); const restored = notesPersistence.read(undefined, JSON.parse(saved), new Set(["notes"])); expect(restored).toEqual({ notes: ["First", "Second"] }); expect(notesPersistence.read(undefined, { notes: [12] }, new Set(["notes"]))).toBeUndefined(); expect(notesPersistence.write(state, undefined, new Set())).toEqual({}); const bindings = resolveToolcraftModuleBindings(resolution.modules, { notes: NotesSurface }); const view = render(<>{bindings.map(({ moduleId, binding: Surface }) => )}); expect(screen.getByLabelText("Notes").textContent).toBe("First; Second"); expect(view.container.querySelector('[data-panel-id="notes"]')).not.toBeNull(); const absent = resolveToolcraftModuleBindings([], { notes: NotesSurface }); view.rerender(<>{absent.map(({ moduleId, binding: Surface }) => )}); expect(screen.queryByLabelText("Notes")).toBeNull(); }); it("rejects command and persistence ownership collisions instead of overwriting a handler", () => { expect(() => assertUniqueToolcraftCommandOwners({ notes: notesCommands, duplicate: notesCommands })).toThrow('command "notes.replace" has multiple owners'); const codec = { fields: ["notes"] as const, read: () => undefined, write: () => ({ notes: [] }) }; expect(() => createToolcraftPersistenceCodec({ first: codec, second: codec })).toThrow('Persistence field "notes" has multiple owners'); // @ts-expect-error A notes module requires a matching executable entry in its catalog. expect(() => resolveToolcraftModuleBindings([{ id: "notes" }], {})).toThrow('Missing runtime binding'); }); });