import { describe, expect, it } from "vitest"; import { layersModule } from "../modules/built-ins/layers/layers-module"; import { mediaSourceModule } from "../modules/built-ins/media-source/media-source-module"; import { timelineModule } from "../modules/built-ins/timeline/timeline-module"; import { defineToolcraft } from "./define-toolcraft"; import { resolveToolcraftPersistencePlan } from "./persistence-plan"; import type { ResolvedToolcraftLocalStoragePersistenceSchema, ToolcraftLocalStoragePersistenceSchema, } from "./types"; function assertResolvedPersistenceTypeIsReadonly( resolved: ResolvedToolcraftLocalStoragePersistenceSchema, explicit: ToolcraftLocalStoragePersistenceSchema, ): void { // @ts-expect-error Resolved persistence is immutable after normalization. resolved.version = 3; // @ts-expect-error Resolved persistence slices are immutable after normalization. resolved.include.push("media"); // @ts-expect-error Resolved persistence value targets are immutable after normalization. resolved.additionalValueTargets.push("composition.layout"); explicit.version = 3; } void assertResolvedPersistenceTypeIsReadonly; function createBaseSchema() { return defineToolcraft({ base: { identity: { id: "base-tool", title: "Base Tool" }, canvas: { enabled: true }, panels: {}, }, modules: [], }); } describe("resolveToolcraftPersistencePlan", () => { it("persists the frozen base workspace slices by default", () => { const app = createBaseSchema(); expect(app.persistence).toEqual({ additionalValueTargets: [], include: ["canvas", "panels", "values"], key: "toolcraft:base-tool:state:v2", storage: "localStorage", version: 2, }); expect(app.persistence.storage).toBe("localStorage"); if (app.persistence.storage === "localStorage") { expect(Object.isFrozen(app.persistence)).toBe(true); expect(Object.isFrozen(app.persistence.additionalValueTargets)).toBe( true, ); expect(Object.isFrozen(app.persistence.include)).toBe(true); } }); it("adds layers only when the resolved layers panel exists", () => { const app = defineToolcraft({ base: { identity: { id: "layers", title: "Layers" }, canvas: { enabled: true }, panels: {}, }, modules: [layersModule()], }); expect(app.persistence).toMatchObject({ include: ["canvas", "layers", "panels", "values"], }); }); it("adds timeline only when the resolved timeline is enabled", () => { const enabled = defineToolcraft({ base: { identity: { id: "timeline", title: "Timeline" }, canvas: { enabled: true }, panels: {}, }, modules: [timelineModule({ mode: "keyframes" })], }); const disabled = defineToolcraft({ base: { identity: { id: "no-timeline", title: "No Timeline" }, canvas: { enabled: true }, panels: {}, }, modules: [], }); expect(enabled.persistence).toMatchObject({ include: ["canvas", "panels", "timeline", "values"], }); expect(disabled.persistence).toMatchObject({ include: ["canvas", "panels", "values"], }); }); it("adds media for declared default assets", () => { const app = defineToolcraft({ base: { identity: { id: "defaults", title: "Defaults" }, canvas: { enabled: true }, media: { defaultAssets: [ { ingressPolicy: "prepared-source", position: { x: 0, y: 0 }, sourceSize: { height: 512, unit: "px", width: 512 }, assetKind: "image", dataUrl: "data:image/png;base64,AA==", fileName: "default.png", }, ], }, panels: {}, }, modules: [mediaSourceModule()], }); expect(app.persistence).toMatchObject({ include: ["canvas", "media", "panels", "values"], }); }); it("uses resolved requirements as the only optional-slice authority", () => { const common = { identity: Object.freeze({ id: "product-mode", title: "Product Mode" }), persistence: undefined, }; expect( resolveToolcraftPersistencePlan({ ...common, requiredSlices: [], }), ).toMatchObject({ include: ["canvas", "panels", "values"] }); expect( resolveToolcraftPersistencePlan({ ...common, requiredSlices: ["timeline", "layers", "media", "timeline"], }), ).toMatchObject({ include: ["canvas", "layers", "media", "panels", "timeline", "values"], }); }); it("adds media for canvas upload capability", () => { const app = defineToolcraft({ base: { identity: { id: "canvas-upload", title: "Canvas Upload" }, canvas: { enabled: true, upload: true }, panels: {}, }, modules: [mediaSourceModule()], }); expect(app.persistence).toMatchObject({ include: ["canvas", "media", "panels", "values"], }); }); it("adds media for panel fileDrop capability", () => { const app = defineToolcraft({ base: { identity: { id: "panel-upload", title: "Panel Upload" }, canvas: { enabled: true }, panels: { controls: { sections: [ { id: "test-section-1", controls: { source: { applicability: { mode: "always" as const }, assetKind: "file", target: "source.files", type: "fileDrop", }, }, title: "Source", }, ], title: "Controls", }, }, }, modules: [mediaSourceModule()], }); expect(app.persistence).toMatchObject({ include: ["canvas", "media", "panels", "values"], }); }); it("does not add media to a truly media-free app", () => { expect(createBaseSchema().persistence).toMatchObject({ include: ["canvas", "panels", "values"], }); }); it("rejects storage none when modules require persisted slices", () => { expect(() => defineToolcraft({ base: { identity: { id: "private", title: "Private" }, canvas: { enabled: true }, panels: {}, persistence: { storage: "none" }, }, modules: [layersModule(), timelineModule({ mode: "keyframes" })], }), ).toThrow(/conflicts with resolved persistence slices/); }); it("derives key and version while sorting a unique mandatory union", () => { const app = defineToolcraft({ base: { identity: { id: "explicit", title: "Explicit" }, canvas: { enabled: true, upload: true }, panels: {}, persistence: { additionalValueTargets: [ " composition.layout ", "composition.layout", "", ], storage: "localStorage", }, }, modules: [ layersModule(), mediaSourceModule(), timelineModule({ mode: "keyframes" }), ], }); expect(app.persistence).toEqual({ additionalValueTargets: ["composition.layout"], include: ["canvas", "layers", "media", "panels", "timeline", "values"], key: "toolcraft:explicit:state:v2", storage: "localStorage", version: 2, }); }); it("normalizes additional targets before rejecting the reserved nested-address namespace", () => { expect(() => defineToolcraft({ base: { identity: { id: "reserved-target", title: "Reserved target" }, canvas: { enabled: true }, panels: {}, persistence: { additionalValueTargets: [" toolcraft:collection-item:v1:forged "], storage: "localStorage", }, }, modules: [], }), ).toThrow(/reserved-collection-item-target/u); }); });