regressionTests.test.tsx 32 KB

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