import { describe, expect, it } from "vitest"; import { toolcraftReducer } from "./reducer"; import { createState } from "./reducer-test-utils"; describe("toolcraftReducer layers", () => { it("adds and selects runtime layers and groups", () => { const withGroup = toolcraftReducer(createState(), { layer: { displayName: "Scene Group", id: "group-1", kind: "group", name: "scene-group", }, type: "layers.add", }); const state = toolcraftReducer(withGroup, { insertIndex: 1, layer: { displayName: "Layer 1", id: "layer-1", name: "layer-1", parentGroupId: "group-1", }, type: "layers.add", }); expect(state.layers).toEqual([ { collapsed: false, displayName: "Scene Group", id: "group-1", kind: "group", name: "scene-group", parentGroupId: undefined, visible: true, }, { collapsed: undefined, displayName: "Layer 1", id: "layer-1", kind: "layer", name: "layer-1", parentGroupId: "group-1", visible: true, }, ]); expect(state.selectedLayerId).toBe("layer-1"); }); it("renames layers and toggles visibility and collapsed groups", () => { const withGroup = toolcraftReducer(createState(), { layer: { id: "group-1", kind: "group", name: "Scene Group" }, type: "layers.add", }); const hidden = toolcraftReducer(withGroup, { layerId: "group-1", type: "layers.toggleVisibility", }); const collapsed = toolcraftReducer(hidden, { layerId: "group-1", type: "layers.toggleCollapsed", }); const renamed = toolcraftReducer(collapsed, { layerId: "group-1", name: "Main Scene", type: "layers.rename", }); expect(renamed.layers[0]).toMatchObject({ collapsed: true, displayName: "Main Scene", visible: false, }); expect(renamed.history.undo.at(-1)?.label).toBe("Rename layer"); }); it("deletes groups with their children", () => { const withGroup = toolcraftReducer(createState(), { layer: { id: "group-1", kind: "group", name: "Group 1" }, type: "layers.add", }); const withChild = toolcraftReducer(withGroup, { layer: { id: "layer-1", name: "Layer 1", parentGroupId: "group-1" }, type: "layers.add", }); const withSibling = toolcraftReducer(withChild, { layer: { id: "layer-2", name: "Layer 2" }, type: "layers.add", }); const state = toolcraftReducer(withSibling, { layerId: "group-1", type: "layers.delete", }); expect(state.layers.map((layer) => layer.id)).toEqual(["layer-2"]); expect(state.selectedLayerId).toBe("layer-2"); }); it("deletes nested groups with every descendant layer", () => { let state = createState(); state = toolcraftReducer(state, { layer: { id: "group-1", kind: "group", name: "Group 1" }, type: "layers.add", }); state = toolcraftReducer(state, { layer: { id: "group-2", kind: "group", name: "Group 2", parentGroupId: "group-1" }, type: "layers.add", }); state = toolcraftReducer(state, { layer: { id: "layer-1", name: "Layer 1", parentGroupId: "group-2" }, type: "layers.add", }); state = toolcraftReducer(state, { layer: { id: "layer-2", name: "Layer 2" }, type: "layers.add", }); const deleted = toolcraftReducer(state, { layerId: "group-1", type: "layers.delete", }); expect(deleted.layers.map((layer) => layer.id)).toEqual(["layer-2"]); expect(deleted.selectedLayerId).toBe("layer-2"); }); it("deletes media assets attached to a layer", () => { const imported = toolcraftReducer(createState(), { asset: { dataUrl: "data:image/png;base64,test", fileName: "material.png", mimeType: "image/png", position: { x: 0, y: 0 }, size: { width: 1024, height: 768, unit: "px" }, }, type: "media.import", }); const state = toolcraftReducer(imported, { layerId: "layer-1", type: "layers.delete", }); expect(state.layers).toEqual([]); expect(state.mediaAssets).toEqual([]); expect(state.selectedLayerId).toBeNull(); }); it("moves layers into groups and back to root", () => { const withGroup = toolcraftReducer(createState(), { layer: { id: "group-1", kind: "group", name: "Group 1" }, type: "layers.add", }); const withLayer = toolcraftReducer(withGroup, { layer: { id: "layer-1", name: "Layer 1" }, type: "layers.add", }); const grouped = toolcraftReducer(withLayer, { layerIds: ["layer-1"], parentGroupId: "group-1", type: "layers.moveToGroup", }); const rooted = toolcraftReducer(grouped, { layerIds: ["layer-1"], parentGroupId: null, type: "layers.moveToGroup", }); expect(grouped.layers.find((layer) => layer.id === "layer-1")?.parentGroupId).toBe("group-1"); expect(grouped.history.undo.at(-1)?.label).toBe("Move layers to group"); expect(rooted.layers.find((layer) => layer.id === "layer-1")?.parentGroupId).toBeUndefined(); expect(rooted.history.undo.at(-1)?.label).toBe("Move layers to root"); }); it("opens collapsed groups when moving layers into them", () => { const withGroup = toolcraftReducer(createState(), { layer: { id: "group-1", kind: "group", name: "Group 1" }, type: "layers.add", }); const withCollapsedGroup = toolcraftReducer(withGroup, { layerId: "group-1", type: "layers.toggleCollapsed", }); const withLayer = toolcraftReducer(withCollapsedGroup, { layer: { id: "layer-1", name: "Layer 1" }, type: "layers.add", }); const grouped = toolcraftReducer(withLayer, { layerIds: ["layer-1"], parentGroupId: "group-1", type: "layers.moveToGroup", }); expect(grouped.layers.find((layer) => layer.id === "group-1")?.collapsed).toBe(false); expect(grouped.layers.find((layer) => layer.id === "layer-1")?.parentGroupId).toBe("group-1"); }); it("places moved layers directly under the target group", () => { const withFirst = toolcraftReducer(createState(), { layer: { id: "layer-1", name: "Layer 1" }, type: "layers.add", }); const withGroup = toolcraftReducer(withFirst, { layer: { id: "group-1", kind: "group", name: "Group 1" }, type: "layers.add", }); const withExistingChild = toolcraftReducer(withGroup, { layer: { id: "layer-2", name: "Layer 2", parentGroupId: "group-1" }, type: "layers.add", }); const withDraggedLayer = toolcraftReducer(withExistingChild, { layer: { id: "layer-3", name: "Layer 3" }, type: "layers.add", }); const state = toolcraftReducer(withDraggedLayer, { layerIds: ["layer-3"], parentGroupId: "group-1", type: "layers.moveToGroup", }); expect(state.layers.map((layer) => layer.id)).toEqual([ "layer-1", "group-1", "layer-3", "layer-2", ]); expect(state.layers.find((layer) => layer.id === "layer-3")?.parentGroupId).toBe("group-1"); }); it("does not move groups into their own descendants", () => { const withParent = toolcraftReducer(createState(), { layer: { id: "group-1", kind: "group", name: "Group 1" }, type: "layers.add", }); const withChild = toolcraftReducer(withParent, { layer: { id: "group-2", kind: "group", name: "Group 2", parentGroupId: "group-1" }, type: "layers.add", }); const state = toolcraftReducer(withChild, { layerIds: ["group-1"], parentGroupId: "group-2", type: "layers.moveToGroup", }); expect(state).toBe(withChild); }); it("reorders layers and supports undo and redo", () => { const withFirst = toolcraftReducer(createState(), { layer: { id: "layer-1", name: "Layer 1" }, type: "layers.add", }); const withSecond = toolcraftReducer(withFirst, { layer: { id: "layer-2", name: "Layer 2" }, type: "layers.add", }); const reordered = toolcraftReducer(withSecond, { layers: [withSecond.layers[1]!, withSecond.layers[0]!], selectedLayerId: "layer-1", type: "layers.reorder", }); const undone = toolcraftReducer(reordered, { type: "history.undo" }); const redone = toolcraftReducer(undone, { type: "history.redo" }); expect(reordered.layers.map((layer) => layer.id)).toEqual(["layer-2", "layer-1"]); expect(reordered.selectedLayerId).toBe("layer-1"); expect(undone.layers.map((layer) => layer.id)).toEqual(["layer-1", "layer-2"]); expect(redone.layers.map((layer) => layer.id)).toEqual(["layer-2", "layer-1"]); }); it("preserves nested move undo and redo", () => { let state = createState(); state = toolcraftReducer(state, { layer: { id: "group-1", kind: "group", name: "Group 1" }, type: "layers.add", }); state = toolcraftReducer(state, { layer: { id: "group-2", kind: "group", name: "Group 2" }, type: "layers.add", }); state = toolcraftReducer(state, { layer: { id: "layer-1", name: "Layer 1", parentGroupId: "group-1" }, type: "layers.add", }); const moved = toolcraftReducer(state, { layerIds: ["layer-1"], parentGroupId: "group-2", type: "layers.moveToGroup", }); const undone = toolcraftReducer(moved, { type: "history.undo" }); const redone = toolcraftReducer(undone, { type: "history.redo" }); expect(moved.layers.find((layer) => layer.id === "layer-1")?.parentGroupId).toBe("group-2"); expect(undone.layers.find((layer) => layer.id === "layer-1")?.parentGroupId).toBe("group-1"); expect(redone.layers.find((layer) => layer.id === "layer-1")?.parentGroupId).toBe("group-2"); }); });