diff options
| author | kj_sh604 | 2026-03-15 16:19:35 -0400 |
|---|---|---|
| committer | kj_sh604 | 2026-03-15 16:19:35 -0400 |
| commit | 6ec259a0e71174651bae95d4628138bf6fd68742 (patch) | |
| tree | 5e33c6a5ec091ecabfcb257fdc7b6a88ed8754ac /packages/excalidraw/element/elbowArrow.test.tsx | |
| parent | 16c8578b15c727f22921f8a80a56ee4d4e7f2272 (diff) | |
refactor: packages/
Diffstat (limited to 'packages/excalidraw/element/elbowArrow.test.tsx')
| -rw-r--r-- | packages/excalidraw/element/elbowArrow.test.tsx | 408 |
1 files changed, 408 insertions, 0 deletions
diff --git a/packages/excalidraw/element/elbowArrow.test.tsx b/packages/excalidraw/element/elbowArrow.test.tsx new file mode 100644 index 0000000..c00eae9 --- /dev/null +++ b/packages/excalidraw/element/elbowArrow.test.tsx @@ -0,0 +1,408 @@ +import React from "react"; +import Scene from "../scene/Scene"; +import { API } from "../tests/helpers/api"; +import { Pointer, UI } from "../tests/helpers/ui"; +import { + act, + fireEvent, + GlobalTestState, + queryByTestId, + render, +} from "../tests/test-utils"; +import { bindLinearElement } from "./binding"; +import { Excalidraw, mutateElement } from "../index"; +import type { + ExcalidrawArrowElement, + ExcalidrawBindableElement, + ExcalidrawElbowArrowElement, +} from "./types"; +import { ARROW_TYPE } from "../constants"; +import "../../utils/test-utils"; +import type { LocalPoint } from "@excalidraw/math"; +import { pointFrom } from "@excalidraw/math"; +import { actionDuplicateSelection } from "../actions/actionDuplicateSelection"; +import { actionSelectAll } from "../actions"; + +const { h } = window; + +const mouse = new Pointer("mouse"); + +describe("elbow arrow segment move", () => { + beforeEach(async () => { + localStorage.clear(); + await render(<Excalidraw handleKeyboardGlobally={true} />); + }); + + it("can move the second segment of a fully connected elbow arrow", () => { + UI.createElement("rectangle", { + x: -100, + y: -50, + width: 100, + height: 100, + }); + UI.createElement("rectangle", { + x: 200, + y: 150, + width: 100, + height: 100, + }); + + UI.clickTool("arrow"); + UI.clickOnTestId("elbow-arrow"); + + mouse.reset(); + mouse.moveTo(0, 0); + mouse.click(); + mouse.moveTo(200, 200); + mouse.click(); + + mouse.reset(); + mouse.moveTo(100, 100); + mouse.down(); + mouse.moveTo(115, 100); + mouse.up(); + + const arrow = h.scene.getSelectedElements( + h.state, + )[0] as ExcalidrawElbowArrowElement; + + expect(h.state.selectedElementIds).toEqual({ [arrow.id]: true }); + expect(arrow.fixedSegments?.length).toBe(1); + + expect(arrow.points).toCloselyEqualPoints([ + [0, 0], + [110, 0], + [110, 200], + [190, 200], + ]); + + mouse.reset(); + mouse.moveTo(105, 74.275); + mouse.doubleClick(); + + expect(arrow.points).toCloselyEqualPoints([ + [0, 0], + [110, 0], + [110, 200], + [190, 200], + ]); + }); + + it("can move the second segment of an unconnected elbow arrow", () => { + UI.clickTool("arrow"); + UI.clickOnTestId("elbow-arrow"); + + mouse.reset(); + mouse.moveTo(0, 0); + mouse.click(); + mouse.moveTo(250, 200); + mouse.click(); + + mouse.reset(); + mouse.moveTo(125, 100); + mouse.down(); + mouse.moveTo(130, 100); + mouse.up(); + + const arrow = h.scene.getSelectedElements( + h.state, + )[0] as ExcalidrawArrowElement; + + expect(arrow.points).toCloselyEqualPoints([ + [0, 0], + [130, 0], + [130, 200], + [250, 200], + ]); + + mouse.reset(); + mouse.moveTo(130, 100); + mouse.doubleClick(); + + expect(arrow.points).toCloselyEqualPoints([ + [0, 0], + [125, 0], + [125, 200], + [250, 200], + ]); + }); +}); + +describe("elbow arrow routing", () => { + it("can properly generate orthogonal arrow points", () => { + const scene = new Scene(); + const arrow = API.createElement({ + type: "arrow", + elbowed: true, + }) as ExcalidrawElbowArrowElement; + scene.insertElement(arrow); + mutateElement(arrow, { + points: [ + pointFrom<LocalPoint>(-45 - arrow.x, -100.1 - arrow.y), + pointFrom<LocalPoint>(45 - arrow.x, 99.9 - arrow.y), + ], + }); + expect(arrow.points).toEqual([ + [0, 0], + [0, 100], + [90, 100], + [90, 200], + ]); + expect(arrow.x).toEqual(-45); + expect(arrow.y).toEqual(-100.1); + expect(arrow.width).toEqual(90); + expect(arrow.height).toEqual(200); + }); + it("can generate proper points for bound elbow arrow", () => { + const scene = new Scene(); + const rectangle1 = API.createElement({ + type: "rectangle", + x: -150, + y: -150, + width: 100, + height: 100, + }) as ExcalidrawBindableElement; + const rectangle2 = API.createElement({ + type: "rectangle", + x: 50, + y: 50, + width: 100, + height: 100, + }) as ExcalidrawBindableElement; + const arrow = API.createElement({ + type: "arrow", + elbowed: true, + x: -45, + y: -100.1, + width: 90, + height: 200, + points: [pointFrom(0, 0), pointFrom(90, 200)], + }) as ExcalidrawElbowArrowElement; + scene.insertElement(rectangle1); + scene.insertElement(rectangle2); + scene.insertElement(arrow); + const elementsMap = scene.getNonDeletedElementsMap(); + bindLinearElement(arrow, rectangle1, "start", elementsMap); + bindLinearElement(arrow, rectangle2, "end", elementsMap); + + expect(arrow.startBinding).not.toBe(null); + expect(arrow.endBinding).not.toBe(null); + + mutateElement(arrow, { + points: [pointFrom<LocalPoint>(0, 0), pointFrom<LocalPoint>(90, 200)], + }); + + expect(arrow.points).toEqual([ + [0, 0], + [45, 0], + [45, 200], + [90, 200], + ]); + }); +}); + +describe("elbow arrow ui", () => { + beforeEach(async () => { + localStorage.clear(); + await render(<Excalidraw handleKeyboardGlobally={true} />); + + fireEvent.contextMenu(GlobalTestState.interactiveCanvas, { + button: 2, + clientX: 1, + clientY: 1, + }); + const contextMenu = UI.queryContextMenu(); + fireEvent.click(queryByTestId(contextMenu!, "stats")!); + }); + + it("can follow bound shapes", 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"); + + expect(h.state.currentItemArrowType).toBe(ARROW_TYPE.elbow); + + mouse.reset(); + mouse.moveTo(-43, -99); + mouse.click(); + mouse.moveTo(43, 99); + mouse.click(); + + const arrow = h.scene.getSelectedElements( + h.state, + )[0] as ExcalidrawArrowElement; + + expect(arrow.type).toBe("arrow"); + expect(arrow.elbowed).toBe(true); + expect(arrow.points).toEqual([ + [0, 0], + [45, 0], + [45, 200], + [90, 200], + ]); + }); + + it("can follow bound rotated shapes", 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; + + mouse.click(51, 51); + + const inputAngle = UI.queryStatsProperty("A")?.querySelector( + ".drag-input", + ) as HTMLInputElement; + UI.updateInput(inputAngle, String("40")); + + expect(arrow.points.map((point) => point.map(Math.round))).toEqual([ + [0, 0], + [35, 0], + [35, 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("keeps arrow shape when only the bound 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], + [45, 0], + [45, 200], + [90, 200], + ]); + }); +}); |
