mirror of
				https://github.com/excalidraw/excalidraw.git
				synced 2025-10-31 10:54:33 +01:00 
			
		
		
		
	 e8def8da8d
			
		
	
	e8def8da8d
	
	
	
		
			
			* feat: integrate mermaidToExcalidraw * create mermaid to excal dialog * allow mermaid syntax and export in preview * fix * fix webpack config * fix markdown error by using named export * center preview * set elements as selected when inserted onto canvas * persist mermaid data to storage * store canvas data in refs * load mermaid lazily * tweak design * compute width, height correctly for arrows * fix undefined vertex issue * add mermaid icon in dropdown * add a note in dialog * reset preview when error * show error in preview when error * show mermaid error messgae react way * design tweaks * add example and docs link * fix * tweak design to remove scroll bar * show a spinner unless mermaid loaded * regenerate ids when needed via programmatic api, this makes sure for mermaid diagrams ids are regenerated * tweak * add option to transform viewport to scene coords in transform api * make opts optional and use 100% zoom when inserting to canvas * fix arrow bindings in safari and firefox * fix elements insert position and viewport centering * fix: Update start/end points by 0.5 so bindings don't overlap with start/end bound element coordinates. * defer rendering the preview * tweak text * fix tests * remove only * make design responsive * fix: show extra tools dropdown in mobile * fix mobile css * width auto * upgrade mermaid-to-excalidraw * don't pass appState in deps as its not used * upgrade mermaid-to-excalidraw to fix firefox issue * use types from mermaid-to-excalidraw * upgrade mermaid-to-excalidraw * use stable version of mermaid-to-excalidraw * upgrade mermaid-to-excalidraw * fix width of shapes toolbar for smaller screen size and also fix regression of mobile menu * use i18n * better api * enable test coverage in ui * Add tests * use common utils to update and get text editor * updgrade mermaid-to-excalidraw to support sequence diagrams * fix test * don't update arrow container height anytime in when redrawing text bounding box * increase size limit * increase size limit of vendor to 900kb * use openDialog for mermaid * upgrade mermaid-to-excalidraw * update frame id post generation * upgrade mermaid-to-excalidraw to add entity codes support * update size limit * upgrade mermaid-to-excalidraw package with frame api changes * upgrade mermaid-to-excalidraw to remove directive and use config * don't highlight mermaid tool and remove unused api setSelection * stop using loading state to update text area * move some styling to scss * review fixes * use modifiedTableIcon props and remove stale snap * css * dialog css * fix snap * use dialog border * change mermaidToExcalidrawLib to state * better styling of errors * make modal bigger * fix mobile * update snaps * fix icon color * fix dark mode insert button color * horizontally center spinner * render canvas conditionally on loaded state * rd tweaks * tweak class names * remove max height * typo in example * upgrade mermaid-to-excalidraw * simplify error state * fix height & overflow on vertical breakpoint * fix lint * show errors in overlay * set textarea font family * reduce opacity * update snap * upgrade to mermaid 0.1.2 --------- Co-authored-by: dwelle <luzar.david@gmail.com>
		
			
				
	
	
		
			1231 lines
		
	
	
		
			37 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			1231 lines
		
	
	
		
			37 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
| import ReactDOM from "react-dom";
 | |
| import {
 | |
|   ExcalidrawElement,
 | |
|   ExcalidrawLinearElement,
 | |
|   ExcalidrawTextElementWithContainer,
 | |
|   FontString,
 | |
| } from "../element/types";
 | |
| import { Excalidraw } from "../packages/excalidraw/index";
 | |
| import { centerPoint } from "../math";
 | |
| import { reseed } from "../random";
 | |
| import * as Renderer from "../renderer/renderScene";
 | |
| 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 { queryByTestId, queryByText } from "@testing-library/react";
 | |
| import {
 | |
|   getBoundTextElementPosition,
 | |
|   wrapText,
 | |
|   getBoundTextMaxWidth,
 | |
| } from "../element/textElement";
 | |
| import * as textElementUtils from "../element/textElement";
 | |
| import { ROUNDNESS, VERTICAL_ALIGN } from "../constants";
 | |
| import { vi } from "vitest";
 | |
| 
 | |
| const renderInteractiveScene = vi.spyOn(Renderer, "renderInteractiveScene");
 | |
| const renderStaticScene = vi.spyOn(Renderer, "renderStaticScene");
 | |
| 
 | |
| const { h } = window;
 | |
| const font = "20px Cascadia, width: Segoe UI Emoji" as FontString;
 | |
| 
 | |
| describe("Test Linear Elements", () => {
 | |
|   let container: HTMLElement;
 | |
|   let interactiveCanvas: HTMLCanvasElement;
 | |
| 
 | |
|   beforeEach(async () => {
 | |
|     // Unmount ReactDOM from root
 | |
|     ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
 | |
|     localStorage.clear();
 | |
|     renderInteractiveScene.mockClear();
 | |
|     renderStaticScene.mockClear();
 | |
|     reseed(7);
 | |
|     const comp = await render(<Excalidraw handleKeyboardGlobally={true} />);
 | |
|     h.state.width = 1000;
 | |
|     h.state.height = 1000;
 | |
|     container = comp.container;
 | |
|     interactiveCanvas = container.querySelector("canvas.interactive")!;
 | |
|   });
 | |
| 
 | |
|   const p1: Point = [20, 20];
 | |
|   const p2: Point = [60, 20];
 | |
|   const midpoint = centerPoint(p1, p2);
 | |
|   const delta = 50;
 | |
|   const mouse = new Pointer("mouse");
 | |
| 
 | |
|   const createTwoPointerLinearElement = (
 | |
|     type: ExcalidrawLinearElement["type"],
 | |
|     roundness: ExcalidrawElement["roundness"] = null,
 | |
|     roughness: ExcalidrawLinearElement["roughness"] = 0,
 | |
|   ) => {
 | |
|     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]],
 | |
|       ],
 | |
|       roundness,
 | |
|     });
 | |
|     h.elements = [line];
 | |
| 
 | |
|     mouse.clickAt(p1[0], p1[1]);
 | |
|     return line;
 | |
|   };
 | |
| 
 | |
|   const createThreePointerLinearElement = (
 | |
|     type: ExcalidrawLinearElement["type"],
 | |
|     roundness: ExcalidrawElement["roundness"] = null,
 | |
|     roughness: ExcalidrawLinearElement["roughness"] = 0,
 | |
|   ) => {
 | |
|     //dragging line from midpoint
 | |
|     const p3 = [midpoint[0] + delta - p1[0], midpoint[1] + delta - p1[1]];
 | |
|     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]],
 | |
|       ],
 | |
|       roundness,
 | |
|     });
 | |
|     h.elements = [line];
 | |
|     mouse.clickAt(p1[0], p1[1]);
 | |
|     return line;
 | |
|   };
 | |
| 
 | |
|   const enterLineEditingMode = (
 | |
|     line: ExcalidrawLinearElement,
 | |
|     selectProgrammatically = false,
 | |
|   ) => {
 | |
|     if (selectProgrammatically) {
 | |
|       API.setSelectedElements([line]);
 | |
|     } else {
 | |
|       mouse.clickAt(p1[0], p1[1]);
 | |
|     }
 | |
|     Keyboard.withModifierKeys({ ctrl: true }, () => {
 | |
|       Keyboard.keyPress(KEYS.ENTER);
 | |
|     });
 | |
|     expect(h.state.editingLinearElement?.elementId).toEqual(line.id);
 | |
|   };
 | |
| 
 | |
|   const drag = (startPoint: Point, endPoint: Point) => {
 | |
|     fireEvent.pointerDown(interactiveCanvas, {
 | |
|       clientX: startPoint[0],
 | |
|       clientY: startPoint[1],
 | |
|     });
 | |
|     fireEvent.pointerMove(interactiveCanvas, {
 | |
|       clientX: endPoint[0],
 | |
|       clientY: endPoint[1],
 | |
|     });
 | |
|     fireEvent.pointerUp(interactiveCanvas, {
 | |
|       clientX: endPoint[0],
 | |
|       clientY: endPoint[1],
 | |
|     });
 | |
|   };
 | |
| 
 | |
|   const deletePoint = (point: Point) => {
 | |
|     fireEvent.pointerDown(interactiveCanvas, {
 | |
|       clientX: point[0],
 | |
|       clientY: point[1],
 | |
|     });
 | |
|     fireEvent.pointerUp(interactiveCanvas, {
 | |
|       clientX: point[0],
 | |
|       clientY: point[1],
 | |
|     });
 | |
|     Keyboard.keyPress(KEYS.DELETE);
 | |
|   };
 | |
| 
 | |
|   it("should not drag line and add midpoint until dragged beyond a threshold", () => {
 | |
|     createTwoPointerLinearElement("line");
 | |
|     const line = h.elements[0] as ExcalidrawLinearElement;
 | |
|     const originalX = line.x;
 | |
|     const originalY = line.y;
 | |
|     expect(line.points.length).toEqual(2);
 | |
| 
 | |
|     mouse.clickAt(midpoint[0], midpoint[1]);
 | |
|     drag(midpoint, [midpoint[0] + 1, midpoint[1] + 1]);
 | |
| 
 | |
|     expect(line.points.length).toEqual(2);
 | |
| 
 | |
|     expect(line.x).toBe(originalX);
 | |
|     expect(line.y).toBe(originalY);
 | |
|     expect(line.points.length).toEqual(2);
 | |
| 
 | |
|     drag(midpoint, [midpoint[0] + delta, midpoint[1] + delta]);
 | |
|     expect(line.x).toBe(originalX);
 | |
|     expect(line.y).toBe(originalY);
 | |
|     expect(line.points.length).toEqual(3);
 | |
|   });
 | |
| 
 | |
|   it("should allow dragging line from midpoint in 2 pointer lines outside editor", async () => {
 | |
|     createTwoPointerLinearElement("line");
 | |
|     const line = h.elements[0] as ExcalidrawLinearElement;
 | |
| 
 | |
|     expect(renderInteractiveScene).toHaveBeenCalledTimes(5);
 | |
|     expect(renderStaticScene).toHaveBeenCalledTimes(5);
 | |
|     expect((h.elements[0] as ExcalidrawLinearElement).points.length).toEqual(2);
 | |
| 
 | |
|     // drag line from midpoint
 | |
|     drag(midpoint, [midpoint[0] + delta, midpoint[1] + delta]);
 | |
|     expect(renderInteractiveScene).toHaveBeenCalledTimes(9);
 | |
|     expect(renderStaticScene).toHaveBeenCalledTimes(7);
 | |
|     expect(line.points.length).toEqual(3);
 | |
|     expect(line.points).toMatchInlineSnapshot(`
 | |
|       [
 | |
|         [
 | |
|           0,
 | |
|           0,
 | |
|         ],
 | |
|         [
 | |
|           70,
 | |
|           50,
 | |
|         ],
 | |
|         [
 | |
|           40,
 | |
|           0,
 | |
|         ],
 | |
|       ]
 | |
|     `);
 | |
|   });
 | |
| 
 | |
|   it("should allow entering and exiting line editor via context menu", () => {
 | |
|     createTwoPointerLinearElement("line");
 | |
|     fireEvent.contextMenu(GlobalTestState.interactiveCanvas, {
 | |
|       button: 2,
 | |
|       clientX: midpoint[0],
 | |
|       clientY: midpoint[1],
 | |
|     });
 | |
|     // Enter line editor
 | |
|     let contextMenu = document.querySelector(".context-menu");
 | |
|     fireEvent.contextMenu(GlobalTestState.interactiveCanvas, {
 | |
|       button: 2,
 | |
|       clientX: midpoint[0],
 | |
|       clientY: midpoint[1],
 | |
|     });
 | |
|     fireEvent.click(queryByText(contextMenu as HTMLElement, "Edit line")!);
 | |
| 
 | |
|     expect(h.state.editingLinearElement?.elementId).toEqual(h.elements[0].id);
 | |
| 
 | |
|     // Exiting line editor
 | |
|     fireEvent.contextMenu(GlobalTestState.interactiveCanvas, {
 | |
|       button: 2,
 | |
|       clientX: midpoint[0],
 | |
|       clientY: midpoint[1],
 | |
|     });
 | |
|     contextMenu = document.querySelector(".context-menu");
 | |
|     fireEvent.contextMenu(GlobalTestState.interactiveCanvas, {
 | |
|       button: 2,
 | |
|       clientX: midpoint[0],
 | |
|       clientY: midpoint[1],
 | |
|     });
 | |
|     fireEvent.click(
 | |
|       queryByText(contextMenu as HTMLElement, "Exit line editor")!,
 | |
|     );
 | |
|     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");
 | |
|       const line = h.elements[0] as ExcalidrawLinearElement;
 | |
|       const originalX = line.x;
 | |
|       const originalY = line.y;
 | |
|       enterLineEditingMode(line);
 | |
| 
 | |
|       expect(line.points.length).toEqual(2);
 | |
| 
 | |
|       mouse.clickAt(midpoint[0], midpoint[1]);
 | |
|       expect(line.points.length).toEqual(2);
 | |
| 
 | |
|       drag(midpoint, [midpoint[0] + 1, midpoint[1] + 1]);
 | |
|       expect(line.x).toBe(originalX);
 | |
|       expect(line.y).toBe(originalY);
 | |
|       expect(line.points.length).toEqual(3);
 | |
|     });
 | |
| 
 | |
|     it("should allow dragging line from midpoint in 2 pointer lines", async () => {
 | |
|       createTwoPointerLinearElement("line");
 | |
| 
 | |
|       const line = h.elements[0] as ExcalidrawLinearElement;
 | |
|       enterLineEditingMode(line);
 | |
| 
 | |
|       // drag line from midpoint
 | |
|       drag(midpoint, [midpoint[0] + delta, midpoint[1] + delta]);
 | |
|       expect(renderInteractiveScene).toHaveBeenCalledTimes(13);
 | |
|       expect(renderStaticScene).toHaveBeenCalledTimes(6);
 | |
| 
 | |
|       expect(line.points.length).toEqual(3);
 | |
|       expect(line.points).toMatchInlineSnapshot(`
 | |
|         [
 | |
|           [
 | |
|             0,
 | |
|             0,
 | |
|           ],
 | |
|           [
 | |
|             70,
 | |
|             50,
 | |
|           ],
 | |
|           [
 | |
|             40,
 | |
|             0,
 | |
|           ],
 | |
|         ]
 | |
|       `);
 | |
|     });
 | |
| 
 | |
|     it("should update the midpoints when element roundness changed", async () => {
 | |
|       createThreePointerLinearElement("line");
 | |
| 
 | |
|       const line = h.elements[0] as ExcalidrawLinearElement;
 | |
|       expect(line.points.length).toEqual(3);
 | |
| 
 | |
|       enterLineEditingMode(line);
 | |
| 
 | |
|       const midPointsWithSharpEdge = LinearElementEditor.getEditorMidPoints(
 | |
|         line,
 | |
|         h.state,
 | |
|       );
 | |
| 
 | |
|       // update roundness
 | |
|       fireEvent.click(screen.getByTitle("Round"));
 | |
| 
 | |
|       expect(renderInteractiveScene).toHaveBeenCalledTimes(10);
 | |
|       expect(renderStaticScene).toHaveBeenCalledTimes(8);
 | |
| 
 | |
|       const midPointsWithRoundEdge = LinearElementEditor.getEditorMidPoints(
 | |
|         h.elements[0] as ExcalidrawLinearElement,
 | |
|         h.state,
 | |
|       );
 | |
|       expect(midPointsWithRoundEdge[0]).not.toEqual(midPointsWithSharpEdge[0]);
 | |
|       expect(midPointsWithRoundEdge[1]).not.toEqual(midPointsWithSharpEdge[1]);
 | |
| 
 | |
|       expect(midPointsWithRoundEdge).toMatchInlineSnapshot(`
 | |
|         [
 | |
|           [
 | |
|             55.9697848965255,
 | |
|             47.442326230998205,
 | |
|           ],
 | |
|           [
 | |
|             76.08587175006699,
 | |
|             43.294165939653226,
 | |
|           ],
 | |
|         ]
 | |
|       `);
 | |
|     });
 | |
| 
 | |
|     it("should update all the midpoints when element position changed", async () => {
 | |
|       createThreePointerLinearElement("line", {
 | |
|         type: ROUNDNESS.PROPORTIONAL_RADIUS,
 | |
|       });
 | |
| 
 | |
|       const line = h.elements[0] as ExcalidrawLinearElement;
 | |
|       expect(line.points.length).toEqual(3);
 | |
|       enterLineEditingMode(line);
 | |
| 
 | |
|       const points = LinearElementEditor.getPointsGlobalCoordinates(line);
 | |
|       expect([line.x, line.y]).toEqual(points[0]);
 | |
| 
 | |
|       const midPoints = LinearElementEditor.getEditorMidPoints(line, h.state);
 | |
| 
 | |
|       const startPoint = centerPoint(points[0], midPoints[0] as Point);
 | |
|       const deltaX = 50;
 | |
|       const deltaY = 20;
 | |
|       const endPoint: Point = [startPoint[0] + deltaX, startPoint[1] + deltaY];
 | |
| 
 | |
|       // Move the element
 | |
|       drag(startPoint, endPoint);
 | |
| 
 | |
|       expect(renderInteractiveScene).toHaveBeenCalledTimes(14);
 | |
|       expect(renderStaticScene).toHaveBeenCalledTimes(9);
 | |
| 
 | |
|       expect([line.x, line.y]).toEqual([
 | |
|         points[0][0] + deltaX,
 | |
|         points[0][1] + deltaY,
 | |
|       ]);
 | |
| 
 | |
|       const newMidPoints = LinearElementEditor.getEditorMidPoints(
 | |
|         line,
 | |
|         h.state,
 | |
|       );
 | |
|       expect(midPoints[0]).not.toEqual(newMidPoints[0]);
 | |
|       expect(midPoints[1]).not.toEqual(newMidPoints[1]);
 | |
|       expect(newMidPoints).toMatchInlineSnapshot(`
 | |
|         [
 | |
|           [
 | |
|             105.96978489652551,
 | |
|             67.4423262309982,
 | |
|           ],
 | |
|           [
 | |
|             126.08587175006699,
 | |
|             63.294165939653226,
 | |
|           ],
 | |
|         ]
 | |
|       `);
 | |
|     });
 | |
| 
 | |
|     describe("When edges are round", () => {
 | |
|       // 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, 45];
 | |
|       const lastSegmentMidpoint: Point = [75, 40];
 | |
| 
 | |
|       let line: ExcalidrawLinearElement;
 | |
| 
 | |
|       beforeEach(() => {
 | |
|         line = createThreePointerLinearElement("line");
 | |
| 
 | |
|         expect(line.points.length).toEqual(3);
 | |
| 
 | |
|         enterLineEditingMode(line);
 | |
|       });
 | |
| 
 | |
|       it("should allow dragging lines from midpoints in between segments", async () => {
 | |
|         // drag line via first segment midpoint
 | |
|         drag(firstSegmentMidpoint, [
 | |
|           firstSegmentMidpoint[0] + delta,
 | |
|           firstSegmentMidpoint[1] + delta,
 | |
|         ]);
 | |
|         expect(line.points.length).toEqual(4);
 | |
| 
 | |
|         // drag line from last segment midpoint
 | |
|         drag(lastSegmentMidpoint, [
 | |
|           lastSegmentMidpoint[0] + delta,
 | |
|           lastSegmentMidpoint[1] + delta,
 | |
|         ]);
 | |
| 
 | |
|         expect(renderInteractiveScene).toHaveBeenCalledTimes(19);
 | |
|         expect(renderStaticScene).toHaveBeenCalledTimes(9);
 | |
| 
 | |
|         expect(line.points.length).toEqual(5);
 | |
| 
 | |
|         expect((h.elements[0] as ExcalidrawLinearElement).points)
 | |
|           .toMatchInlineSnapshot(`
 | |
|             [
 | |
|               [
 | |
|                 0,
 | |
|                 0,
 | |
|               ],
 | |
|               [
 | |
|                 85,
 | |
|                 75,
 | |
|               ],
 | |
|               [
 | |
|                 70,
 | |
|                 50,
 | |
|               ],
 | |
|               [
 | |
|                 105,
 | |
|                 70,
 | |
|               ],
 | |
|               [
 | |
|                 40,
 | |
|                 0,
 | |
|               ],
 | |
|             ]
 | |
|           `);
 | |
|       });
 | |
| 
 | |
|       it("should update only the first segment midpoint when its point is dragged", async () => {
 | |
|         const points = LinearElementEditor.getPointsGlobalCoordinates(line);
 | |
|         const midPoints = LinearElementEditor.getEditorMidPoints(line, h.state);
 | |
| 
 | |
|         const hitCoords: Point = [points[0][0], points[0][1]];
 | |
| 
 | |
|         // Drag from first point
 | |
|         drag(hitCoords, [hitCoords[0] - delta, hitCoords[1] - delta]);
 | |
| 
 | |
|         expect(renderInteractiveScene).toHaveBeenCalledTimes(14);
 | |
|         expect(renderStaticScene).toHaveBeenCalledTimes(8);
 | |
| 
 | |
|         const newPoints = LinearElementEditor.getPointsGlobalCoordinates(line);
 | |
|         expect([newPoints[0][0], newPoints[0][1]]).toEqual([
 | |
|           points[0][0] - delta,
 | |
|           points[0][1] - delta,
 | |
|         ]);
 | |
| 
 | |
|         const newMidPoints = LinearElementEditor.getEditorMidPoints(
 | |
|           line,
 | |
|           h.state,
 | |
|         );
 | |
| 
 | |
|         expect(midPoints[0]).not.toEqual(newMidPoints[0]);
 | |
|         expect(midPoints[1]).toEqual(newMidPoints[1]);
 | |
|       });
 | |
| 
 | |
|       it("should hide midpoints in the segment when points moved close", async () => {
 | |
|         const points = LinearElementEditor.getPointsGlobalCoordinates(line);
 | |
|         const midPoints = LinearElementEditor.getEditorMidPoints(line, h.state);
 | |
| 
 | |
|         const hitCoords: Point = [points[0][0], points[0][1]];
 | |
| 
 | |
|         // Drag from first point
 | |
|         drag(hitCoords, [hitCoords[0] + delta, hitCoords[1] + delta]);
 | |
| 
 | |
|         expect(renderInteractiveScene).toHaveBeenCalledTimes(14);
 | |
|         expect(renderStaticScene).toHaveBeenCalledTimes(8);
 | |
| 
 | |
|         const newPoints = LinearElementEditor.getPointsGlobalCoordinates(line);
 | |
|         expect([newPoints[0][0], newPoints[0][1]]).toEqual([
 | |
|           points[0][0] + delta,
 | |
|           points[0][1] + delta,
 | |
|         ]);
 | |
| 
 | |
|         const newMidPoints = LinearElementEditor.getEditorMidPoints(
 | |
|           line,
 | |
|           h.state,
 | |
|         );
 | |
|         // This midpoint is hidden since the points are too close
 | |
|         expect(newMidPoints[0]).toBeNull();
 | |
|         expect(midPoints[1]).toEqual(newMidPoints[1]);
 | |
|       });
 | |
| 
 | |
|       it("should remove the midpoint when one of the points in the segment is deleted", async () => {
 | |
|         const line = h.elements[0] as ExcalidrawLinearElement;
 | |
|         enterLineEditingMode(line);
 | |
|         const points = LinearElementEditor.getPointsGlobalCoordinates(line);
 | |
| 
 | |
|         // dragging line from last segment midpoint
 | |
|         drag(lastSegmentMidpoint, [
 | |
|           lastSegmentMidpoint[0] + 50,
 | |
|           lastSegmentMidpoint[1] + 50,
 | |
|         ]);
 | |
|         expect(line.points.length).toEqual(4);
 | |
| 
 | |
|         const midPoints = LinearElementEditor.getEditorMidPoints(line, h.state);
 | |
| 
 | |
|         // delete 3rd point
 | |
|         deletePoint(points[2]);
 | |
|         expect(line.points.length).toEqual(3);
 | |
|         expect(renderInteractiveScene).toHaveBeenCalledTimes(20);
 | |
|         expect(renderStaticScene).toHaveBeenCalledTimes(9);
 | |
| 
 | |
|         const newMidPoints = LinearElementEditor.getEditorMidPoints(
 | |
|           line,
 | |
|           h.state,
 | |
|         );
 | |
|         expect(newMidPoints.length).toEqual(2);
 | |
|         expect(midPoints[0]).toEqual(newMidPoints[0]);
 | |
|         expect(midPoints[1]).toEqual(newMidPoints[1]);
 | |
|       });
 | |
|     });
 | |
| 
 | |
|     describe("When edges are round", () => {
 | |
|       // 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,
 | |
|       ];
 | |
|       const lastSegmentMidpoint: Point = [
 | |
|         76.08587175006699, 43.294165939653226,
 | |
|       ];
 | |
|       let line: ExcalidrawLinearElement;
 | |
| 
 | |
|       beforeEach(() => {
 | |
|         line = createThreePointerLinearElement("line", {
 | |
|           type: ROUNDNESS.PROPORTIONAL_RADIUS,
 | |
|         });
 | |
|         expect(line.points.length).toEqual(3);
 | |
| 
 | |
|         enterLineEditingMode(line);
 | |
|       });
 | |
| 
 | |
|       it("should allow dragging lines from midpoints in between segments", async () => {
 | |
|         // drag line from first segment midpoint
 | |
|         drag(firstSegmentMidpoint, [
 | |
|           firstSegmentMidpoint[0] + delta,
 | |
|           firstSegmentMidpoint[1] + delta,
 | |
|         ]);
 | |
|         expect(line.points.length).toEqual(4);
 | |
| 
 | |
|         // drag line from last segment midpoint
 | |
|         drag(lastSegmentMidpoint, [
 | |
|           lastSegmentMidpoint[0] + delta,
 | |
|           lastSegmentMidpoint[1] + delta,
 | |
|         ]);
 | |
|         expect(renderInteractiveScene).toHaveBeenCalledTimes(19);
 | |
|         expect(renderStaticScene).toHaveBeenCalledTimes(9);
 | |
|         expect(line.points.length).toEqual(5);
 | |
| 
 | |
|         expect((h.elements[0] as ExcalidrawLinearElement).points)
 | |
|           .toMatchInlineSnapshot(`
 | |
|             [
 | |
|               [
 | |
|                 0,
 | |
|                 0,
 | |
|               ],
 | |
|               [
 | |
|                 85.96978489652551,
 | |
|                 77.4423262309982,
 | |
|               ],
 | |
|               [
 | |
|                 70,
 | |
|                 50,
 | |
|               ],
 | |
|               [
 | |
|                 106.08587175006699,
 | |
|                 73.29416593965323,
 | |
|               ],
 | |
|               [
 | |
|                 40,
 | |
|                 0,
 | |
|               ],
 | |
|             ]
 | |
|           `);
 | |
|       });
 | |
| 
 | |
|       it("should update all the midpoints when its point is dragged", async () => {
 | |
|         const points = LinearElementEditor.getPointsGlobalCoordinates(line);
 | |
|         const midPoints = LinearElementEditor.getEditorMidPoints(line, h.state);
 | |
| 
 | |
|         const hitCoords: Point = [points[0][0], points[0][1]];
 | |
| 
 | |
|         // Drag from first point
 | |
|         drag(hitCoords, [hitCoords[0] - delta, hitCoords[1] - delta]);
 | |
| 
 | |
|         const newPoints = LinearElementEditor.getPointsGlobalCoordinates(line);
 | |
|         expect([newPoints[0][0], newPoints[0][1]]).toEqual([
 | |
|           points[0][0] - delta,
 | |
|           points[0][1] - delta,
 | |
|         ]);
 | |
| 
 | |
|         const newMidPoints = LinearElementEditor.getEditorMidPoints(
 | |
|           line,
 | |
|           h.state,
 | |
|         );
 | |
| 
 | |
|         expect(midPoints[0]).not.toEqual(newMidPoints[0]);
 | |
|         expect(midPoints[1]).not.toEqual(newMidPoints[1]);
 | |
|         expect(newMidPoints).toMatchInlineSnapshot(`
 | |
|           [
 | |
|             [
 | |
|               31.884084517616053,
 | |
|               23.13275505472383,
 | |
|             ],
 | |
|             [
 | |
|               77.74792546875662,
 | |
|               44.57840982272327,
 | |
|             ],
 | |
|           ]
 | |
|         `);
 | |
|       });
 | |
| 
 | |
|       it("should hide midpoints in the segment when points moved close", async () => {
 | |
|         const points = LinearElementEditor.getPointsGlobalCoordinates(line);
 | |
|         const midPoints = LinearElementEditor.getEditorMidPoints(line, h.state);
 | |
| 
 | |
|         const hitCoords: Point = [points[0][0], points[0][1]];
 | |
| 
 | |
|         // Drag from first point
 | |
|         drag(hitCoords, [hitCoords[0] + delta, hitCoords[1] + delta]);
 | |
| 
 | |
|         expect(renderInteractiveScene).toHaveBeenCalledTimes(14);
 | |
|         expect(renderStaticScene).toHaveBeenCalledTimes(8);
 | |
| 
 | |
|         const newPoints = LinearElementEditor.getPointsGlobalCoordinates(line);
 | |
|         expect([newPoints[0][0], newPoints[0][1]]).toEqual([
 | |
|           points[0][0] + delta,
 | |
|           points[0][1] + delta,
 | |
|         ]);
 | |
| 
 | |
|         const newMidPoints = LinearElementEditor.getEditorMidPoints(
 | |
|           line,
 | |
|           h.state,
 | |
|         );
 | |
|         // This mid point is hidden due to point being too close
 | |
|         expect(newMidPoints[0]).toBeNull();
 | |
|         expect(newMidPoints[1]).not.toEqual(midPoints[1]);
 | |
|       });
 | |
| 
 | |
|       it("should update all the midpoints when a point is deleted", async () => {
 | |
|         drag(lastSegmentMidpoint, [
 | |
|           lastSegmentMidpoint[0] + delta,
 | |
|           lastSegmentMidpoint[1] + delta,
 | |
|         ]);
 | |
|         expect(line.points.length).toEqual(4);
 | |
| 
 | |
|         const midPoints = LinearElementEditor.getEditorMidPoints(line, h.state);
 | |
|         const points = LinearElementEditor.getPointsGlobalCoordinates(line);
 | |
| 
 | |
|         // delete 3rd point
 | |
|         deletePoint(points[2]);
 | |
|         expect(line.points.length).toEqual(3);
 | |
| 
 | |
|         const newMidPoints = LinearElementEditor.getEditorMidPoints(
 | |
|           line,
 | |
|           h.state,
 | |
|         );
 | |
|         expect(newMidPoints.length).toEqual(2);
 | |
|         expect(midPoints[0]).not.toEqual(newMidPoints[0]);
 | |
|         expect(midPoints[1]).not.toEqual(newMidPoints[1]);
 | |
|         expect(newMidPoints).toMatchInlineSnapshot(`
 | |
|           [
 | |
|             [
 | |
|               55.9697848965255,
 | |
|               47.442326230998205,
 | |
|             ],
 | |
|             [
 | |
|               76.08587175006699,
 | |
|               43.294165939653226,
 | |
|             ],
 | |
|           ]
 | |
|         `);
 | |
|       });
 | |
|     });
 | |
| 
 | |
|     it("in-editor dragging a line point covered by another element", () => {
 | |
|       createTwoPointerLinearElement("line");
 | |
|       const line = h.elements[0] as ExcalidrawLinearElement;
 | |
|       h.elements = [
 | |
|         line,
 | |
|         API.createElement({
 | |
|           type: "rectangle",
 | |
|           x: line.x - 50,
 | |
|           y: line.y - 50,
 | |
|           width: 100,
 | |
|           height: 100,
 | |
|           backgroundColor: "red",
 | |
|           fillStyle: "solid",
 | |
|         }),
 | |
|       ];
 | |
|       const dragEndPositionOffset = [100, 100] as const;
 | |
|       API.setSelectedElements([line]);
 | |
|       enterLineEditingMode(line, true);
 | |
|       drag(
 | |
|         [line.points[0][0] + line.x, line.points[0][1] + line.y],
 | |
|         [dragEndPositionOffset[0] + line.x, dragEndPositionOffset[1] + line.y],
 | |
|       );
 | |
|       expect(line.points).toMatchInlineSnapshot(`
 | |
|         [
 | |
|           [
 | |
|             0,
 | |
|             0,
 | |
|           ],
 | |
|           [
 | |
|             -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, getBoundTextMaxWidth(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(`
 | |
|           {
 | |
|             "x": 25,
 | |
|             "y": 10,
 | |
|           }
 | |
|         `);
 | |
|       });
 | |
| 
 | |
|       it("should return correct position for arrow with odd points", () => {
 | |
|         createThreePointerLinearElement("arrow", {
 | |
|           type: ROUNDNESS.PROPORTIONAL_RADIUS,
 | |
|         });
 | |
|         const arrow = h.elements[0] as ExcalidrawLinearElement;
 | |
|         const { textElement, container } = createBoundTextElement(
 | |
|           DEFAULT_TEXT,
 | |
|           arrow,
 | |
|         );
 | |
| 
 | |
|         const position = LinearElementEditor.getBoundTextElementPosition(
 | |
|           container,
 | |
|           textElement,
 | |
|         );
 | |
|         expect(position).toMatchInlineSnapshot(`
 | |
|           {
 | |
|             "x": 75,
 | |
|             "y": 60,
 | |
|           }
 | |
|         `);
 | |
|       });
 | |
| 
 | |
|       it("should return correct position for arrow with even points", () => {
 | |
|         createThreePointerLinearElement("arrow", {
 | |
|           type: ROUNDNESS.PROPORTIONAL_RADIUS,
 | |
|         });
 | |
|         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(`
 | |
|           {
 | |
|             "x": 85.82201843191861,
 | |
|             "y": 75.63461309860818,
 | |
|           }
 | |
|         `);
 | |
|       });
 | |
|     });
 | |
| 
 | |
|     it("should match styles for text editor", () => {
 | |
|       createTwoPointerLinearElement("arrow");
 | |
|       Keyboard.keyPress(KEYS.ENTER);
 | |
|       const editor = document.querySelector(
 | |
|         ".excalidraw-textEditorContainer > textarea",
 | |
|       ) as HTMLTextAreaElement;
 | |
|       expect(editor).toMatchSnapshot();
 | |
|     });
 | |
| 
 | |
|     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();
 | |
|     });
 | |
| 
 | |
|     // TODO fix #7029 and rewrite this test
 | |
|     it.todo(
 | |
|       "should not rotate the bound text and update position of bound text and bounding box correctly when arrow rotated",
 | |
|     );
 | |
| 
 | |
|     it("should resize and position the bound text and bounding box correctly when 3 pointer arrow element resized", () => {
 | |
|       createThreePointerLinearElement("arrow", {
 | |
|         type: ROUNDNESS.PROPORTIONAL_RADIUS,
 | |
|       });
 | |
| 
 | |
|       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(`
 | |
|           {
 | |
|             "x": 75,
 | |
|             "y": 60,
 | |
|           }
 | |
|         `);
 | |
|       expect(textElement.text).toMatchInlineSnapshot(`
 | |
|         "Online whiteboard 
 | |
|         collaboration made 
 | |
|         easy"
 | |
|       `);
 | |
|       expect(LinearElementEditor.getElementAbsoluteCoords(container, true))
 | |
|         .toMatchInlineSnapshot(`
 | |
|           [
 | |
|             20,
 | |
|             20,
 | |
|             105,
 | |
|             80,
 | |
|             55.45893770831013,
 | |
|             45,
 | |
|           ]
 | |
|         `);
 | |
| 
 | |
|       UI.resize(container, "ne", [300, 200]);
 | |
| 
 | |
|       expect({ width: container.width, height: container.height })
 | |
|         .toMatchInlineSnapshot(`
 | |
|           {
 | |
|             "height": 130,
 | |
|             "width": 366.11716195150507,
 | |
|           }
 | |
|         `);
 | |
| 
 | |
|       expect(getBoundTextElementPosition(container, textElement))
 | |
|         .toMatchInlineSnapshot(`
 | |
|           {
 | |
|             "x": 271.11716195150507,
 | |
|             "y": 45,
 | |
|           }
 | |
|         `);
 | |
|       expect((h.elements[1] as ExcalidrawTextElementWithContainer).text)
 | |
|         .toMatchInlineSnapshot(`
 | |
|           "Online whiteboard 
 | |
|           collaboration made easy"
 | |
|         `);
 | |
|       expect(LinearElementEditor.getElementAbsoluteCoords(container, true))
 | |
|         .toMatchInlineSnapshot(`
 | |
|           [
 | |
|             20,
 | |
|             35,
 | |
|             501.11716195150507,
 | |
|             95,
 | |
|             205.4589377083102,
 | |
|             52.5,
 | |
|           ]
 | |
|         `);
 | |
|     });
 | |
| 
 | |
|     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(`
 | |
|           {
 | |
|             "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(`
 | |
|           {
 | |
|             "height": 130,
 | |
|             "width": 340,
 | |
|           }
 | |
|         `);
 | |
| 
 | |
|       expect(getBoundTextElementPosition(container, textElement))
 | |
|         .toMatchInlineSnapshot(`
 | |
|           {
 | |
|             "x": 75,
 | |
|             "y": -5,
 | |
|           }
 | |
|         `);
 | |
|       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: -10,
 | |
|         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, getBoundTextMaxWidth(arrow)),
 | |
|       ).toMatchInlineSnapshot(`
 | |
|         "Online whiteboard 
 | |
|         collaboration made easy"
 | |
|       `);
 | |
|       const handleBindTextResizeSpy = vi.spyOn(
 | |
|         textElementUtils,
 | |
|         "handleBindTextResize",
 | |
|       );
 | |
| 
 | |
|       mouse.select(rect);
 | |
|       mouse.downAt(rect.x, rect.y);
 | |
|       mouse.moveTo(200, 0);
 | |
|       mouse.upAt(200, 0);
 | |
| 
 | |
|       expect(arrow.width).toBe(200);
 | |
|       expect(rect.x).toBe(200);
 | |
|       expect(rect.y).toBe(0);
 | |
|       expect(handleBindTextResizeSpy).toHaveBeenCalledWith(
 | |
|         h.elements[1],
 | |
|         false,
 | |
|       );
 | |
|       expect(
 | |
|         wrapText(textElement.originalText, font, getBoundTextMaxWidth(arrow)),
 | |
|       ).toMatchInlineSnapshot(`
 | |
|         "Online whiteboard 
 | |
|         collaboration made 
 | |
|         easy"
 | |
|       `);
 | |
|     });
 | |
| 
 | |
|     it("should not render horizontal 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-left")).toBeNull();
 | |
|       expect(queryByTestId(container, "align-horizontal-center")).toBeNull();
 | |
|       expect(queryByTestId(container, "align-right")).toBeNull();
 | |
|     });
 | |
| 
 | |
|     it("should update label coords when a label binded via context menu is unbinded", async () => {
 | |
|       createTwoPointerLinearElement("arrow");
 | |
|       const text = API.createElement({
 | |
|         type: "text",
 | |
|         text: "Hello Excalidraw",
 | |
|       });
 | |
|       expect(text.x).toBe(0);
 | |
|       expect(text.y).toBe(0);
 | |
| 
 | |
|       h.elements = [h.elements[0], text];
 | |
| 
 | |
|       const container = h.elements[0];
 | |
|       API.setSelectedElements([container, text]);
 | |
|       fireEvent.contextMenu(GlobalTestState.interactiveCanvas, {
 | |
|         button: 2,
 | |
|         clientX: 20,
 | |
|         clientY: 30,
 | |
|       });
 | |
|       let contextMenu = document.querySelector(".context-menu");
 | |
| 
 | |
|       fireEvent.click(
 | |
|         queryByText(contextMenu as HTMLElement, "Bind text to the container")!,
 | |
|       );
 | |
|       expect(container.boundElements).toStrictEqual([
 | |
|         { id: h.elements[1].id, type: "text" },
 | |
|       ]);
 | |
|       expect(text.containerId).toBe(container.id);
 | |
|       expect(text.verticalAlign).toBe(VERTICAL_ALIGN.MIDDLE);
 | |
| 
 | |
|       mouse.reset();
 | |
|       mouse.clickAt(
 | |
|         container.x + container.width / 2,
 | |
|         container.y + container.height / 2,
 | |
|       );
 | |
|       mouse.down();
 | |
|       mouse.up();
 | |
|       API.setSelectedElements([h.elements[0], h.elements[1]]);
 | |
| 
 | |
|       fireEvent.contextMenu(GlobalTestState.interactiveCanvas, {
 | |
|         button: 2,
 | |
|         clientX: 20,
 | |
|         clientY: 30,
 | |
|       });
 | |
|       contextMenu = document.querySelector(".context-menu");
 | |
|       fireEvent.click(queryByText(contextMenu as HTMLElement, "Unbind text")!);
 | |
|       expect(container.boundElements).toEqual([]);
 | |
|       expect(text).toEqual(
 | |
|         expect.objectContaining({
 | |
|           containerId: null,
 | |
|           width: 160,
 | |
|           height: 25,
 | |
|           x: -40,
 | |
|           y: 7.5,
 | |
|         }),
 | |
|       );
 | |
|     });
 | |
| 
 | |
|     it("should not update label position when arrow dragged", () => {
 | |
|       createTwoPointerLinearElement("arrow");
 | |
|       let arrow = h.elements[0] as ExcalidrawLinearElement;
 | |
|       createBoundTextElement(DEFAULT_TEXT, arrow);
 | |
|       let label = h.elements[1] as ExcalidrawTextElementWithContainer;
 | |
|       expect(arrow.x).toBe(20);
 | |
|       expect(arrow.y).toBe(20);
 | |
|       expect(label.x).toBe(0);
 | |
|       expect(label.y).toBe(0);
 | |
|       mouse.reset();
 | |
|       mouse.select(arrow);
 | |
|       mouse.select(label);
 | |
|       mouse.downAt(arrow.x, arrow.y);
 | |
|       mouse.moveTo(arrow.x + 20, arrow.y + 30);
 | |
|       mouse.up(arrow.x + 20, arrow.y + 30);
 | |
| 
 | |
|       arrow = h.elements[0] as ExcalidrawLinearElement;
 | |
|       label = h.elements[1] as ExcalidrawTextElementWithContainer;
 | |
|       expect(arrow.x).toBe(80);
 | |
|       expect(arrow.y).toBe(100);
 | |
|       expect(label.x).toBe(0);
 | |
|       expect(label.y).toBe(0);
 | |
|     });
 | |
|   });
 | |
| });
 |