linearElementEditor.test.tsx 42 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415
  1. import { pointCenter, pointFrom } from "@excalidraw/math";
  2. import { act, queryByTestId, queryByText } from "@testing-library/react";
  3. import React from "react";
  4. import { vi } from "vitest";
  5. import {
  6. ROUNDNESS,
  7. VERTICAL_ALIGN,
  8. KEYS,
  9. reseed,
  10. arrayToMap,
  11. } from "@excalidraw/common";
  12. import { Excalidraw } from "@excalidraw/excalidraw";
  13. import * as InteractiveCanvas from "@excalidraw/excalidraw/renderer/interactiveScene";
  14. import * as StaticScene from "@excalidraw/excalidraw/renderer/staticScene";
  15. import { API } from "@excalidraw/excalidraw/tests/helpers/api";
  16. import { Keyboard, Pointer, UI } from "@excalidraw/excalidraw/tests/helpers/ui";
  17. import {
  18. screen,
  19. render,
  20. fireEvent,
  21. GlobalTestState,
  22. unmountComponent,
  23. } from "@excalidraw/excalidraw/tests/test-utils";
  24. import type { GlobalPoint, LocalPoint } from "@excalidraw/math";
  25. import { wrapText } from "../src";
  26. import * as textElementUtils from "../src/textElement";
  27. import { getBoundTextElementPosition, getBoundTextMaxWidth } from "../src";
  28. import { LinearElementEditor } from "../src";
  29. import { newArrowElement } from "../src";
  30. import type {
  31. ExcalidrawElement,
  32. ExcalidrawLinearElement,
  33. ExcalidrawTextElementWithContainer,
  34. FontString,
  35. } from "../src/types";
  36. const renderInteractiveScene = vi.spyOn(
  37. InteractiveCanvas,
  38. "renderInteractiveScene",
  39. );
  40. const renderStaticScene = vi.spyOn(StaticScene, "renderStaticScene");
  41. const { h } = window;
  42. const font = "20px Cascadia, width: Segoe UI Emoji" as FontString;
  43. describe("Test Linear Elements", () => {
  44. let container: HTMLElement;
  45. let interactiveCanvas: HTMLCanvasElement;
  46. beforeEach(async () => {
  47. unmountComponent();
  48. localStorage.clear();
  49. renderInteractiveScene.mockClear();
  50. renderStaticScene.mockClear();
  51. reseed(7);
  52. const comp = await render(<Excalidraw handleKeyboardGlobally={true} />);
  53. h.state.width = 1000;
  54. h.state.height = 1000;
  55. container = comp.container;
  56. interactiveCanvas = container.querySelector("canvas.interactive")!;
  57. });
  58. const p1 = pointFrom<GlobalPoint>(20, 20);
  59. const p2 = pointFrom<GlobalPoint>(60, 20);
  60. const midpoint = pointCenter<GlobalPoint>(p1, p2);
  61. const delta = 50;
  62. const mouse = new Pointer("mouse");
  63. const createTwoPointerLinearElement = (
  64. type: ExcalidrawLinearElement["type"],
  65. roundness: ExcalidrawElement["roundness"] = null,
  66. roughness: ExcalidrawLinearElement["roughness"] = 0,
  67. ) => {
  68. const line = API.createElement({
  69. x: p1[0],
  70. y: p1[1],
  71. width: p2[0] - p1[0],
  72. height: 0,
  73. type,
  74. roughness,
  75. points: [pointFrom(0, 0), pointFrom(p2[0] - p1[0], p2[1] - p1[1])],
  76. roundness,
  77. });
  78. API.setElements([line]);
  79. mouse.clickAt(p1[0], p1[1]);
  80. return line;
  81. };
  82. const createThreePointerLinearElement = (
  83. type: ExcalidrawLinearElement["type"],
  84. roundness: ExcalidrawElement["roundness"] = null,
  85. roughness: ExcalidrawLinearElement["roughness"] = 0,
  86. ) => {
  87. //dragging line from midpoint
  88. const p3 = [midpoint[0] + delta - p1[0], midpoint[1] + delta - p1[1]];
  89. const line = API.createElement({
  90. x: p1[0],
  91. y: p1[1],
  92. width: p3[0] - p1[0],
  93. height: 0,
  94. type,
  95. roughness,
  96. points: [
  97. pointFrom(0, 0),
  98. pointFrom(p3[0], p3[1]),
  99. pointFrom(p2[0] - p1[0], p2[1] - p1[1]),
  100. ],
  101. roundness,
  102. });
  103. h.app.scene.mutateElement(line, { points: line.points });
  104. API.setElements([line]);
  105. mouse.clickAt(p1[0], p1[1]);
  106. return line;
  107. };
  108. const enterLineEditingMode = (
  109. line: ExcalidrawLinearElement,
  110. selectProgrammatically = false,
  111. ) => {
  112. if (selectProgrammatically) {
  113. API.setSelectedElements([line]);
  114. } else {
  115. mouse.clickAt(p1[0], p1[1]);
  116. }
  117. Keyboard.withModifierKeys({ ctrl: true }, () => {
  118. Keyboard.keyPress(KEYS.ENTER);
  119. });
  120. expect(h.state.editingLinearElement?.elementId).toEqual(line.id);
  121. };
  122. const drag = (startPoint: GlobalPoint, endPoint: GlobalPoint) => {
  123. fireEvent.pointerDown(interactiveCanvas, {
  124. clientX: startPoint[0],
  125. clientY: startPoint[1],
  126. });
  127. fireEvent.pointerMove(interactiveCanvas, {
  128. clientX: endPoint[0],
  129. clientY: endPoint[1],
  130. });
  131. fireEvent.pointerUp(interactiveCanvas, {
  132. clientX: endPoint[0],
  133. clientY: endPoint[1],
  134. });
  135. };
  136. const deletePoint = (point: GlobalPoint) => {
  137. fireEvent.pointerDown(interactiveCanvas, {
  138. clientX: point[0],
  139. clientY: point[1],
  140. });
  141. fireEvent.pointerUp(interactiveCanvas, {
  142. clientX: point[0],
  143. clientY: point[1],
  144. });
  145. Keyboard.keyPress(KEYS.DELETE);
  146. };
  147. it("should normalize the element points at creation", () => {
  148. const element = newArrowElement({
  149. type: "arrow",
  150. points: [pointFrom<LocalPoint>(0.5, 0), pointFrom<LocalPoint>(100, 100)],
  151. x: 0,
  152. y: 0,
  153. });
  154. expect(element.points).toEqual([
  155. pointFrom<LocalPoint>(0.5, 0),
  156. pointFrom<LocalPoint>(100, 100),
  157. ]);
  158. new LinearElementEditor(element, arrayToMap(h.elements));
  159. expect(element.points).toEqual([
  160. pointFrom<LocalPoint>(0, 0),
  161. pointFrom<LocalPoint>(99.5, 100),
  162. ]);
  163. });
  164. it("should not drag line and add midpoint until dragged beyond a threshold", () => {
  165. createTwoPointerLinearElement("line");
  166. const line = h.elements[0] as ExcalidrawLinearElement;
  167. const originalX = line.x;
  168. const originalY = line.y;
  169. expect(line.points.length).toEqual(2);
  170. mouse.clickAt(midpoint[0], midpoint[1]);
  171. drag(midpoint, pointFrom(midpoint[0] + 1, midpoint[1] + 1));
  172. expect(line.points.length).toEqual(2);
  173. expect(line.x).toBe(originalX);
  174. expect(line.y).toBe(originalY);
  175. expect(line.points.length).toEqual(2);
  176. drag(midpoint, pointFrom(midpoint[0] + delta, midpoint[1] + delta));
  177. expect(line.x).toBe(originalX);
  178. expect(line.y).toBe(originalY);
  179. expect(line.points.length).toEqual(3);
  180. });
  181. it("should allow dragging line from midpoint in 2 pointer lines outside editor", async () => {
  182. createTwoPointerLinearElement("line");
  183. const line = h.elements[0] as ExcalidrawLinearElement;
  184. expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(`5`);
  185. expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`5`);
  186. expect((h.elements[0] as ExcalidrawLinearElement).points.length).toEqual(2);
  187. // drag line from midpoint
  188. drag(midpoint, pointFrom(midpoint[0] + delta, midpoint[1] + delta));
  189. expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(`9`);
  190. expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`7`);
  191. expect(line.points.length).toEqual(3);
  192. expect(line.points).toMatchInlineSnapshot(`
  193. [
  194. [
  195. 0,
  196. 0,
  197. ],
  198. [
  199. 70,
  200. 50,
  201. ],
  202. [
  203. 40,
  204. 0,
  205. ],
  206. ]
  207. `);
  208. });
  209. it("should allow entering and exiting line editor via context menu", () => {
  210. createTwoPointerLinearElement("line");
  211. fireEvent.contextMenu(GlobalTestState.interactiveCanvas, {
  212. button: 2,
  213. clientX: midpoint[0],
  214. clientY: midpoint[1],
  215. });
  216. // Enter line editor
  217. const contextMenu = document.querySelector(".context-menu");
  218. fireEvent.contextMenu(GlobalTestState.interactiveCanvas, {
  219. button: 2,
  220. clientX: midpoint[0],
  221. clientY: midpoint[1],
  222. });
  223. fireEvent.click(queryByText(contextMenu as HTMLElement, "Edit line")!);
  224. expect(h.state.editingLinearElement?.elementId).toEqual(h.elements[0].id);
  225. });
  226. it("should enter line editor when using double clicked with ctrl key", () => {
  227. createTwoPointerLinearElement("line");
  228. expect(h.state.editingLinearElement?.elementId).toBeUndefined();
  229. Keyboard.withModifierKeys({ ctrl: true }, () => {
  230. mouse.doubleClick();
  231. });
  232. expect(h.state.editingLinearElement?.elementId).toEqual(h.elements[0].id);
  233. });
  234. describe("Inside editor", () => {
  235. it("should not drag line and add midpoint when dragged irrespective of threshold", () => {
  236. createTwoPointerLinearElement("line");
  237. const line = h.elements[0] as ExcalidrawLinearElement;
  238. const originalX = line.x;
  239. const originalY = line.y;
  240. enterLineEditingMode(line);
  241. expect(line.points.length).toEqual(2);
  242. mouse.clickAt(midpoint[0], midpoint[1]);
  243. expect(line.points.length).toEqual(2);
  244. drag(midpoint, pointFrom(midpoint[0] + 1, midpoint[1] + 1));
  245. expect(line.x).toBe(originalX);
  246. expect(line.y).toBe(originalY);
  247. expect(line.points.length).toEqual(3);
  248. });
  249. it("should allow dragging line from midpoint in 2 pointer lines", async () => {
  250. createTwoPointerLinearElement("line");
  251. const line = h.elements[0] as ExcalidrawLinearElement;
  252. enterLineEditingMode(line);
  253. // drag line from midpoint
  254. drag(midpoint, pointFrom(midpoint[0] + delta, midpoint[1] + delta));
  255. expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
  256. `12`,
  257. );
  258. expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`6`);
  259. expect(line.points.length).toEqual(3);
  260. expect(line.points).toMatchInlineSnapshot(`
  261. [
  262. [
  263. 0,
  264. 0,
  265. ],
  266. [
  267. 70,
  268. 50,
  269. ],
  270. [
  271. 40,
  272. 0,
  273. ],
  274. ]
  275. `);
  276. });
  277. it("should update the midpoints when element roundness changed", async () => {
  278. createThreePointerLinearElement("line");
  279. const line = h.elements[0] as ExcalidrawLinearElement;
  280. expect(line.points.length).toEqual(3);
  281. enterLineEditingMode(line);
  282. const midPointsWithSharpEdge = LinearElementEditor.getEditorMidPoints(
  283. line,
  284. h.app.scene.getNonDeletedElementsMap(),
  285. h.state,
  286. );
  287. // update roundness
  288. fireEvent.click(screen.getByTitle("Round"));
  289. expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
  290. `9`,
  291. );
  292. expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`6`);
  293. const midPointsWithRoundEdge = LinearElementEditor.getEditorMidPoints(
  294. h.elements[0] as ExcalidrawLinearElement,
  295. h.app.scene.getNonDeletedElementsMap(),
  296. h.state,
  297. );
  298. expect(midPointsWithRoundEdge[0]).not.toEqual(midPointsWithSharpEdge[0]);
  299. expect(midPointsWithRoundEdge[1]).not.toEqual(midPointsWithSharpEdge[1]);
  300. expect(midPointsWithRoundEdge).toMatchInlineSnapshot(`
  301. [
  302. [
  303. "55.96978",
  304. "47.44233",
  305. ],
  306. [
  307. "76.08587",
  308. "43.29417",
  309. ],
  310. ]
  311. `);
  312. });
  313. it("should update all the midpoints when element position changed", async () => {
  314. const elementsMap = arrayToMap(h.elements);
  315. createThreePointerLinearElement("line", {
  316. type: ROUNDNESS.PROPORTIONAL_RADIUS,
  317. });
  318. const line = h.elements[0] as ExcalidrawLinearElement;
  319. expect(line.points.length).toEqual(3);
  320. enterLineEditingMode(line);
  321. const points = LinearElementEditor.getPointsGlobalCoordinates(
  322. line,
  323. elementsMap,
  324. );
  325. expect([line.x, line.y]).toEqual(points[0]);
  326. const midPoints = LinearElementEditor.getEditorMidPoints(
  327. line,
  328. h.app.scene.getNonDeletedElementsMap(),
  329. h.state,
  330. );
  331. const startPoint = pointCenter(points[0], midPoints[0]!);
  332. const deltaX = 50;
  333. const deltaY = 20;
  334. const endPoint = pointFrom<GlobalPoint>(
  335. startPoint[0] + deltaX,
  336. startPoint[1] + deltaY,
  337. );
  338. // Move the element
  339. drag(startPoint, endPoint);
  340. expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
  341. `12`,
  342. );
  343. expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`7`);
  344. expect([line.x, line.y]).toEqual([
  345. points[0][0] + deltaX,
  346. points[0][1] + deltaY,
  347. ]);
  348. const newMidPoints = LinearElementEditor.getEditorMidPoints(
  349. line,
  350. h.app.scene.getNonDeletedElementsMap(),
  351. h.state,
  352. );
  353. expect(midPoints[0]).not.toEqual(newMidPoints[0]);
  354. expect(midPoints[1]).not.toEqual(newMidPoints[1]);
  355. expect(newMidPoints).toMatchInlineSnapshot(`
  356. [
  357. [
  358. "105.96978",
  359. "67.44233",
  360. ],
  361. [
  362. "126.08587",
  363. "63.29417",
  364. ],
  365. ]
  366. `);
  367. });
  368. describe("When edges are round", () => {
  369. // This is the expected midpoint for line with round edge
  370. // hence hardcoding it so if later some bug is introduced
  371. // this will fail and we can fix it
  372. const firstSegmentMidpoint = pointFrom<GlobalPoint>(55, 45);
  373. const lastSegmentMidpoint = pointFrom<GlobalPoint>(75, 40);
  374. let line: ExcalidrawLinearElement;
  375. beforeEach(() => {
  376. line = createThreePointerLinearElement("line");
  377. expect(line.points.length).toEqual(3);
  378. enterLineEditingMode(line);
  379. });
  380. it("should allow dragging lines from midpoints in between segments", async () => {
  381. // drag line via first segment midpoint
  382. drag(
  383. firstSegmentMidpoint,
  384. pointFrom(
  385. firstSegmentMidpoint[0] + delta,
  386. firstSegmentMidpoint[1] + delta,
  387. ),
  388. );
  389. expect(line.points.length).toEqual(4);
  390. // drag line from last segment midpoint
  391. drag(
  392. lastSegmentMidpoint,
  393. pointFrom(
  394. lastSegmentMidpoint[0] + delta,
  395. lastSegmentMidpoint[1] + delta,
  396. ),
  397. );
  398. expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
  399. `16`,
  400. );
  401. expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`7`);
  402. expect(line.points.length).toEqual(5);
  403. expect((h.elements[0] as ExcalidrawLinearElement).points)
  404. .toMatchInlineSnapshot(`
  405. [
  406. [
  407. 0,
  408. 0,
  409. ],
  410. [
  411. 85,
  412. 75,
  413. ],
  414. [
  415. 70,
  416. 50,
  417. ],
  418. [
  419. 105,
  420. 70,
  421. ],
  422. [
  423. 40,
  424. 0,
  425. ],
  426. ]
  427. `);
  428. });
  429. it("should update only the first segment midpoint when its point is dragged", async () => {
  430. const elementsMap = arrayToMap(h.elements);
  431. const points = LinearElementEditor.getPointsGlobalCoordinates(
  432. line,
  433. elementsMap,
  434. );
  435. const midPoints = LinearElementEditor.getEditorMidPoints(
  436. line,
  437. h.app.scene.getNonDeletedElementsMap(),
  438. h.state,
  439. );
  440. const hitCoords = pointFrom<GlobalPoint>(points[0][0], points[0][1]);
  441. // Drag from first point
  442. drag(hitCoords, pointFrom(hitCoords[0] - delta, hitCoords[1] - delta));
  443. expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
  444. `12`,
  445. );
  446. expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`6`);
  447. const newPoints = LinearElementEditor.getPointsGlobalCoordinates(
  448. line,
  449. elementsMap,
  450. );
  451. expect([newPoints[0][0], newPoints[0][1]]).toEqual([
  452. points[0][0] - delta,
  453. points[0][1] - delta,
  454. ]);
  455. const newMidPoints = LinearElementEditor.getEditorMidPoints(
  456. line,
  457. h.app.scene.getNonDeletedElementsMap(),
  458. h.state,
  459. );
  460. expect(midPoints[0]).not.toEqual(newMidPoints[0]);
  461. expect(midPoints[1]).toEqual(newMidPoints[1]);
  462. });
  463. it("should hide midpoints in the segment when points moved close", async () => {
  464. const elementsMap = arrayToMap(h.elements);
  465. const points = LinearElementEditor.getPointsGlobalCoordinates(
  466. line,
  467. elementsMap,
  468. );
  469. const midPoints = LinearElementEditor.getEditorMidPoints(
  470. line,
  471. h.app.scene.getNonDeletedElementsMap(),
  472. h.state,
  473. );
  474. const hitCoords = pointFrom<GlobalPoint>(points[0][0], points[0][1]);
  475. // Drag from first point
  476. drag(hitCoords, pointFrom(hitCoords[0] + delta, hitCoords[1] + delta));
  477. expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
  478. `12`,
  479. );
  480. expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`6`);
  481. const newPoints = LinearElementEditor.getPointsGlobalCoordinates(
  482. line,
  483. elementsMap,
  484. );
  485. expect([newPoints[0][0], newPoints[0][1]]).toEqual([
  486. points[0][0] + delta,
  487. points[0][1] + delta,
  488. ]);
  489. const newMidPoints = LinearElementEditor.getEditorMidPoints(
  490. line,
  491. h.app.scene.getNonDeletedElementsMap(),
  492. h.state,
  493. );
  494. // This midpoint is hidden since the points are too close
  495. expect(newMidPoints[0]).toBeNull();
  496. expect(midPoints[1]).toEqual(newMidPoints[1]);
  497. });
  498. it("should remove the midpoint when one of the points in the segment is deleted", async () => {
  499. const line = h.elements[0] as ExcalidrawLinearElement;
  500. enterLineEditingMode(line);
  501. const points = LinearElementEditor.getPointsGlobalCoordinates(
  502. line,
  503. arrayToMap(h.elements),
  504. );
  505. // dragging line from last segment midpoint
  506. drag(
  507. lastSegmentMidpoint,
  508. pointFrom(lastSegmentMidpoint[0] + 50, lastSegmentMidpoint[1] + 50),
  509. );
  510. expect(line.points.length).toEqual(4);
  511. const midPoints = LinearElementEditor.getEditorMidPoints(
  512. line,
  513. h.app.scene.getNonDeletedElementsMap(),
  514. h.state,
  515. );
  516. // delete 3rd point
  517. deletePoint(points[2]);
  518. expect(line.points.length).toEqual(3);
  519. expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
  520. `18`,
  521. );
  522. expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`7`);
  523. const newMidPoints = LinearElementEditor.getEditorMidPoints(
  524. line,
  525. h.app.scene.getNonDeletedElementsMap(),
  526. h.state,
  527. );
  528. expect(newMidPoints.length).toEqual(2);
  529. expect(midPoints[0]).toEqual(newMidPoints[0]);
  530. expect(midPoints[1]).toEqual(newMidPoints[1]);
  531. });
  532. });
  533. describe("When edges are round", () => {
  534. // This is the expected midpoint for line with round edge
  535. // hence hardcoding it so if later some bug is introduced
  536. // this will fail and we can fix it
  537. const firstSegmentMidpoint = pointFrom<GlobalPoint>(
  538. 55.9697848965255,
  539. 47.442326230998205,
  540. );
  541. const lastSegmentMidpoint = pointFrom<GlobalPoint>(
  542. 76.08587175006699,
  543. 43.294165939653226,
  544. );
  545. let line: ExcalidrawLinearElement;
  546. beforeEach(() => {
  547. line = createThreePointerLinearElement("line", {
  548. type: ROUNDNESS.PROPORTIONAL_RADIUS,
  549. });
  550. expect(line.points.length).toEqual(3);
  551. enterLineEditingMode(line);
  552. });
  553. it("should allow dragging lines from midpoints in between segments", async () => {
  554. // drag line from first segment midpoint
  555. drag(
  556. firstSegmentMidpoint,
  557. pointFrom(
  558. firstSegmentMidpoint[0] + delta,
  559. firstSegmentMidpoint[1] + delta,
  560. ),
  561. );
  562. expect(line.points.length).toEqual(4);
  563. // drag line from last segment midpoint
  564. drag(
  565. lastSegmentMidpoint,
  566. pointFrom(
  567. lastSegmentMidpoint[0] + delta,
  568. lastSegmentMidpoint[1] + delta,
  569. ),
  570. );
  571. expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
  572. `16`,
  573. );
  574. expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`7`);
  575. expect(line.points.length).toEqual(5);
  576. expect((h.elements[0] as ExcalidrawLinearElement).points)
  577. .toMatchInlineSnapshot(`
  578. [
  579. [
  580. 0,
  581. 0,
  582. ],
  583. [
  584. "85.96978",
  585. "77.44233",
  586. ],
  587. [
  588. 70,
  589. 50,
  590. ],
  591. [
  592. "106.08587",
  593. "73.29417",
  594. ],
  595. [
  596. 40,
  597. 0,
  598. ],
  599. ]
  600. `);
  601. });
  602. it("should update all the midpoints when its point is dragged", async () => {
  603. const elementsMap = arrayToMap(h.elements);
  604. const points = LinearElementEditor.getPointsGlobalCoordinates(
  605. line,
  606. elementsMap,
  607. );
  608. const midPoints = LinearElementEditor.getEditorMidPoints(
  609. line,
  610. h.app.scene.getNonDeletedElementsMap(),
  611. h.state,
  612. );
  613. const hitCoords = pointFrom<GlobalPoint>(points[0][0], points[0][1]);
  614. // Drag from first point
  615. drag(hitCoords, pointFrom(hitCoords[0] - delta, hitCoords[1] - delta));
  616. const newPoints = LinearElementEditor.getPointsGlobalCoordinates(
  617. line,
  618. elementsMap,
  619. );
  620. expect([newPoints[0][0], newPoints[0][1]]).toEqual([
  621. points[0][0] - delta,
  622. points[0][1] - delta,
  623. ]);
  624. const newMidPoints = LinearElementEditor.getEditorMidPoints(
  625. line,
  626. h.app.scene.getNonDeletedElementsMap(),
  627. h.state,
  628. );
  629. expect(midPoints[0]).not.toEqual(newMidPoints[0]);
  630. expect(midPoints[1]).not.toEqual(newMidPoints[1]);
  631. expect(newMidPoints).toMatchInlineSnapshot(`
  632. [
  633. [
  634. "31.88408",
  635. "23.13276",
  636. ],
  637. [
  638. "77.74793",
  639. "44.57841",
  640. ],
  641. ]
  642. `);
  643. });
  644. it("should hide midpoints in the segment when points moved close", async () => {
  645. const elementsMap = arrayToMap(h.elements);
  646. const points = LinearElementEditor.getPointsGlobalCoordinates(
  647. line,
  648. elementsMap,
  649. );
  650. const midPoints = LinearElementEditor.getEditorMidPoints(
  651. line,
  652. h.app.scene.getNonDeletedElementsMap(),
  653. h.state,
  654. );
  655. const hitCoords = pointFrom<GlobalPoint>(points[0][0], points[0][1]);
  656. // Drag from first point
  657. drag(hitCoords, pointFrom(hitCoords[0] + delta, hitCoords[1] + delta));
  658. expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
  659. `12`,
  660. );
  661. expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`6`);
  662. const newPoints = LinearElementEditor.getPointsGlobalCoordinates(
  663. line,
  664. elementsMap,
  665. );
  666. expect([newPoints[0][0], newPoints[0][1]]).toEqual([
  667. points[0][0] + delta,
  668. points[0][1] + delta,
  669. ]);
  670. const newMidPoints = LinearElementEditor.getEditorMidPoints(
  671. line,
  672. h.app.scene.getNonDeletedElementsMap(),
  673. h.state,
  674. );
  675. // This mid point is hidden due to point being too close
  676. expect(newMidPoints[0]).toBeNull();
  677. expect(newMidPoints[1]).not.toEqual(midPoints[1]);
  678. });
  679. it("should update all the midpoints when a point is deleted", async () => {
  680. const elementsMap = arrayToMap(h.elements);
  681. drag(
  682. lastSegmentMidpoint,
  683. pointFrom(
  684. lastSegmentMidpoint[0] + delta,
  685. lastSegmentMidpoint[1] + delta,
  686. ),
  687. );
  688. expect(line.points.length).toEqual(4);
  689. const midPoints = LinearElementEditor.getEditorMidPoints(
  690. line,
  691. h.app.scene.getNonDeletedElementsMap(),
  692. h.state,
  693. );
  694. const points = LinearElementEditor.getPointsGlobalCoordinates(
  695. line,
  696. elementsMap,
  697. );
  698. // delete 3rd point
  699. deletePoint(points[2]);
  700. expect(line.points.length).toEqual(3);
  701. const newMidPoints = LinearElementEditor.getEditorMidPoints(
  702. line,
  703. h.app.scene.getNonDeletedElementsMap(),
  704. h.state,
  705. );
  706. expect(newMidPoints.length).toEqual(2);
  707. expect(midPoints[0]).not.toEqual(newMidPoints[0]);
  708. expect(midPoints[1]).not.toEqual(newMidPoints[1]);
  709. expect(newMidPoints).toMatchInlineSnapshot(`
  710. [
  711. [
  712. "55.96978",
  713. "47.44233",
  714. ],
  715. [
  716. "76.08587",
  717. "43.29417",
  718. ],
  719. ]
  720. `);
  721. });
  722. });
  723. it("in-editor dragging a line point covered by another element", () => {
  724. createTwoPointerLinearElement("line");
  725. const line = h.elements[0] as ExcalidrawLinearElement;
  726. API.setElements([
  727. line,
  728. API.createElement({
  729. type: "rectangle",
  730. x: line.x - 50,
  731. y: line.y - 50,
  732. width: 100,
  733. height: 100,
  734. backgroundColor: "red",
  735. fillStyle: "solid",
  736. }),
  737. ]);
  738. const dragEndPositionOffset = [100, 100] as const;
  739. API.setSelectedElements([line]);
  740. enterLineEditingMode(line, true);
  741. drag(
  742. pointFrom(line.points[0][0] + line.x, line.points[0][1] + line.y),
  743. pointFrom(
  744. dragEndPositionOffset[0] + line.x,
  745. dragEndPositionOffset[1] + line.y,
  746. ),
  747. );
  748. expect(line.points).toMatchInlineSnapshot(`
  749. [
  750. [
  751. 0,
  752. 0,
  753. ],
  754. [
  755. -60,
  756. -100,
  757. ],
  758. ]
  759. `);
  760. });
  761. });
  762. describe("Test bound text element", () => {
  763. const DEFAULT_TEXT = "Online whiteboard collaboration made easy";
  764. const createBoundTextElement = (
  765. text: string,
  766. container: ExcalidrawLinearElement,
  767. ) => {
  768. const textElement = API.createElement({
  769. type: "text",
  770. x: 0,
  771. y: 0,
  772. text: wrapText(text, font, getBoundTextMaxWidth(container, null)),
  773. containerId: container.id,
  774. width: 30,
  775. height: 20,
  776. }) as ExcalidrawTextElementWithContainer;
  777. container = {
  778. ...container,
  779. boundElements: (container.boundElements || []).concat({
  780. type: "text",
  781. id: textElement.id,
  782. }),
  783. };
  784. const elements: ExcalidrawElement[] = [];
  785. h.elements.forEach((element) => {
  786. if (element.id === container.id) {
  787. elements.push(container);
  788. } else {
  789. elements.push(element);
  790. }
  791. });
  792. const updatedTextElement = { ...textElement, originalText: text };
  793. API.setElements([...elements, updatedTextElement]);
  794. return { textElement: updatedTextElement, container };
  795. };
  796. describe("Test getBoundTextElementPosition", () => {
  797. it("should return correct position for 2 pointer arrow", () => {
  798. createTwoPointerLinearElement("arrow");
  799. const arrow = h.elements[0] as ExcalidrawLinearElement;
  800. const { textElement, container } = createBoundTextElement(
  801. DEFAULT_TEXT,
  802. arrow,
  803. );
  804. const position = LinearElementEditor.getBoundTextElementPosition(
  805. container,
  806. textElement,
  807. arrayToMap(h.elements),
  808. );
  809. expect(position).toMatchInlineSnapshot(`
  810. {
  811. "x": 25,
  812. "y": 10,
  813. }
  814. `);
  815. });
  816. it("should return correct position for arrow with odd points", () => {
  817. createThreePointerLinearElement("arrow", {
  818. type: ROUNDNESS.PROPORTIONAL_RADIUS,
  819. });
  820. const arrow = h.elements[0] as ExcalidrawLinearElement;
  821. const { textElement, container } = createBoundTextElement(
  822. DEFAULT_TEXT,
  823. arrow,
  824. );
  825. const position = LinearElementEditor.getBoundTextElementPosition(
  826. container,
  827. textElement,
  828. arrayToMap(h.elements),
  829. );
  830. expect(position).toMatchInlineSnapshot(`
  831. {
  832. "x": 75,
  833. "y": 60,
  834. }
  835. `);
  836. });
  837. it("should return correct position for arrow with even points", () => {
  838. createThreePointerLinearElement("arrow", {
  839. type: ROUNDNESS.PROPORTIONAL_RADIUS,
  840. });
  841. const arrow = h.elements[0] as ExcalidrawLinearElement;
  842. const { textElement, container } = createBoundTextElement(
  843. DEFAULT_TEXT,
  844. arrow,
  845. );
  846. enterLineEditingMode(container);
  847. // This is the expected midpoint for line with round edge
  848. // hence hardcoding it so if later some bug is introduced
  849. // this will fail and we can fix it
  850. const firstSegmentMidpoint = pointFrom<GlobalPoint>(
  851. 55.9697848965255,
  852. 47.442326230998205,
  853. );
  854. // drag line from first segment midpoint
  855. drag(
  856. firstSegmentMidpoint,
  857. pointFrom(
  858. firstSegmentMidpoint[0] + delta,
  859. firstSegmentMidpoint[1] + delta,
  860. ),
  861. );
  862. const position = LinearElementEditor.getBoundTextElementPosition(
  863. container,
  864. textElement,
  865. arrayToMap(h.elements),
  866. );
  867. expect(position).toMatchInlineSnapshot(`
  868. {
  869. "x": "85.82202",
  870. "y": "75.63461",
  871. }
  872. `);
  873. });
  874. });
  875. it("should match styles for text editor", () => {
  876. createTwoPointerLinearElement("arrow");
  877. Keyboard.keyPress(KEYS.ENTER);
  878. const editor = document.querySelector(
  879. ".excalidraw-textEditorContainer > textarea",
  880. ) as HTMLTextAreaElement;
  881. expect(editor).toMatchSnapshot();
  882. });
  883. it("should bind text to arrow when double clicked", async () => {
  884. createTwoPointerLinearElement("arrow");
  885. const arrow = h.elements[0] as ExcalidrawLinearElement;
  886. expect(h.elements.length).toBe(1);
  887. expect(h.elements[0].id).toBe(arrow.id);
  888. mouse.doubleClickAt(arrow.x, arrow.y);
  889. expect(h.elements.length).toBe(2);
  890. const text = h.elements[1] as ExcalidrawTextElementWithContainer;
  891. expect(text.type).toBe("text");
  892. expect(text.containerId).toBe(arrow.id);
  893. mouse.down();
  894. const editor = document.querySelector(
  895. ".excalidraw-textEditorContainer > textarea",
  896. ) as HTMLTextAreaElement;
  897. fireEvent.change(editor, {
  898. target: { value: DEFAULT_TEXT },
  899. });
  900. Keyboard.exitTextEditor(editor);
  901. expect(arrow.boundElements).toStrictEqual([
  902. { id: text.id, type: "text" },
  903. ]);
  904. expect(
  905. (h.elements[1] as ExcalidrawTextElementWithContainer).text,
  906. ).toMatchSnapshot();
  907. });
  908. it("should bind text to arrow when clicked on arrow and enter pressed", async () => {
  909. const arrow = createTwoPointerLinearElement("arrow");
  910. expect(h.elements.length).toBe(1);
  911. expect(h.elements[0].id).toBe(arrow.id);
  912. Keyboard.keyPress(KEYS.ENTER);
  913. expect(h.elements.length).toBe(2);
  914. const textElement = h.elements[1] as ExcalidrawTextElementWithContainer;
  915. expect(textElement.type).toBe("text");
  916. expect(textElement.containerId).toBe(arrow.id);
  917. const editor = document.querySelector(
  918. ".excalidraw-textEditorContainer > textarea",
  919. ) as HTMLTextAreaElement;
  920. fireEvent.change(editor, {
  921. target: { value: DEFAULT_TEXT },
  922. });
  923. Keyboard.exitTextEditor(editor);
  924. expect(arrow.boundElements).toStrictEqual([
  925. { id: textElement.id, type: "text" },
  926. ]);
  927. expect(
  928. (h.elements[1] as ExcalidrawTextElementWithContainer).text,
  929. ).toMatchSnapshot();
  930. });
  931. it("should not bind text to line when double clicked", async () => {
  932. const line = createTwoPointerLinearElement("line");
  933. expect(h.elements.length).toBe(1);
  934. mouse.doubleClickAt(line.x, line.y);
  935. expect(h.elements.length).toBe(2);
  936. const text = h.elements[1] as ExcalidrawTextElementWithContainer;
  937. expect(text.type).toBe("text");
  938. expect(text.containerId).toBeNull();
  939. expect(line.boundElements).toBeNull();
  940. });
  941. // TODO fix #7029 and rewrite this test
  942. it.todo(
  943. "should not rotate the bound text and update position of bound text and bounding box correctly when arrow rotated",
  944. );
  945. it("should resize and position the bound text and bounding box correctly when 3 pointer arrow element resized", () => {
  946. createThreePointerLinearElement("arrow", {
  947. type: ROUNDNESS.PROPORTIONAL_RADIUS,
  948. });
  949. const arrow = h.elements[0] as ExcalidrawLinearElement;
  950. const { textElement, container } = createBoundTextElement(
  951. DEFAULT_TEXT,
  952. arrow,
  953. );
  954. expect(container.width).toBe(70);
  955. expect(container.height).toBe(50);
  956. expect(
  957. getBoundTextElementPosition(
  958. container,
  959. textElement,
  960. arrayToMap(h.elements),
  961. ),
  962. ).toMatchInlineSnapshot(`
  963. {
  964. "x": 75,
  965. "y": 60,
  966. }
  967. `);
  968. expect(textElement.text).toMatchSnapshot();
  969. expect(
  970. LinearElementEditor.getElementAbsoluteCoords(
  971. container,
  972. h.app.scene.getNonDeletedElementsMap(),
  973. true,
  974. ),
  975. ).toMatchInlineSnapshot(`
  976. [
  977. 20,
  978. 20,
  979. 105,
  980. 80,
  981. "55.45894",
  982. 45,
  983. ]
  984. `);
  985. UI.resize(container, "ne", [300, 200]);
  986. expect({ width: container.width, height: container.height })
  987. .toMatchInlineSnapshot(`
  988. {
  989. "height": 130,
  990. "width": "366.11716",
  991. }
  992. `);
  993. expect(
  994. getBoundTextElementPosition(
  995. container,
  996. textElement,
  997. arrayToMap(h.elements),
  998. ),
  999. ).toMatchInlineSnapshot(`
  1000. {
  1001. "x": "271.11716",
  1002. "y": 45,
  1003. }
  1004. `);
  1005. expect(
  1006. (h.elements[1] as ExcalidrawTextElementWithContainer).text,
  1007. ).toMatchSnapshot();
  1008. expect(
  1009. LinearElementEditor.getElementAbsoluteCoords(
  1010. container,
  1011. h.app.scene.getNonDeletedElementsMap(),
  1012. true,
  1013. ),
  1014. ).toMatchInlineSnapshot(`
  1015. [
  1016. 20,
  1017. 35,
  1018. "501.11716",
  1019. 95,
  1020. "205.45894",
  1021. "52.50000",
  1022. ]
  1023. `);
  1024. });
  1025. it("should resize and position the bound text correctly when 2 pointer linear element resized", () => {
  1026. createTwoPointerLinearElement("arrow");
  1027. const arrow = h.elements[0] as ExcalidrawLinearElement;
  1028. const { textElement, container } = createBoundTextElement(
  1029. DEFAULT_TEXT,
  1030. arrow,
  1031. );
  1032. expect(container.width).toBe(40);
  1033. const elementsMap = arrayToMap(h.elements);
  1034. expect(getBoundTextElementPosition(container, textElement, elementsMap))
  1035. .toMatchInlineSnapshot(`
  1036. {
  1037. "x": 25,
  1038. "y": 10,
  1039. }
  1040. `);
  1041. expect(textElement.text).toMatchSnapshot();
  1042. const points = LinearElementEditor.getPointsGlobalCoordinates(
  1043. container,
  1044. elementsMap,
  1045. );
  1046. // Drag from last point
  1047. drag(points[1], pointFrom(points[1][0] + 300, points[1][1]));
  1048. expect({ width: container.width, height: container.height })
  1049. .toMatchInlineSnapshot(`
  1050. {
  1051. "height": 130,
  1052. "width": 340,
  1053. }
  1054. `);
  1055. expect(getBoundTextElementPosition(container, textElement, elementsMap))
  1056. .toMatchInlineSnapshot(`
  1057. {
  1058. "x": 75,
  1059. "y": -5,
  1060. }
  1061. `);
  1062. expect(textElement.text).toMatchSnapshot();
  1063. });
  1064. it("should not render vertical align tool when element selected", () => {
  1065. createTwoPointerLinearElement("arrow");
  1066. const arrow = h.elements[0] as ExcalidrawLinearElement;
  1067. createBoundTextElement(DEFAULT_TEXT, arrow);
  1068. API.setSelectedElements([arrow]);
  1069. expect(queryByTestId(container, "align-top")).toBeNull();
  1070. expect(queryByTestId(container, "align-middle")).toBeNull();
  1071. expect(queryByTestId(container, "align-bottom")).toBeNull();
  1072. });
  1073. it("should wrap the bound text when arrow bound container moves", async () => {
  1074. const rect = UI.createElement("rectangle", {
  1075. x: 400,
  1076. width: 200,
  1077. height: 500,
  1078. });
  1079. const arrow = UI.createElement("arrow", {
  1080. x: -10,
  1081. y: 250,
  1082. width: 400,
  1083. height: 1,
  1084. });
  1085. mouse.select(arrow);
  1086. Keyboard.keyPress(KEYS.ENTER);
  1087. const editor = document.querySelector(
  1088. ".excalidraw-textEditorContainer > textarea",
  1089. ) as HTMLTextAreaElement;
  1090. fireEvent.change(editor, { target: { value: DEFAULT_TEXT } });
  1091. Keyboard.exitTextEditor(editor);
  1092. const textElement = h.elements[2] as ExcalidrawTextElementWithContainer;
  1093. expect(arrow.endBinding?.elementId).toBe(rect.id);
  1094. expect(arrow.width).toBe(400);
  1095. expect(rect.x).toBe(400);
  1096. expect(rect.y).toBe(0);
  1097. expect(
  1098. wrapText(
  1099. textElement.originalText,
  1100. font,
  1101. getBoundTextMaxWidth(arrow, null),
  1102. ),
  1103. ).toMatchSnapshot();
  1104. const handleBindTextResizeSpy = vi.spyOn(
  1105. textElementUtils,
  1106. "handleBindTextResize",
  1107. );
  1108. mouse.select(rect);
  1109. mouse.downAt(rect.x, rect.y);
  1110. mouse.moveTo(200, 0);
  1111. mouse.upAt(200, 0);
  1112. expect(arrow.width).toBeCloseTo(204, 0);
  1113. expect(rect.x).toBe(200);
  1114. expect(rect.y).toBe(0);
  1115. expect(handleBindTextResizeSpy).toHaveBeenCalledWith(
  1116. h.elements[0],
  1117. h.app.scene,
  1118. "nw",
  1119. false,
  1120. );
  1121. expect(
  1122. wrapText(
  1123. textElement.originalText,
  1124. font,
  1125. getBoundTextMaxWidth(arrow, null),
  1126. ),
  1127. ).toMatchSnapshot();
  1128. });
  1129. it("should not render horizontal align tool when element selected", () => {
  1130. createTwoPointerLinearElement("arrow");
  1131. const arrow = h.elements[0] as ExcalidrawLinearElement;
  1132. createBoundTextElement(DEFAULT_TEXT, arrow);
  1133. API.setSelectedElements([arrow]);
  1134. expect(queryByTestId(container, "align-left")).toBeNull();
  1135. expect(queryByTestId(container, "align-horizontal-center")).toBeNull();
  1136. expect(queryByTestId(container, "align-right")).toBeNull();
  1137. });
  1138. it("should update label coords when a label binded via context menu is unbinded", async () => {
  1139. createTwoPointerLinearElement("arrow");
  1140. const text = API.createElement({
  1141. type: "text",
  1142. text: "Hello Excalidraw",
  1143. });
  1144. expect(text.x).toBe(0);
  1145. expect(text.y).toBe(0);
  1146. API.setElements([h.elements[0], text]);
  1147. const container = h.elements[0];
  1148. API.setSelectedElements([container, text]);
  1149. fireEvent.contextMenu(GlobalTestState.interactiveCanvas, {
  1150. button: 2,
  1151. clientX: 20,
  1152. clientY: 30,
  1153. });
  1154. let contextMenu = document.querySelector(".context-menu");
  1155. fireEvent.click(
  1156. queryByText(contextMenu as HTMLElement, "Bind text to the container")!,
  1157. );
  1158. expect(container.boundElements).toStrictEqual([
  1159. { id: h.elements[1].id, type: "text" },
  1160. ]);
  1161. expect(text.containerId).toBe(container.id);
  1162. expect(text.verticalAlign).toBe(VERTICAL_ALIGN.MIDDLE);
  1163. mouse.reset();
  1164. mouse.clickAt(
  1165. container.x + container.width / 2,
  1166. container.y + container.height / 2,
  1167. );
  1168. mouse.down();
  1169. mouse.up();
  1170. API.setSelectedElements([h.elements[0], h.elements[1]]);
  1171. fireEvent.contextMenu(GlobalTestState.interactiveCanvas, {
  1172. button: 2,
  1173. clientX: 20,
  1174. clientY: 30,
  1175. });
  1176. contextMenu = document.querySelector(".context-menu");
  1177. fireEvent.click(queryByText(contextMenu as HTMLElement, "Unbind text")!);
  1178. expect(container.boundElements).toEqual([]);
  1179. expect(text).toEqual(
  1180. expect.objectContaining({
  1181. containerId: null,
  1182. width: 160,
  1183. height: 25,
  1184. x: -40,
  1185. y: 7.5,
  1186. }),
  1187. );
  1188. });
  1189. it("should not update label position when arrow dragged", () => {
  1190. createTwoPointerLinearElement("arrow");
  1191. let arrow = h.elements[0] as ExcalidrawLinearElement;
  1192. createBoundTextElement(DEFAULT_TEXT, arrow);
  1193. let label = h.elements[1] as ExcalidrawTextElementWithContainer;
  1194. expect(arrow.x).toBe(20);
  1195. expect(arrow.y).toBe(20);
  1196. expect(label.x).toBe(0);
  1197. expect(label.y).toBe(0);
  1198. mouse.reset();
  1199. mouse.select(arrow);
  1200. mouse.select(label);
  1201. mouse.downAt(arrow.x, arrow.y);
  1202. mouse.moveTo(arrow.x + 20, arrow.y + 30);
  1203. mouse.up(arrow.x + 20, arrow.y + 30);
  1204. arrow = h.elements[0] as ExcalidrawLinearElement;
  1205. label = h.elements[1] as ExcalidrawTextElementWithContainer;
  1206. expect(arrow.x).toBe(80);
  1207. expect(arrow.y).toBe(100);
  1208. expect(label.x).toBe(0);
  1209. expect(label.y).toBe(0);
  1210. });
  1211. });
  1212. describe("Test moving linear element points", () => {
  1213. it("should move the endpoint in the negative direction correctly when the start point is also moved in the positive direction", async () => {
  1214. const line = createThreePointerLinearElement("arrow");
  1215. const [origStartX, origStartY] = [line.x, line.y];
  1216. act(() => {
  1217. LinearElementEditor.movePoints(
  1218. line,
  1219. h.app.scene,
  1220. new Map([
  1221. [
  1222. 0,
  1223. {
  1224. point: pointFrom(
  1225. line.points[0][0] + 10,
  1226. line.points[0][1] + 10,
  1227. ),
  1228. },
  1229. ],
  1230. [
  1231. line.points.length - 1,
  1232. {
  1233. point: pointFrom(
  1234. line.points[line.points.length - 1][0] - 10,
  1235. line.points[line.points.length - 1][1] - 10,
  1236. ),
  1237. },
  1238. ],
  1239. ]),
  1240. );
  1241. });
  1242. expect(line.x).toBe(origStartX + 10);
  1243. expect(line.y).toBe(origStartY + 10);
  1244. expect(line.points[line.points.length - 1][0]).toBe(20);
  1245. expect(line.points[line.points.length - 1][1]).toBe(-20);
  1246. });
  1247. });
  1248. });