import { vi } from "vitest"; import type { ExcalidrawElementSkeleton } from "./transform"; import { convertToExcalidrawElements } from "./transform"; import type { ExcalidrawArrowElement } from "../element/types"; const opts = { regenerateIds: false }; describe("Test Transform", () => { it("should generate id unless opts.regenerateIds is set to false explicitly", () => { const elements = [ { type: "rectangle", x: 100, y: 100, id: "rect-1", }, ]; let data = convertToExcalidrawElements( elements as ExcalidrawElementSkeleton[], ); expect(data.length).toBe(1); expect(data[0].id).toBe("id0"); data = convertToExcalidrawElements( elements as ExcalidrawElementSkeleton[], opts, ); expect(data[0].id).toBe("rect-1"); }); it("should transform regular shapes", () => { const elements = [ { type: "rectangle", x: 100, y: 100, }, { type: "ellipse", x: 100, y: 250, }, { type: "diamond", x: 100, y: 400, }, { type: "rectangle", x: 300, y: 100, width: 200, height: 100, backgroundColor: "#c0eb75", strokeWidth: 2, }, { type: "ellipse", x: 300, y: 250, width: 200, height: 100, backgroundColor: "#ffc9c9", strokeStyle: "dotted", fillStyle: "solid", strokeWidth: 2, }, { type: "diamond", x: 300, y: 400, width: 200, height: 100, backgroundColor: "#a5d8ff", strokeColor: "#1971c2", strokeStyle: "dashed", fillStyle: "cross-hatch", strokeWidth: 2, }, ]; convertToExcalidrawElements( elements as ExcalidrawElementSkeleton[], opts, ).forEach((ele) => { expect(ele).toMatchSnapshot({ seed: expect.any(Number), versionNonce: expect.any(Number), id: expect.any(String), }); }); }); it("should transform text element", () => { const elements = [ { type: "text", x: 100, y: 100, text: "HELLO WORLD!", }, { type: "text", x: 100, y: 150, text: "STYLED HELLO WORLD!", fontSize: 20, strokeColor: "#5f3dc4", }, ]; convertToExcalidrawElements( elements as ExcalidrawElementSkeleton[], opts, ).forEach((ele) => { expect(ele).toMatchSnapshot({ seed: expect.any(Number), versionNonce: expect.any(Number), id: expect.any(String), }); }); }); it("should transform linear elements", () => { const elements = [ { type: "arrow", x: 100, y: 20, }, { type: "arrow", x: 450, y: 20, startArrowhead: "dot", endArrowhead: "triangle", strokeColor: "#1971c2", strokeWidth: 2, }, { type: "line", x: 100, y: 60, }, { type: "line", x: 450, y: 60, strokeColor: "#2f9e44", strokeWidth: 2, strokeStyle: "dotted", }, ]; const excalidrawElements = convertToExcalidrawElements( elements as ExcalidrawElementSkeleton[], opts, ); expect(excalidrawElements.length).toBe(4); excalidrawElements.forEach((ele) => { expect(ele).toMatchSnapshot({ seed: expect.any(Number), versionNonce: expect.any(Number), id: expect.any(String), }); }); }); it("should transform to text containers when label provided", () => { const elements = [ { type: "rectangle", x: 100, y: 100, label: { text: "RECTANGLE TEXT CONTAINER", }, }, { type: "ellipse", x: 500, y: 100, width: 200, label: { text: "ELLIPSE TEXT CONTAINER", }, }, { type: "diamond", x: 100, y: 150, width: 280, label: { text: "DIAMOND\nTEXT CONTAINER", }, }, { type: "diamond", x: 100, y: 400, width: 300, backgroundColor: "#fff3bf", strokeWidth: 2, label: { text: "STYLED DIAMOND TEXT CONTAINER", strokeColor: "#099268", fontSize: 20, }, }, { type: "rectangle", x: 500, y: 300, width: 200, strokeColor: "#c2255c", label: { text: "TOP LEFT ALIGNED RECTANGLE TEXT CONTAINER", textAlign: "left", verticalAlign: "top", fontSize: 20, }, }, { type: "ellipse", x: 500, y: 500, strokeColor: "#f08c00", backgroundColor: "#ffec99", width: 200, label: { text: "STYLED ELLIPSE TEXT CONTAINER", strokeColor: "#c2255c", }, }, ]; const excalidrawElements = convertToExcalidrawElements( elements as ExcalidrawElementSkeleton[], opts, ); expect(excalidrawElements.length).toBe(12); excalidrawElements.forEach((ele) => { expect(ele).toMatchSnapshot({ seed: expect.any(Number), versionNonce: expect.any(Number), id: expect.any(String), }); }); }); it("should transform to labelled arrows when label provided for arrows", () => { const elements = [ { type: "arrow", x: 100, y: 100, label: { text: "LABELED ARROW", }, }, { type: "arrow", x: 100, y: 200, label: { text: "STYLED LABELED ARROW", strokeColor: "#099268", fontSize: 20, }, }, { type: "arrow", x: 100, y: 300, strokeColor: "#1098ad", strokeWidth: 2, label: { text: "ANOTHER STYLED LABELLED ARROW", }, }, { type: "arrow", x: 100, y: 400, strokeColor: "#1098ad", strokeWidth: 2, label: { text: "ANOTHER STYLED LABELLED ARROW", strokeColor: "#099268", }, }, ]; const excalidrawElements = convertToExcalidrawElements( elements as ExcalidrawElementSkeleton[], opts, ); expect(excalidrawElements.length).toBe(8); excalidrawElements.forEach((ele) => { expect(ele).toMatchSnapshot({ seed: expect.any(Number), versionNonce: expect.any(Number), id: expect.any(String), }); }); }); describe("Test Frames", () => { it("should transform frames and update frame ids when regenerated", () => { const elementsSkeleton: ExcalidrawElementSkeleton[] = [ { type: "rectangle", x: 10, y: 10, strokeWidth: 2, id: "1", }, { type: "diamond", x: 120, y: 20, backgroundColor: "#fff3bf", strokeWidth: 2, label: { text: "HELLO EXCALIDRAW", strokeColor: "#099268", fontSize: 30, }, id: "2", }, { type: "frame", children: ["1", "2"], name: "My frame", }, ]; const excalidrawElements = convertToExcalidrawElements( elementsSkeleton, opts, ); expect(excalidrawElements.length).toBe(4); excalidrawElements.forEach((ele) => { expect(ele).toMatchObject({ seed: expect.any(Number), versionNonce: expect.any(Number), id: expect.any(String), }); }); }); it("should consider max of calculated and frame dimensions when provided", () => { const elementsSkeleton: ExcalidrawElementSkeleton[] = [ { type: "rectangle", x: 10, y: 10, strokeWidth: 2, id: "1", }, { type: "diamond", x: 120, y: 20, backgroundColor: "#fff3bf", strokeWidth: 2, label: { text: "HELLO EXCALIDRAW", strokeColor: "#099268", fontSize: 30, }, id: "2", }, { type: "frame", children: ["1", "2"], name: "My frame", width: 800, height: 100, }, ]; const excalidrawElements = convertToExcalidrawElements( elementsSkeleton, opts, ); const frame = excalidrawElements.find((ele) => ele.type === "frame")!; expect(frame.width).toBe(800); expect(frame.height).toBe(126); }); }); describe("Test arrow bindings", () => { it("should bind arrows to shapes when start / end provided without ids", () => { const elements = [ { type: "arrow", x: 255, y: 239, label: { text: "HELLO WORLD!!", }, start: { type: "rectangle", }, end: { type: "ellipse", }, }, ]; const excalidrawElements = convertToExcalidrawElements( elements as ExcalidrawElementSkeleton[], opts, ); expect(excalidrawElements.length).toBe(4); const [arrow, text, rectangle, ellipse] = excalidrawElements; expect(arrow).toMatchObject({ type: "arrow", x: 255, y: 239, boundElements: [{ id: text.id, type: "text" }], startBinding: { elementId: rectangle.id, focus: 0, gap: 1, }, endBinding: { elementId: ellipse.id, focus: 0, }, }); expect(text).toMatchObject({ x: 240, y: 226.5, type: "text", text: "HELLO WORLD!!", containerId: arrow.id, }); expect(rectangle).toMatchObject({ x: 155, y: 189, type: "rectangle", boundElements: [ { id: arrow.id, type: "arrow", }, ], }); expect(ellipse).toMatchObject({ x: 355, y: 189, type: "ellipse", boundElements: [ { id: arrow.id, type: "arrow", }, ], }); excalidrawElements.forEach((ele) => { expect(ele).toMatchSnapshot({ seed: expect.any(Number), versionNonce: expect.any(Number), id: expect.any(String), }); }); }); it("should bind arrows to text when start / end provided without ids", () => { const elements = [ { type: "arrow", x: 255, y: 239, label: { text: "HELLO WORLD!!", }, start: { type: "text", text: "HEYYYYY", }, end: { type: "text", text: "WHATS UP ?", }, }, ]; const excalidrawElements = convertToExcalidrawElements( elements as ExcalidrawElementSkeleton[], opts, ); expect(excalidrawElements.length).toBe(4); const [arrow, text1, text2, text3] = excalidrawElements; expect(arrow).toMatchObject({ type: "arrow", x: 255, y: 239, boundElements: [{ id: text1.id, type: "text" }], startBinding: { elementId: text2.id, focus: 0, gap: 1, }, endBinding: { elementId: text3.id, focus: 0, }, }); expect(text1).toMatchObject({ x: 240, y: 226.5, type: "text", text: "HELLO WORLD!!", containerId: arrow.id, }); expect(text2).toMatchObject({ x: 185, y: 226.5, type: "text", boundElements: [ { id: arrow.id, type: "arrow", }, ], }); expect(text3).toMatchObject({ x: 355, y: 226.5, type: "text", boundElements: [ { id: arrow.id, type: "arrow", }, ], }); excalidrawElements.forEach((ele) => { expect(ele).toMatchSnapshot({ seed: expect.any(Number), versionNonce: expect.any(Number), id: expect.any(String), }); }); }); it("should bind arrows to existing shapes when start / end provided with ids", () => { const elements = [ { type: "ellipse", id: "ellipse-1", strokeColor: "#66a80f", x: 630, y: 316, width: 300, height: 300, backgroundColor: "#d8f5a2", }, { type: "diamond", id: "diamond-1", strokeColor: "#9c36b5", width: 140, x: 96, y: 400, }, { type: "arrow", x: 247, y: 420, width: 395, height: 35, strokeColor: "#1864ab", start: { type: "rectangle", width: 300, height: 300, }, end: { id: "ellipse-1", }, }, { type: "arrow", x: 227, y: 450, width: 400, strokeColor: "#e67700", start: { id: "diamond-1", }, end: { id: "ellipse-1", }, }, ]; const excalidrawElements = convertToExcalidrawElements( elements as ExcalidrawElementSkeleton[], opts, ); expect(excalidrawElements.length).toBe(5); excalidrawElements.forEach((ele) => { expect(ele).toMatchSnapshot({ seed: expect.any(Number), versionNonce: expect.any(Number), id: expect.any(String), }); }); }); it("should bind arrows to existing text elements when start / end provided with ids", () => { const elements = [ { x: 100, y: 239, type: "text", text: "HEYYYYY", id: "text-1", strokeColor: "#c2255c", }, { type: "text", id: "text-2", x: 560, y: 239, text: "Whats up ?", }, { type: "arrow", x: 255, y: 239, label: { text: "HELLO WORLD!!", }, start: { id: "text-1", }, end: { id: "text-2", }, }, ]; const excalidrawElements = convertToExcalidrawElements( elements as ExcalidrawElementSkeleton[], opts, ); expect(excalidrawElements.length).toBe(4); excalidrawElements.forEach((ele) => { expect(ele).toMatchSnapshot({ seed: expect.any(Number), versionNonce: expect.any(Number), id: expect.any(String), }); }); }); it("should bind arrows to existing elements if ids are correct", () => { const consoleErrorSpy = vi .spyOn(console, "error") .mockImplementationOnce(() => void 0); const elements = [ { x: 100, y: 239, type: "text", text: "HEYYYYY", id: "text-1", strokeColor: "#c2255c", }, { type: "rectangle", x: 560, y: 139, id: "rect-1", width: 100, height: 200, backgroundColor: "#bac8ff", }, { type: "arrow", x: 255, y: 239, label: { text: "HELLO WORLD!!", }, start: { id: "text-13", }, end: { id: "rect-11", }, }, ]; const excalidrawElements = convertToExcalidrawElements( elements as ExcalidrawElementSkeleton[], opts, ); expect(excalidrawElements.length).toBe(4); const [, , arrow, text] = excalidrawElements; expect(arrow).toMatchObject({ type: "arrow", x: 255, y: 239, boundElements: [ { id: text.id, type: "text", }, ], startBinding: null, endBinding: null, }); expect(consoleErrorSpy).toHaveBeenCalledTimes(2); expect(consoleErrorSpy).toHaveBeenNthCalledWith( 1, "No element for start binding with id text-13 found", ); expect(consoleErrorSpy).toHaveBeenNthCalledWith( 2, "No element for end binding with id rect-11 found", ); }); it("should bind when ids referenced before the element data", () => { const elements = [ { type: "arrow", x: 255, y: 239, end: { id: "rect-1", }, }, { type: "rectangle", x: 560, y: 139, id: "rect-1", width: 100, height: 200, backgroundColor: "#bac8ff", }, ]; const excalidrawElements = convertToExcalidrawElements( elements as ExcalidrawElementSkeleton[], opts, ); expect(excalidrawElements.length).toBe(2); const [arrow, rect] = excalidrawElements; expect((arrow as ExcalidrawArrowElement).endBinding).toStrictEqual({ elementId: "rect-1", focus: 0, gap: 205, }); expect(rect.boundElements).toStrictEqual([ { id: arrow.id, type: "arrow", }, ]); }); }); it("should not allow duplicate ids", () => { const consoleErrorSpy = vi .spyOn(console, "error") .mockImplementationOnce(() => void 0); const elements = [ { type: "rectangle", x: 300, y: 100, id: "rect-1", width: 100, height: 200, }, { type: "rectangle", x: 100, y: 200, id: "rect-1", width: 100, height: 200, }, ]; const excalidrawElements = convertToExcalidrawElements( elements as ExcalidrawElementSkeleton[], opts, ); expect(excalidrawElements.length).toBe(1); expect(excalidrawElements[0]).toMatchSnapshot({ seed: expect.any(Number), versionNonce: expect.any(Number), }); expect(consoleErrorSpy).toHaveBeenCalledWith( "Duplicate id found for rect-1", ); }); it("should contains customData if provided", () => { const rawData = [ { type: "rectangle", x: 100, y: 100, customData: { createdBy: "user01" }, }, ]; const convertedElements = convertToExcalidrawElements( rawData as ExcalidrawElementSkeleton[], opts, ); expect(convertedElements[0].customData).toStrictEqual({ createdBy: "user01", }); }); it("should transform the elements correctly when linear elements have single point", () => { const elements: ExcalidrawElementSkeleton[] = [ { id: "B", type: "rectangle", groupIds: ["subgraph_group_B"], x: 0, y: 0, width: 166.03125, height: 163, label: { groupIds: ["subgraph_group_B"], text: "B", fontSize: 20, verticalAlign: "top", }, }, { id: "A", type: "rectangle", groupIds: ["subgraph_group_A"], x: 364.546875, y: 0, width: 120.265625, height: 114, label: { groupIds: ["subgraph_group_A"], text: "A", fontSize: 20, verticalAlign: "top", }, }, { id: "Alice", type: "rectangle", groupIds: ["subgraph_group_A"], x: 389.546875, y: 35, width: 70.265625, height: 44, strokeWidth: 2, label: { groupIds: ["subgraph_group_A"], text: "Alice", fontSize: 20, }, link: null, }, { id: "Bob", type: "rectangle", groupIds: ["subgraph_group_B"], x: 54.76953125, y: 35, width: 56.4921875, height: 44, strokeWidth: 2, label: { groupIds: ["subgraph_group_B"], text: "Bob", fontSize: 20, }, link: null, }, { id: "Bob_Alice", type: "arrow", groupIds: [], x: 111.262, y: 57, strokeWidth: 2, points: [ [0, 0], [272.985, 0], ], label: { text: "How are you?", fontSize: 20, groupIds: [], }, roundness: { type: 2, }, start: { id: "Bob", }, end: { id: "Alice", }, }, { id: "Bob_B", type: "arrow", groupIds: [], x: 77.017, y: 79, strokeWidth: 2, points: [[0, 0]], label: { text: "Friendship", fontSize: 20, groupIds: [], }, roundness: { type: 2, }, start: { id: "Bob", }, end: { id: "B", }, }, ]; const excalidrawElements = convertToExcalidrawElements(elements, opts); expect(excalidrawElements.length).toBe(12); excalidrawElements.forEach((ele) => { expect(ele).toMatchSnapshot({ seed: expect.any(Number), versionNonce: expect.any(Number), id: expect.any(String), }); }); }); });