import { describe, expect, it } from "vitest"; import { useEditorStore } from "./editor-store"; import { createProject } from "@pien-studio/editor-core"; describe("editor store", () => { it("saves and loads project from indexeddb", async () => { useEditorStore.getState().resetProject(); useEditorStore.getState().addLayerByType("sticker"); const savedId = useEditorStore.getState().project.id; await useEditorStore.getState().saveCurrentProject(); useEditorStore.getState().resetProject(); expect(useEditorStore.getState().project.id).not.toBe(savedId); await useEditorStore.getState().loadProjectById(savedId); expect(useEditorStore.getState().project.id).toBe(savedId); }); it("imports and exports project json", () => { useEditorStore.getState().resetProject(); useEditorStore.getState().addLayerByType("raster"); const json = useEditorStore.getState().exportProjectToJson(); useEditorStore.getState().resetProject(); const result = useEditorStore.getState().importProjectFromJson(json); expect(result.ok).toBe(true); expect(useEditorStore.getState().project.layers.length).toBe(1); }); it("adds layer by type and removes selected layer", () => { useEditorStore.getState().resetProject(); useEditorStore.getState().addLayerByType("text"); expect(useEditorStore.getState().project.layers[0]?.type).toBe("text"); useEditorStore.getState().removeSelectedLayer(); expect(useEditorStore.getState().project.layers).toHaveLength(0); }); it("updates selected layer position directly", () => { useEditorStore.getState().resetProject(); useEditorStore.getState().addLayerByType("text"); useEditorStore.getState().setSelectedLayerPosition(220, 140); const layer = useEditorStore.getState().project.layers[0]; expect(layer?.x).toBe(220); expect(layer?.y).toBe(140); }); it("records pointer draft transforms in undo history on commit", () => { useEditorStore.getState().resetProject(); useEditorStore.getState().addLayerByType("raster"); const initial = useEditorStore.getState().project.layers[0]; expect(initial).toBeDefined(); useEditorStore.getState().setSelectedLayerPositionDraft(180, 150); useEditorStore.getState().setSelectedLayerPosition(180, 150); expect(useEditorStore.getState().canUndo).toBe(true); useEditorStore.getState().undo(); const movedBack = useEditorStore.getState().project.layers[0]; expect(movedBack?.x).toBe(initial?.x); expect(movedBack?.y).toBe(initial?.y); }); it("reorders layers up and down", () => { useEditorStore.getState().resetProject(); useEditorStore.getState().addLayerByType("text"); useEditorStore.getState().addLayerByType("raster"); const layers = useEditorStore.getState().project.layers; const [first, second] = layers; expect(first.id).not.toBe(second.id); useEditorStore.getState().selectLayer(first.id); useEditorStore.getState().moveSelectedLayerOrder("up"); const after = useEditorStore.getState().project.layers; expect(after[0].id).toBe(second.id); expect(after[1].id).toBe(first.id); }); it("applies effects, toggles visibility, and tracks undo", () => { useEditorStore.getState().resetProject(); useEditorStore.getState().addLayerByType("raster"); const layerId = useEditorStore.getState().project.layers[0]?.id; expect(layerId).toBeDefined(); useEditorStore.getState().setLayerEffect(layerId!, { kind: "face-blur", enabled: true, method: "gaussian", amount: 99, regions: [{ x: Number.NaN, y: 1, width: 0, height: 2 }], }); useEditorStore.getState().setEffectEnabled(layerId!, "face-blur", false); useEditorStore.getState().setLayerVisible(layerId!, false); const layer = useEditorStore.getState().project.layers[0]; expect(layer?.visible).toBe(false); expect(layer?.effects[0]).toMatchObject({ amount: 40, enabled: false, regions: [{ x: 0, y: 1, width: 1, height: 2 }] }); expect(useEditorStore.getState().canUndo).toBe(true); useEditorStore.getState().undo(); expect(useEditorStore.getState().project.layers[0]?.visible).toBe(true); }); it("commits and cancels draft transactions", () => { useEditorStore.getState().resetProject(); useEditorStore.getState().addLayerByType("text"); const initial = useEditorStore.getState().project.layers[0]; expect(initial).toBeDefined(); useEditorStore.getState().startTransaction(); useEditorStore.getState().setSelectedLayerPositionDraft(300, 301); useEditorStore.getState().cancelTransaction(); expect(useEditorStore.getState().project.layers[0]?.x).toBe(initial?.x); useEditorStore.getState().startTransaction(); useEditorStore.getState().setSelectedLayerPositionDraft(310, 311); useEditorStore.getState().commitTransaction(); expect(useEditorStore.getState().project.layers[0]?.x).toBe(310); expect(useEditorStore.getState().canUndo).toBe(true); }); it("sets canvas size and supports redo and timeline jumps", () => { useEditorStore.getState().resetProject(); useEditorStore.getState().setCanvasSize(222.4, 333.6); useEditorStore.getState().addLayerByType("text"); expect(useEditorStore.getState().project.canvas).toMatchObject({ width: 222, height: 334 }); useEditorStore.getState().undo(); expect(useEditorStore.getState().project.layers).toHaveLength(0); expect(useEditorStore.getState().canRedo).toBe(true); useEditorStore.getState().redo(); expect(useEditorStore.getState().project.layers).toHaveLength(1); useEditorStore.getState().setSelectedLayerRotation(45); useEditorStore.getState().jumpToPast(1); expect(useEditorStore.getState().project.layers).toHaveLength(0); useEditorStore.getState().jumpToFuture(2); expect(useEditorStore.getState().project.layers[0]?.rotation).toBe(45); }); it("rejects invalid project json and replaces projects", () => { useEditorStore.getState().resetProject(); const originalId = useEditorStore.getState().project.id; expect(useEditorStore.getState().importProjectFromJson("nope")).toEqual({ ok: false, error: "Invalid JSON" }); expect(useEditorStore.getState().project.id).toBe(originalId); const project = createProject("replacement"); useEditorStore.getState().setProject(project); expect(useEditorStore.getState().project.title).toBe("replacement"); expect(useEditorStore.getState().isDirty).toBe(false); }); });