selection.test.tsx 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382
  1. import ReactDOM from "react-dom";
  2. import {
  3. render,
  4. fireEvent,
  5. mockBoundingClientRect,
  6. restoreOriginalGetBoundingClientRect,
  7. assertSelectedElements,
  8. } from "./test-utils";
  9. import ExcalidrawApp from "../excalidraw-app";
  10. import * as Renderer from "../renderer/renderScene";
  11. import { KEYS } from "../keys";
  12. import { reseed } from "../random";
  13. import { API } from "./helpers/api";
  14. import { Keyboard, Pointer } from "./helpers/ui";
  15. // Unmount ReactDOM from root
  16. ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
  17. const renderScene = jest.spyOn(Renderer, "renderScene");
  18. beforeEach(() => {
  19. localStorage.clear();
  20. renderScene.mockClear();
  21. reseed(7);
  22. });
  23. const { h } = window;
  24. const mouse = new Pointer("mouse");
  25. describe("inner box-selection", () => {
  26. beforeEach(async () => {
  27. await render(<ExcalidrawApp />);
  28. });
  29. it("selecting elements visually nested inside another", async () => {
  30. const rect1 = API.createElement({
  31. type: "rectangle",
  32. x: 0,
  33. y: 0,
  34. width: 300,
  35. height: 300,
  36. backgroundColor: "red",
  37. fillStyle: "solid",
  38. });
  39. const rect2 = API.createElement({
  40. type: "rectangle",
  41. x: 50,
  42. y: 50,
  43. width: 50,
  44. height: 50,
  45. });
  46. const rect3 = API.createElement({
  47. type: "rectangle",
  48. x: 150,
  49. y: 150,
  50. width: 50,
  51. height: 50,
  52. });
  53. h.elements = [rect1, rect2, rect3];
  54. Keyboard.withModifierKeys({ ctrl: true }, () => {
  55. mouse.downAt(40, 40);
  56. mouse.moveTo(290, 290);
  57. mouse.up();
  58. assertSelectedElements([rect2.id, rect3.id]);
  59. });
  60. });
  61. it("selecting grouped elements visually nested inside another", async () => {
  62. const rect1 = API.createElement({
  63. type: "rectangle",
  64. x: 0,
  65. y: 0,
  66. width: 300,
  67. height: 300,
  68. backgroundColor: "red",
  69. fillStyle: "solid",
  70. });
  71. const rect2 = API.createElement({
  72. type: "rectangle",
  73. x: 50,
  74. y: 50,
  75. width: 50,
  76. height: 50,
  77. groupIds: ["A"],
  78. });
  79. const rect3 = API.createElement({
  80. type: "rectangle",
  81. x: 150,
  82. y: 150,
  83. width: 50,
  84. height: 50,
  85. groupIds: ["A"],
  86. });
  87. h.elements = [rect1, rect2, rect3];
  88. Keyboard.withModifierKeys({ ctrl: true }, () => {
  89. mouse.downAt(40, 40);
  90. mouse.moveTo(rect2.x + rect2.width + 10, rect2.y + rect2.height + 10);
  91. mouse.up();
  92. assertSelectedElements([rect2.id, rect3.id]);
  93. expect(h.state.selectedGroupIds).toEqual({ A: true });
  94. });
  95. });
  96. it("selecting & deselecting grouped elements visually nested inside another", async () => {
  97. const rect1 = API.createElement({
  98. type: "rectangle",
  99. x: 0,
  100. y: 0,
  101. width: 300,
  102. height: 300,
  103. backgroundColor: "red",
  104. fillStyle: "solid",
  105. });
  106. const rect2 = API.createElement({
  107. type: "rectangle",
  108. x: 50,
  109. y: 50,
  110. width: 50,
  111. height: 50,
  112. groupIds: ["A"],
  113. });
  114. const rect3 = API.createElement({
  115. type: "rectangle",
  116. x: 150,
  117. y: 150,
  118. width: 50,
  119. height: 50,
  120. groupIds: ["A"],
  121. });
  122. h.elements = [rect1, rect2, rect3];
  123. Keyboard.withModifierKeys({ ctrl: true }, () => {
  124. mouse.downAt(rect2.x - 20, rect2.x - 20);
  125. mouse.moveTo(rect2.x + rect2.width + 10, rect2.y + rect2.height + 10);
  126. assertSelectedElements([rect2.id, rect3.id]);
  127. expect(h.state.selectedGroupIds).toEqual({ A: true });
  128. mouse.moveTo(rect2.x - 10, rect2.y - 10);
  129. assertSelectedElements([rect1.id]);
  130. expect(h.state.selectedGroupIds).toEqual({});
  131. mouse.up();
  132. });
  133. });
  134. });
  135. describe("selection element", () => {
  136. it("create selection element on pointer down", async () => {
  137. const { getByToolName, container } = await render(<ExcalidrawApp />);
  138. // select tool
  139. const tool = getByToolName("selection");
  140. fireEvent.click(tool);
  141. const canvas = container.querySelector("canvas")!;
  142. fireEvent.pointerDown(canvas, { clientX: 60, clientY: 100 });
  143. expect(renderScene).toHaveBeenCalledTimes(4);
  144. const selectionElement = h.state.selectionElement!;
  145. expect(selectionElement).not.toBeNull();
  146. expect(selectionElement.type).toEqual("selection");
  147. expect([selectionElement.x, selectionElement.y]).toEqual([60, 100]);
  148. expect([selectionElement.width, selectionElement.height]).toEqual([0, 0]);
  149. // TODO: There is a memory leak if pointer up is not triggered
  150. fireEvent.pointerUp(canvas);
  151. });
  152. it("resize selection element on pointer move", async () => {
  153. const { getByToolName, container } = await render(<ExcalidrawApp />);
  154. // select tool
  155. const tool = getByToolName("selection");
  156. fireEvent.click(tool);
  157. const canvas = container.querySelector("canvas")!;
  158. fireEvent.pointerDown(canvas, { clientX: 60, clientY: 100 });
  159. fireEvent.pointerMove(canvas, { clientX: 150, clientY: 30 });
  160. expect(renderScene).toHaveBeenCalledTimes(5);
  161. const selectionElement = h.state.selectionElement!;
  162. expect(selectionElement).not.toBeNull();
  163. expect(selectionElement.type).toEqual("selection");
  164. expect([selectionElement.x, selectionElement.y]).toEqual([60, 30]);
  165. expect([selectionElement.width, selectionElement.height]).toEqual([90, 70]);
  166. // TODO: There is a memory leak if pointer up is not triggered
  167. fireEvent.pointerUp(canvas);
  168. });
  169. it("remove selection element on pointer up", async () => {
  170. const { getByToolName, container } = await render(<ExcalidrawApp />);
  171. // select tool
  172. const tool = getByToolName("selection");
  173. fireEvent.click(tool);
  174. const canvas = container.querySelector("canvas")!;
  175. fireEvent.pointerDown(canvas, { clientX: 60, clientY: 100 });
  176. fireEvent.pointerMove(canvas, { clientX: 150, clientY: 30 });
  177. fireEvent.pointerUp(canvas);
  178. expect(renderScene).toHaveBeenCalledTimes(6);
  179. expect(h.state.selectionElement).toBeNull();
  180. });
  181. });
  182. describe("select single element on the scene", () => {
  183. beforeAll(() => {
  184. mockBoundingClientRect();
  185. });
  186. afterAll(() => {
  187. restoreOriginalGetBoundingClientRect();
  188. });
  189. it("rectangle", async () => {
  190. const { getByToolName, container } = await render(<ExcalidrawApp />);
  191. const canvas = container.querySelector("canvas")!;
  192. {
  193. // create element
  194. const tool = getByToolName("rectangle");
  195. fireEvent.click(tool);
  196. fireEvent.pointerDown(canvas, { clientX: 30, clientY: 20 });
  197. fireEvent.pointerMove(canvas, { clientX: 60, clientY: 70 });
  198. fireEvent.pointerUp(canvas);
  199. fireEvent.keyDown(document, {
  200. key: KEYS.ESCAPE,
  201. });
  202. }
  203. const tool = getByToolName("selection");
  204. fireEvent.click(tool);
  205. // click on a line on the rectangle
  206. fireEvent.pointerDown(canvas, { clientX: 45, clientY: 20 });
  207. fireEvent.pointerUp(canvas);
  208. expect(renderScene).toHaveBeenCalledTimes(10);
  209. expect(h.state.selectionElement).toBeNull();
  210. expect(h.elements.length).toEqual(1);
  211. expect(h.state.selectedElementIds[h.elements[0].id]).toBeTruthy();
  212. h.elements.forEach((element) => expect(element).toMatchSnapshot());
  213. });
  214. it("diamond", async () => {
  215. const { getByToolName, container } = await render(<ExcalidrawApp />);
  216. const canvas = container.querySelector("canvas")!;
  217. {
  218. // create element
  219. const tool = getByToolName("diamond");
  220. fireEvent.click(tool);
  221. fireEvent.pointerDown(canvas, { clientX: 30, clientY: 20 });
  222. fireEvent.pointerMove(canvas, { clientX: 60, clientY: 70 });
  223. fireEvent.pointerUp(canvas);
  224. fireEvent.keyDown(document, {
  225. key: KEYS.ESCAPE,
  226. });
  227. }
  228. const tool = getByToolName("selection");
  229. fireEvent.click(tool);
  230. // click on a line on the rectangle
  231. fireEvent.pointerDown(canvas, { clientX: 45, clientY: 20 });
  232. fireEvent.pointerUp(canvas);
  233. expect(renderScene).toHaveBeenCalledTimes(10);
  234. expect(h.state.selectionElement).toBeNull();
  235. expect(h.elements.length).toEqual(1);
  236. expect(h.state.selectedElementIds[h.elements[0].id]).toBeTruthy();
  237. h.elements.forEach((element) => expect(element).toMatchSnapshot());
  238. });
  239. it("ellipse", async () => {
  240. const { getByToolName, container } = await render(<ExcalidrawApp />);
  241. const canvas = container.querySelector("canvas")!;
  242. {
  243. // create element
  244. const tool = getByToolName("ellipse");
  245. fireEvent.click(tool);
  246. fireEvent.pointerDown(canvas, { clientX: 30, clientY: 20 });
  247. fireEvent.pointerMove(canvas, { clientX: 60, clientY: 70 });
  248. fireEvent.pointerUp(canvas);
  249. fireEvent.keyDown(document, {
  250. key: KEYS.ESCAPE,
  251. });
  252. }
  253. const tool = getByToolName("selection");
  254. fireEvent.click(tool);
  255. // click on a line on the rectangle
  256. fireEvent.pointerDown(canvas, { clientX: 45, clientY: 20 });
  257. fireEvent.pointerUp(canvas);
  258. expect(renderScene).toHaveBeenCalledTimes(10);
  259. expect(h.state.selectionElement).toBeNull();
  260. expect(h.elements.length).toEqual(1);
  261. expect(h.state.selectedElementIds[h.elements[0].id]).toBeTruthy();
  262. h.elements.forEach((element) => expect(element).toMatchSnapshot());
  263. });
  264. it("arrow", async () => {
  265. const { getByToolName, container } = await render(<ExcalidrawApp />);
  266. const canvas = container.querySelector("canvas")!;
  267. {
  268. // create element
  269. const tool = getByToolName("arrow");
  270. fireEvent.click(tool);
  271. fireEvent.pointerDown(canvas, { clientX: 30, clientY: 20 });
  272. fireEvent.pointerMove(canvas, { clientX: 60, clientY: 70 });
  273. fireEvent.pointerUp(canvas);
  274. fireEvent.keyDown(document, {
  275. key: KEYS.ESCAPE,
  276. });
  277. }
  278. /*
  279. 1 2 3 4 5 6 7 8 9
  280. 1
  281. 2 x
  282. 3
  283. 4 .
  284. 5
  285. 6
  286. 7 x
  287. 8
  288. 9
  289. */
  290. const tool = getByToolName("selection");
  291. fireEvent.click(tool);
  292. // click on a line on the arrow
  293. fireEvent.pointerDown(canvas, { clientX: 40, clientY: 40 });
  294. fireEvent.pointerUp(canvas);
  295. expect(renderScene).toHaveBeenCalledTimes(10);
  296. expect(h.state.selectionElement).toBeNull();
  297. expect(h.elements.length).toEqual(1);
  298. expect(h.state.selectedElementIds[h.elements[0].id]).toBeTruthy();
  299. h.elements.forEach((element) => expect(element).toMatchSnapshot());
  300. });
  301. it("arrow escape", async () => {
  302. const { getByToolName, container } = await render(<ExcalidrawApp />);
  303. const canvas = container.querySelector("canvas")!;
  304. {
  305. // create element
  306. const tool = getByToolName("line");
  307. fireEvent.click(tool);
  308. fireEvent.pointerDown(canvas, { clientX: 30, clientY: 20 });
  309. fireEvent.pointerMove(canvas, { clientX: 60, clientY: 70 });
  310. fireEvent.pointerUp(canvas);
  311. fireEvent.keyDown(document, {
  312. key: KEYS.ESCAPE,
  313. });
  314. }
  315. /*
  316. 1 2 3 4 5 6 7 8 9
  317. 1
  318. 2 x
  319. 3
  320. 4 .
  321. 5
  322. 6
  323. 7 x
  324. 8
  325. 9
  326. */
  327. const tool = getByToolName("selection");
  328. fireEvent.click(tool);
  329. // click on a line on the arrow
  330. fireEvent.pointerDown(canvas, { clientX: 40, clientY: 40 });
  331. fireEvent.pointerUp(canvas);
  332. expect(renderScene).toHaveBeenCalledTimes(10);
  333. expect(h.state.selectionElement).toBeNull();
  334. expect(h.elements.length).toEqual(1);
  335. expect(h.state.selectedElementIds[h.elements[0].id]).toBeTruthy();
  336. h.elements.forEach((element) => expect(element).toMatchSnapshot());
  337. });
  338. });