binding.test.tsx 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312
  1. import { fireEvent, render } from "./test-utils";
  2. import ExcalidrawApp from "../excalidraw-app";
  3. import { UI, Pointer, Keyboard } from "./helpers/ui";
  4. import { getTransformHandles } from "../element/transformHandles";
  5. import { API } from "./helpers/api";
  6. import { KEYS } from "../keys";
  7. import { actionCreateContainerFromText } from "../actions/actionBoundText";
  8. const { h } = window;
  9. const mouse = new Pointer("mouse");
  10. describe("element binding", () => {
  11. beforeEach(async () => {
  12. await render(<ExcalidrawApp />);
  13. });
  14. //@TODO fix the test with rotation
  15. it.skip("rotation of arrow should rebind both ends", () => {
  16. const rectLeft = UI.createElement("rectangle", {
  17. x: 0,
  18. width: 200,
  19. height: 500,
  20. });
  21. const rectRight = UI.createElement("rectangle", {
  22. x: 400,
  23. width: 200,
  24. height: 500,
  25. });
  26. const arrow = UI.createElement("arrow", {
  27. x: 210,
  28. y: 250,
  29. width: 180,
  30. height: 1,
  31. });
  32. expect(arrow.startBinding?.elementId).toBe(rectLeft.id);
  33. expect(arrow.endBinding?.elementId).toBe(rectRight.id);
  34. const rotation = getTransformHandles(arrow, h.state.zoom, "mouse")
  35. .rotation!;
  36. const rotationHandleX = rotation[0] + rotation[2] / 2;
  37. const rotationHandleY = rotation[1] + rotation[3] / 2;
  38. mouse.down(rotationHandleX, rotationHandleY);
  39. mouse.move(300, 400);
  40. mouse.up();
  41. expect(arrow.angle).toBeGreaterThan(0.7 * Math.PI);
  42. expect(arrow.angle).toBeLessThan(1.3 * Math.PI);
  43. expect(arrow.startBinding?.elementId).toBe(rectRight.id);
  44. expect(arrow.endBinding?.elementId).toBe(rectLeft.id);
  45. });
  46. // TODO fix & reenable once we rewrite tests to work with concurrency
  47. it.skip(
  48. "editing arrow and moving its head to bind it to element A, finalizing the" +
  49. "editing by clicking on element A should end up selecting A",
  50. async () => {
  51. UI.createElement("rectangle", {
  52. y: 0,
  53. size: 100,
  54. });
  55. // Create arrow bound to rectangle
  56. UI.clickTool("arrow");
  57. mouse.down(50, -100);
  58. mouse.up(0, 80);
  59. // Edit arrow with multi-point
  60. mouse.doubleClick();
  61. // move arrow head
  62. mouse.down();
  63. mouse.up(0, 10);
  64. expect(API.getSelectedElement().type).toBe("arrow");
  65. // NOTE this mouse down/up + await needs to be done in order to repro
  66. // the issue, due to https://github.com/excalidraw/excalidraw/blob/46bff3daceb602accf60c40a84610797260fca94/src/components/App.tsx#L740
  67. mouse.reset();
  68. expect(h.state.editingLinearElement).not.toBe(null);
  69. mouse.down(0, 0);
  70. await new Promise((r) => setTimeout(r, 100));
  71. expect(h.state.editingLinearElement).toBe(null);
  72. expect(API.getSelectedElement().type).toBe("rectangle");
  73. mouse.up();
  74. expect(API.getSelectedElement().type).toBe("rectangle");
  75. },
  76. );
  77. it("should bind/unbind arrow when moving it with keyboard", () => {
  78. const rectangle = UI.createElement("rectangle", {
  79. x: 75,
  80. y: 0,
  81. size: 100,
  82. });
  83. // Creates arrow 1px away from bidding with rectangle
  84. const arrow = UI.createElement("arrow", {
  85. x: 0,
  86. y: 0,
  87. size: 50,
  88. });
  89. expect(arrow.endBinding).toBe(null);
  90. expect(API.getSelectedElement().type).toBe("arrow");
  91. Keyboard.keyPress(KEYS.ARROW_RIGHT);
  92. expect(arrow.endBinding?.elementId).toBe(rectangle.id);
  93. Keyboard.keyPress(KEYS.ARROW_LEFT);
  94. expect(arrow.endBinding).toBe(null);
  95. });
  96. it("should unbind on bound element deletion", () => {
  97. const rectangle = UI.createElement("rectangle", {
  98. x: 60,
  99. y: 0,
  100. size: 100,
  101. });
  102. const arrow = UI.createElement("arrow", {
  103. x: 0,
  104. y: 0,
  105. size: 50,
  106. });
  107. expect(arrow.endBinding?.elementId).toBe(rectangle.id);
  108. mouse.select(rectangle);
  109. expect(API.getSelectedElement().type).toBe("rectangle");
  110. Keyboard.keyDown(KEYS.DELETE);
  111. expect(arrow.endBinding).toBe(null);
  112. });
  113. it("should unbind on text element deletion by submitting empty text", async () => {
  114. const text = API.createElement({
  115. type: "text",
  116. text: "ola",
  117. x: 60,
  118. y: 0,
  119. width: 100,
  120. height: 100,
  121. });
  122. h.elements = [text];
  123. const arrow = UI.createElement("arrow", {
  124. x: 0,
  125. y: 0,
  126. size: 50,
  127. });
  128. expect(arrow.endBinding?.elementId).toBe(text.id);
  129. // edit text element and submit
  130. // -------------------------------------------------------------------------
  131. UI.clickTool("text");
  132. mouse.clickAt(text.x + 50, text.y + 50);
  133. const editor = document.querySelector(
  134. ".excalidraw-textEditorContainer > textarea",
  135. ) as HTMLTextAreaElement;
  136. expect(editor).not.toBe(null);
  137. fireEvent.change(editor, { target: { value: "" } });
  138. fireEvent.keyDown(editor, { key: KEYS.ESCAPE });
  139. expect(
  140. document.querySelector(".excalidraw-textEditorContainer > textarea"),
  141. ).toBe(null);
  142. expect(arrow.endBinding).toBe(null);
  143. });
  144. it("should keep binding on text update", async () => {
  145. const text = API.createElement({
  146. type: "text",
  147. text: "ola",
  148. x: 60,
  149. y: 0,
  150. width: 100,
  151. height: 100,
  152. });
  153. h.elements = [text];
  154. const arrow = UI.createElement("arrow", {
  155. x: 0,
  156. y: 0,
  157. size: 50,
  158. });
  159. expect(arrow.endBinding?.elementId).toBe(text.id);
  160. // delete text element by submitting empty text
  161. // -------------------------------------------------------------------------
  162. UI.clickTool("text");
  163. mouse.clickAt(text.x + 50, text.y + 50);
  164. const editor = document.querySelector(
  165. ".excalidraw-textEditorContainer > textarea",
  166. ) as HTMLTextAreaElement;
  167. expect(editor).not.toBe(null);
  168. fireEvent.change(editor, { target: { value: "asdasdasdasdas" } });
  169. fireEvent.keyDown(editor, { key: KEYS.ESCAPE });
  170. expect(
  171. document.querySelector(".excalidraw-textEditorContainer > textarea"),
  172. ).toBe(null);
  173. expect(arrow.endBinding?.elementId).toBe(text.id);
  174. });
  175. it("should update binding when text containerized", async () => {
  176. const rectangle1 = API.createElement({
  177. type: "rectangle",
  178. id: "rectangle1",
  179. width: 100,
  180. height: 100,
  181. boundElements: [
  182. { id: "arrow1", type: "arrow" },
  183. { id: "arrow2", type: "arrow" },
  184. ],
  185. });
  186. const arrow1 = API.createElement({
  187. type: "arrow",
  188. id: "arrow1",
  189. points: [
  190. [0, 0],
  191. [0, -87.45777932247563],
  192. ],
  193. startBinding: {
  194. elementId: "rectangle1",
  195. focus: 0.2,
  196. gap: 7,
  197. },
  198. endBinding: {
  199. elementId: "text1",
  200. focus: 0.2,
  201. gap: 7,
  202. },
  203. });
  204. const arrow2 = API.createElement({
  205. type: "arrow",
  206. id: "arrow2",
  207. points: [
  208. [0, 0],
  209. [0, -87.45777932247563],
  210. ],
  211. startBinding: {
  212. elementId: "text1",
  213. focus: 0.2,
  214. gap: 7,
  215. },
  216. endBinding: {
  217. elementId: "rectangle1",
  218. focus: 0.2,
  219. gap: 7,
  220. },
  221. });
  222. const text1 = API.createElement({
  223. type: "text",
  224. id: "text1",
  225. text: "ola",
  226. boundElements: [
  227. { id: "arrow1", type: "arrow" },
  228. { id: "arrow2", type: "arrow" },
  229. ],
  230. });
  231. h.elements = [rectangle1, arrow1, arrow2, text1];
  232. API.setSelectedElements([text1]);
  233. expect(h.state.selectedElementIds[text1.id]).toBe(true);
  234. h.app.actionManager.executeAction(actionCreateContainerFromText);
  235. // new text container will be placed before the text element
  236. const container = h.elements.at(-2)!;
  237. expect(container.type).toBe("rectangle");
  238. expect(container.id).not.toBe(rectangle1.id);
  239. expect(container).toEqual(
  240. expect.objectContaining({
  241. boundElements: expect.arrayContaining([
  242. {
  243. type: "text",
  244. id: text1.id,
  245. },
  246. {
  247. type: "arrow",
  248. id: arrow1.id,
  249. },
  250. {
  251. type: "arrow",
  252. id: arrow2.id,
  253. },
  254. ]),
  255. }),
  256. );
  257. expect(arrow1.startBinding?.elementId).toBe(rectangle1.id);
  258. expect(arrow1.endBinding?.elementId).toBe(container.id);
  259. expect(arrow2.startBinding?.elementId).toBe(container.id);
  260. expect(arrow2.endBinding?.elementId).toBe(rectangle1.id);
  261. });
  262. });