feat: complete TASK-WP4-02 canvas element editing
This commit is contained in:
@@ -0,0 +1,113 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import type { CanvasState } from "@dada/shared-contracts";
|
||||
|
||||
type CanvasElement = CanvasState["elements"][number];
|
||||
|
||||
import {
|
||||
CanvasElementController,
|
||||
createStaticStickerElement,
|
||||
type CanvasElementIdentity,
|
||||
} from "../../apps/web/src/editor-elements.js";
|
||||
import { createEditorCanvasState } from "../../apps/web/src/editor-canvas.js";
|
||||
|
||||
function identity(index: number): CanvasElementIdentity {
|
||||
return {
|
||||
createdAt: "2026-08-03T00:00:00.000Z",
|
||||
elementId: `00000000-0000-4000-8000-${String(index).padStart(12, "0")}`,
|
||||
};
|
||||
}
|
||||
|
||||
function sticker(index: number, overrides: Partial<CanvasElement> = {}) {
|
||||
return createStaticStickerElement({
|
||||
assetId: `STK${String(index).padStart(3, "0")}`,
|
||||
identity: identity(index),
|
||||
position: { x: 0.5, y: 0.5 },
|
||||
resourceVersion: "fixture-v1",
|
||||
zIndex: index - 1,
|
||||
...overrides,
|
||||
});
|
||||
}
|
||||
|
||||
function state(elements: CanvasElement[] = []): CanvasState {
|
||||
return { ...createEditorCanvasState({ assetId: null, ratio: "3:4" }), elements };
|
||||
}
|
||||
|
||||
describe("TDD-WP4-CAN-001 canvas selection and limit", () => {
|
||||
it("keeps fifty editable elements and rejects the fifty-first without mutation", () => {
|
||||
const controller = new CanvasElementController(state());
|
||||
for (let index = 1; index <= 50; index += 1) controller.add(sticker(index));
|
||||
const before = controller.value;
|
||||
expect(before.elements).toHaveLength(50);
|
||||
expect(() => controller.add(sticker(51))).toThrowError("canvas_element_limit_reached");
|
||||
expect(controller.value).toEqual(before);
|
||||
});
|
||||
|
||||
it("cycles overlapping hits, resets after pointer movement, and exposes only point candidates", () => {
|
||||
const controller = new CanvasElementController(state([
|
||||
sticker(1, { position: { x: 0.5, y: 0.5 }, z_index: 0 }),
|
||||
sticker(2, { position: { x: 0.5, y: 0.5 }, z_index: 1 }),
|
||||
sticker(3, { position: { x: 0.8, y: 0.8 }, z_index: 2 }),
|
||||
]));
|
||||
expect(controller.selectAt({ x: 0.5, y: 0.5 })).toEqual([identity(2).elementId]);
|
||||
expect(controller.selectAt({ x: 0.5, y: 0.5 })).toEqual([identity(1).elementId]);
|
||||
controller.pointerMoved();
|
||||
expect(controller.selectAt({ x: 0.5, y: 0.5 })).toEqual([identity(2).elementId]);
|
||||
expect(controller.candidatesAt({ x: 0.5, y: 0.5 }).map((entry) => entry.element_id)).toEqual([
|
||||
identity(2).elementId,
|
||||
identity(1).elementId,
|
||||
]);
|
||||
});
|
||||
|
||||
it("supports Shift/multi-mode toggles and marquee selection without creating a layer panel", () => {
|
||||
const controller = new CanvasElementController(state([
|
||||
sticker(1, { position: { x: 0.2, y: 0.2 }, z_index: 0 }),
|
||||
sticker(2, { position: { x: 0.4, y: 0.4 }, z_index: 1 }),
|
||||
sticker(3, { position: { x: 0.8, y: 0.8 }, z_index: 2 }),
|
||||
]));
|
||||
controller.selectAt({ x: 0.2, y: 0.2 });
|
||||
controller.selectAt({ x: 0.4, y: 0.4 }, { append: true });
|
||||
expect(controller.selectedIds).toEqual([identity(1).elementId, identity(2).elementId]);
|
||||
controller.selectAt({ x: 0.2, y: 0.2 }, { append: true });
|
||||
expect(controller.selectedIds).toEqual([identity(2).elementId]);
|
||||
expect(controller.marqueeSelect({ height: 0.5, width: 0.5, x: 0.1, y: 0.1 })).toEqual([
|
||||
identity(1).elementId,
|
||||
identity(2).elementId,
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("TDD-WP4-STK-001 sticker transformations", () => {
|
||||
it("moves with center snapping and applies scale, rotation, flip and opacity", () => {
|
||||
const controller = new CanvasElementController(state([sticker(1, { position: { x: 0.42, y: 0.48 } })]));
|
||||
controller.selectById(identity(1).elementId);
|
||||
const moved = controller.moveSelected({ x: 0.075, y: 0.015 });
|
||||
expect(moved.guides).toEqual(expect.arrayContaining(["canvas-center-x", "canvas-center-y"]));
|
||||
expect(controller.value.elements[0]?.position).toEqual({ x: 0.5, y: 0.5 });
|
||||
expect(controller.moveSelected({ x: 0.01, y: 0 }, { snap: false }).guides).toEqual([]);
|
||||
expect(controller.value.elements[0]?.position).toEqual({ x: 0.51, y: 0.5 });
|
||||
controller.scaleSelected(1.5);
|
||||
controller.rotateSelected(45);
|
||||
controller.flipSelectedHorizontal();
|
||||
controller.setSelectedOpacity(0.35);
|
||||
expect(controller.value.elements[0]).toMatchObject({
|
||||
opacity: 0.35,
|
||||
rotation: 45,
|
||||
scale: { x: 1.5, y: 1.5 },
|
||||
style_parameters: { flip_horizontal: true },
|
||||
});
|
||||
});
|
||||
|
||||
it("duplicates, reorders and deletes selected stickers while preserving stable resource identity", () => {
|
||||
const controller = new CanvasElementController(state([sticker(1), sticker(2, { position: { x: 0.7, y: 0.7 } })]));
|
||||
controller.selectById(identity(1).elementId);
|
||||
controller.duplicateSelected(() => identity(10));
|
||||
const duplicate = controller.value.elements.find((entry) => entry.element_id === identity(10).elementId);
|
||||
expect(duplicate).toMatchObject({ resource_version: "fixture-v1", template_or_asset_id: "STK001" });
|
||||
controller.changeLayer("back");
|
||||
expect(controller.value.elements.find((entry) => entry.element_id === identity(10).elementId)?.z_index).toBe(0);
|
||||
controller.changeLayer("front");
|
||||
expect(controller.value.elements.find((entry) => entry.element_id === identity(10).elementId)?.z_index).toBe(2);
|
||||
controller.deleteSelected();
|
||||
expect(controller.value.elements.map((entry) => entry.element_id)).not.toContain(identity(10).elementId);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user