Fix tests

This commit is contained in:
Mark Tolmacs 2025-03-28 18:17:39 +01:00
parent ea5ad1412c
commit e90350b7d1
9 changed files with 432 additions and 258 deletions

View file

@ -18,7 +18,9 @@ const mouse = new Pointer("mouse");
describe("element binding", () => { describe("element binding", () => {
beforeEach(async () => { beforeEach(async () => {
localStorage.clear();
await render(<Excalidraw handleKeyboardGlobally={true} />); await render(<Excalidraw handleKeyboardGlobally={true} />);
mouse.reset();
}); });
it("should create valid binding if duplicate start/end points", async () => { it("should create valid binding if duplicate start/end points", async () => {
@ -89,8 +91,16 @@ describe("element binding", () => {
}); });
}); });
//@TODO fix the test with rotation // UX RATIONALE: We are not aware of any use-case where the user would want to
it.skip("rotation of arrow should rebind both ends", () => { // 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", { const rectLeft = UI.createElement("rectangle", {
x: 0, x: 0,
width: 200, width: 200,
@ -123,12 +133,13 @@ describe("element binding", () => {
mouse.up(); mouse.up();
expect(arrow.angle).toBeGreaterThan(0.7 * Math.PI); expect(arrow.angle).toBeGreaterThan(0.7 * Math.PI);
expect(arrow.angle).toBeLessThan(1.3 * Math.PI); expect(arrow.angle).toBeLessThan(1.3 * Math.PI);
expect(arrow.startBinding?.elementId).toBe(rectRight.id); expect(arrow.startBinding).toBe(null);
expect(arrow.endBinding?.elementId).toBe(rectLeft.id); expect(arrow.endBinding).toBe(null);
}); },
);
// TODO fix & reenable once we rewrite tests to work with concurrency // 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 arrow and moving its head to bind it to element A, finalizing the" +
"editing by clicking on element A should end up selecting A", "editing by clicking on element A should end up selecting A",
async () => { async () => {
@ -142,7 +153,10 @@ describe("element binding", () => {
mouse.up(0, 80); mouse.up(0, 80);
// Edit arrow with multi-point // Edit arrow with multi-point
Keyboard.withModifierKeys({ ctrl: true }, () => {
mouse.doubleClick(); mouse.doubleClick();
});
// move arrow head // move arrow head
mouse.down(); mouse.down();
mouse.up(0, 10); 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 // the issue, due to https://github.com/excalidraw/excalidraw/blob/46bff3daceb602accf60c40a84610797260fca94/src/components/App.tsx#L740
mouse.reset(); mouse.reset();
expect(h.state.editingLinearElement).not.toBe(null); expect(h.state.editingLinearElement).not.toBe(null);
mouse.down(0, 0); mouse.click();
await new Promise((r) => setTimeout(r, 100));
expect(h.state.editingLinearElement).toBe(null);
expect(API.getSelectedElement().type).toBe("rectangle");
mouse.up();
expect(API.getSelectedElement().type).toBe("rectangle"); expect(API.getSelectedElement().type).toBe("rectangle");
}, },
); );
@ -187,23 +197,24 @@ describe("element binding", () => {
expect(arrow.endBinding?.elementId).toBe(rectangle.id); expect(arrow.endBinding?.elementId).toBe(rectangle.id);
Keyboard.keyPress(KEYS.ARROW_LEFT); Keyboard.keyPress(KEYS.ARROW_LEFT);
expect(arrow.endBinding?.elementId).toBe(rectangle.id); expect(arrow.endBinding?.elementId).toBe(rectangle.id);
expect(API.getSelectedElement().type).toBe("arrow");
// Sever connection // Sever connection
expect(API.getSelectedElement().type).toBe("arrow");
Keyboard.withModifierKeys({ shift: true }, () => { Keyboard.withModifierKeys({ shift: true }, () => {
// We have to move a significant distance to get out of the binding zone // We have to move a significant distance to get out of the binding zone
Keyboard.keyPress(KEYS.ARROW_LEFT); Array.from({ length: 10 }).forEach(() => {
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);
}); });
});
expect(arrow.endBinding).toBe(null); expect(arrow.endBinding).toBe(null);
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); Keyboard.keyPress(KEYS.ARROW_RIGHT);
});
});
// We are back in the binding zone but we shouldn't rebind
expect(arrow.endBinding).toBe(null); expect(arrow.endBinding).toBe(null);
}); });

View file

@ -10,11 +10,14 @@ import {
import { Excalidraw } from "@excalidraw/excalidraw"; 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 { 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 { import {
act, act,
@ -28,7 +31,10 @@ import type { LocalPoint } from "@excalidraw/math";
import { mutateElement } from "../src/mutateElement"; import { mutateElement } from "../src/mutateElement";
import { duplicateElement, duplicateElements } from "../src/duplicate"; import { duplicateElement, duplicateElements } from "../src/duplicate";
import type { ExcalidrawLinearElement } from "../src/types"; import type {
ExcalidrawArrowElement,
ExcalidrawLinearElement,
} from "../src/types";
const { h } = window; const { h } = window;
const mouse = new Pointer("mouse"); 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", () => { describe("duplication z-order", () => {
beforeEach(async () => { beforeEach(async () => {
await render(<Excalidraw />); await render(<Excalidraw />);

View file

@ -3,14 +3,11 @@ import { pointFrom } from "@excalidraw/math";
import { Excalidraw, mutateElement } from "@excalidraw/excalidraw"; import { Excalidraw, mutateElement } from "@excalidraw/excalidraw";
import Scene from "@excalidraw/excalidraw/scene/Scene"; 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 { API } from "@excalidraw/excalidraw/tests/helpers/api";
import { Pointer, UI } from "@excalidraw/excalidraw/tests/helpers/ui"; import { Pointer, UI } from "@excalidraw/excalidraw/tests/helpers/ui";
import { import {
act,
fireEvent, fireEvent,
GlobalTestState, GlobalTestState,
queryByTestId, queryByTestId,
@ -301,114 +298,4 @@ describe("elbow arrow ui", () => {
[103, 165], [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],
]);
});
}); });

View file

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

View file

@ -87,6 +87,16 @@ describe("flipping arrowheads", () => {
await render(<Excalidraw />); 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", () => { it("flipping bound arrow should flip arrowheads only", () => {
const rect = API.createElement({ const rect = API.createElement({
type: "rectangle", type: "rectangle",
@ -123,6 +133,7 @@ describe("flipping arrowheads", () => {
expect(API.getElement(arrow).endArrowhead).toBe("arrow"); expect(API.getElement(arrow).endArrowhead).toBe("arrow");
}); });
// UX RATIONALE: See above for the reasoning.
it("flipping bound arrow should flip arrowheads only 2", () => { it("flipping bound arrow should flip arrowheads only 2", () => {
const rect = API.createElement({ const rect = API.createElement({
type: "rectangle", type: "rectangle",
@ -164,7 +175,9 @@ describe("flipping arrowheads", () => {
expect(API.getElement(arrow).endArrowhead).toBe("circle"); 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({ const arrow = API.createElement({
type: "arrow", type: "arrow",
id: "arrow1", id: "arrow1",

View file

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

View file

@ -101,3 +101,139 @@ exports[`move element > rectangle 5`] = `
"y": 40, "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",
}
`;

View file

@ -109,8 +109,10 @@ describe("move element", () => {
expect(h.state.selectedElementIds[rectB.id]).toBeTruthy(); expect(h.state.selectedElementIds[rectB.id]).toBeTruthy();
expect([rectA.x, rectA.y]).toEqual([0, 0]); expect([rectA.x, rectA.y]).toEqual([0, 0]);
expect([rectB.x, rectB.y]).toEqual([200, 0]); expect([rectB.x, rectB.y]).toEqual([200, 0]);
expect([arrow.x, arrow.y]).toEqual([110, 50]); expect([Math.round(arrow.x), Math.round(arrow.y)]).toEqual([104, 50]);
expect([arrow.width, arrow.height]).toEqual([80, 80]); expect([Math.round(arrow.width), Math.round(arrow.height)]).toEqual([
93, 81,
]);
renderInteractiveScene.mockClear(); renderInteractiveScene.mockClear();
renderStaticScene.mockClear(); renderStaticScene.mockClear();
@ -128,8 +130,11 @@ describe("move element", () => {
expect(h.state.selectedElementIds[rectB.id]).toBeTruthy(); expect(h.state.selectedElementIds[rectB.id]).toBeTruthy();
expect([rectA.x, rectA.y]).toEqual([0, 0]); expect([rectA.x, rectA.y]).toEqual([0, 0]);
expect([rectB.x, rectB.y]).toEqual([201, 2]); 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()); h.elements.forEach((element) => expect(element).toMatchSnapshot());
}); });

View file

@ -3,6 +3,8 @@ import { expect } from "vitest";
import { reseed } from "@excalidraw/common"; import { reseed } from "@excalidraw/common";
import "@excalidraw/utils/test-utils";
import { Excalidraw } from "../index"; import { Excalidraw } from "../index";
import { UI } from "./helpers/ui"; 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.endBinding?.elementId).toEqual(ellipse.id);
expect(ellipseArrow.x).toEqual(0); expect(ellipseArrow.x).toEqual(0);
expect(ellipseArrow.y).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).toCloselyEqualPoints([
expect(ellipseArrow.points[1][1]).toBeCloseTo(125.79, 1); [0, 0],
[90.1827, 98.5896],
]);
expect(textArrow.endBinding?.elementId).toEqual(text.id); expect(textArrow.endBinding?.elementId).toEqual(text.id);
expect(textArrow.x).toEqual(360); expect(textArrow.x).toEqual(360);
expect(textArrow.y).toEqual(300); expect(textArrow.y).toEqual(300);
expect(textArrow.points[0]).toEqual([0, 0]); expect(textArrow.points[0]).toEqual([0, 0]);
expect(textArrow.points[1][0]).toBeCloseTo(-94, 0); expect(textArrow.points[1][0]).toBeCloseTo(-95, 0);
expect(textArrow.points[1][1]).toBeCloseTo(-116.1, 0); expect(textArrow.points[1][1]).toBeCloseTo(-129.1, 0);
}); });