2026-05-10 03:06:54 +07:00
|
|
|
import { describe, expect, it } from "vitest";
|
|
|
|
|
import { useEditorStore } from "./editor-store";
|
2026-05-24 21:05:21 +07:00
|
|
|
import { createProject } from "@pien-studio/editor-core";
|
2026-05-10 03:06:54 +07:00
|
|
|
|
|
|
|
|
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();
|
2026-05-21 00:02:12 +07:00
|
|
|
useEditorStore.getState().addLayerByType("raster");
|
2026-05-10 03:06:54 +07:00
|
|
|
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();
|
2026-05-21 00:02:12 +07:00
|
|
|
useEditorStore.getState().addLayerByType("raster");
|
2026-05-10 03:06:54 +07:00
|
|
|
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");
|
2026-05-21 00:02:12 +07:00
|
|
|
useEditorStore.getState().addLayerByType("raster");
|
2026-05-10 03:06:54 +07:00
|
|
|
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);
|
|
|
|
|
});
|
2026-05-24 21:05:21 +07:00
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
});
|
2026-05-10 03:06:54 +07:00
|
|
|
});
|