|  | @@ -1,20 +1,30 @@
 | 
	
		
			
				|  |  |  import ReactDOM from "react-dom";
 | 
	
		
			
				|  |  | -import { ExcalidrawLinearElement } from "../element/types";
 | 
	
		
			
				|  |  | +import {
 | 
	
		
			
				|  |  | +  ExcalidrawElement,
 | 
	
		
			
				|  |  | +  ExcalidrawLinearElement,
 | 
	
		
			
				|  |  | +  ExcalidrawTextElementWithContainer,
 | 
	
		
			
				|  |  | +  FontString,
 | 
	
		
			
				|  |  | +} from "../element/types";
 | 
	
		
			
				|  |  |  import ExcalidrawApp from "../excalidraw-app";
 | 
	
		
			
				|  |  |  import { centerPoint } from "../math";
 | 
	
		
			
				|  |  |  import { reseed } from "../random";
 | 
	
		
			
				|  |  |  import * as Renderer from "../renderer/renderScene";
 | 
	
		
			
				|  |  | -import { Keyboard, Pointer } from "./helpers/ui";
 | 
	
		
			
				|  |  | +import { Keyboard, Pointer, UI } from "./helpers/ui";
 | 
	
		
			
				|  |  |  import { screen, render, fireEvent, GlobalTestState } from "./test-utils";
 | 
	
		
			
				|  |  |  import { API } from "../tests/helpers/api";
 | 
	
		
			
				|  |  |  import { Point } from "../types";
 | 
	
		
			
				|  |  |  import { KEYS } from "../keys";
 | 
	
		
			
				|  |  |  import { LinearElementEditor } from "../element/linearElementEditor";
 | 
	
		
			
				|  |  | -import { queryByText } from "@testing-library/react";
 | 
	
		
			
				|  |  | +import { queryByTestId, queryByText } from "@testing-library/react";
 | 
	
		
			
				|  |  | +import { resize, rotate } from "./utils";
 | 
	
		
			
				|  |  | +import { getBoundTextElementPosition, wrapText } from "../element/textElement";
 | 
	
		
			
				|  |  | +import { getMaxContainerWidth } from "../element/newElement";
 | 
	
		
			
				|  |  | +import * as textElementUtils from "../element/textElement";
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |  const renderScene = jest.spyOn(Renderer, "renderScene");
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |  const { h } = window;
 | 
	
		
			
				|  |  | +const font = "20px Cascadia, width: Segoe UI Emoji" as FontString;
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |  describe("Test Linear Elements", () => {
 | 
	
		
			
				|  |  |    let container: HTMLElement;
 | 
	
	
		
			
				|  | @@ -44,23 +54,23 @@ describe("Test Linear Elements", () => {
 | 
	
		
			
				|  |  |      strokeSharpness: ExcalidrawLinearElement["strokeSharpness"] = "sharp",
 | 
	
		
			
				|  |  |      roughness: ExcalidrawLinearElement["roughness"] = 0,
 | 
	
		
			
				|  |  |    ) => {
 | 
	
		
			
				|  |  | -    h.elements = [
 | 
	
		
			
				|  |  | -      API.createElement({
 | 
	
		
			
				|  |  | -        x: p1[0],
 | 
	
		
			
				|  |  | -        y: p1[1],
 | 
	
		
			
				|  |  | -        width: p2[0] - p1[0],
 | 
	
		
			
				|  |  | -        height: 0,
 | 
	
		
			
				|  |  | -        type,
 | 
	
		
			
				|  |  | -        roughness,
 | 
	
		
			
				|  |  | -        points: [
 | 
	
		
			
				|  |  | -          [0, 0],
 | 
	
		
			
				|  |  | -          [p2[0] - p1[0], p2[1] - p1[1]],
 | 
	
		
			
				|  |  | -        ],
 | 
	
		
			
				|  |  | -        strokeSharpness,
 | 
	
		
			
				|  |  | -      }),
 | 
	
		
			
				|  |  | -    ];
 | 
	
		
			
				|  |  | +    const line = API.createElement({
 | 
	
		
			
				|  |  | +      x: p1[0],
 | 
	
		
			
				|  |  | +      y: p1[1],
 | 
	
		
			
				|  |  | +      width: p2[0] - p1[0],
 | 
	
		
			
				|  |  | +      height: 0,
 | 
	
		
			
				|  |  | +      type,
 | 
	
		
			
				|  |  | +      roughness,
 | 
	
		
			
				|  |  | +      points: [
 | 
	
		
			
				|  |  | +        [0, 0],
 | 
	
		
			
				|  |  | +        [p2[0] - p1[0], p2[1] - p1[1]],
 | 
	
		
			
				|  |  | +      ],
 | 
	
		
			
				|  |  | +      strokeSharpness,
 | 
	
		
			
				|  |  | +    });
 | 
	
		
			
				|  |  | +    h.elements = [line];
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |      mouse.clickAt(p1[0], p1[1]);
 | 
	
		
			
				|  |  | +    return line;
 | 
	
		
			
				|  |  |    };
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |    const createThreePointerLinearElement = (
 | 
	
	
		
			
				|  | @@ -70,23 +80,23 @@ describe("Test Linear Elements", () => {
 | 
	
		
			
				|  |  |    ) => {
 | 
	
		
			
				|  |  |      //dragging line from midpoint
 | 
	
		
			
				|  |  |      const p3 = [midpoint[0] + delta - p1[0], midpoint[1] + delta - p1[1]];
 | 
	
		
			
				|  |  | -    h.elements = [
 | 
	
		
			
				|  |  | -      API.createElement({
 | 
	
		
			
				|  |  | -        x: p1[0],
 | 
	
		
			
				|  |  | -        y: p1[1],
 | 
	
		
			
				|  |  | -        width: p3[0] - p1[0],
 | 
	
		
			
				|  |  | -        height: 0,
 | 
	
		
			
				|  |  | -        type,
 | 
	
		
			
				|  |  | -        roughness,
 | 
	
		
			
				|  |  | -        points: [
 | 
	
		
			
				|  |  | -          [0, 0],
 | 
	
		
			
				|  |  | -          [p3[0], p3[1]],
 | 
	
		
			
				|  |  | -          [p2[0] - p1[0], p2[1] - p1[1]],
 | 
	
		
			
				|  |  | -        ],
 | 
	
		
			
				|  |  | -        strokeSharpness,
 | 
	
		
			
				|  |  | -      }),
 | 
	
		
			
				|  |  | -    ];
 | 
	
		
			
				|  |  | +    const line = API.createElement({
 | 
	
		
			
				|  |  | +      x: p1[0],
 | 
	
		
			
				|  |  | +      y: p1[1],
 | 
	
		
			
				|  |  | +      width: p3[0] - p1[0],
 | 
	
		
			
				|  |  | +      height: 0,
 | 
	
		
			
				|  |  | +      type,
 | 
	
		
			
				|  |  | +      roughness,
 | 
	
		
			
				|  |  | +      points: [
 | 
	
		
			
				|  |  | +        [0, 0],
 | 
	
		
			
				|  |  | +        [p3[0], p3[1]],
 | 
	
		
			
				|  |  | +        [p2[0] - p1[0], p2[1] - p1[1]],
 | 
	
		
			
				|  |  | +      ],
 | 
	
		
			
				|  |  | +      strokeSharpness,
 | 
	
		
			
				|  |  | +    });
 | 
	
		
			
				|  |  | +    h.elements = [line];
 | 
	
		
			
				|  |  |      mouse.clickAt(p1[0], p1[1]);
 | 
	
		
			
				|  |  | +    return line;
 | 
	
		
			
				|  |  |    };
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |    const enterLineEditingMode = (
 | 
	
	
		
			
				|  | @@ -98,7 +108,9 @@ describe("Test Linear Elements", () => {
 | 
	
		
			
				|  |  |      } else {
 | 
	
		
			
				|  |  |        mouse.clickAt(p1[0], p1[1]);
 | 
	
		
			
				|  |  |      }
 | 
	
		
			
				|  |  | -    Keyboard.keyPress(KEYS.ENTER);
 | 
	
		
			
				|  |  | +    Keyboard.withModifierKeys({ ctrl: true }, () => {
 | 
	
		
			
				|  |  | +      Keyboard.keyPress(KEYS.ENTER);
 | 
	
		
			
				|  |  | +    });
 | 
	
		
			
				|  |  |      expect(h.state.editingLinearElement?.elementId).toEqual(line.id);
 | 
	
		
			
				|  |  |    };
 | 
	
		
			
				|  |  |  
 | 
	
	
		
			
				|  | @@ -216,6 +228,16 @@ describe("Test Linear Elements", () => {
 | 
	
		
			
				|  |  |      expect(h.state.editingLinearElement?.elementId).toBeUndefined();
 | 
	
		
			
				|  |  |    });
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | +  it("should enter line editor when using double clicked with ctrl key", () => {
 | 
	
		
			
				|  |  | +    createTwoPointerLinearElement("line");
 | 
	
		
			
				|  |  | +    expect(h.state.editingLinearElement?.elementId).toBeUndefined();
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    Keyboard.withModifierKeys({ ctrl: true }, () => {
 | 
	
		
			
				|  |  | +      mouse.doubleClick();
 | 
	
		
			
				|  |  | +    });
 | 
	
		
			
				|  |  | +    expect(h.state.editingLinearElement?.elementId).toEqual(h.elements[0].id);
 | 
	
		
			
				|  |  | +  });
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  |    describe("Inside editor", () => {
 | 
	
		
			
				|  |  |      it("should not drag line and add midpoint when dragged irrespective of threshold", () => {
 | 
	
		
			
				|  |  |        createTwoPointerLinearElement("line");
 | 
	
	
		
			
				|  | @@ -358,8 +380,8 @@ describe("Test Linear Elements", () => {
 | 
	
		
			
				|  |  |        let line: ExcalidrawLinearElement;
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |        beforeEach(() => {
 | 
	
		
			
				|  |  | -        createThreePointerLinearElement("line");
 | 
	
		
			
				|  |  | -        line = h.elements[0] as ExcalidrawLinearElement;
 | 
	
		
			
				|  |  | +        line = createThreePointerLinearElement("line");
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  |          expect(line.points.length).toEqual(3);
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |          enterLineEditingMode(line);
 | 
	
	
		
			
				|  | @@ -478,7 +500,7 @@ describe("Test Linear Elements", () => {
 | 
	
		
			
				|  |  |          // delete 3rd point
 | 
	
		
			
				|  |  |          deletePoint(points[2]);
 | 
	
		
			
				|  |  |          expect(line.points.length).toEqual(3);
 | 
	
		
			
				|  |  | -        expect(renderScene).toHaveBeenCalledTimes(21);
 | 
	
		
			
				|  |  | +        expect(renderScene).toHaveBeenCalledTimes(22);
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |          const newMidPoints = LinearElementEditor.getEditorMidPoints(
 | 
	
		
			
				|  |  |            line,
 | 
	
	
		
			
				|  | @@ -503,8 +525,7 @@ describe("Test Linear Elements", () => {
 | 
	
		
			
				|  |  |        let line: ExcalidrawLinearElement;
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |        beforeEach(() => {
 | 
	
		
			
				|  |  | -        createThreePointerLinearElement("line", "round");
 | 
	
		
			
				|  |  | -        line = h.elements[0] as ExcalidrawLinearElement;
 | 
	
		
			
				|  |  | +        line = createThreePointerLinearElement("line", "round");
 | 
	
		
			
				|  |  |          expect(line.points.length).toEqual(3);
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |          enterLineEditingMode(line);
 | 
	
	
		
			
				|  | @@ -667,7 +688,6 @@ describe("Test Linear Elements", () => {
 | 
	
		
			
				|  |  |            fillStyle: "solid",
 | 
	
		
			
				|  |  |          }),
 | 
	
		
			
				|  |  |        ];
 | 
	
		
			
				|  |  | -      const origPoints = line.points.map((point) => [...point]);
 | 
	
		
			
				|  |  |        const dragEndPositionOffset = [100, 100] as const;
 | 
	
		
			
				|  |  |        API.setSelectedElements([line]);
 | 
	
		
			
				|  |  |        enterLineEditingMode(line, true);
 | 
	
	
		
			
				|  | @@ -682,11 +702,457 @@ describe("Test Linear Elements", () => {
 | 
	
		
			
				|  |  |              0,
 | 
	
		
			
				|  |  |            ],
 | 
	
		
			
				|  |  |            Array [
 | 
	
		
			
				|  |  | -            ${origPoints[1][0] - dragEndPositionOffset[0]},
 | 
	
		
			
				|  |  | -            ${origPoints[1][1] - dragEndPositionOffset[1]},
 | 
	
		
			
				|  |  | +            -60,
 | 
	
		
			
				|  |  | +            -100,
 | 
	
		
			
				|  |  |            ],
 | 
	
		
			
				|  |  |          ]
 | 
	
		
			
				|  |  |        `);
 | 
	
		
			
				|  |  |      });
 | 
	
		
			
				|  |  |    });
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +  describe("Test bound text element", () => {
 | 
	
		
			
				|  |  | +    const DEFAULT_TEXT = "Online whiteboard collaboration made easy";
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    const createBoundTextElement = (
 | 
	
		
			
				|  |  | +      text: string,
 | 
	
		
			
				|  |  | +      container: ExcalidrawLinearElement,
 | 
	
		
			
				|  |  | +    ) => {
 | 
	
		
			
				|  |  | +      const textElement = API.createElement({
 | 
	
		
			
				|  |  | +        type: "text",
 | 
	
		
			
				|  |  | +        x: 0,
 | 
	
		
			
				|  |  | +        y: 0,
 | 
	
		
			
				|  |  | +        text: wrapText(text, font, getMaxContainerWidth(container)),
 | 
	
		
			
				|  |  | +        containerId: container.id,
 | 
	
		
			
				|  |  | +        width: 30,
 | 
	
		
			
				|  |  | +        height: 20,
 | 
	
		
			
				|  |  | +      }) as ExcalidrawTextElementWithContainer;
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +      container = {
 | 
	
		
			
				|  |  | +        ...container,
 | 
	
		
			
				|  |  | +        boundElements: (container.boundElements || []).concat({
 | 
	
		
			
				|  |  | +          type: "text",
 | 
	
		
			
				|  |  | +          id: textElement.id,
 | 
	
		
			
				|  |  | +        }),
 | 
	
		
			
				|  |  | +      };
 | 
	
		
			
				|  |  | +      const elements: ExcalidrawElement[] = [];
 | 
	
		
			
				|  |  | +      h.elements.forEach((element) => {
 | 
	
		
			
				|  |  | +        if (element.id === container.id) {
 | 
	
		
			
				|  |  | +          elements.push(container);
 | 
	
		
			
				|  |  | +        } else {
 | 
	
		
			
				|  |  | +          elements.push(element);
 | 
	
		
			
				|  |  | +        }
 | 
	
		
			
				|  |  | +      });
 | 
	
		
			
				|  |  | +      const updatedTextElement = { ...textElement, originalText: text };
 | 
	
		
			
				|  |  | +      h.elements = [...elements, updatedTextElement];
 | 
	
		
			
				|  |  | +      return { textElement: updatedTextElement, container };
 | 
	
		
			
				|  |  | +    };
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    describe("Test getBoundTextElementPosition", () => {
 | 
	
		
			
				|  |  | +      it("should return correct position for 2 pointer arrow", () => {
 | 
	
		
			
				|  |  | +        createTwoPointerLinearElement("arrow");
 | 
	
		
			
				|  |  | +        const arrow = h.elements[0] as ExcalidrawLinearElement;
 | 
	
		
			
				|  |  | +        const { textElement, container } = createBoundTextElement(
 | 
	
		
			
				|  |  | +          DEFAULT_TEXT,
 | 
	
		
			
				|  |  | +          arrow,
 | 
	
		
			
				|  |  | +        );
 | 
	
		
			
				|  |  | +        const position = LinearElementEditor.getBoundTextElementPosition(
 | 
	
		
			
				|  |  | +          container,
 | 
	
		
			
				|  |  | +          textElement,
 | 
	
		
			
				|  |  | +        );
 | 
	
		
			
				|  |  | +        expect(position).toMatchInlineSnapshot(`
 | 
	
		
			
				|  |  | +          Object {
 | 
	
		
			
				|  |  | +            "x": 25,
 | 
	
		
			
				|  |  | +            "y": 10,
 | 
	
		
			
				|  |  | +          }
 | 
	
		
			
				|  |  | +        `);
 | 
	
		
			
				|  |  | +      });
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +      it("should return correct position for arrow with odd points", () => {
 | 
	
		
			
				|  |  | +        createThreePointerLinearElement("arrow", "round");
 | 
	
		
			
				|  |  | +        const arrow = h.elements[0] as ExcalidrawLinearElement;
 | 
	
		
			
				|  |  | +        const { textElement, container } = createBoundTextElement(
 | 
	
		
			
				|  |  | +          DEFAULT_TEXT,
 | 
	
		
			
				|  |  | +          arrow,
 | 
	
		
			
				|  |  | +        );
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        const position = LinearElementEditor.getBoundTextElementPosition(
 | 
	
		
			
				|  |  | +          container,
 | 
	
		
			
				|  |  | +          textElement,
 | 
	
		
			
				|  |  | +        );
 | 
	
		
			
				|  |  | +        expect(position).toMatchInlineSnapshot(`
 | 
	
		
			
				|  |  | +          Object {
 | 
	
		
			
				|  |  | +            "x": 75,
 | 
	
		
			
				|  |  | +            "y": 60,
 | 
	
		
			
				|  |  | +          }
 | 
	
		
			
				|  |  | +        `);
 | 
	
		
			
				|  |  | +      });
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +      it("should return correct position for arrow with even points", () => {
 | 
	
		
			
				|  |  | +        createThreePointerLinearElement("arrow", "round");
 | 
	
		
			
				|  |  | +        const arrow = h.elements[0] as ExcalidrawLinearElement;
 | 
	
		
			
				|  |  | +        const { textElement, container } = createBoundTextElement(
 | 
	
		
			
				|  |  | +          DEFAULT_TEXT,
 | 
	
		
			
				|  |  | +          arrow,
 | 
	
		
			
				|  |  | +        );
 | 
	
		
			
				|  |  | +        enterLineEditingMode(container);
 | 
	
		
			
				|  |  | +        // This is the expected midpoint for line with round edge
 | 
	
		
			
				|  |  | +        // hence hardcoding it so if later some bug is introduced
 | 
	
		
			
				|  |  | +        // this will fail and we can fix it
 | 
	
		
			
				|  |  | +        const firstSegmentMidpoint: Point = [
 | 
	
		
			
				|  |  | +          55.9697848965255, 47.442326230998205,
 | 
	
		
			
				|  |  | +        ];
 | 
	
		
			
				|  |  | +        // drag line from first segment midpoint
 | 
	
		
			
				|  |  | +        drag(firstSegmentMidpoint, [
 | 
	
		
			
				|  |  | +          firstSegmentMidpoint[0] + delta,
 | 
	
		
			
				|  |  | +          firstSegmentMidpoint[1] + delta,
 | 
	
		
			
				|  |  | +        ]);
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        const position = LinearElementEditor.getBoundTextElementPosition(
 | 
	
		
			
				|  |  | +          container,
 | 
	
		
			
				|  |  | +          textElement,
 | 
	
		
			
				|  |  | +        );
 | 
	
		
			
				|  |  | +        expect(position).toMatchInlineSnapshot(`
 | 
	
		
			
				|  |  | +          Object {
 | 
	
		
			
				|  |  | +            "x": 85.82201843191861,
 | 
	
		
			
				|  |  | +            "y": 75.63461309860818,
 | 
	
		
			
				|  |  | +          }
 | 
	
		
			
				|  |  | +        `);
 | 
	
		
			
				|  |  | +      });
 | 
	
		
			
				|  |  | +    });
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    it("should bind text to arrow when double clicked", async () => {
 | 
	
		
			
				|  |  | +      createTwoPointerLinearElement("arrow");
 | 
	
		
			
				|  |  | +      const arrow = h.elements[0] as ExcalidrawLinearElement;
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +      expect(h.elements.length).toBe(1);
 | 
	
		
			
				|  |  | +      expect(h.elements[0].id).toBe(arrow.id);
 | 
	
		
			
				|  |  | +      mouse.doubleClickAt(arrow.x, arrow.y);
 | 
	
		
			
				|  |  | +      expect(h.elements.length).toBe(2);
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +      const text = h.elements[1] as ExcalidrawTextElementWithContainer;
 | 
	
		
			
				|  |  | +      expect(text.type).toBe("text");
 | 
	
		
			
				|  |  | +      expect(text.containerId).toBe(arrow.id);
 | 
	
		
			
				|  |  | +      mouse.down();
 | 
	
		
			
				|  |  | +      const editor = document.querySelector(
 | 
	
		
			
				|  |  | +        ".excalidraw-textEditorContainer > textarea",
 | 
	
		
			
				|  |  | +      ) as HTMLTextAreaElement;
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +      fireEvent.change(editor, {
 | 
	
		
			
				|  |  | +        target: { value: DEFAULT_TEXT },
 | 
	
		
			
				|  |  | +      });
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +      await new Promise((r) => setTimeout(r, 0));
 | 
	
		
			
				|  |  | +      editor.blur();
 | 
	
		
			
				|  |  | +      expect(arrow.boundElements).toStrictEqual([
 | 
	
		
			
				|  |  | +        { id: text.id, type: "text" },
 | 
	
		
			
				|  |  | +      ]);
 | 
	
		
			
				|  |  | +      expect((h.elements[1] as ExcalidrawTextElementWithContainer).text)
 | 
	
		
			
				|  |  | +        .toMatchInlineSnapshot(`
 | 
	
		
			
				|  |  | +        "Online whiteboard 
 | 
	
		
			
				|  |  | +        collaboration made 
 | 
	
		
			
				|  |  | +        easy"
 | 
	
		
			
				|  |  | +      `);
 | 
	
		
			
				|  |  | +    });
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    it("should bind text to arrow when clicked on arrow and enter pressed", async () => {
 | 
	
		
			
				|  |  | +      const arrow = createTwoPointerLinearElement("arrow");
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +      expect(h.elements.length).toBe(1);
 | 
	
		
			
				|  |  | +      expect(h.elements[0].id).toBe(arrow.id);
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +      Keyboard.keyPress(KEYS.ENTER);
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +      expect(h.elements.length).toBe(2);
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +      const textElement = h.elements[1] as ExcalidrawTextElementWithContainer;
 | 
	
		
			
				|  |  | +      expect(textElement.type).toBe("text");
 | 
	
		
			
				|  |  | +      expect(textElement.containerId).toBe(arrow.id);
 | 
	
		
			
				|  |  | +      const editor = document.querySelector(
 | 
	
		
			
				|  |  | +        ".excalidraw-textEditorContainer > textarea",
 | 
	
		
			
				|  |  | +      ) as HTMLTextAreaElement;
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +      await new Promise((r) => setTimeout(r, 0));
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +      fireEvent.change(editor, {
 | 
	
		
			
				|  |  | +        target: { value: DEFAULT_TEXT },
 | 
	
		
			
				|  |  | +      });
 | 
	
		
			
				|  |  | +      editor.blur();
 | 
	
		
			
				|  |  | +      expect(arrow.boundElements).toStrictEqual([
 | 
	
		
			
				|  |  | +        { id: textElement.id, type: "text" },
 | 
	
		
			
				|  |  | +      ]);
 | 
	
		
			
				|  |  | +      expect((h.elements[1] as ExcalidrawTextElementWithContainer).text)
 | 
	
		
			
				|  |  | +        .toMatchInlineSnapshot(`
 | 
	
		
			
				|  |  | +        "Online whiteboard 
 | 
	
		
			
				|  |  | +        collaboration made 
 | 
	
		
			
				|  |  | +        easy"
 | 
	
		
			
				|  |  | +      `);
 | 
	
		
			
				|  |  | +    });
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    it("should not bind text to line when double clicked", async () => {
 | 
	
		
			
				|  |  | +      const line = createTwoPointerLinearElement("line");
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +      expect(h.elements.length).toBe(1);
 | 
	
		
			
				|  |  | +      mouse.doubleClickAt(line.x, line.y);
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +      expect(h.elements.length).toBe(2);
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +      const text = h.elements[1] as ExcalidrawTextElementWithContainer;
 | 
	
		
			
				|  |  | +      expect(text.type).toBe("text");
 | 
	
		
			
				|  |  | +      expect(text.containerId).toBeNull();
 | 
	
		
			
				|  |  | +      expect(line.boundElements).toBeNull();
 | 
	
		
			
				|  |  | +    });
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    it("should not rotate the bound text and update position of bound text and bounding box correctly when arrow rotated", () => {
 | 
	
		
			
				|  |  | +      createThreePointerLinearElement("arrow", "round");
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +      const arrow = h.elements[0] as ExcalidrawLinearElement;
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +      const { textElement, container } = createBoundTextElement(
 | 
	
		
			
				|  |  | +        DEFAULT_TEXT,
 | 
	
		
			
				|  |  | +        arrow,
 | 
	
		
			
				|  |  | +      );
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +      expect(container.angle).toBe(0);
 | 
	
		
			
				|  |  | +      expect(textElement.angle).toBe(0);
 | 
	
		
			
				|  |  | +      expect(getBoundTextElementPosition(arrow, textElement))
 | 
	
		
			
				|  |  | +        .toMatchInlineSnapshot(`
 | 
	
		
			
				|  |  | +        Object {
 | 
	
		
			
				|  |  | +          "x": 75,
 | 
	
		
			
				|  |  | +          "y": 60,
 | 
	
		
			
				|  |  | +        }
 | 
	
		
			
				|  |  | +      `);
 | 
	
		
			
				|  |  | +      expect(textElement.text).toMatchInlineSnapshot(`
 | 
	
		
			
				|  |  | +        "Online whiteboard 
 | 
	
		
			
				|  |  | +        collaboration made 
 | 
	
		
			
				|  |  | +        easy"
 | 
	
		
			
				|  |  | +      `);
 | 
	
		
			
				|  |  | +      expect(LinearElementEditor.getElementAbsoluteCoords(container, true))
 | 
	
		
			
				|  |  | +        .toMatchInlineSnapshot(`
 | 
	
		
			
				|  |  | +        Array [
 | 
	
		
			
				|  |  | +          20,
 | 
	
		
			
				|  |  | +          20,
 | 
	
		
			
				|  |  | +          105,
 | 
	
		
			
				|  |  | +          80,
 | 
	
		
			
				|  |  | +          55.45893770831013,
 | 
	
		
			
				|  |  | +          45,
 | 
	
		
			
				|  |  | +        ]
 | 
	
		
			
				|  |  | +      `);
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +      rotate(container, -35, 55);
 | 
	
		
			
				|  |  | +      expect(container.angle).toMatchInlineSnapshot(`1.3988061968364685`);
 | 
	
		
			
				|  |  | +      expect(textElement.angle).toBe(0);
 | 
	
		
			
				|  |  | +      expect(getBoundTextElementPosition(container, textElement))
 | 
	
		
			
				|  |  | +        .toMatchInlineSnapshot(`
 | 
	
		
			
				|  |  | +        Object {
 | 
	
		
			
				|  |  | +          "x": 21.73926141863671,
 | 
	
		
			
				|  |  | +          "y": 73.31003398390868,
 | 
	
		
			
				|  |  | +        }
 | 
	
		
			
				|  |  | +      `);
 | 
	
		
			
				|  |  | +      expect(textElement.text).toMatchInlineSnapshot(`
 | 
	
		
			
				|  |  | +        "Online whiteboard 
 | 
	
		
			
				|  |  | +        collaboration made 
 | 
	
		
			
				|  |  | +        easy"
 | 
	
		
			
				|  |  | +      `);
 | 
	
		
			
				|  |  | +      expect(LinearElementEditor.getElementAbsoluteCoords(container, true))
 | 
	
		
			
				|  |  | +        .toMatchInlineSnapshot(`
 | 
	
		
			
				|  |  | +        Array [
 | 
	
		
			
				|  |  | +          20,
 | 
	
		
			
				|  |  | +          20,
 | 
	
		
			
				|  |  | +          102.41961302274555,
 | 
	
		
			
				|  |  | +          86.49012635273976,
 | 
	
		
			
				|  |  | +          55.45893770831013,
 | 
	
		
			
				|  |  | +          45,
 | 
	
		
			
				|  |  | +        ]
 | 
	
		
			
				|  |  | +      `);
 | 
	
		
			
				|  |  | +    });
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    it("should resize and position the bound text and bounding box correctly when 3 pointer arrow element resized", () => {
 | 
	
		
			
				|  |  | +      createThreePointerLinearElement("arrow", "round");
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +      const arrow = h.elements[0] as ExcalidrawLinearElement;
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +      const { textElement, container } = createBoundTextElement(
 | 
	
		
			
				|  |  | +        DEFAULT_TEXT,
 | 
	
		
			
				|  |  | +        arrow,
 | 
	
		
			
				|  |  | +      );
 | 
	
		
			
				|  |  | +      expect(container.width).toBe(70);
 | 
	
		
			
				|  |  | +      expect(container.height).toBe(50);
 | 
	
		
			
				|  |  | +      expect(getBoundTextElementPosition(container, textElement))
 | 
	
		
			
				|  |  | +        .toMatchInlineSnapshot(`
 | 
	
		
			
				|  |  | +        Object {
 | 
	
		
			
				|  |  | +          "x": 75,
 | 
	
		
			
				|  |  | +          "y": 60,
 | 
	
		
			
				|  |  | +        }
 | 
	
		
			
				|  |  | +      `);
 | 
	
		
			
				|  |  | +      expect(textElement.text).toMatchInlineSnapshot(`
 | 
	
		
			
				|  |  | +        "Online whiteboard 
 | 
	
		
			
				|  |  | +        collaboration made 
 | 
	
		
			
				|  |  | +        easy"
 | 
	
		
			
				|  |  | +      `);
 | 
	
		
			
				|  |  | +      expect(LinearElementEditor.getElementAbsoluteCoords(container, true))
 | 
	
		
			
				|  |  | +        .toMatchInlineSnapshot(`
 | 
	
		
			
				|  |  | +        Array [
 | 
	
		
			
				|  |  | +          20,
 | 
	
		
			
				|  |  | +          20,
 | 
	
		
			
				|  |  | +          105,
 | 
	
		
			
				|  |  | +          80,
 | 
	
		
			
				|  |  | +          55.45893770831013,
 | 
	
		
			
				|  |  | +          45,
 | 
	
		
			
				|  |  | +        ]
 | 
	
		
			
				|  |  | +      `);
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +      resize(container, "ne", [300, 200]);
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +      expect({ width: container.width, height: container.height })
 | 
	
		
			
				|  |  | +        .toMatchInlineSnapshot(`
 | 
	
		
			
				|  |  | +        Object {
 | 
	
		
			
				|  |  | +          "height": 10,
 | 
	
		
			
				|  |  | +          "width": 367,
 | 
	
		
			
				|  |  | +        }
 | 
	
		
			
				|  |  | +      `);
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +      expect(getBoundTextElementPosition(container, textElement))
 | 
	
		
			
				|  |  | +        .toMatchInlineSnapshot(`
 | 
	
		
			
				|  |  | +        Object {
 | 
	
		
			
				|  |  | +          "x": 386.5,
 | 
	
		
			
				|  |  | +          "y": 70,
 | 
	
		
			
				|  |  | +        }
 | 
	
		
			
				|  |  | +      `);
 | 
	
		
			
				|  |  | +      expect((h.elements[1] as ExcalidrawTextElementWithContainer).text)
 | 
	
		
			
				|  |  | +        .toMatchInlineSnapshot(`
 | 
	
		
			
				|  |  | +        "Online whiteboard 
 | 
	
		
			
				|  |  | +        collaboration made easy"
 | 
	
		
			
				|  |  | +      `);
 | 
	
		
			
				|  |  | +      expect(LinearElementEditor.getElementAbsoluteCoords(container, true))
 | 
	
		
			
				|  |  | +        .toMatchInlineSnapshot(`
 | 
	
		
			
				|  |  | +        Array [
 | 
	
		
			
				|  |  | +          20,
 | 
	
		
			
				|  |  | +          60,
 | 
	
		
			
				|  |  | +          391.8122896842806,
 | 
	
		
			
				|  |  | +          70,
 | 
	
		
			
				|  |  | +          205.9061448421403,
 | 
	
		
			
				|  |  | +          65,
 | 
	
		
			
				|  |  | +        ]
 | 
	
		
			
				|  |  | +      `);
 | 
	
		
			
				|  |  | +    });
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    it("should resize and position the bound text correctly when 2 pointer linear element resized", () => {
 | 
	
		
			
				|  |  | +      createTwoPointerLinearElement("arrow");
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +      const arrow = h.elements[0] as ExcalidrawLinearElement;
 | 
	
		
			
				|  |  | +      const { textElement, container } = createBoundTextElement(
 | 
	
		
			
				|  |  | +        DEFAULT_TEXT,
 | 
	
		
			
				|  |  | +        arrow,
 | 
	
		
			
				|  |  | +      );
 | 
	
		
			
				|  |  | +      expect(container.width).toBe(40);
 | 
	
		
			
				|  |  | +      expect(getBoundTextElementPosition(container, textElement))
 | 
	
		
			
				|  |  | +        .toMatchInlineSnapshot(`
 | 
	
		
			
				|  |  | +        Object {
 | 
	
		
			
				|  |  | +          "x": 25,
 | 
	
		
			
				|  |  | +          "y": 10,
 | 
	
		
			
				|  |  | +        }
 | 
	
		
			
				|  |  | +      `);
 | 
	
		
			
				|  |  | +      expect(textElement.text).toMatchInlineSnapshot(`
 | 
	
		
			
				|  |  | +        "Online whiteboard 
 | 
	
		
			
				|  |  | +        collaboration made 
 | 
	
		
			
				|  |  | +        easy"
 | 
	
		
			
				|  |  | +      `);
 | 
	
		
			
				|  |  | +      const points = LinearElementEditor.getPointsGlobalCoordinates(container);
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +      // Drag from last point
 | 
	
		
			
				|  |  | +      drag(points[1], [points[1][0] + 300, points[1][1]]);
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +      expect({ width: container.width, height: container.height })
 | 
	
		
			
				|  |  | +        .toMatchInlineSnapshot(`
 | 
	
		
			
				|  |  | +        Object {
 | 
	
		
			
				|  |  | +          "height": 0,
 | 
	
		
			
				|  |  | +          "width": 340,
 | 
	
		
			
				|  |  | +        }
 | 
	
		
			
				|  |  | +      `);
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +      expect(getBoundTextElementPosition(container, textElement))
 | 
	
		
			
				|  |  | +        .toMatchInlineSnapshot(`
 | 
	
		
			
				|  |  | +        Object {
 | 
	
		
			
				|  |  | +          "x": 189.5,
 | 
	
		
			
				|  |  | +          "y": 20,
 | 
	
		
			
				|  |  | +        }
 | 
	
		
			
				|  |  | +      `);
 | 
	
		
			
				|  |  | +      expect(textElement.text).toMatchInlineSnapshot(`
 | 
	
		
			
				|  |  | +        "Online whiteboard 
 | 
	
		
			
				|  |  | +        collaboration made easy"
 | 
	
		
			
				|  |  | +      `);
 | 
	
		
			
				|  |  | +    });
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    it("should not render vertical align tool when element selected", () => {
 | 
	
		
			
				|  |  | +      createTwoPointerLinearElement("arrow");
 | 
	
		
			
				|  |  | +      const arrow = h.elements[0] as ExcalidrawLinearElement;
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +      createBoundTextElement(DEFAULT_TEXT, arrow);
 | 
	
		
			
				|  |  | +      API.setSelectedElements([arrow]);
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +      expect(queryByTestId(container, "align-top")).toBeNull();
 | 
	
		
			
				|  |  | +      expect(queryByTestId(container, "align-middle")).toBeNull();
 | 
	
		
			
				|  |  | +      expect(queryByTestId(container, "align-bottom")).toBeNull();
 | 
	
		
			
				|  |  | +    });
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    it("should wrap the bound text when arrow bound container moves", async () => {
 | 
	
		
			
				|  |  | +      const rect = UI.createElement("rectangle", {
 | 
	
		
			
				|  |  | +        x: 400,
 | 
	
		
			
				|  |  | +        width: 200,
 | 
	
		
			
				|  |  | +        height: 500,
 | 
	
		
			
				|  |  | +      });
 | 
	
		
			
				|  |  | +      const arrow = UI.createElement("arrow", {
 | 
	
		
			
				|  |  | +        x: 210,
 | 
	
		
			
				|  |  | +        y: 250,
 | 
	
		
			
				|  |  | +        width: 400,
 | 
	
		
			
				|  |  | +        height: 1,
 | 
	
		
			
				|  |  | +      });
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +      mouse.select(arrow);
 | 
	
		
			
				|  |  | +      Keyboard.keyPress(KEYS.ENTER);
 | 
	
		
			
				|  |  | +      const editor = document.querySelector(
 | 
	
		
			
				|  |  | +        ".excalidraw-textEditorContainer > textarea",
 | 
	
		
			
				|  |  | +      ) as HTMLTextAreaElement;
 | 
	
		
			
				|  |  | +      await new Promise((r) => setTimeout(r, 0));
 | 
	
		
			
				|  |  | +      fireEvent.change(editor, { target: { value: DEFAULT_TEXT } });
 | 
	
		
			
				|  |  | +      editor.blur();
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +      const textElement = h.elements[2] as ExcalidrawTextElementWithContainer;
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +      expect(arrow.endBinding?.elementId).toBe(rect.id);
 | 
	
		
			
				|  |  | +      expect(arrow.width).toBe(400);
 | 
	
		
			
				|  |  | +      expect(rect.x).toBe(400);
 | 
	
		
			
				|  |  | +      expect(rect.y).toBe(0);
 | 
	
		
			
				|  |  | +      expect(
 | 
	
		
			
				|  |  | +        wrapText(textElement.originalText, font, getMaxContainerWidth(arrow)),
 | 
	
		
			
				|  |  | +      ).toMatchInlineSnapshot(`
 | 
	
		
			
				|  |  | +        "Online whiteboard collaboration
 | 
	
		
			
				|  |  | +        made easy"
 | 
	
		
			
				|  |  | +      `);
 | 
	
		
			
				|  |  | +      const handleBindTextResizeSpy = jest.spyOn(
 | 
	
		
			
				|  |  | +        textElementUtils,
 | 
	
		
			
				|  |  | +        "handleBindTextResize",
 | 
	
		
			
				|  |  | +      );
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +      mouse.select(rect);
 | 
	
		
			
				|  |  | +      mouse.downAt(rect.x, rect.y);
 | 
	
		
			
				|  |  | +      mouse.moveTo(200, 0);
 | 
	
		
			
				|  |  | +      mouse.upAt(200, 0);
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +      expect(arrow.width).toBe(170);
 | 
	
		
			
				|  |  | +      expect(rect.x).toBe(200);
 | 
	
		
			
				|  |  | +      expect(rect.y).toBe(0);
 | 
	
		
			
				|  |  | +      expect(handleBindTextResizeSpy).toHaveBeenCalledWith(
 | 
	
		
			
				|  |  | +        h.elements[1],
 | 
	
		
			
				|  |  | +        false,
 | 
	
		
			
				|  |  | +      );
 | 
	
		
			
				|  |  | +      expect(
 | 
	
		
			
				|  |  | +        wrapText(textElement.originalText, font, getMaxContainerWidth(arrow)),
 | 
	
		
			
				|  |  | +      ).toMatchInlineSnapshot(`
 | 
	
		
			
				|  |  | +        "Online whiteboard 
 | 
	
		
			
				|  |  | +        collaboration made 
 | 
	
		
			
				|  |  | +        easy"
 | 
	
		
			
				|  |  | +      `);
 | 
	
		
			
				|  |  | +    });
 | 
	
		
			
				|  |  | +  });
 | 
	
		
			
				|  |  |  });
 |