regressionTests.test.tsx 32 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184
  1. import React from "react";
  2. import ReactDOM from "react-dom";
  3. import type { ExcalidrawElement } from "../element/types";
  4. import { CODES, KEYS } from "../keys";
  5. import { Excalidraw } from "../index";
  6. import { reseed } from "../random";
  7. import * as StaticScene from "../renderer/staticScene";
  8. import { setDateTimeForTests } from "../utils";
  9. import { API } from "./helpers/api";
  10. import { Keyboard, Pointer, UI } from "./helpers/ui";
  11. import {
  12. assertSelectedElements,
  13. fireEvent,
  14. render,
  15. screen,
  16. togglePopover,
  17. } from "./test-utils";
  18. import { FONT_FAMILY } from "../constants";
  19. import { vi } from "vitest";
  20. const { h } = window;
  21. const renderStaticScene = vi.spyOn(StaticScene, "renderStaticScene");
  22. const mouse = new Pointer("mouse");
  23. const finger1 = new Pointer("touch", 1);
  24. const finger2 = new Pointer("touch", 2);
  25. /**
  26. * This is always called at the end of your test, so usually you don't need to call it.
  27. * However, if you have a long test, you might want to call it during the test so it's easier
  28. * to debug where a test failure came from.
  29. */
  30. const checkpoint = (name: string) => {
  31. expect(renderStaticScene.mock.calls.length).toMatchSnapshot(
  32. `[${name}] number of renders`,
  33. );
  34. expect(h.state).toMatchSnapshot(`[${name}] appState`);
  35. expect(h.history).toMatchSnapshot(`[${name}] history`);
  36. expect(h.elements.length).toMatchSnapshot(`[${name}] number of elements`);
  37. h.elements.forEach((element, i) =>
  38. expect(element).toMatchSnapshot(`[${name}] element ${i}`),
  39. );
  40. };
  41. beforeEach(async () => {
  42. // Unmount ReactDOM from root
  43. ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
  44. localStorage.clear();
  45. renderStaticScene.mockClear();
  46. reseed(7);
  47. setDateTimeForTests("201933152653");
  48. mouse.reset();
  49. finger1.reset();
  50. finger2.reset();
  51. await render(<Excalidraw handleKeyboardGlobally={true} />);
  52. API.setAppState({ height: 768, width: 1024 });
  53. });
  54. afterEach(() => {
  55. checkpoint("end of test");
  56. });
  57. describe("regression tests", () => {
  58. it("draw every type of shape", () => {
  59. UI.clickTool("rectangle");
  60. mouse.down(10, -10);
  61. mouse.up(20, 10);
  62. UI.clickTool("diamond");
  63. mouse.down(10, -10);
  64. mouse.up(20, 10);
  65. UI.clickTool("ellipse");
  66. mouse.down(10, -10);
  67. mouse.up(20, 10);
  68. UI.clickTool("arrow");
  69. mouse.down(40, -10);
  70. mouse.up(50, 10);
  71. UI.clickTool("line");
  72. mouse.down(40, -10);
  73. mouse.up(50, 10);
  74. UI.clickTool("arrow");
  75. mouse.click(40, -10);
  76. mouse.click(50, 10);
  77. mouse.click(30, 10);
  78. Keyboard.keyPress(KEYS.ENTER);
  79. UI.clickTool("line");
  80. mouse.click(40, -20);
  81. mouse.click(50, 10);
  82. mouse.click(30, 10);
  83. Keyboard.keyPress(KEYS.ENTER);
  84. UI.clickTool("freedraw");
  85. mouse.down(40, -20);
  86. mouse.up(50, 10);
  87. expect(h.elements.map((element) => element.type)).toEqual([
  88. "rectangle",
  89. "diamond",
  90. "ellipse",
  91. "arrow",
  92. "line",
  93. "arrow",
  94. "line",
  95. "freedraw",
  96. ]);
  97. });
  98. it("click to select a shape", () => {
  99. UI.clickTool("rectangle");
  100. mouse.down(10, 10);
  101. mouse.up(10, 10);
  102. const firstRectPos = mouse.getPosition();
  103. UI.clickTool("rectangle");
  104. mouse.down(10, -10);
  105. mouse.up(10, 10);
  106. const prevSelectedId = API.getSelectedElement().id;
  107. mouse.restorePosition(...firstRectPos);
  108. mouse.click();
  109. expect(API.getSelectedElement().id).not.toEqual(prevSelectedId);
  110. });
  111. for (const [keys, shape, shouldSelect] of [
  112. [`2${KEYS.R}`, "rectangle", true],
  113. [`3${KEYS.D}`, "diamond", true],
  114. [`4${KEYS.O}`, "ellipse", true],
  115. [`5${KEYS.A}`, "arrow", true],
  116. [`6${KEYS.L}`, "line", true],
  117. [`7${KEYS.P}`, "freedraw", false],
  118. ] as [string, ExcalidrawElement["type"], boolean][]) {
  119. for (const key of keys) {
  120. it(`key ${key} selects ${shape} tool`, () => {
  121. Keyboard.keyPress(key);
  122. expect(h.state.activeTool.type).toBe(shape);
  123. mouse.down(10, 10);
  124. mouse.up(10, 10);
  125. if (shouldSelect) {
  126. expect(API.getSelectedElement().type).toBe(shape);
  127. }
  128. });
  129. }
  130. }
  131. it("change the properties of a shape", () => {
  132. UI.clickTool("rectangle");
  133. mouse.down(10, 10);
  134. mouse.up(10, 10);
  135. togglePopover("Background");
  136. UI.clickOnTestId("color-yellow");
  137. UI.clickOnTestId("color-red");
  138. togglePopover("Stroke");
  139. UI.clickOnTestId("color-blue");
  140. expect(API.getSelectedElement().backgroundColor).toBe("#ffc9c9");
  141. expect(API.getSelectedElement().strokeColor).toBe("#1971c2");
  142. });
  143. it("click on an element and drag it", () => {
  144. UI.clickTool("rectangle");
  145. mouse.down(10, 10);
  146. mouse.up(10, 10);
  147. const { x: prevX, y: prevY } = API.getSelectedElement();
  148. mouse.down(-8, -8);
  149. mouse.up(10, 10);
  150. const { x: nextX, y: nextY } = API.getSelectedElement();
  151. expect(nextX).toBeGreaterThan(prevX);
  152. expect(nextY).toBeGreaterThan(prevY);
  153. checkpoint("dragged");
  154. mouse.down();
  155. mouse.up(-10, -10);
  156. const { x, y } = API.getSelectedElement();
  157. expect(x).toBe(prevX);
  158. expect(y).toBe(prevY);
  159. });
  160. it("alt-drag duplicates an element", () => {
  161. UI.clickTool("rectangle");
  162. mouse.down(10, 10);
  163. mouse.up(10, 10);
  164. expect(
  165. h.elements.filter((element) => element.type === "rectangle").length,
  166. ).toBe(1);
  167. Keyboard.withModifierKeys({ alt: true }, () => {
  168. mouse.down(-8, -8);
  169. mouse.up(10, 10);
  170. });
  171. expect(
  172. h.elements.filter((element) => element.type === "rectangle").length,
  173. ).toBe(2);
  174. });
  175. it("click-drag to select a group", () => {
  176. UI.clickTool("rectangle");
  177. mouse.down(10, 10);
  178. mouse.up(10, 10);
  179. UI.clickTool("rectangle");
  180. mouse.down(10, -10);
  181. mouse.up(10, 10);
  182. const finalPosition = mouse.getPosition();
  183. UI.clickTool("rectangle");
  184. mouse.down(10, -10);
  185. mouse.up(10, 10);
  186. mouse.restorePosition(0, 0);
  187. mouse.down();
  188. mouse.restorePosition(...finalPosition);
  189. mouse.up(5, 5);
  190. expect(
  191. h.elements.filter((element) => h.state.selectedElementIds[element.id])
  192. .length,
  193. ).toBe(2);
  194. });
  195. it("shift-click to multiselect, then drag", () => {
  196. UI.clickTool("rectangle");
  197. mouse.down(10, 10);
  198. mouse.up(10, 10);
  199. UI.clickTool("rectangle");
  200. mouse.down(10, -10);
  201. mouse.up(10, 10);
  202. const prevRectsXY = h.elements
  203. .filter((element) => element.type === "rectangle")
  204. .map((element) => ({ x: element.x, y: element.y }));
  205. mouse.reset();
  206. mouse.click(10, 10);
  207. Keyboard.withModifierKeys({ shift: true }, () => {
  208. mouse.click(20, 0);
  209. });
  210. mouse.down();
  211. mouse.up(10, 10);
  212. h.elements
  213. .filter((element) => element.type === "rectangle")
  214. .forEach((element, i) => {
  215. expect(element.x).toBeGreaterThan(prevRectsXY[i].x);
  216. expect(element.y).toBeGreaterThan(prevRectsXY[i].y);
  217. });
  218. });
  219. it("pinch-to-zoom works", () => {
  220. expect(h.state.zoom.value).toBe(1);
  221. finger1.down(50, 50);
  222. finger2.down(60, 50);
  223. finger1.move(-10, 0);
  224. expect(h.state.zoom.value).toBeGreaterThan(1);
  225. const zoomed = h.state.zoom.value;
  226. finger1.move(5, 0);
  227. finger2.move(-5, 0);
  228. expect(h.state.zoom.value).toBeLessThan(zoomed);
  229. });
  230. it("two-finger scroll works", () => {
  231. // scroll horizontally vertically
  232. const startScrollY = h.state.scrollY;
  233. finger1.downAt(0, 0);
  234. finger2.downAt(10, 0);
  235. finger1.clientY -= 10;
  236. finger2.clientY -= 10;
  237. finger1.moveTo();
  238. finger2.moveTo();
  239. finger1.upAt();
  240. finger2.upAt();
  241. expect(h.state.scrollY).toBeLessThan(startScrollY);
  242. // scroll horizontally
  243. const startScrollX = h.state.scrollX;
  244. finger1.downAt();
  245. finger2.downAt();
  246. finger1.clientX += 10;
  247. finger2.clientX += 10;
  248. finger1.moveTo();
  249. finger2.moveTo();
  250. finger1.upAt();
  251. finger2.upAt();
  252. expect(h.state.scrollX).toBeGreaterThan(startScrollX);
  253. });
  254. it("spacebar + drag scrolls the canvas", () => {
  255. const { scrollX: startScrollX, scrollY: startScrollY } = h.state;
  256. Keyboard.keyDown(KEYS.SPACE);
  257. mouse.down(50, 50);
  258. mouse.up(60, 60);
  259. Keyboard.keyUp(KEYS.SPACE);
  260. const { scrollX, scrollY } = h.state;
  261. expect(scrollX).not.toEqual(startScrollX);
  262. expect(scrollY).not.toEqual(startScrollY);
  263. });
  264. it("arrow keys", () => {
  265. UI.clickTool("rectangle");
  266. mouse.down(10, 10);
  267. mouse.up(10, 10);
  268. Keyboard.keyPress(KEYS.ARROW_LEFT);
  269. Keyboard.keyPress(KEYS.ARROW_LEFT);
  270. Keyboard.keyPress(KEYS.ARROW_RIGHT);
  271. Keyboard.keyPress(KEYS.ARROW_UP);
  272. Keyboard.keyPress(KEYS.ARROW_UP);
  273. Keyboard.keyPress(KEYS.ARROW_DOWN);
  274. expect(h.elements[0].x).toBe(9);
  275. expect(h.elements[0].y).toBe(9);
  276. });
  277. it("undo/redo drawing an element", () => {
  278. UI.clickTool("rectangle");
  279. mouse.down(10, -10);
  280. mouse.up(20, 10);
  281. UI.clickTool("rectangle");
  282. mouse.down(10, 0);
  283. mouse.up(30, 20);
  284. UI.clickTool("arrow");
  285. mouse.click(60, -10);
  286. mouse.click(60, 10);
  287. mouse.click(40, 10);
  288. Keyboard.keyPress(KEYS.ENTER);
  289. expect(h.elements.filter((element) => !element.isDeleted).length).toBe(3);
  290. Keyboard.withModifierKeys({ ctrl: true }, () => {
  291. Keyboard.keyPress(KEYS.Z);
  292. Keyboard.keyPress(KEYS.Z);
  293. Keyboard.keyPress(KEYS.Z);
  294. });
  295. expect(h.elements.filter((element) => !element.isDeleted).length).toBe(2);
  296. Keyboard.withModifierKeys({ ctrl: true }, () => {
  297. Keyboard.keyPress(KEYS.Z);
  298. });
  299. expect(h.elements.filter((element) => !element.isDeleted).length).toBe(1);
  300. Keyboard.withModifierKeys({ ctrl: true, shift: true }, () => {
  301. Keyboard.keyPress(KEYS.Z);
  302. });
  303. expect(h.elements.filter((element) => !element.isDeleted).length).toBe(2);
  304. });
  305. it("noop interaction after undo shouldn't create history entry", () => {
  306. expect(API.getUndoStack().length).toBe(0);
  307. UI.clickTool("rectangle");
  308. mouse.down(10, 10);
  309. mouse.up(10, 10);
  310. const firstElementEndPoint = mouse.getPosition();
  311. UI.clickTool("rectangle");
  312. mouse.down(10, -10);
  313. mouse.up(10, 10);
  314. const secondElementEndPoint = mouse.getPosition();
  315. expect(API.getUndoStack().length).toBe(2);
  316. Keyboard.withModifierKeys({ ctrl: true }, () => {
  317. Keyboard.keyPress(KEYS.Z);
  318. });
  319. expect(API.getUndoStack().length).toBe(1);
  320. // clicking an element shouldn't add to history
  321. mouse.restorePosition(...firstElementEndPoint);
  322. mouse.click();
  323. expect(API.getUndoStack().length).toBe(1);
  324. Keyboard.withModifierKeys({ shift: true, ctrl: true }, () => {
  325. Keyboard.keyPress(KEYS.Z);
  326. });
  327. expect(API.getUndoStack().length).toBe(2);
  328. // clicking an element should add to history
  329. mouse.click();
  330. expect(API.getUndoStack().length).toBe(3);
  331. const firstSelectedElementId = API.getSelectedElement().id;
  332. // same for clicking the element just redo-ed
  333. mouse.restorePosition(...secondElementEndPoint);
  334. mouse.click();
  335. expect(API.getUndoStack().length).toBe(4);
  336. expect(API.getSelectedElement().id).not.toEqual(firstSelectedElementId);
  337. });
  338. it("zoom hotkeys", () => {
  339. expect(h.state.zoom.value).toBe(1);
  340. fireEvent.keyDown(document, {
  341. code: CODES.EQUAL,
  342. ctrlKey: true,
  343. });
  344. fireEvent.keyUp(document, {
  345. code: CODES.EQUAL,
  346. ctrlKey: true,
  347. });
  348. expect(h.state.zoom.value).toBeGreaterThan(1);
  349. fireEvent.keyDown(document, {
  350. code: CODES.MINUS,
  351. ctrlKey: true,
  352. });
  353. fireEvent.keyUp(document, {
  354. code: CODES.MINUS,
  355. ctrlKey: true,
  356. });
  357. expect(h.state.zoom.value).toBe(1);
  358. });
  359. it("make a group and duplicate it", () => {
  360. UI.clickTool("rectangle");
  361. mouse.down(10, 10);
  362. mouse.up(10, 10);
  363. UI.clickTool("rectangle");
  364. mouse.down(10, -10);
  365. mouse.up(10, 10);
  366. UI.clickTool("rectangle");
  367. mouse.down(10, -10);
  368. mouse.up(10, 10);
  369. const end = mouse.getPosition();
  370. mouse.reset();
  371. mouse.down();
  372. mouse.restorePosition(...end);
  373. mouse.up();
  374. expect(h.elements.length).toBe(3);
  375. for (const element of h.elements) {
  376. expect(element.groupIds.length).toBe(0);
  377. expect(h.state.selectedElementIds[element.id]).toBe(true);
  378. }
  379. Keyboard.withModifierKeys({ ctrl: true }, () => {
  380. Keyboard.keyPress(KEYS.G);
  381. });
  382. for (const element of h.elements) {
  383. expect(element.groupIds.length).toBe(1);
  384. }
  385. Keyboard.withModifierKeys({ alt: true }, () => {
  386. mouse.restorePosition(...end);
  387. mouse.down();
  388. mouse.up(10, 10);
  389. });
  390. expect(h.elements.length).toBe(6);
  391. const groups = new Set();
  392. for (const element of h.elements) {
  393. for (const groupId of element.groupIds) {
  394. groups.add(groupId);
  395. }
  396. }
  397. expect(groups.size).toBe(2);
  398. });
  399. it("should group elements and ungroup them", () => {
  400. UI.clickTool("rectangle");
  401. mouse.down(10, 10);
  402. mouse.up(10, 10);
  403. UI.clickTool("rectangle");
  404. mouse.down(10, -10);
  405. mouse.up(10, 10);
  406. UI.clickTool("rectangle");
  407. mouse.down(10, -10);
  408. mouse.up(10, 10);
  409. const end = mouse.getPosition();
  410. mouse.reset();
  411. mouse.down();
  412. mouse.restorePosition(...end);
  413. mouse.up();
  414. for (const element of h.elements) {
  415. expect(element.groupIds.length).toBe(0);
  416. }
  417. Keyboard.withModifierKeys({ ctrl: true }, () => {
  418. Keyboard.keyPress(KEYS.G);
  419. });
  420. for (const element of h.elements) {
  421. expect(element.groupIds.length).toBe(1);
  422. }
  423. mouse.moveTo(-10, -10); // the NW resizing handle is at [0, 0], so moving further
  424. mouse.down();
  425. mouse.restorePosition(...end);
  426. mouse.up();
  427. Keyboard.withModifierKeys({ ctrl: true, shift: true }, () => {
  428. Keyboard.keyPress(KEYS.G);
  429. });
  430. for (const element of h.elements) {
  431. expect(element.groupIds.length).toBe(0);
  432. }
  433. });
  434. it("double click to edit a group", () => {
  435. UI.clickTool("rectangle");
  436. mouse.down(10, 10);
  437. mouse.up(10, 10);
  438. UI.clickTool("rectangle");
  439. mouse.down(10, -10);
  440. mouse.up(10, 10);
  441. UI.clickTool("rectangle");
  442. mouse.down(10, -10);
  443. mouse.up(10, 10);
  444. Keyboard.withModifierKeys({ ctrl: true }, () => {
  445. Keyboard.keyPress(KEYS.A);
  446. Keyboard.keyPress(KEYS.G);
  447. });
  448. expect(API.getSelectedElements().length).toBe(3);
  449. expect(h.state.editingGroupId).toBe(null);
  450. mouse.doubleClick();
  451. expect(API.getSelectedElements().length).toBe(1);
  452. expect(h.state.editingGroupId).not.toBe(null);
  453. });
  454. it("adjusts z order when grouping", () => {
  455. const positions: number[][] = [];
  456. UI.clickTool("rectangle");
  457. mouse.down(10, 10);
  458. mouse.up(10, 10);
  459. positions.push(mouse.getPosition());
  460. UI.clickTool("rectangle");
  461. mouse.down(10, -10);
  462. mouse.up(10, 10);
  463. positions.push(mouse.getPosition());
  464. UI.clickTool("rectangle");
  465. mouse.down(10, -10);
  466. mouse.up(10, 10);
  467. positions.push(mouse.getPosition());
  468. const ids = h.elements.map((element) => element.id);
  469. mouse.restorePosition(...positions[0]);
  470. mouse.click();
  471. mouse.restorePosition(...positions[2]);
  472. Keyboard.withModifierKeys({ shift: true }, () => {
  473. mouse.click();
  474. });
  475. Keyboard.withModifierKeys({ ctrl: true }, () => {
  476. Keyboard.keyPress(KEYS.G);
  477. });
  478. expect(h.elements.map((element) => element.id)).toEqual([
  479. ids[1],
  480. ids[0],
  481. ids[2],
  482. ]);
  483. });
  484. it("supports nested groups", () => {
  485. const rectA = UI.createElement("rectangle", { position: 0, size: 50 });
  486. const rectB = UI.createElement("rectangle", { position: 100, size: 50 });
  487. const rectC = UI.createElement("rectangle", { position: 200, size: 50 });
  488. Keyboard.withModifierKeys({ ctrl: true }, () => {
  489. Keyboard.keyPress(KEYS.A);
  490. Keyboard.keyPress(KEYS.G);
  491. });
  492. mouse.doubleClickOn(rectC);
  493. Keyboard.withModifierKeys({ shift: true }, () => {
  494. mouse.clickOn(rectA);
  495. });
  496. Keyboard.withModifierKeys({ ctrl: true }, () => {
  497. Keyboard.keyPress(KEYS.G);
  498. });
  499. expect(rectC.groupIds.length).toBe(2);
  500. expect(rectA.groupIds).toEqual(rectC.groupIds);
  501. expect(rectB.groupIds).toEqual(rectA.groupIds.slice(1));
  502. mouse.click(0, 100);
  503. expect(API.getSelectedElements().length).toBe(0);
  504. mouse.clickOn(rectA);
  505. expect(API.getSelectedElements().length).toBe(3);
  506. expect(h.state.editingGroupId).toBe(null);
  507. mouse.doubleClickOn(rectA);
  508. expect(API.getSelectedElements().length).toBe(2);
  509. expect(h.state.editingGroupId).toBe(rectA.groupIds[1]);
  510. mouse.doubleClickOn(rectA);
  511. expect(API.getSelectedElements().length).toBe(1);
  512. expect(h.state.editingGroupId).toBe(rectA.groupIds[0]);
  513. // click outside current (sub)group
  514. mouse.clickOn(rectB);
  515. expect(API.getSelectedElements().length).toBe(3);
  516. mouse.doubleClickOn(rectB);
  517. expect(API.getSelectedElements().length).toBe(1);
  518. });
  519. it("updates fontSize & fontFamily appState", () => {
  520. UI.clickTool("text");
  521. expect(h.state.currentItemFontFamily).toEqual(FONT_FAMILY.Excalifont);
  522. fireEvent.click(screen.getByTitle(/code/i));
  523. expect(h.state.currentItemFontFamily).toEqual(FONT_FAMILY["Comic Shanns"]);
  524. });
  525. it("deselects selected element, on pointer up, when click hits element bounding box but doesn't hit the element", () => {
  526. UI.clickTool("ellipse");
  527. mouse.down();
  528. mouse.up(100, 100);
  529. expect(API.getSelectedElements().length).toBe(1);
  530. // hits bounding box without hitting element
  531. mouse.down(98, 98);
  532. mouse.up();
  533. expect(API.getSelectedElements().length).toBe(0);
  534. });
  535. it("switches selected element on pointer down", () => {
  536. UI.clickTool("rectangle");
  537. mouse.down();
  538. mouse.up(10, 10);
  539. UI.clickTool("ellipse");
  540. mouse.down(10, 10);
  541. mouse.up(10, 10);
  542. expect(API.getSelectedElement().type).toBe("ellipse");
  543. // pointer down on rectangle
  544. mouse.reset();
  545. mouse.down();
  546. expect(API.getSelectedElement().type).toBe("rectangle");
  547. });
  548. it("can drag element that covers another element, while another elem is selected", () => {
  549. UI.clickTool("rectangle");
  550. mouse.down(100, 100);
  551. mouse.up(200, 200);
  552. UI.clickTool("rectangle");
  553. mouse.reset();
  554. mouse.down(100, 100);
  555. mouse.up(200, 200);
  556. UI.clickTool("ellipse");
  557. mouse.reset();
  558. mouse.down(300, 300);
  559. mouse.up(350, 350);
  560. expect(API.getSelectedElement().type).toBe("ellipse");
  561. // pointer down on rectangle
  562. mouse.reset();
  563. mouse.down(100, 100);
  564. mouse.up(200, 200);
  565. expect(API.getSelectedElement().type).toBe("rectangle");
  566. });
  567. it("deselects selected element on pointer down when pointer doesn't hit any element", () => {
  568. UI.clickTool("rectangle");
  569. mouse.down();
  570. mouse.up(10, 10);
  571. expect(API.getSelectedElements().length).toBe(1);
  572. // pointer down on space without elements
  573. mouse.down(100, 100);
  574. expect(API.getSelectedElements().length).toBe(0);
  575. });
  576. it("Drags selected element when hitting only bounding box and keeps element selected", () => {
  577. UI.clickTool("ellipse");
  578. mouse.down();
  579. mouse.up(10, 10);
  580. const { x: prevX, y: prevY } = API.getSelectedElement();
  581. API.clearSelection();
  582. // drag element from point on bounding box that doesn't hit element
  583. mouse.reset();
  584. mouse.down(8, 8);
  585. mouse.up(25, 25);
  586. expect(API.getSelectedElement().x).toEqual(prevX + 25);
  587. expect(API.getSelectedElement().y).toEqual(prevY + 25);
  588. });
  589. it(
  590. "given selected element A with lower z-index than unselected element B and given B is partially over A " +
  591. "when clicking intersection between A and B " +
  592. "B should be selected on pointer up",
  593. () => {
  594. // set background color since default is transparent
  595. // and transparent elements can't be selected by clicking inside of them
  596. const rect1 = API.createElement({
  597. type: "rectangle",
  598. backgroundColor: "red",
  599. x: 0,
  600. y: 0,
  601. width: 1000,
  602. height: 1000,
  603. });
  604. const rect2 = API.createElement({
  605. type: "rectangle",
  606. backgroundColor: "red",
  607. x: 500,
  608. y: 500,
  609. width: 500,
  610. height: 500,
  611. });
  612. API.setElements([rect1, rect2]);
  613. mouse.select(rect1);
  614. // pointerdown on rect2 covering rect1 while rect1 is selected should
  615. // retain rect1 selection
  616. mouse.down(900, 900);
  617. expect(API.getSelectedElement().id).toBe(rect1.id);
  618. // pointerup should select rect2
  619. mouse.up();
  620. expect(API.getSelectedElement().id).toBe(rect2.id);
  621. },
  622. );
  623. it(
  624. "given selected element A with lower z-index than unselected element B and given B is partially over A " +
  625. "when dragging on intersection between A and B " +
  626. "A should be dragged and keep being selected",
  627. () => {
  628. const rect1 = API.createElement({
  629. type: "rectangle",
  630. backgroundColor: "red",
  631. x: 0,
  632. y: 0,
  633. width: 1000,
  634. height: 1000,
  635. });
  636. const rect2 = API.createElement({
  637. type: "rectangle",
  638. backgroundColor: "red",
  639. x: 500,
  640. y: 500,
  641. width: 500,
  642. height: 500,
  643. });
  644. API.setElements([rect1, rect2]);
  645. mouse.select(rect1);
  646. expect(API.getSelectedElement().id).toBe(rect1.id);
  647. const { x: prevX, y: prevY } = API.getSelectedElement();
  648. // pointer down on intersection between ellipse and rectangle
  649. mouse.down(900, 900);
  650. mouse.up(100, 100);
  651. expect(API.getSelectedElement().id).toBe(rect1.id);
  652. expect(API.getSelectedElement().x).toEqual(prevX + 100);
  653. expect(API.getSelectedElement().y).toEqual(prevY + 100);
  654. },
  655. );
  656. it("deselects group of selected elements on pointer down when pointer doesn't hit any element", () => {
  657. UI.clickTool("rectangle");
  658. mouse.down();
  659. mouse.up(10, 10);
  660. UI.clickTool("ellipse");
  661. mouse.down(100, 100);
  662. mouse.up(10, 10);
  663. // Selects first element without deselecting the second element
  664. // Second element is already selected because creating it was our last action
  665. mouse.reset();
  666. Keyboard.withModifierKeys({ shift: true }, () => {
  667. mouse.click(5, 5);
  668. });
  669. expect(API.getSelectedElements().length).toBe(2);
  670. // pointer down on space without elements
  671. mouse.reset();
  672. mouse.down(500, 500);
  673. expect(API.getSelectedElements().length).toBe(0);
  674. });
  675. it("switches from group of selected elements to another element on pointer down", () => {
  676. UI.clickTool("rectangle");
  677. mouse.down();
  678. mouse.up(10, 10);
  679. UI.clickTool("ellipse");
  680. mouse.down(100, 100);
  681. mouse.up(100, 100);
  682. UI.clickTool("diamond");
  683. mouse.down(100, 100);
  684. mouse.up(100, 100);
  685. // Selects ellipse without deselecting the diamond
  686. // Diamond is already selected because creating it was our last action
  687. mouse.reset();
  688. Keyboard.withModifierKeys({ shift: true }, () => {
  689. mouse.click(110, 160);
  690. });
  691. expect(API.getSelectedElements().length).toBe(2);
  692. // select rectangle
  693. mouse.reset();
  694. mouse.down();
  695. expect(API.getSelectedElement().type).toBe("rectangle");
  696. });
  697. it("deselects group of selected elements on pointer up when pointer hits common bounding box without hitting any element", () => {
  698. UI.clickTool("rectangle");
  699. mouse.down();
  700. mouse.up(10, 10);
  701. UI.clickTool("ellipse");
  702. mouse.down(100, 100);
  703. mouse.up(10, 10);
  704. // Selects first element without deselecting the second element
  705. // Second element is already selected because creating it was our last action
  706. mouse.reset();
  707. Keyboard.withModifierKeys({ shift: true }, () => {
  708. mouse.click(5, 5);
  709. });
  710. // pointer down on common bounding box without hitting any of the elements
  711. mouse.reset();
  712. mouse.down(50, 50);
  713. expect(API.getSelectedElements().length).toBe(2);
  714. mouse.up();
  715. expect(API.getSelectedElements().length).toBe(0);
  716. });
  717. it("drags selected elements from point inside common bounding box that doesn't hit any element and keeps elements selected after dragging", () => {
  718. UI.clickTool("rectangle");
  719. mouse.down();
  720. mouse.up(10, 10);
  721. UI.clickTool("ellipse");
  722. mouse.down(100, 100);
  723. mouse.up(10, 10);
  724. // Selects first element without deselecting the second element
  725. // Second element is already selected because creating it was our last action
  726. mouse.reset();
  727. Keyboard.withModifierKeys({ shift: true }, () => {
  728. mouse.click(5, 5);
  729. });
  730. expect(API.getSelectedElements().length).toBe(2);
  731. const { x: firstElementPrevX, y: firstElementPrevY } =
  732. API.getSelectedElements()[0];
  733. const { x: secondElementPrevX, y: secondElementPrevY } =
  734. API.getSelectedElements()[1];
  735. // drag elements from point on common bounding box that doesn't hit any of the elements
  736. mouse.reset();
  737. mouse.down(50, 50);
  738. mouse.up(25, 25);
  739. expect(API.getSelectedElements()[0].x).toEqual(firstElementPrevX + 25);
  740. expect(API.getSelectedElements()[0].y).toEqual(firstElementPrevY + 25);
  741. expect(API.getSelectedElements()[1].x).toEqual(secondElementPrevX + 25);
  742. expect(API.getSelectedElements()[1].y).toEqual(secondElementPrevY + 25);
  743. expect(API.getSelectedElements().length).toBe(2);
  744. });
  745. it(
  746. "given a group of selected elements with an element that is not selected inside the group common bounding box " +
  747. "when element that is not selected is clicked " +
  748. "should switch selection to not selected element on pointer up",
  749. () => {
  750. UI.clickTool("rectangle");
  751. mouse.down();
  752. mouse.up(10, 10);
  753. UI.clickTool("ellipse");
  754. mouse.down(100, 100);
  755. mouse.up(100, 100);
  756. UI.clickTool("diamond");
  757. mouse.down(100, 100);
  758. mouse.up(100, 100);
  759. // Selects rectangle without deselecting the diamond
  760. // Diamond is already selected because creating it was our last action
  761. mouse.reset();
  762. Keyboard.withModifierKeys({ shift: true }, () => {
  763. mouse.click();
  764. });
  765. // pointer down on ellipse
  766. mouse.down(110, 160);
  767. expect(API.getSelectedElements().length).toBe(2);
  768. mouse.up();
  769. expect(API.getSelectedElement().type).toBe("ellipse");
  770. },
  771. );
  772. it(
  773. "given a selected element A and a not selected element B with higher z-index than A " +
  774. "and given B partially overlaps A " +
  775. "when there's a shift-click on the overlapped section B is added to the selection",
  776. () => {
  777. UI.clickTool("rectangle");
  778. // change background color since default is transparent
  779. // and transparent elements can't be selected by clicking inside of them
  780. togglePopover("Background");
  781. UI.clickOnTestId("color-red");
  782. mouse.down();
  783. mouse.up(1000, 1000);
  784. // draw ellipse partially over rectangle.
  785. // since ellipse was created after rectangle it has an higher z-index.
  786. // we don't need to change background color again since change above
  787. // affects next drawn elements.
  788. UI.clickTool("ellipse");
  789. mouse.reset();
  790. mouse.down(500, 500);
  791. mouse.up(1000, 1000);
  792. // select rectangle
  793. mouse.reset();
  794. mouse.click();
  795. // click on intersection between ellipse and rectangle
  796. Keyboard.withModifierKeys({ shift: true }, () => {
  797. mouse.click(900, 900);
  798. });
  799. expect(API.getSelectedElements().length).toBe(2);
  800. },
  801. );
  802. it("shift click on selected element should deselect it on pointer up", () => {
  803. UI.clickTool("rectangle");
  804. mouse.down();
  805. mouse.up(10, 10);
  806. // Rectangle is already selected since creating
  807. // it was our last action
  808. Keyboard.withModifierKeys({ shift: true }, () => {
  809. mouse.down(-8, -8);
  810. });
  811. expect(API.getSelectedElements().length).toBe(1);
  812. Keyboard.withModifierKeys({ shift: true }, () => {
  813. mouse.up();
  814. });
  815. expect(API.getSelectedElements().length).toBe(0);
  816. });
  817. it("single-clicking on a subgroup of a selected group should not alter selection", () => {
  818. const rect1 = UI.createElement("rectangle", {
  819. x: 10,
  820. });
  821. const rect2 = UI.createElement("rectangle", {
  822. x: 50,
  823. });
  824. UI.group([rect1, rect2]);
  825. const rect3 = UI.createElement("rectangle", {
  826. x: 10,
  827. y: 50,
  828. });
  829. const rect4 = UI.createElement("rectangle", {
  830. x: 50,
  831. y: 50,
  832. });
  833. UI.group([rect3, rect4]);
  834. Keyboard.withModifierKeys({ ctrl: true }, () => {
  835. Keyboard.keyPress(KEYS.A);
  836. Keyboard.keyPress(KEYS.G);
  837. });
  838. const selectedGroupIds_prev = h.state.selectedGroupIds;
  839. const selectedElements_prev = API.getSelectedElements();
  840. mouse.clickOn(rect3);
  841. expect(h.state.selectedGroupIds).toEqual(selectedGroupIds_prev);
  842. expect(API.getSelectedElements()).toEqual(selectedElements_prev);
  843. });
  844. it("deleting last but one element in editing group should unselect the group", () => {
  845. const rect1 = UI.createElement("rectangle", { x: 10 });
  846. const rect2 = UI.createElement("rectangle", { x: 50 });
  847. UI.group([rect1, rect2]);
  848. mouse.doubleClickOn(rect1);
  849. Keyboard.keyDown(KEYS.DELETE);
  850. // Clicking on the deleted element, hence in the empty space
  851. mouse.clickOn(rect1);
  852. expect(h.state.selectedGroupIds).toEqual({});
  853. expect(API.getSelectedElements()).toEqual([]);
  854. // Clicking back in and expecting no group selection
  855. mouse.clickOn(rect2);
  856. expect(h.state.selectedGroupIds).toEqual({ [rect2.groupIds[0]]: false });
  857. expect(API.getSelectedElements()).toEqual([rect2.get()]);
  858. });
  859. it("Cmd/Ctrl-click exclusively select element under pointer", () => {
  860. const rect1 = UI.createElement("rectangle", { x: 0 });
  861. const rect2 = UI.createElement("rectangle", { x: 30 });
  862. UI.group([rect1, rect2]);
  863. assertSelectedElements(rect1, rect2);
  864. Keyboard.withModifierKeys({ ctrl: true }, () => {
  865. mouse.clickOn(rect1);
  866. });
  867. assertSelectedElements(rect1);
  868. API.clearSelection();
  869. Keyboard.withModifierKeys({ ctrl: true }, () => {
  870. mouse.clickOn(rect1);
  871. });
  872. assertSelectedElements(rect1);
  873. const rect3 = UI.createElement("rectangle", { x: 60 });
  874. UI.group([rect1, rect3]);
  875. assertSelectedElements(rect1, rect2, rect3);
  876. mouse.reset();
  877. Keyboard.withModifierKeys({ ctrl: true }, () => {
  878. mouse.click(10, 5);
  879. });
  880. assertSelectedElements(rect1);
  881. API.clearSelection();
  882. Keyboard.withModifierKeys({ ctrl: true }, () => {
  883. mouse.clickOn(rect3);
  884. });
  885. assertSelectedElements(rect3);
  886. });
  887. });
  888. it(
  889. "given element A and group of elements B and given both are selected " +
  890. "when user clicks on B, on pointer up " +
  891. "only elements from B should be selected",
  892. () => {
  893. const rect1 = UI.createElement("rectangle", { y: 0 });
  894. const rect2 = UI.createElement("rectangle", { y: 30 });
  895. const rect3 = UI.createElement("rectangle", { y: 60 });
  896. UI.group([rect1, rect3]);
  897. expect(API.getSelectedElements().length).toBe(2);
  898. expect(Object.keys(h.state.selectedGroupIds).length).toBe(1);
  899. // Select second rectangle without deselecting group
  900. Keyboard.withModifierKeys({ shift: true }, () => {
  901. mouse.clickOn(rect2);
  902. });
  903. expect(API.getSelectedElements().length).toBe(3);
  904. // clicking on first rectangle that is part of the group should select
  905. // that group (exclusively)
  906. mouse.clickOn(rect1);
  907. expect(API.getSelectedElements().length).toBe(2);
  908. expect(Object.keys(h.state.selectedGroupIds).length).toBe(1);
  909. },
  910. );
  911. it(
  912. "given element A and group of elements B and given both are selected " +
  913. "when user shift-clicks on B, on pointer up " +
  914. "only element A should be selected",
  915. () => {
  916. UI.clickTool("rectangle");
  917. mouse.down();
  918. mouse.up(100, 100);
  919. UI.clickTool("rectangle");
  920. mouse.down(10, 10);
  921. mouse.up(100, 100);
  922. UI.clickTool("rectangle");
  923. mouse.down(10, 10);
  924. mouse.up(100, 100);
  925. // Select first rectangle while keeping third one selected.
  926. // Third rectangle is selected because it was the last element to be created.
  927. mouse.reset();
  928. Keyboard.withModifierKeys({ shift: true }, () => {
  929. mouse.click();
  930. });
  931. // Create group with first and third rectangle
  932. Keyboard.withModifierKeys({ ctrl: true }, () => {
  933. Keyboard.keyPress(KEYS.G);
  934. });
  935. expect(API.getSelectedElements().length).toBe(2);
  936. const selectedGroupIds = Object.keys(h.state.selectedGroupIds);
  937. expect(selectedGroupIds.length).toBe(1);
  938. // Select second rectangle without deselecting group
  939. Keyboard.withModifierKeys({ shift: true }, () => {
  940. mouse.click(110, 110);
  941. });
  942. expect(API.getSelectedElements().length).toBe(3);
  943. // Pointer down o first rectangle that is part of the group
  944. mouse.reset();
  945. Keyboard.withModifierKeys({ shift: true }, () => {
  946. mouse.down();
  947. });
  948. expect(API.getSelectedElements().length).toBe(3);
  949. Keyboard.withModifierKeys({ shift: true }, () => {
  950. mouse.up();
  951. });
  952. expect(API.getSelectedElements().length).toBe(1);
  953. },
  954. );