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 = {}) { 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); }); });