import { describe, expect, it } from "vitest"; import { defineToolcraft } from "../schema/define-toolcraft"; import { createToolcraftState } from "./create-template-state"; import { importImageMedia } from "./reducer-media-test-utils"; import { toolcraftReducer } from "./reducer"; import { createState } from "./reducer-test-utils"; describe("toolcraftReducer media import sizing", () => { it("imports media as an editable-canvas-sized layer and records history", () => { const state = createState(); const next = importImageMedia(state, { fileName: "material.png", position: { x: 10, y: 20 }, size: state.canvas.size, }); expect(next.layers).toEqual([ { displayName: "material", id: "layer-1", kind: "layer", name: "material", visible: true, }, ]); expect(next.mediaAssets).toEqual([ expect.objectContaining({ assetKind: "image", fileName: "material.png", id: "media-1", layerId: "layer-1", mimeType: "image/png", position: { x: 10, y: 20 }, size: state.canvas.size, lifecycle: "restoring", resourceRef: expect.stringMatching(/^media:image:sha256:/u), }), ]); expect(next.selectedLayerId).toBe("layer-1"); expect(next.history.undo.at(-1)?.label).toBe("Import media"); }); it("deletes imported media without deleting the owning layer", () => { const state = importImageMedia(createState(), { fileName: "material.png", position: { x: 10, y: 20 }, size: { height: 512, unit: "px", width: 512 }, }); const next = toolcraftReducer(state, { mediaId: "media-1", type: "media.delete", }); expect(next.layers).toEqual(state.layers); expect(next.selectedLayerId).toBe("layer-1"); expect(next.mediaAssets).toEqual([]); expect(next.history.undo.at(-1)?.label).toBe("Delete media"); }); it("keeps the current editable-output canvas size when importing background/source images", () => { const app = defineToolcraft({ canvas: { enabled: true, size: { height: 720, unit: "px", width: 1280 }, upload: true, }, panels: {}, }); const state = createToolcraftState(app); const imported = importImageMedia(state, { dataUrl: "data:image/png;base64,wide", fileName: "wide-source.png", position: { x: 42, y: 24 }, size: { height: 1000, unit: "px", width: 4000 }, }); expect(imported.schema.canvas.sizing).toEqual({ mode: "editable-output" }); expect(imported.canvas.size).toEqual({ height: 720, unit: "px", width: 1280 }); expect(imported.mediaAssets[0]).toMatchObject({ fileName: "wide-source.png", position: { x: 42, y: 24 }, size: { height: 1000, unit: "px", width: 4000 }, }); expect(imported.history.undo.at(-1)?.after).not.toHaveProperty("canvas.size"); }); it("keeps manually edited editable-output canvas size when importing source images", () => { const app = defineToolcraft({ canvas: { enabled: true, upload: true, }, panels: { controls: { sections: [], title: "Controls", }, }, }); const widthChanged = toolcraftReducer(createToolcraftState(app), { target: "canvas.size.width", type: "controls.setValue", value: "1440", }); const heightChanged = toolcraftReducer(widthChanged, { target: "canvas.size.height", type: "controls.setValue", value: "900", }); const imported = importImageMedia(heightChanged, { dataUrl: "data:image/png;base64,tall", fileName: "tall-source.png", size: { height: 4000, unit: "px", width: 1000 }, }); expect(imported.schema.canvas.sizing).toEqual({ mode: "editable-output" }); expect(imported.canvas.size).toEqual({ height: 900, unit: "px", width: 1440 }); expect(imported.values["canvas.size.width"]).toBe(1440); expect(imported.values["canvas.size.height"]).toBe(900); expect(imported.mediaAssets[0]).toMatchObject({ fileName: "tall-source.png", size: { height: 4000, unit: "px", width: 1000 }, }); expect(imported.history.undo.at(-1)?.after).not.toHaveProperty("canvas.size"); }); it("sizes explicit intrinsic media apps from the imported image and replaces single-layer media", () => { const app = defineToolcraft({ canvas: { enabled: true, sizing: { mode: "intrinsic-media" }, upload: true }, panels: {}, }); const state = createToolcraftState(app); const first = importImageMedia(state, { dataUrl: "data:image/png;base64,first", fileName: "first.png", position: { x: 42, y: 24 }, size: { height: 600, unit: "px", width: 800 }, }); const second = importImageMedia(first, { dataUrl: "data:image/png;base64,second", fileName: "second.png", position: { x: -20, y: 10 }, size: { height: 720, unit: "px", width: 1280 }, }); expect(first.canvas.size).toEqual({ height: 600, unit: "px", width: 800 }); expect(first.mediaAssets[0]).toMatchObject({ fileName: "first.png", position: { x: 0, y: 0 }, size: { height: 600, unit: "px", width: 800 }, }); expect(first.history.undo.at(-1)?.after).toMatchObject({ "canvas.size": { height: 600, unit: "px", width: 800 }, }); expect(second.canvas.size).toEqual({ height: 720, unit: "px", width: 1280 }); expect(second.layers).toHaveLength(1); expect(second.mediaAssets).toHaveLength(1); expect(second.mediaAssets[0]).toMatchObject({ fileName: "second.png", id: "media-1", layerId: "layer-1", position: { x: 0, y: 0 }, size: { height: 720, unit: "px", width: 1280 }, }); }); it("appends single-layer media when import is explicitly non-replacing", () => { const state = createState(); const first = importImageMedia( state, { dataUrl: "data:image/png;base64,first", fileName: "first.png", size: state.canvas.size, }, { replaceExisting: false }, ); const second = importImageMedia( first, { dataUrl: "data:image/png;base64,second", fileName: "second.png", size: state.canvas.size, }, { replaceExisting: false }, ); expect(second.layers.map((layer) => layer.id)).toEqual(["layer-1", "layer-2"]); expect(second.mediaAssets.map((asset) => asset.id)).toEqual(["media-1", "media-2"]); expect(second.mediaAssets.map((asset) => asset.fileName)).toEqual([ "first.png", "second.png", ]); expect(second.selectedLayerId).toBe("layer-2"); }); });