From 6ec259a0e71174651bae95d4628138bf6fd68742 Mon Sep 17 00:00:00 2001 From: kj_sh604 Date: Sun, 15 Mar 2026 16:19:35 -0400 Subject: refactor: packages/ --- packages/excalidraw/data/transform.test.ts | 970 +++++++++++++++++++++++++++++ 1 file changed, 970 insertions(+) create mode 100644 packages/excalidraw/data/transform.test.ts (limited to 'packages/excalidraw/data/transform.test.ts') diff --git a/packages/excalidraw/data/transform.test.ts b/packages/excalidraw/data/transform.test.ts new file mode 100644 index 0000000..a36af0c --- /dev/null +++ b/packages/excalidraw/data/transform.test.ts @@ -0,0 +1,970 @@ +import { vi } from "vitest"; +import type { ExcalidrawElementSkeleton } from "./transform"; +import { convertToExcalidrawElements } from "./transform"; +import type { ExcalidrawArrowElement } from "../element/types"; +import { pointFrom } from "@excalidraw/math"; + +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", () => { + const elements: 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", + }, + ]; + + it("should transform frames and update frame ids when regenerated", () => { + const elementsSkeleton: ExcalidrawElementSkeleton[] = [ + ...elements, + { + 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 user defined frame dimensions over calculated when provided", () => { + const elementsSkeleton: ExcalidrawElementSkeleton[] = [ + ...elements, + { + 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(100); + }); + + it("should consider user defined frame coordinates calculated when provided", () => { + const elementsSkeleton: ExcalidrawElementSkeleton[] = [ + ...elements, + { + type: "frame", + children: ["1", "2"], + name: "My frame", + x: 100, + y: 300, + }, + ]; + const excalidrawElements = convertToExcalidrawElements( + elementsSkeleton, + opts, + ); + const frame = excalidrawElements.find((ele) => ele.type === "frame")!; + expect(frame.x).toBe(100); + expect(frame.y).toBe(300); + }); + }); + + 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: 14, + }); + 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: [pointFrom(0, 0), pointFrom(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: [pointFrom(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), + }); + }); + }); +}); -- cgit v1.2.3