aboutsummaryrefslogtreecommitdiffstats
path: root/packages/excalidraw/data/transform.test.ts
diff options
context:
space:
mode:
Diffstat (limited to 'packages/excalidraw/data/transform.test.ts')
-rw-r--r--packages/excalidraw/data/transform.test.ts970
1 files changed, 970 insertions, 0 deletions
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),
+ });
+ });
+ });
+});