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 { defineToolcraft } from "../schema/define-toolcraft"; import { createToolcraftState } from "./create-template-state"; import { toolcraftReducer } from "../composition/public-state"; import type { ToolcraftCommand } from "./types"; function schemaWithLayers() { return defineToolcraft({ base: { identity: { id: "runtime-test", title: "Toolcraft App" }, canvas: { enabled: true, upload: true }, panels: { controls: { sections: [ { id: "test-section-1", controls: { source: { applicability: { mode: "always" as const }, defaultValue: null, multiple: true, target: "source.media", type: "fileDrop", }, }, }, ], title: "Controls", }, }, }, modules: [layersModule(), mediaSourceModule()], }); } describe("media.importBatch", () => { it("commits a complete logical batch through one history boundary", () => { const state = createToolcraftState(schemaWithLayers()); const imported = toolcraftReducer(state, { assets: [ { asset: { assetKind: "image", fileName: "one.png", lifecycle: "ready", mimeType: "image/png", position: { x: 0, y: 0 }, resourceRef: "media:image:one", sourceTarget: "source.media", sourceSize: { height: 300, unit: "px", width: 400 }, }, policy: "prepared-source", }, { asset: { assetKind: "image", fileName: "two.png", lifecycle: "ready", mimeType: "image/png", position: { x: 0, y: 0 }, resourceRef: "media:image:two", sourceTarget: "source.media", sourceSize: { height: 600, unit: "px", width: 800 }, }, policy: "prepared-source", }, ], replaceExisting: false, type: "media.importBatch", }); expect(imported.mediaAssets).toHaveLength(2); expect(imported.mediaAssets.map((asset) => asset.assetKind)).toEqual([ "image", "image", ]); expect(imported.mediaAssets).toEqual([ expect.objectContaining({ position: { x: 0, y: 0 }, size: state.canvas.size, sourceSize: { height: 300, unit: "px", width: 400 }, }), expect.objectContaining({ position: { x: 0, y: 0 }, size: state.canvas.size, sourceSize: { height: 600, unit: "px", width: 800 }, }), ]); expect(new Set(imported.mediaAssets.map((asset) => asset.id)).size).toBe(2); expect( new Set(imported.mediaAssets.map((asset) => asset.layerId)).size, ).toBe(2); expect(imported.history.undo).toHaveLength(1); expect(imported.history.undo[0]?.label).toBe("Import media"); }); it("rejects the removed single-asset media.import command", () => { const state = createToolcraftState(schemaWithLayers()); const removedCommand = { asset: { dataUrl: "data:image/png;base64,bGVnYWN5", fileName: "legacy.png", mimeType: "image/png", position: { x: 0, y: 0 }, size: { height: 300, unit: "px", width: 400 }, }, type: "media.import", } as const; // @ts-expect-error Single-asset media.import is not a current command. const typedCommand: ToolcraftCommand = removedCommand; expect(typedCommand).toBe(removedCommand); expect(() => toolcraftReducer(state, removedCommand as unknown as ToolcraftCommand), ).toThrow("Unsupported Toolcraft command: media.import"); }); it("uses decoded source geometry for prepared images on an infinite canvas", () => { const state = createToolcraftState(schemaWithLayers(), { canvas: { mode: "infinite" }, }); const imported = toolcraftReducer(state, { assets: [ { asset: { assetKind: "image", fileName: "infinite.png", lifecycle: "ready", mimeType: "image/png", position: { x: 120, y: -40 }, resourceRef: "media:image:infinite", sourceSize: { height: 300, unit: "px", width: 400 }, }, policy: "prepared-source", }, ], type: "media.importBatch", }); expect(imported.mediaAssets[0]).toMatchObject({ position: { x: 120, y: -40 }, size: { height: 300, unit: "px", width: 400 }, sourceSize: { height: 300, unit: "px", width: 400 }, }); }); it("resizes intrinsic finite canvas geometry from a prepared image once", () => { const schema = defineToolcraft({ base: { identity: { id: "runtime-test", title: "Toolcraft fixture", }, canvas: { enabled: true, sizing: { mode: "intrinsic-media" }, upload: true, }, panels: {}, }, modules: [mediaSourceModule()], }); const state = createToolcraftState(schema); const imported = toolcraftReducer(state, { assets: [ { asset: { assetKind: "image", fileName: "intrinsic.png", lifecycle: "ready", mimeType: "image/png", position: { x: 120, y: -40 }, resourceRef: "media:image:intrinsic", sourceSize: { height: 600, unit: "px", width: 800 }, }, policy: "prepared-source", }, ], type: "media.importBatch", }); expect(imported.canvas.size).toEqual({ height: 600, unit: "px", width: 800, }); expect(imported.mediaAssets[0]).toMatchObject({ position: { x: 120, y: -40 }, size: { height: 600, unit: "px", width: 800 }, sourceSize: { height: 600, unit: "px", width: 800 }, }); }); it("uses canonical source geometry for intrinsic resizing without changing its scene frame", () => { const schema = defineToolcraft({ base: { identity: { id: "runtime-test", title: "Toolcraft fixture", }, canvas: { enabled: true, sizing: { mode: "intrinsic-media" }, upload: true, }, panels: {}, }, modules: [mediaSourceModule()], }); const state = createToolcraftState(schema); const imported = toolcraftReducer(state, { assets: [ { asset: { assetKind: "image", fileName: "authored.png", lifecycle: "ready", mimeType: "image/png", position: { x: 75, y: -25 }, resourceRef: "media:image:authored", size: { height: 180, unit: "px", width: 320 }, sourceSize: { height: 900, unit: "px", width: 1600 }, }, policy: "canonical-runtime", }, ], type: "media.importBatch", }); expect(imported.canvas.size).toEqual({ height: 900, unit: "px", width: 1600, }); expect(imported.mediaAssets[0]).toMatchObject({ position: { x: 75, y: -25 }, size: { height: 180, unit: "px", width: 320 }, sourceSize: { height: 900, unit: "px", width: 1600 }, }); }); });