Browse Source

Fix tests

Mark Tolmacs 5 months ago
parent
commit
e90350b7d1

+ 66 - 55
packages/element/tests/binding.test.tsx

@@ -18,7 +18,9 @@ const mouse = new Pointer("mouse");
 
 describe("element binding", () => {
   beforeEach(async () => {
+    localStorage.clear();
     await render(<Excalidraw handleKeyboardGlobally={true} />);
+    mouse.reset();
   });
 
   it("should create valid binding if duplicate start/end points", async () => {
@@ -89,46 +91,55 @@ describe("element binding", () => {
     });
   });
 
-  //@TODO fix the test with rotation
-  it.skip("rotation of arrow should rebind both ends", () => {
-    const rectLeft = UI.createElement("rectangle", {
-      x: 0,
-      width: 200,
-      height: 500,
-    });
-    const rectRight = UI.createElement("rectangle", {
-      x: 400,
-      width: 200,
-      height: 500,
-    });
-    const arrow = UI.createElement("arrow", {
-      x: 210,
-      y: 250,
-      width: 180,
-      height: 1,
-    });
-    expect(arrow.startBinding?.elementId).toBe(rectLeft.id);
-    expect(arrow.endBinding?.elementId).toBe(rectRight.id);
-
-    const rotation = getTransformHandles(
-      arrow,
-      h.state.zoom,
-      arrayToMap(h.elements),
-      "mouse",
-    ).rotation!;
-    const rotationHandleX = rotation[0] + rotation[2] / 2;
-    const rotationHandleY = rotation[1] + rotation[3] / 2;
-    mouse.down(rotationHandleX, rotationHandleY);
-    mouse.move(300, 400);
-    mouse.up();
-    expect(arrow.angle).toBeGreaterThan(0.7 * Math.PI);
-    expect(arrow.angle).toBeLessThan(1.3 * Math.PI);
-    expect(arrow.startBinding?.elementId).toBe(rectRight.id);
-    expect(arrow.endBinding?.elementId).toBe(rectLeft.id);
-  });
+  // UX RATIONALE: We are not aware of any use-case where the user would want to
+  // have the arrow rebind after rotation but not when the arrow shaft is
+  // dragged so either the start or the end point is in the binding range of a
+  // bindable element. So to remain consistent, we only "rebind" if at the end
+  // of the rotation the original binding would remain the same (i.e. like we
+  // would've evaluated binding only at the end of the operation).
+  it(
+    "rotation of arrow should not rebind on both ends if rotated enough to" +
+      " not be in the binding range of the original elements",
+    () => {
+      const rectLeft = UI.createElement("rectangle", {
+        x: 0,
+        width: 200,
+        height: 500,
+      });
+      const rectRight = UI.createElement("rectangle", {
+        x: 400,
+        width: 200,
+        height: 500,
+      });
+      const arrow = UI.createElement("arrow", {
+        x: 210,
+        y: 250,
+        width: 180,
+        height: 1,
+      });
+      expect(arrow.startBinding?.elementId).toBe(rectLeft.id);
+      expect(arrow.endBinding?.elementId).toBe(rectRight.id);
+
+      const rotation = getTransformHandles(
+        arrow,
+        h.state.zoom,
+        arrayToMap(h.elements),
+        "mouse",
+      ).rotation!;
+      const rotationHandleX = rotation[0] + rotation[2] / 2;
+      const rotationHandleY = rotation[1] + rotation[3] / 2;
+      mouse.down(rotationHandleX, rotationHandleY);
+      mouse.move(300, 400);
+      mouse.up();
+      expect(arrow.angle).toBeGreaterThan(0.7 * Math.PI);
+      expect(arrow.angle).toBeLessThan(1.3 * Math.PI);
+      expect(arrow.startBinding).toBe(null);
+      expect(arrow.endBinding).toBe(null);
+    },
+  );
 
   // TODO fix & reenable once we rewrite tests to work with concurrency
-  it.skip(
+  it(
     "editing arrow and moving its head to bind it to element A, finalizing the" +
       "editing by clicking on element A should end up selecting A",
     async () => {
@@ -142,7 +153,10 @@ describe("element binding", () => {
       mouse.up(0, 80);
 
       // Edit arrow with multi-point
-      mouse.doubleClick();
+      Keyboard.withModifierKeys({ ctrl: true }, () => {
+        mouse.doubleClick();
+      });
+
       // move arrow head
       mouse.down();
       mouse.up(0, 10);
@@ -152,11 +166,7 @@ describe("element binding", () => {
       // the issue, due to https://github.com/excalidraw/excalidraw/blob/46bff3daceb602accf60c40a84610797260fca94/src/components/App.tsx#L740
       mouse.reset();
       expect(h.state.editingLinearElement).not.toBe(null);
-      mouse.down(0, 0);
-      await new Promise((r) => setTimeout(r, 100));
-      expect(h.state.editingLinearElement).toBe(null);
-      expect(API.getSelectedElement().type).toBe("rectangle");
-      mouse.up();
+      mouse.click();
       expect(API.getSelectedElement().type).toBe("rectangle");
     },
   );
@@ -187,23 +197,24 @@ describe("element binding", () => {
     expect(arrow.endBinding?.elementId).toBe(rectangle.id);
     Keyboard.keyPress(KEYS.ARROW_LEFT);
     expect(arrow.endBinding?.elementId).toBe(rectangle.id);
+    expect(API.getSelectedElement().type).toBe("arrow");
 
     // Sever connection
-    expect(API.getSelectedElement().type).toBe("arrow");
     Keyboard.withModifierKeys({ shift: true }, () => {
       // We have to move a significant distance to get out of the binding zone
-      Keyboard.keyPress(KEYS.ARROW_LEFT);
-      Keyboard.keyPress(KEYS.ARROW_LEFT);
-      Keyboard.keyPress(KEYS.ARROW_LEFT);
-      Keyboard.keyPress(KEYS.ARROW_LEFT);
-      Keyboard.keyPress(KEYS.ARROW_LEFT);
-      Keyboard.keyPress(KEYS.ARROW_LEFT);
-      Keyboard.keyPress(KEYS.ARROW_LEFT);
-      Keyboard.keyPress(KEYS.ARROW_LEFT);
-      Keyboard.keyPress(KEYS.ARROW_LEFT);
+      Array.from({ length: 10 }).forEach(() => {
+        Keyboard.keyPress(KEYS.ARROW_LEFT);
+      });
     });
     expect(arrow.endBinding).toBe(null);
-    Keyboard.keyPress(KEYS.ARROW_RIGHT);
+
+    Keyboard.withModifierKeys({ shift: true }, () => {
+      // We have to move a significant distance to return to the binding
+      Array.from({ length: 10 }).forEach(() => {
+        Keyboard.keyPress(KEYS.ARROW_RIGHT);
+      });
+    });
+    // We are back in the binding zone but we shouldn't rebind
     expect(arrow.endBinding).toBe(null);
   });
 

+ 125 - 3
packages/element/tests/duplicate.test.tsx

@@ -10,11 +10,14 @@ import {
 
 import { Excalidraw } from "@excalidraw/excalidraw";
 
-import { actionDuplicateSelection } from "@excalidraw/excalidraw/actions";
+import {
+  actionDuplicateSelection,
+  actionSelectAll,
+} from "@excalidraw/excalidraw/actions";
 
 import { API } from "@excalidraw/excalidraw/tests/helpers/api";
 
-import { UI, Keyboard, Pointer } from "@excalidraw/excalidraw/tests/helpers/ui";
+import { Keyboard, Pointer, UI } from "@excalidraw/excalidraw/tests/helpers/ui";
 
 import {
   act,
@@ -28,7 +31,10 @@ import type { LocalPoint } from "@excalidraw/math";
 import { mutateElement } from "../src/mutateElement";
 import { duplicateElement, duplicateElements } from "../src/duplicate";
 
-import type { ExcalidrawLinearElement } from "../src/types";
+import type {
+  ExcalidrawArrowElement,
+  ExcalidrawLinearElement,
+} from "../src/types";
 
 const { h } = window;
 const mouse = new Pointer("mouse");
@@ -408,6 +414,122 @@ describe("duplicating multiple elements", () => {
   });
 });
 
+describe("elbow arrow duplication", () => {
+  beforeEach(async () => {
+    await render(<Excalidraw />);
+  });
+
+  it("keeps arrow shape when the whole set of arrow and bindables are duplicated", async () => {
+    UI.createElement("rectangle", {
+      x: -150,
+      y: -150,
+      width: 100,
+      height: 100,
+    });
+    UI.createElement("rectangle", {
+      x: 50,
+      y: 50,
+      width: 100,
+      height: 100,
+    });
+
+    UI.clickTool("arrow");
+    UI.clickOnTestId("elbow-arrow");
+
+    mouse.reset();
+    mouse.moveTo(-43, -99);
+    mouse.click();
+    mouse.moveTo(43, 99);
+    mouse.click();
+
+    const arrow = h.scene.getSelectedElements(
+      h.state,
+    )[0] as ExcalidrawArrowElement;
+    const originalArrowId = arrow.id;
+
+    expect(arrow.startBinding).not.toBe(null);
+    expect(arrow.endBinding).not.toBe(null);
+
+    act(() => {
+      h.app.actionManager.executeAction(actionSelectAll);
+    });
+
+    act(() => {
+      h.app.actionManager.executeAction(actionDuplicateSelection);
+    });
+
+    expect(h.elements.length).toEqual(6);
+
+    const duplicatedArrow = h.scene.getSelectedElements(
+      h.state,
+    )[2] as ExcalidrawArrowElement;
+
+    expect(duplicatedArrow.id).not.toBe(originalArrowId);
+    expect(duplicatedArrow.type).toBe("arrow");
+    expect(duplicatedArrow.elbowed).toBe(true);
+    expect(duplicatedArrow.points).toEqual([
+      [0, 0],
+      [45, 0],
+      [45, 200],
+      [90, 200],
+    ]);
+    expect(arrow.startBinding).not.toBe(null);
+    expect(arrow.endBinding).not.toBe(null);
+  });
+
+  it("changes arrow shape to unbind variant if only the connected elbow arrow is duplicated", async () => {
+    UI.createElement("rectangle", {
+      x: -150,
+      y: -150,
+      width: 100,
+      height: 100,
+    });
+    UI.createElement("rectangle", {
+      x: 50,
+      y: 50,
+      width: 100,
+      height: 100,
+    });
+
+    UI.clickTool("arrow");
+    UI.clickOnTestId("elbow-arrow");
+
+    mouse.reset();
+    mouse.moveTo(-43, -99);
+    mouse.click();
+    mouse.moveTo(43, 99);
+    mouse.click();
+
+    const arrow = h.scene.getSelectedElements(
+      h.state,
+    )[0] as ExcalidrawArrowElement;
+    const originalArrowId = arrow.id;
+
+    expect(arrow.startBinding).not.toBe(null);
+    expect(arrow.endBinding).not.toBe(null);
+
+    act(() => {
+      h.app.actionManager.executeAction(actionDuplicateSelection);
+    });
+
+    expect(h.elements.length).toEqual(4);
+
+    const duplicatedArrow = h.scene.getSelectedElements(
+      h.state,
+    )[0] as ExcalidrawArrowElement;
+
+    expect(duplicatedArrow.id).not.toBe(originalArrowId);
+    expect(duplicatedArrow.type).toBe("arrow");
+    expect(duplicatedArrow.elbowed).toBe(true);
+    expect(duplicatedArrow.points).toEqual([
+      [0, 0],
+      [0, 100],
+      [90, 100],
+      [90, 200],
+    ]);
+  });
+});
+
 describe("duplication z-order", () => {
   beforeEach(async () => {
     await render(<Excalidraw />);

+ 0 - 113
packages/element/tests/elbowArrow.test.tsx

@@ -3,14 +3,11 @@ import { pointFrom } from "@excalidraw/math";
 import { Excalidraw, mutateElement } from "@excalidraw/excalidraw";
 
 import Scene from "@excalidraw/excalidraw/scene/Scene";
-import { actionSelectAll } from "@excalidraw/excalidraw/actions";
-import { actionDuplicateSelection } from "@excalidraw/excalidraw/actions/actionDuplicateSelection";
 
 import { API } from "@excalidraw/excalidraw/tests/helpers/api";
 import { Pointer, UI } from "@excalidraw/excalidraw/tests/helpers/ui";
 
 import {
-  act,
   fireEvent,
   GlobalTestState,
   queryByTestId,
@@ -301,114 +298,4 @@ describe("elbow arrow ui", () => {
       [103, 165],
     ]);
   });
-
-  it("keeps arrow shape when the whole set of arrow and bindables are duplicated", async () => {
-    UI.createElement("rectangle", {
-      x: -150,
-      y: -150,
-      width: 100,
-      height: 100,
-    });
-    UI.createElement("rectangle", {
-      x: 50,
-      y: 50,
-      width: 100,
-      height: 100,
-    });
-
-    UI.clickTool("arrow");
-    UI.clickOnTestId("elbow-arrow");
-
-    mouse.reset();
-    mouse.moveTo(-43, -99);
-    mouse.click();
-    mouse.moveTo(43, 99);
-    mouse.click();
-
-    const arrow = h.scene.getSelectedElements(
-      h.state,
-    )[0] as ExcalidrawArrowElement;
-    const originalArrowId = arrow.id;
-
-    expect(arrow.startBinding).not.toBe(null);
-    expect(arrow.endBinding).not.toBe(null);
-
-    act(() => {
-      h.app.actionManager.executeAction(actionSelectAll);
-    });
-
-    act(() => {
-      h.app.actionManager.executeAction(actionDuplicateSelection);
-    });
-
-    expect(h.elements.length).toEqual(6);
-
-    const duplicatedArrow = h.scene.getSelectedElements(
-      h.state,
-    )[2] as ExcalidrawArrowElement;
-
-    expect(duplicatedArrow.id).not.toBe(originalArrowId);
-    expect(duplicatedArrow.type).toBe("arrow");
-    expect(duplicatedArrow.elbowed).toBe(true);
-    expect(duplicatedArrow.points).toEqual([
-      [0, 0],
-      [45, 0],
-      [45, 200],
-      [90, 200],
-    ]);
-    expect(arrow.startBinding).not.toBe(null);
-    expect(arrow.endBinding).not.toBe(null);
-  });
-
-  it("changes arrow shape to unbind variant if only the connected elbow arrow is duplicated", async () => {
-    UI.createElement("rectangle", {
-      x: -150,
-      y: -150,
-      width: 100,
-      height: 100,
-    });
-    UI.createElement("rectangle", {
-      x: 50,
-      y: 50,
-      width: 100,
-      height: 100,
-    });
-
-    UI.clickTool("arrow");
-    UI.clickOnTestId("elbow-arrow");
-
-    mouse.reset();
-    mouse.moveTo(-43, -99);
-    mouse.click();
-    mouse.moveTo(43, 99);
-    mouse.click();
-
-    const arrow = h.scene.getSelectedElements(
-      h.state,
-    )[0] as ExcalidrawArrowElement;
-    const originalArrowId = arrow.id;
-
-    expect(arrow.startBinding).not.toBe(null);
-    expect(arrow.endBinding).not.toBe(null);
-
-    act(() => {
-      h.app.actionManager.executeAction(actionDuplicateSelection);
-    });
-
-    expect(h.elements.length).toEqual(4);
-
-    const duplicatedArrow = h.scene.getSelectedElements(
-      h.state,
-    )[0] as ExcalidrawArrowElement;
-
-    expect(duplicatedArrow.id).not.toBe(originalArrowId);
-    expect(duplicatedArrow.type).toBe("arrow");
-    expect(duplicatedArrow.elbowed).toBe(true);
-    expect(duplicatedArrow.points).toEqual([
-      [0, 0],
-      [0, 100],
-      [90, 100],
-      [90, 200],
-    ]);
-  });
 });

+ 13 - 9
packages/element/tests/resize.test.tsx

@@ -15,6 +15,8 @@ import {
   unmountComponent,
 } from "@excalidraw/excalidraw/tests/test-utils";
 
+import { FIXED_BINDING_DISTANCE } from "@excalidraw/element/binding";
+
 import type { LocalPoint } from "@excalidraw/math";
 
 import { isLinearElement } from "../src/typeChecks";
@@ -1004,14 +1006,14 @@ describe("multiple selection", () => {
       size: 100,
     });
     const leftBoundArrow = UI.createElement("arrow", {
-      x: -110,
+      x: -100 - FIXED_BINDING_DISTANCE,
       y: 50,
       width: 100,
       height: 0,
     });
 
     const rightBoundArrow = UI.createElement("arrow", {
-      x: 210,
+      x: 200 + FIXED_BINDING_DISTANCE,
       y: 50,
       width: -100,
       height: 0,
@@ -1032,9 +1034,9 @@ describe("multiple selection", () => {
       shift: true,
     });
 
-    expect(leftBoundArrow.x).toBeCloseTo(-110);
+    expect(leftBoundArrow.x).toBeCloseTo(-100 - FIXED_BINDING_DISTANCE);
     expect(leftBoundArrow.y).toBeCloseTo(50);
-    expect(leftBoundArrow.width).toBeCloseTo(146, 0);
+    expect(leftBoundArrow.width).toBeCloseTo(146 - FIXED_BINDING_DISTANCE, 0);
     expect(leftBoundArrow.height).toBeCloseTo(7, 0);
     expect(leftBoundArrow.angle).toEqual(0);
     expect(leftBoundArrow.startBinding).toBeNull();
@@ -1044,15 +1046,17 @@ describe("multiple selection", () => {
     );
     expect(leftBoundArrow.endBinding?.focus).toBe(leftArrowBinding.focus);
 
-    expect(rightBoundArrow.x).toBeCloseTo(210);
+    expect(rightBoundArrow.x).toBeCloseTo(210 - FIXED_BINDING_DISTANCE);
     expect(rightBoundArrow.y).toBeCloseTo(
       (selectionHeight - 50) * (1 - scale) + 50,
+      0,
     );
-    expect(rightBoundArrow.width).toBeCloseTo(100 * scale);
+    //console.log(JSON.stringify(h.elements));
+    expect(rightBoundArrow.width).toBeCloseTo(100 * scale + 1, 0);
     expect(rightBoundArrow.height).toBeCloseTo(0);
     expect(rightBoundArrow.angle).toEqual(0);
     expect(rightBoundArrow.startBinding).toBeNull();
-    expect(rightBoundArrow.endBinding?.gap).toBeCloseTo(8.0952);
+    expect(rightBoundArrow.endBinding?.gap).toBeCloseTo(FIXED_BINDING_DISTANCE);
     expect(rightBoundArrow.endBinding?.elementId).toBe(
       rightArrowBinding.elementId,
     );
@@ -1339,8 +1343,8 @@ describe("multiple selection", () => {
 
     expect(boundArrow.x).toBeCloseTo(380 * scaleX);
     expect(boundArrow.y).toBeCloseTo(240 * scaleY);
-    expect(boundArrow.points[1][0]).toBeCloseTo(-60 * scaleX);
-    expect(boundArrow.points[1][1]).toBeCloseTo(-80 * scaleY);
+    expect(boundArrow.points[1][0]).toBeCloseTo(-60 * scaleX - 2, 0);
+    expect(boundArrow.points[1][1]).toBeCloseTo(-80 * scaleY + 2, 0);
 
     expect(arrowLabelPos.x + arrowLabel.width / 2).toBeCloseTo(
       boundArrow.x + boundArrow.points[1][0] / 2,

+ 14 - 1
packages/excalidraw/actions/actionFlip.test.tsx

@@ -87,6 +87,16 @@ describe("flipping arrowheads", () => {
     await render(<Excalidraw />);
   });
 
+  // UX RATIONALE: If we flip bound arrows by the center axes then there could
+  // be a case where the bindable objects are offset and the arrow would lay
+  // outside both bindable objects binding range, yet remain bound to then,
+  // resulting in a jump on movement.
+  //
+  // We are aware that 2+ point simple arrows behave incorrectly when flipped
+  // this way but it was decided that there is no known use case for this so
+  // left as it is.
+  //
+  // Demo: https://excalidraw.com/#json=isE-S8LqNlD1u-LsS8Ezz,iZZ09PPasp6OWbGtJwOUGQ
   it("flipping bound arrow should flip arrowheads only", () => {
     const rect = API.createElement({
       type: "rectangle",
@@ -123,6 +133,7 @@ describe("flipping arrowheads", () => {
     expect(API.getElement(arrow).endArrowhead).toBe("arrow");
   });
 
+  // UX RATIONALE: See above for the reasoning.
   it("flipping bound arrow should flip arrowheads only 2", () => {
     const rect = API.createElement({
       type: "rectangle",
@@ -164,7 +175,9 @@ describe("flipping arrowheads", () => {
     expect(API.getElement(arrow).endArrowhead).toBe("circle");
   });
 
-  it("flipping unbound arrow shouldn't flip arrowheads", () => {
+  // UX RATIONALE: Unbound arrows are not constrained by other elements and
+  // should behave like any other element when flipped for consisency.
+  it("flipping unbound arrow should mirror on horizontal or vertical axis", () => {
     const arrow = API.createElement({
       type: "arrow",
       id: "arrow1",

+ 61 - 69
packages/excalidraw/tests/__snapshots__/history.test.tsx.snap

@@ -198,7 +198,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
   "fillStyle": "solid",
   "frameId": null,
   "groupIds": [],
-  "height": "99.58947",
+  "height": "99.23572",
   "id": "id172",
   "index": "a2",
   "isDeleted": false,
@@ -212,8 +212,8 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
       0,
     ],
     [
-      "99.58947",
-      "99.58947",
+      "96.42891",
+      "99.23572",
     ],
   ],
   "roughness": 1,
@@ -228,8 +228,8 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
   "type": "arrow",
   "updated": 1,
   "version": 40,
-  "width": "99.58947",
-  "x": 0,
+  "width": "96.42891",
+  "x": "3.53553",
   "y": 0,
 }
 `;
@@ -295,10 +295,10 @@ History {
             "deleted": {
               "endBinding": {
                 "elementId": "id171",
-                "focus": "0.01099",
+                "focus": "0.01140",
                 "gap": 5,
               },
-              "height": "0.96335",
+              "height": "1.00000",
               "points": [
                 [
                   0,
@@ -306,22 +306,22 @@ History {
                 ],
                 [
                   "92.92893",
-                  "-0.96335",
+                  "-1.00000",
                 ],
               ],
               "startBinding": {
                 "elementId": "id170",
-                "focus": "0.03005",
+                "focus": "0.03119",
                 "gap": 5,
               },
             },
             "inserted": {
               "endBinding": {
                 "elementId": "id171",
-                "focus": "-0.02041",
+                "focus": "-0.02000",
                 "gap": 5,
               },
-              "height": "0.03665",
+              "height": 0,
               "points": [
                 [
                   0,
@@ -329,12 +329,12 @@ History {
                 ],
                 [
                   "92.92893",
-                  "0.03665",
+                  0,
                 ],
               ],
               "startBinding": {
                 "elementId": "id170",
-                "focus": "0.01884",
+                "focus": "0.02000",
                 "gap": 5,
               },
             },
@@ -390,29 +390,27 @@ History {
                 "focus": 0,
                 "gap": 1,
               },
-              "height": "99.58947",
+              "height": "99.23572",
               "points": [
                 [
                   0,
                   0,
                 ],
                 [
-                  "99.58947",
-                  "99.58947",
+                  "96.42891",
+                  "99.23572",
                 ],
               ],
               "startBinding": null,
-              "width": "99.58947",
-              "x": 0,
               "y": 0,
             },
             "inserted": {
               "endBinding": {
                 "elementId": "id171",
-                "focus": "0.01099",
+                "focus": "0.01140",
                 "gap": 5,
               },
-              "height": "0.96335",
+              "height": "1.00000",
               "points": [
                 [
                   0,
@@ -420,17 +418,15 @@ History {
                 ],
                 [
                   "92.92893",
-                  "-0.96335",
+                  "-1.00000",
                 ],
               ],
               "startBinding": {
                 "elementId": "id170",
-                "focus": "0.03005",
+                "focus": "0.03119",
                 "gap": 5,
               },
-              "width": "92.92893",
-              "x": "3.53553",
-              "y": "0.96335",
+              "y": "1.00000",
             },
           },
           "id175" => Delta {
@@ -570,7 +566,7 @@ History {
                   0,
                 ],
                 [
-                  "96.46447",
+                  "92.92893",
                   0,
                 ],
               ],
@@ -584,8 +580,8 @@ History {
               "strokeStyle": "solid",
               "strokeWidth": 2,
               "type": "arrow",
-              "width": "96.46447",
-              "x": 0,
+              "width": "92.92893",
+              "x": "3.53553",
               "y": 0,
             },
             "inserted": {
@@ -808,7 +804,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
       0,
     ],
     [
-      "96.46447",
+      "92.92893",
       0,
     ],
   ],
@@ -824,8 +820,8 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
   "type": "arrow",
   "updated": 1,
   "version": 30,
-  "width": "96.46447",
-  "x": 150,
+  "width": "0.00000",
+  "x": "146.46447",
   "y": 0,
 }
 `;
@@ -925,13 +921,11 @@ History {
                   0,
                 ],
                 [
-                  "96.46447",
+                  "92.92893",
                   0,
                 ],
               ],
               "startBinding": null,
-              "width": "96.46447",
-              "x": 150,
             },
             "inserted": {
               "endBinding": {
@@ -954,8 +948,6 @@ History {
                 "focus": 0,
                 "gap": 5,
               },
-              "width": "0.00000",
-              "x": "146.46447",
             },
           },
         },
@@ -1082,7 +1074,7 @@ History {
                   0,
                 ],
                 [
-                  "96.46447",
+                  "92.92893",
                   0,
                 ],
               ],
@@ -1096,8 +1088,8 @@ History {
               "strokeStyle": "solid",
               "strokeWidth": 2,
               "type": "arrow",
-              "width": "96.46447",
-              "x": 0,
+              "width": "92.92893",
+              "x": "3.53553",
               "y": 0,
             },
             "inserted": {
@@ -2334,7 +2326,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
   "fillStyle": "solid",
   "frameId": null,
   "groupIds": [],
-  "height": "369.21589",
+  "height": "369.23631",
   "id": "id186",
   "index": "a2",
   "isDeleted": false,
@@ -2348,8 +2340,8 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
       0,
     ],
     [
-      "496.84035",
-      "-369.21589",
+      "496.83418",
+      "-369.23631",
     ],
   ],
   "roughness": 1,
@@ -2368,9 +2360,9 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
   "type": "arrow",
   "updated": 1,
   "version": 10,
-  "width": "496.84035",
-  "x": "2.18463",
-  "y": "-38.80748",
+  "width": "496.83418",
+  "x": "2.19080",
+  "y": "-38.78706",
 }
 `;
 
@@ -2507,7 +2499,7 @@ History {
                   0,
                 ],
                 [
-                  "96.46447",
+                  "92.92893",
                   0,
                 ],
               ],
@@ -2525,8 +2517,8 @@ History {
               "strokeStyle": "solid",
               "strokeWidth": 2,
               "type": "arrow",
-              "width": "96.46447",
-              "x": 0,
+              "width": "92.92893",
+              "x": "3.53553",
               "y": 0,
             },
             "inserted": {
@@ -15250,7 +15242,7 @@ History {
                   0,
                 ],
                 [
-                  "96.46447",
+                  "92.92893",
                   0,
                 ],
               ],
@@ -15263,7 +15255,7 @@ History {
                   0,
                 ],
                 [
-                  "96.46447",
+                  "92.92893",
                   0,
                 ],
               ],
@@ -15558,7 +15550,7 @@ History {
                   0,
                 ],
                 [
-                  "96.46447",
+                  "92.92893",
                   0,
                 ],
               ],
@@ -15576,8 +15568,8 @@ History {
               "strokeStyle": "solid",
               "strokeWidth": 2,
               "type": "arrow",
-              "width": "96.46447",
-              "x": 0,
+              "width": "92.92893",
+              "x": "3.53553",
               "y": 0,
             },
             "inserted": {
@@ -16178,7 +16170,7 @@ History {
                   0,
                 ],
                 [
-                  "96.46447",
+                  "92.92893",
                   0,
                 ],
               ],
@@ -16196,8 +16188,8 @@ History {
               "strokeStyle": "solid",
               "strokeWidth": 2,
               "type": "arrow",
-              "width": "96.46447",
-              "x": 0,
+              "width": "92.92893",
+              "x": "3.53553",
               "y": 0,
             },
             "inserted": {
@@ -16798,7 +16790,7 @@ History {
                   0,
                 ],
                 [
-                  "96.46447",
+                  "92.92893",
                   0,
                 ],
               ],
@@ -16816,8 +16808,8 @@ History {
               "strokeStyle": "solid",
               "strokeWidth": 2,
               "type": "arrow",
-              "width": "96.46447",
-              "x": 0,
+              "width": "92.92893",
+              "x": "3.53553",
               "y": 0,
             },
             "inserted": {
@@ -17201,7 +17193,7 @@ History {
                   0,
                 ],
                 [
-                  "96.46447",
+                  "92.92893",
                   0,
                 ],
               ],
@@ -17218,7 +17210,7 @@ History {
                   0,
                 ],
                 [
-                  "96.46447",
+                  "92.92893",
                   0,
                 ],
               ],
@@ -17486,7 +17478,7 @@ History {
                   0,
                 ],
                 [
-                  "96.46447",
+                  "92.92893",
                   0,
                 ],
               ],
@@ -17504,8 +17496,8 @@ History {
               "strokeStyle": "solid",
               "strokeWidth": 2,
               "type": "arrow",
-              "width": "96.46447",
-              "x": 0,
+              "width": "92.92893",
+              "x": "3.53553",
               "y": 0,
             },
             "inserted": {
@@ -17929,7 +17921,7 @@ History {
                   0,
                 ],
                 [
-                  "96.46447",
+                  "92.92893",
                   0,
                 ],
               ],
@@ -17947,7 +17939,7 @@ History {
                   0,
                 ],
                 [
-                  "96.46447",
+                  "92.92893",
                   0,
                 ],
               ],
@@ -18215,7 +18207,7 @@ History {
                   0,
                 ],
                 [
-                  "96.46447",
+                  "92.92893",
                   0,
                 ],
               ],
@@ -18233,8 +18225,8 @@ History {
               "strokeStyle": "solid",
               "strokeWidth": 2,
               "type": "arrow",
-              "width": "96.46447",
-              "x": 0,
+              "width": "92.92893",
+              "x": "3.53553",
               "y": 0,
             },
             "inserted": {

+ 136 - 0
packages/excalidraw/tests/__snapshots__/move.test.tsx.snap

@@ -101,3 +101,139 @@ exports[`move element > rectangle 5`] = `
   "y": 40,
 }
 `;
+
+exports[`move element > rectangles with binding arrow 5`] = `
+{
+  "angle": 0,
+  "backgroundColor": "transparent",
+  "boundElements": [
+    {
+      "id": "id2",
+      "type": "arrow",
+    },
+  ],
+  "customData": undefined,
+  "fillStyle": "solid",
+  "frameId": null,
+  "groupIds": [],
+  "height": 100,
+  "id": "id0",
+  "index": "a0",
+  "isDeleted": false,
+  "link": null,
+  "locked": false,
+  "opacity": 100,
+  "roughness": 1,
+  "roundness": {
+    "type": 3,
+  },
+  "seed": 1278240551,
+  "strokeColor": "#1e1e1e",
+  "strokeStyle": "solid",
+  "strokeWidth": 2,
+  "type": "rectangle",
+  "updated": 1,
+  "version": 4,
+  "versionNonce": 1723083209,
+  "width": 100,
+  "x": 0,
+  "y": 0,
+}
+`;
+
+exports[`move element > rectangles with binding arrow 6`] = `
+{
+  "angle": 0,
+  "backgroundColor": "transparent",
+  "boundElements": [
+    {
+      "id": "id2",
+      "type": "arrow",
+    },
+  ],
+  "customData": undefined,
+  "fillStyle": "solid",
+  "frameId": null,
+  "groupIds": [],
+  "height": 300,
+  "id": "id1",
+  "index": "a1",
+  "isDeleted": false,
+  "link": null,
+  "locked": false,
+  "opacity": 100,
+  "roughness": 1,
+  "roundness": {
+    "type": 3,
+  },
+  "seed": 1150084233,
+  "strokeColor": "#1e1e1e",
+  "strokeStyle": "solid",
+  "strokeWidth": 2,
+  "type": "rectangle",
+  "updated": 1,
+  "version": 7,
+  "versionNonce": 745419401,
+  "width": 300,
+  "x": 201,
+  "y": 2,
+}
+`;
+
+exports[`move element > rectangles with binding arrow 7`] = `
+{
+  "angle": 0,
+  "backgroundColor": "transparent",
+  "boundElements": null,
+  "customData": undefined,
+  "elbowed": false,
+  "endArrowhead": "arrow",
+  "endBinding": {
+    "elementId": "id1",
+    "focus": "-0.40764",
+    "gap": 5,
+  },
+  "fillStyle": "solid",
+  "frameId": null,
+  "groupIds": [],
+  "height": "82.18136",
+  "id": "id2",
+  "index": "a2",
+  "isDeleted": false,
+  "lastCommittedPoint": null,
+  "link": null,
+  "locked": false,
+  "opacity": 100,
+  "points": [
+    [
+      0,
+      0,
+    ],
+    [
+      "93.92893",
+      "82.18136",
+    ],
+  ],
+  "roughness": 1,
+  "roundness": {
+    "type": 2,
+  },
+  "seed": 1604849351,
+  "startArrowhead": null,
+  "startBinding": {
+    "elementId": "id0",
+    "focus": "-0.49801",
+    "gap": 5,
+  },
+  "strokeColor": "#1e1e1e",
+  "strokeStyle": "solid",
+  "strokeWidth": 2,
+  "type": "arrow",
+  "updated": 1,
+  "version": 11,
+  "versionNonce": 1051383431,
+  "width": "93.92893",
+  "x": "103.53553",
+  "y": "50.01536",
+}
+`;

+ 9 - 4
packages/excalidraw/tests/move.test.tsx

@@ -109,8 +109,10 @@ describe("move element", () => {
     expect(h.state.selectedElementIds[rectB.id]).toBeTruthy();
     expect([rectA.x, rectA.y]).toEqual([0, 0]);
     expect([rectB.x, rectB.y]).toEqual([200, 0]);
-    expect([arrow.x, arrow.y]).toEqual([110, 50]);
-    expect([arrow.width, arrow.height]).toEqual([80, 80]);
+    expect([Math.round(arrow.x), Math.round(arrow.y)]).toEqual([104, 50]);
+    expect([Math.round(arrow.width), Math.round(arrow.height)]).toEqual([
+      93, 81,
+    ]);
 
     renderInteractiveScene.mockClear();
     renderStaticScene.mockClear();
@@ -128,8 +130,11 @@ describe("move element", () => {
     expect(h.state.selectedElementIds[rectB.id]).toBeTruthy();
     expect([rectA.x, rectA.y]).toEqual([0, 0]);
     expect([rectB.x, rectB.y]).toEqual([201, 2]);
-    expect([[arrow.x, arrow.y]]).toCloselyEqualPoints([[107.07, 47.07]]);
-    expect([[arrow.width, arrow.height]]).toCloselyEqualPoints([[86.86, 87.3]]);
+
+    expect([[arrow.x, arrow.y]]).toCloselyEqualPoints([[103.53, 50.01]]);
+    expect([[arrow.width, arrow.height]]).toCloselyEqualPoints([
+      [93.9289, 82.1813],
+    ]);
 
     h.elements.forEach((element) => expect(element).toMatchSnapshot());
   });

+ 9 - 5
packages/excalidraw/tests/rotate.test.tsx

@@ -3,6 +3,8 @@ import { expect } from "vitest";
 
 import { reseed } from "@excalidraw/common";
 
+import "@excalidraw/utils/test-utils";
+
 import { Excalidraw } from "../index";
 
 import { UI } from "./helpers/ui";
@@ -71,14 +73,16 @@ test("unselected bound arrows update when rotating their target elements", async
   expect(ellipseArrow.endBinding?.elementId).toEqual(ellipse.id);
   expect(ellipseArrow.x).toEqual(0);
   expect(ellipseArrow.y).toEqual(0);
-  expect(ellipseArrow.points[0]).toEqual([0, 0]);
-  expect(ellipseArrow.points[1][0]).toBeCloseTo(48.98, 1);
-  expect(ellipseArrow.points[1][1]).toBeCloseTo(125.79, 1);
+
+  expect(ellipseArrow.points).toCloselyEqualPoints([
+    [0, 0],
+    [90.1827, 98.5896],
+  ]);
 
   expect(textArrow.endBinding?.elementId).toEqual(text.id);
   expect(textArrow.x).toEqual(360);
   expect(textArrow.y).toEqual(300);
   expect(textArrow.points[0]).toEqual([0, 0]);
-  expect(textArrow.points[1][0]).toBeCloseTo(-94, 0);
-  expect(textArrow.points[1][1]).toBeCloseTo(-116.1, 0);
+  expect(textArrow.points[1][0]).toBeCloseTo(-95, 0);
+  expect(textArrow.points[1][1]).toBeCloseTo(-129.1, 0);
 });