regressionTests.test.tsx 45 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563
  1. import { queryAllByText, queryByText } from "@testing-library/react";
  2. import React from "react";
  3. import ReactDOM from "react-dom";
  4. import { copiedStyles } from "../actions/actionStyles";
  5. import { ShortcutName } from "../actions/shortcuts";
  6. import { ExcalidrawElement } from "../element/types";
  7. import { CODES, KEYS } from "../keys";
  8. import ExcalidrawApp from "../excalidraw-app";
  9. import { reseed } from "../random";
  10. import * as Renderer from "../renderer/renderScene";
  11. import { setDateTimeForTests } from "../utils";
  12. import { API } from "./helpers/api";
  13. import { Keyboard, Pointer, UI } from "./helpers/ui";
  14. import {
  15. fireEvent,
  16. GlobalTestState,
  17. render,
  18. screen,
  19. waitFor,
  20. } from "./test-utils";
  21. import { defaultLang } from "../i18n";
  22. const { h } = window;
  23. const renderScene = jest.spyOn(Renderer, "renderScene");
  24. const assertSelectedElements = (...elements: ExcalidrawElement[]) => {
  25. expect(
  26. API.getSelectedElements().map((element) => {
  27. return element.id;
  28. }),
  29. ).toEqual(expect.arrayContaining(elements.map((element) => element.id)));
  30. };
  31. const mouse = new Pointer("mouse");
  32. const finger1 = new Pointer("touch", 1);
  33. const finger2 = new Pointer("touch", 2);
  34. const clickLabeledElement = (label: string) => {
  35. const element = document.querySelector(`[aria-label='${label}']`);
  36. if (!element) {
  37. throw new Error(`No labeled element found: ${label}`);
  38. }
  39. fireEvent.click(element);
  40. };
  41. /**
  42. * This is always called at the end of your test, so usually you don't need to call it.
  43. * However, if you have a long test, you might want to call it during the test so it's easier
  44. * to debug where a test failure came from.
  45. */
  46. const checkpoint = (name: string) => {
  47. expect(renderScene.mock.calls.length).toMatchSnapshot(
  48. `[${name}] number of renders`,
  49. );
  50. expect(h.state).toMatchSnapshot(`[${name}] appState`);
  51. expect(h.history.getSnapshotForTest()).toMatchSnapshot(`[${name}] history`);
  52. expect(h.elements.length).toMatchSnapshot(`[${name}] number of elements`);
  53. h.elements.forEach((element, i) =>
  54. expect(element).toMatchSnapshot(`[${name}] element ${i}`),
  55. );
  56. };
  57. beforeEach(async () => {
  58. // Unmount ReactDOM from root
  59. ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
  60. localStorage.clear();
  61. renderScene.mockClear();
  62. h.history.clear();
  63. reseed(7);
  64. setDateTimeForTests("201933152653");
  65. mouse.reset();
  66. finger1.reset();
  67. finger2.reset();
  68. await render(<ExcalidrawApp />);
  69. });
  70. afterEach(() => {
  71. checkpoint("end of test");
  72. });
  73. describe("regression tests", () => {
  74. it("draw every type of shape", () => {
  75. UI.clickTool("rectangle");
  76. mouse.down(10, -10);
  77. mouse.up(20, 10);
  78. UI.clickTool("diamond");
  79. mouse.down(10, -10);
  80. mouse.up(20, 10);
  81. UI.clickTool("ellipse");
  82. mouse.down(10, -10);
  83. mouse.up(20, 10);
  84. UI.clickTool("arrow");
  85. mouse.down(40, -10);
  86. mouse.up(50, 10);
  87. UI.clickTool("line");
  88. mouse.down(40, -10);
  89. mouse.up(50, 10);
  90. UI.clickTool("arrow");
  91. mouse.click(40, -10);
  92. mouse.click(50, 10);
  93. mouse.click(30, 10);
  94. Keyboard.keyPress(KEYS.ENTER);
  95. UI.clickTool("line");
  96. mouse.click(40, -20);
  97. mouse.click(50, 10);
  98. mouse.click(30, 10);
  99. Keyboard.keyPress(KEYS.ENTER);
  100. UI.clickTool("draw");
  101. mouse.down(40, -20);
  102. mouse.up(50, 10);
  103. expect(h.elements.map((element) => element.type)).toEqual([
  104. "rectangle",
  105. "diamond",
  106. "ellipse",
  107. "arrow",
  108. "line",
  109. "arrow",
  110. "line",
  111. "draw",
  112. ]);
  113. });
  114. it("click to select a shape", () => {
  115. UI.clickTool("rectangle");
  116. mouse.down(10, 10);
  117. mouse.up(10, 10);
  118. const firstRectPos = mouse.getPosition();
  119. UI.clickTool("rectangle");
  120. mouse.down(10, -10);
  121. mouse.up(10, 10);
  122. const prevSelectedId = API.getSelectedElement().id;
  123. mouse.restorePosition(...firstRectPos);
  124. mouse.click();
  125. expect(API.getSelectedElement().id).not.toEqual(prevSelectedId);
  126. });
  127. for (const [keys, shape, shouldSelect] of [
  128. [`2${KEYS.R}`, "rectangle", true],
  129. [`3${KEYS.D}`, "diamond", true],
  130. [`4${KEYS.E}`, "ellipse", true],
  131. [`5${KEYS.A}`, "arrow", true],
  132. [`6${KEYS.L}`, "line", true],
  133. [`7${KEYS.X}`, "draw", false],
  134. ] as [string, ExcalidrawElement["type"], boolean][]) {
  135. for (const key of keys) {
  136. it(`key ${key} selects ${shape} tool`, () => {
  137. Keyboard.keyPress(key);
  138. expect(h.state.elementType).toBe(shape);
  139. mouse.down(10, 10);
  140. mouse.up(10, 10);
  141. if (shouldSelect) {
  142. expect(API.getSelectedElement().type).toBe(shape);
  143. }
  144. });
  145. }
  146. }
  147. it("change the properties of a shape", () => {
  148. UI.clickTool("rectangle");
  149. mouse.down(10, 10);
  150. mouse.up(10, 10);
  151. clickLabeledElement("Background");
  152. clickLabeledElement("#fa5252");
  153. clickLabeledElement("Stroke");
  154. clickLabeledElement("#5f3dc4");
  155. expect(API.getSelectedElement().backgroundColor).toBe("#fa5252");
  156. expect(API.getSelectedElement().strokeColor).toBe("#5f3dc4");
  157. });
  158. it("click on an element and drag it", () => {
  159. UI.clickTool("rectangle");
  160. mouse.down(10, 10);
  161. mouse.up(10, 10);
  162. const { x: prevX, y: prevY } = API.getSelectedElement();
  163. mouse.down(-10, -10);
  164. mouse.up(10, 10);
  165. const { x: nextX, y: nextY } = API.getSelectedElement();
  166. expect(nextX).toBeGreaterThan(prevX);
  167. expect(nextY).toBeGreaterThan(prevY);
  168. checkpoint("dragged");
  169. mouse.down();
  170. mouse.up(-10, -10);
  171. const { x, y } = API.getSelectedElement();
  172. expect(x).toBe(prevX);
  173. expect(y).toBe(prevY);
  174. });
  175. it("alt-drag duplicates an element", () => {
  176. UI.clickTool("rectangle");
  177. mouse.down(10, 10);
  178. mouse.up(10, 10);
  179. expect(
  180. h.elements.filter((element) => element.type === "rectangle").length,
  181. ).toBe(1);
  182. Keyboard.withModifierKeys({ alt: true }, () => {
  183. mouse.down(-10, -10);
  184. mouse.up(10, 10);
  185. });
  186. expect(
  187. h.elements.filter((element) => element.type === "rectangle").length,
  188. ).toBe(2);
  189. });
  190. it("click-drag to select a group", () => {
  191. UI.clickTool("rectangle");
  192. mouse.down(10, 10);
  193. mouse.up(10, 10);
  194. UI.clickTool("rectangle");
  195. mouse.down(10, -10);
  196. mouse.up(10, 10);
  197. const finalPosition = mouse.getPosition();
  198. UI.clickTool("rectangle");
  199. mouse.down(10, -10);
  200. mouse.up(10, 10);
  201. mouse.restorePosition(0, 0);
  202. mouse.down();
  203. mouse.restorePosition(...finalPosition);
  204. mouse.up(5, 5);
  205. expect(
  206. h.elements.filter((element) => h.state.selectedElementIds[element.id])
  207. .length,
  208. ).toBe(2);
  209. });
  210. it("shift-click to multiselect, then drag", () => {
  211. UI.clickTool("rectangle");
  212. mouse.down(10, 10);
  213. mouse.up(10, 10);
  214. UI.clickTool("rectangle");
  215. mouse.down(10, -10);
  216. mouse.up(10, 10);
  217. const prevRectsXY = h.elements
  218. .filter((element) => element.type === "rectangle")
  219. .map((element) => ({ x: element.x, y: element.y }));
  220. mouse.reset();
  221. mouse.click(10, 10);
  222. Keyboard.withModifierKeys({ shift: true }, () => {
  223. mouse.click(20, 0);
  224. });
  225. mouse.down();
  226. mouse.up(10, 10);
  227. h.elements
  228. .filter((element) => element.type === "rectangle")
  229. .forEach((element, i) => {
  230. expect(element.x).toBeGreaterThan(prevRectsXY[i].x);
  231. expect(element.y).toBeGreaterThan(prevRectsXY[i].y);
  232. });
  233. });
  234. it("pinch-to-zoom works", () => {
  235. expect(h.state.zoom.value).toBe(1);
  236. finger1.down(50, 50);
  237. finger2.down(60, 50);
  238. finger1.move(-10, 0);
  239. expect(h.state.zoom.value).toBeGreaterThan(1);
  240. const zoomed = h.state.zoom.value;
  241. finger1.move(5, 0);
  242. finger2.move(-5, 0);
  243. expect(h.state.zoom.value).toBeLessThan(zoomed);
  244. });
  245. it("two-finger scroll works", () => {
  246. const startScrollY = h.state.scrollY;
  247. finger1.down(50, 50);
  248. finger2.down(60, 50);
  249. finger1.up(0, -10);
  250. finger2.up(0, -10);
  251. expect(h.state.scrollY).toBeLessThan(startScrollY);
  252. const startScrollX = h.state.scrollX;
  253. finger1.restorePosition(50, 50);
  254. finger2.restorePosition(50, 60);
  255. finger1.down();
  256. finger2.down();
  257. finger1.up(10, 0);
  258. finger2.up(10, 0);
  259. expect(h.state.scrollX).toBeGreaterThan(startScrollX);
  260. });
  261. it("spacebar + drag scrolls the canvas", () => {
  262. const { scrollX: startScrollX, scrollY: startScrollY } = h.state;
  263. Keyboard.keyDown(KEYS.SPACE);
  264. mouse.down(50, 50);
  265. mouse.up(60, 60);
  266. Keyboard.keyUp(KEYS.SPACE);
  267. const { scrollX, scrollY } = h.state;
  268. expect(scrollX).not.toEqual(startScrollX);
  269. expect(scrollY).not.toEqual(startScrollY);
  270. });
  271. it("arrow keys", () => {
  272. UI.clickTool("rectangle");
  273. mouse.down(10, 10);
  274. mouse.up(10, 10);
  275. Keyboard.keyPress(KEYS.ARROW_LEFT);
  276. Keyboard.keyPress(KEYS.ARROW_LEFT);
  277. Keyboard.keyPress(KEYS.ARROW_RIGHT);
  278. Keyboard.keyPress(KEYS.ARROW_UP);
  279. Keyboard.keyPress(KEYS.ARROW_UP);
  280. Keyboard.keyPress(KEYS.ARROW_DOWN);
  281. expect(h.elements[0].x).toBe(9);
  282. expect(h.elements[0].y).toBe(9);
  283. });
  284. it("undo/redo drawing an element", () => {
  285. UI.clickTool("rectangle");
  286. mouse.down(10, -10);
  287. mouse.up(20, 10);
  288. UI.clickTool("rectangle");
  289. mouse.down(10, 0);
  290. mouse.up(30, 20);
  291. UI.clickTool("arrow");
  292. mouse.click(60, -10);
  293. mouse.click(60, 10);
  294. mouse.click(40, 10);
  295. Keyboard.keyPress(KEYS.ENTER);
  296. expect(h.elements.filter((element) => !element.isDeleted).length).toBe(3);
  297. Keyboard.withModifierKeys({ ctrl: true }, () => {
  298. Keyboard.keyPress(KEYS.Z);
  299. Keyboard.keyPress(KEYS.Z);
  300. });
  301. expect(h.elements.filter((element) => !element.isDeleted).length).toBe(2);
  302. Keyboard.withModifierKeys({ ctrl: true }, () => {
  303. Keyboard.keyPress(KEYS.Z);
  304. });
  305. expect(h.elements.filter((element) => !element.isDeleted).length).toBe(1);
  306. Keyboard.withModifierKeys({ ctrl: true, shift: true }, () => {
  307. Keyboard.keyPress(KEYS.Z);
  308. });
  309. expect(h.elements.filter((element) => !element.isDeleted).length).toBe(2);
  310. });
  311. it("noop interaction after undo shouldn't create history entry", () => {
  312. expect(API.getStateHistory().length).toBe(1);
  313. UI.clickTool("rectangle");
  314. mouse.down(10, 10);
  315. mouse.up(10, 10);
  316. const firstElementEndPoint = mouse.getPosition();
  317. UI.clickTool("rectangle");
  318. mouse.down(10, -10);
  319. mouse.up(10, 10);
  320. const secondElementEndPoint = mouse.getPosition();
  321. expect(API.getStateHistory().length).toBe(3);
  322. Keyboard.withModifierKeys({ ctrl: true }, () => {
  323. Keyboard.keyPress(KEYS.Z);
  324. });
  325. expect(API.getStateHistory().length).toBe(2);
  326. // clicking an element shouldn't add to history
  327. mouse.restorePosition(...firstElementEndPoint);
  328. mouse.click();
  329. expect(API.getStateHistory().length).toBe(2);
  330. Keyboard.withModifierKeys({ shift: true, ctrl: true }, () => {
  331. Keyboard.keyPress(KEYS.Z);
  332. });
  333. expect(API.getStateHistory().length).toBe(3);
  334. // clicking an element shouldn't add to history
  335. mouse.click();
  336. expect(API.getStateHistory().length).toBe(3);
  337. const firstSelectedElementId = API.getSelectedElement().id;
  338. // same for clicking the element just redo-ed
  339. mouse.restorePosition(...secondElementEndPoint);
  340. mouse.click();
  341. expect(API.getStateHistory().length).toBe(3);
  342. expect(API.getSelectedElement().id).not.toEqual(firstSelectedElementId);
  343. });
  344. it("zoom hotkeys", () => {
  345. expect(h.state.zoom.value).toBe(1);
  346. fireEvent.keyDown(document, { code: CODES.EQUAL, ctrlKey: true });
  347. fireEvent.keyUp(document, { code: CODES.EQUAL, ctrlKey: true });
  348. expect(h.state.zoom.value).toBeGreaterThan(1);
  349. fireEvent.keyDown(document, { code: CODES.MINUS, ctrlKey: true });
  350. fireEvent.keyUp(document, { code: CODES.MINUS, ctrlKey: true });
  351. expect(h.state.zoom.value).toBe(1);
  352. });
  353. it("rerenders UI on language change", async () => {
  354. // select rectangle tool to show properties menu
  355. UI.clickTool("rectangle");
  356. // english lang should display `thin` label
  357. expect(screen.queryByTitle(/thin/i)).not.toBeNull();
  358. fireEvent.change(document.querySelector(".dropdown-select__language")!, {
  359. target: { value: "de-DE" },
  360. });
  361. // switching to german, `thin` label should no longer exist
  362. await waitFor(() => expect(screen.queryByTitle(/thin/i)).toBeNull());
  363. // reset language
  364. fireEvent.change(document.querySelector(".dropdown-select__language")!, {
  365. target: { value: defaultLang.code },
  366. });
  367. // switching back to English
  368. await waitFor(() => expect(screen.queryByTitle(/thin/i)).not.toBeNull());
  369. });
  370. it("make a group and duplicate it", () => {
  371. UI.clickTool("rectangle");
  372. mouse.down(10, 10);
  373. mouse.up(10, 10);
  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. const end = mouse.getPosition();
  381. mouse.reset();
  382. mouse.down();
  383. mouse.restorePosition(...end);
  384. mouse.up();
  385. expect(h.elements.length).toBe(3);
  386. for (const element of h.elements) {
  387. expect(element.groupIds.length).toBe(0);
  388. expect(h.state.selectedElementIds[element.id]).toBe(true);
  389. }
  390. Keyboard.withModifierKeys({ ctrl: true }, () => {
  391. Keyboard.codePress(CODES.G);
  392. });
  393. for (const element of h.elements) {
  394. expect(element.groupIds.length).toBe(1);
  395. }
  396. Keyboard.withModifierKeys({ alt: true }, () => {
  397. mouse.restorePosition(...end);
  398. mouse.down();
  399. mouse.up(10, 10);
  400. });
  401. expect(h.elements.length).toBe(6);
  402. const groups = new Set();
  403. for (const element of h.elements) {
  404. for (const groupId of element.groupIds) {
  405. groups.add(groupId);
  406. }
  407. }
  408. expect(groups.size).toBe(2);
  409. });
  410. it("double click to edit a group", () => {
  411. UI.clickTool("rectangle");
  412. mouse.down(10, 10);
  413. mouse.up(10, 10);
  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. Keyboard.withModifierKeys({ ctrl: true }, () => {
  421. Keyboard.keyPress(KEYS.A);
  422. Keyboard.codePress(CODES.G);
  423. });
  424. expect(API.getSelectedElements().length).toBe(3);
  425. expect(h.state.editingGroupId).toBe(null);
  426. mouse.doubleClick();
  427. expect(API.getSelectedElements().length).toBe(1);
  428. expect(h.state.editingGroupId).not.toBe(null);
  429. });
  430. it("adjusts z order when grouping", () => {
  431. const positions = [];
  432. UI.clickTool("rectangle");
  433. mouse.down(10, 10);
  434. mouse.up(10, 10);
  435. positions.push(mouse.getPosition());
  436. UI.clickTool("rectangle");
  437. mouse.down(10, -10);
  438. mouse.up(10, 10);
  439. positions.push(mouse.getPosition());
  440. UI.clickTool("rectangle");
  441. mouse.down(10, -10);
  442. mouse.up(10, 10);
  443. positions.push(mouse.getPosition());
  444. const ids = h.elements.map((element) => element.id);
  445. mouse.restorePosition(...positions[0]);
  446. mouse.click();
  447. mouse.restorePosition(...positions[2]);
  448. Keyboard.withModifierKeys({ shift: true }, () => {
  449. mouse.click();
  450. });
  451. Keyboard.withModifierKeys({ ctrl: true }, () => {
  452. Keyboard.codePress(CODES.G);
  453. });
  454. expect(h.elements.map((element) => element.id)).toEqual([
  455. ids[1],
  456. ids[0],
  457. ids[2],
  458. ]);
  459. });
  460. it("supports nested groups", () => {
  461. const rectA = UI.createElement("rectangle", { position: 0, size: 50 });
  462. const rectB = UI.createElement("rectangle", { position: 100, size: 50 });
  463. const rectC = UI.createElement("rectangle", { position: 200, size: 50 });
  464. Keyboard.withModifierKeys({ ctrl: true }, () => {
  465. Keyboard.keyPress(KEYS.A);
  466. Keyboard.codePress(CODES.G);
  467. });
  468. mouse.doubleClickOn(rectC);
  469. Keyboard.withModifierKeys({ shift: true }, () => {
  470. mouse.clickOn(rectA);
  471. });
  472. Keyboard.withModifierKeys({ ctrl: true }, () => {
  473. Keyboard.codePress(CODES.G);
  474. });
  475. expect(rectC.groupIds.length).toBe(2);
  476. expect(rectA.groupIds).toEqual(rectC.groupIds);
  477. expect(rectB.groupIds).toEqual(rectA.groupIds.slice(1));
  478. mouse.click(0, 100);
  479. expect(API.getSelectedElements().length).toBe(0);
  480. mouse.clickOn(rectA);
  481. expect(API.getSelectedElements().length).toBe(3);
  482. expect(h.state.editingGroupId).toBe(null);
  483. mouse.doubleClickOn(rectA);
  484. expect(API.getSelectedElements().length).toBe(2);
  485. expect(h.state.editingGroupId).toBe(rectA.groupIds[1]);
  486. mouse.doubleClickOn(rectA);
  487. expect(API.getSelectedElements().length).toBe(1);
  488. expect(h.state.editingGroupId).toBe(rectA.groupIds[0]);
  489. // click outside current (sub)group
  490. mouse.clickOn(rectB);
  491. expect(API.getSelectedElements().length).toBe(3);
  492. mouse.doubleClickOn(rectB);
  493. expect(API.getSelectedElements().length).toBe(1);
  494. });
  495. it("updates fontSize & fontFamily appState", () => {
  496. UI.clickTool("text");
  497. expect(h.state.currentItemFontFamily).toEqual(1); // Virgil
  498. fireEvent.click(screen.getByText(/code/i));
  499. expect(h.state.currentItemFontFamily).toEqual(3); // Cascadia
  500. });
  501. it("shows context menu for canvas", () => {
  502. fireEvent.contextMenu(GlobalTestState.canvas, {
  503. button: 2,
  504. clientX: 1,
  505. clientY: 1,
  506. });
  507. const contextMenu = document.querySelector(".context-menu");
  508. const contextMenuOptions = document.querySelectorAll(".context-menu li");
  509. const expectedShortcutNames: ShortcutName[] = [
  510. "selectAll",
  511. "gridMode",
  512. "zenMode",
  513. "viewMode",
  514. "stats",
  515. ];
  516. expect(contextMenu).not.toBeNull();
  517. expect(contextMenuOptions.length).toBe(expectedShortcutNames.length);
  518. expectedShortcutNames.forEach((shortcutName) => {
  519. expect(
  520. contextMenu?.querySelector(`li[data-testid="${shortcutName}"]`),
  521. ).not.toBeNull();
  522. });
  523. });
  524. it("shows context menu for element", () => {
  525. UI.clickTool("rectangle");
  526. mouse.down(10, 10);
  527. mouse.up(20, 20);
  528. fireEvent.contextMenu(GlobalTestState.canvas, {
  529. button: 2,
  530. clientX: 1,
  531. clientY: 1,
  532. });
  533. const contextMenu = document.querySelector(".context-menu");
  534. const contextMenuOptions = document.querySelectorAll(".context-menu li");
  535. const expectedShortcutNames: ShortcutName[] = [
  536. "copyStyles",
  537. "pasteStyles",
  538. "deleteSelectedElements",
  539. "addToLibrary",
  540. "sendBackward",
  541. "bringForward",
  542. "sendToBack",
  543. "bringToFront",
  544. "duplicateSelection",
  545. ];
  546. expect(contextMenu).not.toBeNull();
  547. expect(contextMenuOptions.length).toBe(expectedShortcutNames.length);
  548. expectedShortcutNames.forEach((shortcutName) => {
  549. expect(
  550. contextMenu?.querySelector(`li[data-testid="${shortcutName}"]`),
  551. ).not.toBeNull();
  552. });
  553. });
  554. it("shows 'Group selection' in context menu for multiple selected elements", () => {
  555. UI.clickTool("rectangle");
  556. mouse.down(10, 10);
  557. mouse.up(10, 10);
  558. UI.clickTool("rectangle");
  559. mouse.down(10, -10);
  560. mouse.up(10, 10);
  561. mouse.reset();
  562. mouse.click(10, 10);
  563. Keyboard.withModifierKeys({ shift: true }, () => {
  564. mouse.click(20, 0);
  565. });
  566. fireEvent.contextMenu(GlobalTestState.canvas, {
  567. button: 2,
  568. clientX: 1,
  569. clientY: 1,
  570. });
  571. const contextMenu = document.querySelector(".context-menu");
  572. const contextMenuOptions = document.querySelectorAll(".context-menu li");
  573. const expectedShortcutNames: ShortcutName[] = [
  574. "copyStyles",
  575. "pasteStyles",
  576. "deleteSelectedElements",
  577. "group",
  578. "addToLibrary",
  579. "sendBackward",
  580. "bringForward",
  581. "sendToBack",
  582. "bringToFront",
  583. "duplicateSelection",
  584. ];
  585. expect(contextMenu).not.toBeNull();
  586. expect(contextMenuOptions.length).toBe(expectedShortcutNames.length);
  587. expectedShortcutNames.forEach((shortcutName) => {
  588. expect(
  589. contextMenu?.querySelector(`li[data-testid="${shortcutName}"]`),
  590. ).not.toBeNull();
  591. });
  592. });
  593. it("shows 'Ungroup selection' in context menu for group inside selected elements", () => {
  594. UI.clickTool("rectangle");
  595. mouse.down(10, 10);
  596. mouse.up(10, 10);
  597. UI.clickTool("rectangle");
  598. mouse.down(10, -10);
  599. mouse.up(10, 10);
  600. mouse.reset();
  601. mouse.click(10, 10);
  602. Keyboard.withModifierKeys({ shift: true }, () => {
  603. mouse.click(20, 0);
  604. });
  605. Keyboard.withModifierKeys({ ctrl: true }, () => {
  606. Keyboard.codePress(CODES.G);
  607. });
  608. fireEvent.contextMenu(GlobalTestState.canvas, {
  609. button: 2,
  610. clientX: 1,
  611. clientY: 1,
  612. });
  613. const contextMenu = document.querySelector(".context-menu");
  614. const contextMenuOptions = document.querySelectorAll(".context-menu li");
  615. const expectedShortcutNames: ShortcutName[] = [
  616. "copyStyles",
  617. "pasteStyles",
  618. "deleteSelectedElements",
  619. "ungroup",
  620. "addToLibrary",
  621. "sendBackward",
  622. "bringForward",
  623. "sendToBack",
  624. "bringToFront",
  625. "duplicateSelection",
  626. ];
  627. expect(contextMenu).not.toBeNull();
  628. expect(contextMenuOptions.length).toBe(expectedShortcutNames.length);
  629. expectedShortcutNames.forEach((shortcutName) => {
  630. expect(
  631. contextMenu?.querySelector(`li[data-testid="${shortcutName}"]`),
  632. ).not.toBeNull();
  633. });
  634. });
  635. it("selecting 'Copy styles' in context menu copies styles", () => {
  636. UI.clickTool("rectangle");
  637. mouse.down(10, 10);
  638. mouse.up(20, 20);
  639. fireEvent.contextMenu(GlobalTestState.canvas, {
  640. button: 2,
  641. clientX: 1,
  642. clientY: 1,
  643. });
  644. const contextMenu = document.querySelector(".context-menu");
  645. expect(copiedStyles).toBe("{}");
  646. fireEvent.click(queryByText(contextMenu as HTMLElement, "Copy styles")!);
  647. expect(copiedStyles).not.toBe("{}");
  648. const element = JSON.parse(copiedStyles);
  649. expect(element).toEqual(API.getSelectedElement());
  650. });
  651. it("selecting 'Paste styles' in context menu pastes styles", () => {
  652. UI.clickTool("rectangle");
  653. mouse.down(10, 10);
  654. mouse.up(20, 20);
  655. UI.clickTool("rectangle");
  656. mouse.down(10, 10);
  657. mouse.up(20, 20);
  658. // Change some styles of second rectangle
  659. clickLabeledElement("Stroke");
  660. clickLabeledElement("#c92a2a");
  661. clickLabeledElement("Background");
  662. clickLabeledElement("#e64980");
  663. // Fill style
  664. fireEvent.click(screen.getByTitle("Cross-hatch"));
  665. // Stroke width
  666. fireEvent.click(screen.getByTitle("Bold"));
  667. // Stroke style
  668. fireEvent.click(screen.getByTitle("Dotted"));
  669. // Roughness
  670. fireEvent.click(screen.getByTitle("Cartoonist"));
  671. // Opacity
  672. fireEvent.change(screen.getByLabelText("Opacity"), {
  673. target: { value: "60" },
  674. });
  675. mouse.reset();
  676. // Copy styles of second rectangle
  677. fireEvent.contextMenu(GlobalTestState.canvas, {
  678. button: 2,
  679. clientX: 40,
  680. clientY: 40,
  681. });
  682. let contextMenu = document.querySelector(".context-menu");
  683. fireEvent.click(queryByText(contextMenu as HTMLElement, "Copy styles")!);
  684. const secondRect = JSON.parse(copiedStyles);
  685. expect(secondRect.id).toBe(h.elements[1].id);
  686. mouse.reset();
  687. // Paste styles to first rectangle
  688. fireEvent.contextMenu(GlobalTestState.canvas, {
  689. button: 2,
  690. clientX: 10,
  691. clientY: 10,
  692. });
  693. contextMenu = document.querySelector(".context-menu");
  694. fireEvent.click(queryByText(contextMenu as HTMLElement, "Paste styles")!);
  695. const firstRect = API.getSelectedElement();
  696. expect(firstRect.id).toBe(h.elements[0].id);
  697. expect(firstRect.strokeColor).toBe("#c92a2a");
  698. expect(firstRect.backgroundColor).toBe("#e64980");
  699. expect(firstRect.fillStyle).toBe("cross-hatch");
  700. expect(firstRect.strokeWidth).toBe(2); // Bold: 2
  701. expect(firstRect.strokeStyle).toBe("dotted");
  702. expect(firstRect.roughness).toBe(2); // Cartoonist: 2
  703. expect(firstRect.opacity).toBe(60);
  704. });
  705. it("selecting 'Delete' in context menu deletes element", () => {
  706. UI.clickTool("rectangle");
  707. mouse.down(10, 10);
  708. mouse.up(20, 20);
  709. fireEvent.contextMenu(GlobalTestState.canvas, {
  710. button: 2,
  711. clientX: 1,
  712. clientY: 1,
  713. });
  714. const contextMenu = document.querySelector(".context-menu");
  715. fireEvent.click(queryAllByText(contextMenu as HTMLElement, "Delete")[0]);
  716. expect(API.getSelectedElements()).toHaveLength(0);
  717. expect(h.elements[0].isDeleted).toBe(true);
  718. });
  719. it("selecting 'Add to library' in context menu adds element to library", async () => {
  720. UI.clickTool("rectangle");
  721. mouse.down(10, 10);
  722. mouse.up(20, 20);
  723. fireEvent.contextMenu(GlobalTestState.canvas, {
  724. button: 2,
  725. clientX: 1,
  726. clientY: 1,
  727. });
  728. const contextMenu = document.querySelector(".context-menu");
  729. fireEvent.click(queryByText(contextMenu as HTMLElement, "Add to library")!);
  730. await waitFor(() => {
  731. const library = localStorage.getItem("excalidraw-library");
  732. expect(library).not.toBeNull();
  733. const addedElement = JSON.parse(library!)[0][0];
  734. expect(addedElement).toEqual(h.elements[0]);
  735. });
  736. });
  737. it("selecting 'Duplicate' in context menu duplicates element", () => {
  738. UI.clickTool("rectangle");
  739. mouse.down(10, 10);
  740. mouse.up(20, 20);
  741. fireEvent.contextMenu(GlobalTestState.canvas, {
  742. button: 2,
  743. clientX: 1,
  744. clientY: 1,
  745. });
  746. const contextMenu = document.querySelector(".context-menu");
  747. fireEvent.click(queryByText(contextMenu as HTMLElement, "Duplicate")!);
  748. expect(h.elements).toHaveLength(2);
  749. const { id: _id0, seed: _seed0, x: _x0, y: _y0, ...rect1 } = h.elements[0];
  750. const { id: _id1, seed: _seed1, x: _x1, y: _y1, ...rect2 } = h.elements[1];
  751. expect(rect1).toEqual(rect2);
  752. });
  753. it("selecting 'Send backward' in context menu sends element backward", () => {
  754. UI.clickTool("rectangle");
  755. mouse.down(10, 10);
  756. mouse.up(20, 20);
  757. UI.clickTool("rectangle");
  758. mouse.down(10, 10);
  759. mouse.up(20, 20);
  760. mouse.reset();
  761. fireEvent.contextMenu(GlobalTestState.canvas, {
  762. button: 2,
  763. clientX: 40,
  764. clientY: 40,
  765. });
  766. const contextMenu = document.querySelector(".context-menu");
  767. const elementsBefore = h.elements;
  768. fireEvent.click(queryByText(contextMenu as HTMLElement, "Send backward")!);
  769. expect(elementsBefore[0].id).toEqual(h.elements[1].id);
  770. expect(elementsBefore[1].id).toEqual(h.elements[0].id);
  771. });
  772. it("selecting 'Bring forward' in context menu brings element forward", () => {
  773. UI.clickTool("rectangle");
  774. mouse.down(10, 10);
  775. mouse.up(20, 20);
  776. UI.clickTool("rectangle");
  777. mouse.down(10, 10);
  778. mouse.up(20, 20);
  779. mouse.reset();
  780. fireEvent.contextMenu(GlobalTestState.canvas, {
  781. button: 2,
  782. clientX: 10,
  783. clientY: 10,
  784. });
  785. const contextMenu = document.querySelector(".context-menu");
  786. const elementsBefore = h.elements;
  787. fireEvent.click(queryByText(contextMenu as HTMLElement, "Bring forward")!);
  788. expect(elementsBefore[0].id).toEqual(h.elements[1].id);
  789. expect(elementsBefore[1].id).toEqual(h.elements[0].id);
  790. });
  791. it("selecting 'Send to back' in context menu sends element to back", () => {
  792. UI.clickTool("rectangle");
  793. mouse.down(10, 10);
  794. mouse.up(20, 20);
  795. UI.clickTool("rectangle");
  796. mouse.down(10, 10);
  797. mouse.up(20, 20);
  798. mouse.reset();
  799. fireEvent.contextMenu(GlobalTestState.canvas, {
  800. button: 2,
  801. clientX: 40,
  802. clientY: 40,
  803. });
  804. const contextMenu = document.querySelector(".context-menu");
  805. const elementsBefore = h.elements;
  806. fireEvent.click(queryByText(contextMenu as HTMLElement, "Send to back")!);
  807. expect(elementsBefore[1].id).toEqual(h.elements[0].id);
  808. });
  809. it("selecting 'Bring to front' in context menu brings element to front", () => {
  810. UI.clickTool("rectangle");
  811. mouse.down(10, 10);
  812. mouse.up(20, 20);
  813. UI.clickTool("rectangle");
  814. mouse.down(10, 10);
  815. mouse.up(20, 20);
  816. mouse.reset();
  817. fireEvent.contextMenu(GlobalTestState.canvas, {
  818. button: 2,
  819. clientX: 10,
  820. clientY: 10,
  821. });
  822. const contextMenu = document.querySelector(".context-menu");
  823. const elementsBefore = h.elements;
  824. fireEvent.click(queryByText(contextMenu as HTMLElement, "Bring to front")!);
  825. expect(elementsBefore[0].id).toEqual(h.elements[1].id);
  826. });
  827. it("selecting 'Group selection' in context menu groups selected elements", () => {
  828. UI.clickTool("rectangle");
  829. mouse.down(10, 10);
  830. mouse.up(20, 20);
  831. UI.clickTool("rectangle");
  832. mouse.down(10, 10);
  833. mouse.up(20, 20);
  834. mouse.reset();
  835. Keyboard.withModifierKeys({ shift: true }, () => {
  836. mouse.click(10, 10);
  837. });
  838. fireEvent.contextMenu(GlobalTestState.canvas, {
  839. button: 2,
  840. clientX: 1,
  841. clientY: 1,
  842. });
  843. const contextMenu = document.querySelector(".context-menu");
  844. fireEvent.click(
  845. queryByText(contextMenu as HTMLElement, "Group selection")!,
  846. );
  847. const selectedGroupIds = Object.keys(h.state.selectedGroupIds);
  848. expect(h.elements[0].groupIds).toEqual(selectedGroupIds);
  849. expect(h.elements[1].groupIds).toEqual(selectedGroupIds);
  850. });
  851. it("selecting 'Ungroup selection' in context menu ungroups selected group", () => {
  852. UI.clickTool("rectangle");
  853. mouse.down(10, 10);
  854. mouse.up(20, 20);
  855. UI.clickTool("rectangle");
  856. mouse.down(10, 10);
  857. mouse.up(20, 20);
  858. mouse.reset();
  859. Keyboard.withModifierKeys({ shift: true }, () => {
  860. mouse.click(10, 10);
  861. });
  862. Keyboard.withModifierKeys({ ctrl: true }, () => {
  863. Keyboard.codePress(CODES.G);
  864. });
  865. fireEvent.contextMenu(GlobalTestState.canvas, {
  866. button: 2,
  867. clientX: 1,
  868. clientY: 1,
  869. });
  870. const contextMenu = document.querySelector(".context-menu");
  871. fireEvent.click(
  872. queryByText(contextMenu as HTMLElement, "Ungroup selection")!,
  873. );
  874. const selectedGroupIds = Object.keys(h.state.selectedGroupIds);
  875. expect(selectedGroupIds).toHaveLength(0);
  876. expect(h.elements[0].groupIds).toHaveLength(0);
  877. expect(h.elements[1].groupIds).toHaveLength(0);
  878. });
  879. it("deselects selected element, on pointer up, when click hits element bounding box but doesn't hit the element", () => {
  880. UI.clickTool("ellipse");
  881. mouse.down();
  882. mouse.up(100, 100);
  883. // hits bounding box without hitting element
  884. mouse.down();
  885. expect(API.getSelectedElements().length).toBe(1);
  886. mouse.up();
  887. expect(API.getSelectedElements().length).toBe(0);
  888. });
  889. it("switches selected element on pointer down", () => {
  890. UI.clickTool("rectangle");
  891. mouse.down();
  892. mouse.up(10, 10);
  893. UI.clickTool("ellipse");
  894. mouse.down(10, 10);
  895. mouse.up(10, 10);
  896. expect(API.getSelectedElement().type).toBe("ellipse");
  897. // pointer down on rectangle
  898. mouse.reset();
  899. mouse.down();
  900. expect(API.getSelectedElement().type).toBe("rectangle");
  901. });
  902. it("can drag element that covers another element, while another elem is selected", () => {
  903. UI.clickTool("rectangle");
  904. mouse.down(100, 100);
  905. mouse.up(200, 200);
  906. UI.clickTool("rectangle");
  907. mouse.reset();
  908. mouse.down(100, 100);
  909. mouse.up(200, 200);
  910. UI.clickTool("ellipse");
  911. mouse.reset();
  912. mouse.down(300, 300);
  913. mouse.up(350, 350);
  914. expect(API.getSelectedElement().type).toBe("ellipse");
  915. // pointer down on rectangle
  916. mouse.reset();
  917. mouse.down(100, 100);
  918. mouse.up(200, 200);
  919. expect(API.getSelectedElement().type).toBe("rectangle");
  920. });
  921. it("deselects selected element on pointer down when pointer doesn't hit any element", () => {
  922. UI.clickTool("rectangle");
  923. mouse.down();
  924. mouse.up(10, 10);
  925. expect(API.getSelectedElements().length).toBe(1);
  926. // pointer down on space without elements
  927. mouse.down(100, 100);
  928. expect(API.getSelectedElements().length).toBe(0);
  929. });
  930. it("Drags selected element when hitting only bounding box and keeps element selected", () => {
  931. UI.clickTool("ellipse");
  932. mouse.down();
  933. mouse.up(10, 10);
  934. const { x: prevX, y: prevY } = API.getSelectedElement();
  935. // drag element from point on bounding box that doesn't hit element
  936. mouse.reset();
  937. mouse.down();
  938. mouse.up(25, 25);
  939. expect(API.getSelectedElement().x).toEqual(prevX + 25);
  940. expect(API.getSelectedElement().y).toEqual(prevY + 25);
  941. });
  942. it(
  943. "given selected element A with lower z-index than unselected element B and given B is partially over A " +
  944. "when clicking intersection between A and B " +
  945. "B should be selected on pointer up",
  946. () => {
  947. UI.clickTool("rectangle");
  948. // change background color since default is transparent
  949. // and transparent elements can't be selected by clicking inside of them
  950. clickLabeledElement("Background");
  951. clickLabeledElement("#fa5252");
  952. mouse.down();
  953. mouse.up(1000, 1000);
  954. // draw ellipse partially over rectangle.
  955. // since ellipse was created after rectangle it has an higher z-index.
  956. // we don't need to change background color again since change above
  957. // affects next drawn elements.
  958. UI.clickTool("ellipse");
  959. mouse.reset();
  960. mouse.down(500, 500);
  961. mouse.up(1000, 1000);
  962. // select rectangle
  963. mouse.reset();
  964. mouse.click();
  965. // pointer down on intersection between ellipse and rectangle
  966. mouse.down(900, 900);
  967. expect(API.getSelectedElement().type).toBe("rectangle");
  968. mouse.up();
  969. expect(API.getSelectedElement().type).toBe("ellipse");
  970. },
  971. );
  972. it(
  973. "given selected element A with lower z-index than unselected element B and given B is partially over A " +
  974. "when dragging on intersection between A and B " +
  975. "A should be dragged and keep being selected",
  976. () => {
  977. UI.clickTool("rectangle");
  978. // change background color since default is transparent
  979. // and transparent elements can't be selected by clicking inside of them
  980. clickLabeledElement("Background");
  981. clickLabeledElement("#fa5252");
  982. mouse.down();
  983. mouse.up(1000, 1000);
  984. // draw ellipse partially over rectangle.
  985. // since ellipse was created after rectangle it has an higher z-index.
  986. // we don't need to change background color again since change above
  987. // affects next drawn elements.
  988. UI.clickTool("ellipse");
  989. mouse.reset();
  990. mouse.down(500, 500);
  991. mouse.up(1000, 1000);
  992. // select rectangle
  993. mouse.reset();
  994. mouse.click();
  995. const { x: prevX, y: prevY } = API.getSelectedElement();
  996. // pointer down on intersection between ellipse and rectangle
  997. mouse.down(900, 900);
  998. mouse.up(100, 100);
  999. expect(API.getSelectedElement().type).toBe("rectangle");
  1000. expect(API.getSelectedElement().x).toEqual(prevX + 100);
  1001. expect(API.getSelectedElement().y).toEqual(prevY + 100);
  1002. },
  1003. );
  1004. it("deselects group of selected elements on pointer down when pointer doesn't hit any element", () => {
  1005. UI.clickTool("rectangle");
  1006. mouse.down();
  1007. mouse.up(10, 10);
  1008. UI.clickTool("ellipse");
  1009. mouse.down(100, 100);
  1010. mouse.up(10, 10);
  1011. // Selects first element without deselecting the second element
  1012. // Second element is already selected because creating it was our last action
  1013. mouse.reset();
  1014. Keyboard.withModifierKeys({ shift: true }, () => {
  1015. mouse.click(5, 5);
  1016. });
  1017. expect(API.getSelectedElements().length).toBe(2);
  1018. // pointer down on space without elements
  1019. mouse.reset();
  1020. mouse.down(500, 500);
  1021. expect(API.getSelectedElements().length).toBe(0);
  1022. });
  1023. it("switches from group of selected elements to another element on pointer down", () => {
  1024. UI.clickTool("rectangle");
  1025. mouse.down();
  1026. mouse.up(10, 10);
  1027. UI.clickTool("ellipse");
  1028. mouse.down(100, 100);
  1029. mouse.up(100, 100);
  1030. UI.clickTool("diamond");
  1031. mouse.down(100, 100);
  1032. mouse.up(100, 100);
  1033. // Selects ellipse without deselecting the diamond
  1034. // Diamond is already selected because creating it was our last action
  1035. mouse.reset();
  1036. Keyboard.withModifierKeys({ shift: true }, () => {
  1037. mouse.click(110, 160);
  1038. });
  1039. expect(API.getSelectedElements().length).toBe(2);
  1040. // select rectangle
  1041. mouse.reset();
  1042. mouse.down();
  1043. expect(API.getSelectedElement().type).toBe("rectangle");
  1044. });
  1045. it("deselects group of selected elements on pointer up when pointer hits common bounding box without hitting any element", () => {
  1046. UI.clickTool("rectangle");
  1047. mouse.down();
  1048. mouse.up(10, 10);
  1049. UI.clickTool("ellipse");
  1050. mouse.down(100, 100);
  1051. mouse.up(10, 10);
  1052. // Selects first element without deselecting the second element
  1053. // Second element is already selected because creating it was our last action
  1054. mouse.reset();
  1055. Keyboard.withModifierKeys({ shift: true }, () => {
  1056. mouse.click(5, 5);
  1057. });
  1058. // pointer down on common bounding box without hitting any of the elements
  1059. mouse.reset();
  1060. mouse.down(50, 50);
  1061. expect(API.getSelectedElements().length).toBe(2);
  1062. mouse.up();
  1063. expect(API.getSelectedElements().length).toBe(0);
  1064. });
  1065. it(
  1066. "drags selected elements from point inside common bounding box that doesn't hit any element " +
  1067. "and keeps elements selected after dragging",
  1068. () => {
  1069. UI.clickTool("rectangle");
  1070. mouse.down();
  1071. mouse.up(10, 10);
  1072. UI.clickTool("ellipse");
  1073. mouse.down(100, 100);
  1074. mouse.up(10, 10);
  1075. // Selects first element without deselecting the second element
  1076. // Second element is already selected because creating it was our last action
  1077. mouse.reset();
  1078. Keyboard.withModifierKeys({ shift: true }, () => {
  1079. mouse.click(5, 5);
  1080. });
  1081. expect(API.getSelectedElements().length).toBe(2);
  1082. const {
  1083. x: firstElementPrevX,
  1084. y: firstElementPrevY,
  1085. } = API.getSelectedElements()[0];
  1086. const {
  1087. x: secondElementPrevX,
  1088. y: secondElementPrevY,
  1089. } = API.getSelectedElements()[1];
  1090. // drag elements from point on common bounding box that doesn't hit any of the elements
  1091. mouse.reset();
  1092. mouse.down(50, 50);
  1093. mouse.up(25, 25);
  1094. expect(API.getSelectedElements()[0].x).toEqual(firstElementPrevX + 25);
  1095. expect(API.getSelectedElements()[0].y).toEqual(firstElementPrevY + 25);
  1096. expect(API.getSelectedElements()[1].x).toEqual(secondElementPrevX + 25);
  1097. expect(API.getSelectedElements()[1].y).toEqual(secondElementPrevY + 25);
  1098. expect(API.getSelectedElements().length).toBe(2);
  1099. },
  1100. );
  1101. it(
  1102. "given a group of selected elements with an element that is not selected inside the group common bounding box " +
  1103. "when element that is not selected is clicked " +
  1104. "should switch selection to not selected element on pointer up",
  1105. () => {
  1106. UI.clickTool("rectangle");
  1107. mouse.down();
  1108. mouse.up(10, 10);
  1109. UI.clickTool("ellipse");
  1110. mouse.down(100, 100);
  1111. mouse.up(100, 100);
  1112. UI.clickTool("diamond");
  1113. mouse.down(100, 100);
  1114. mouse.up(100, 100);
  1115. // Selects rectangle without deselecting the diamond
  1116. // Diamond is already selected because creating it was our last action
  1117. mouse.reset();
  1118. Keyboard.withModifierKeys({ shift: true }, () => {
  1119. mouse.click();
  1120. });
  1121. // pointer down on ellipse
  1122. mouse.down(110, 160);
  1123. expect(API.getSelectedElements().length).toBe(2);
  1124. mouse.up();
  1125. expect(API.getSelectedElement().type).toBe("ellipse");
  1126. },
  1127. );
  1128. it(
  1129. "given a selected element A and a not selected element B with higher z-index than A " +
  1130. "and given B partialy overlaps A " +
  1131. "when there's a shift-click on the overlapped section B is added to the selection",
  1132. () => {
  1133. UI.clickTool("rectangle");
  1134. // change background color since default is transparent
  1135. // and transparent elements can't be selected by clicking inside of them
  1136. clickLabeledElement("Background");
  1137. clickLabeledElement("#fa5252");
  1138. mouse.down();
  1139. mouse.up(1000, 1000);
  1140. // draw ellipse partially over rectangle.
  1141. // since ellipse was created after rectangle it has an higher z-index.
  1142. // we don't need to change background color again since change above
  1143. // affects next drawn elements.
  1144. UI.clickTool("ellipse");
  1145. mouse.reset();
  1146. mouse.down(500, 500);
  1147. mouse.up(1000, 1000);
  1148. // select rectangle
  1149. mouse.reset();
  1150. mouse.click();
  1151. // click on intersection between ellipse and rectangle
  1152. Keyboard.withModifierKeys({ shift: true }, () => {
  1153. mouse.click(900, 900);
  1154. });
  1155. expect(API.getSelectedElements().length).toBe(2);
  1156. },
  1157. );
  1158. it("shift click on selected element should deselect it on pointer up", () => {
  1159. UI.clickTool("rectangle");
  1160. mouse.down();
  1161. mouse.up(10, 10);
  1162. // Rectangle is already selected since creating
  1163. // it was our last action
  1164. Keyboard.withModifierKeys({ shift: true }, () => {
  1165. mouse.down();
  1166. });
  1167. expect(API.getSelectedElements().length).toBe(1);
  1168. Keyboard.withModifierKeys({ shift: true }, () => {
  1169. mouse.up();
  1170. });
  1171. expect(API.getSelectedElements().length).toBe(0);
  1172. });
  1173. it("single-clicking on a subgroup of a selected group should not alter selection", () => {
  1174. const rect1 = UI.createElement("rectangle", { x: 10 });
  1175. const rect2 = UI.createElement("rectangle", { x: 50 });
  1176. UI.group([rect1, rect2]);
  1177. const rect3 = UI.createElement("rectangle", { x: 10, y: 50 });
  1178. const rect4 = UI.createElement("rectangle", { x: 50, y: 50 });
  1179. UI.group([rect3, rect4]);
  1180. Keyboard.withModifierKeys({ ctrl: true }, () => {
  1181. Keyboard.keyPress(KEYS.A);
  1182. Keyboard.codePress(CODES.G);
  1183. });
  1184. const selectedGroupIds_prev = h.state.selectedGroupIds;
  1185. const selectedElements_prev = API.getSelectedElements();
  1186. mouse.clickOn(rect3);
  1187. expect(h.state.selectedGroupIds).toEqual(selectedGroupIds_prev);
  1188. expect(API.getSelectedElements()).toEqual(selectedElements_prev);
  1189. });
  1190. it("Cmd/Ctrl-click exclusively select element under pointer", () => {
  1191. const rect1 = UI.createElement("rectangle", { x: 0 });
  1192. const rect2 = UI.createElement("rectangle", { x: 30 });
  1193. UI.group([rect1, rect2]);
  1194. assertSelectedElements(rect1, rect2);
  1195. Keyboard.withModifierKeys({ ctrl: true }, () => {
  1196. mouse.clickOn(rect1);
  1197. });
  1198. assertSelectedElements(rect1);
  1199. API.clearSelection();
  1200. Keyboard.withModifierKeys({ ctrl: true }, () => {
  1201. mouse.clickOn(rect1);
  1202. });
  1203. assertSelectedElements(rect1);
  1204. const rect3 = UI.createElement("rectangle", { x: 60 });
  1205. UI.group([rect1, rect3]);
  1206. assertSelectedElements(rect1, rect2, rect3);
  1207. Keyboard.withModifierKeys({ ctrl: true }, () => {
  1208. mouse.clickOn(rect1);
  1209. });
  1210. assertSelectedElements(rect1);
  1211. API.clearSelection();
  1212. Keyboard.withModifierKeys({ ctrl: true }, () => {
  1213. mouse.clickOn(rect3);
  1214. });
  1215. assertSelectedElements(rect3);
  1216. });
  1217. it("should show fill icons when element has non transparent background", () => {
  1218. UI.clickTool("rectangle");
  1219. expect(screen.queryByText(/fill/i)).not.toBeNull();
  1220. mouse.down();
  1221. mouse.up(10, 10);
  1222. expect(screen.queryByText(/fill/i)).toBeNull();
  1223. clickLabeledElement("Background");
  1224. clickLabeledElement("#fa5252");
  1225. // select rectangle
  1226. mouse.reset();
  1227. mouse.click();
  1228. expect(screen.queryByText(/fill/i)).not.toBeNull();
  1229. });
  1230. });
  1231. it(
  1232. "given element A and group of elements B and given both are selected " +
  1233. "when user clicks on B, on pointer up " +
  1234. "only elements from B should be selected",
  1235. () => {
  1236. const rect1 = UI.createElement("rectangle", { y: 0 });
  1237. const rect2 = UI.createElement("rectangle", { y: 30 });
  1238. const rect3 = UI.createElement("rectangle", { y: 60 });
  1239. UI.group([rect1, rect3]);
  1240. expect(API.getSelectedElements().length).toBe(2);
  1241. expect(Object.keys(h.state.selectedGroupIds).length).toBe(1);
  1242. // Select second rectangle without deselecting group
  1243. Keyboard.withModifierKeys({ shift: true }, () => {
  1244. mouse.clickOn(rect2);
  1245. });
  1246. expect(API.getSelectedElements().length).toBe(3);
  1247. // clicking on first rectangle that is part of the group should select
  1248. // that group (exclusively)
  1249. mouse.clickOn(rect1);
  1250. expect(API.getSelectedElements().length).toBe(2);
  1251. expect(Object.keys(h.state.selectedGroupIds).length).toBe(1);
  1252. },
  1253. );
  1254. it(
  1255. "given element A and group of elements B and given both are selected " +
  1256. "when user shift-clicks on B, on pointer up " +
  1257. "only element A should be selected",
  1258. () => {
  1259. UI.clickTool("rectangle");
  1260. mouse.down();
  1261. mouse.up(100, 100);
  1262. UI.clickTool("rectangle");
  1263. mouse.down(10, 10);
  1264. mouse.up(100, 100);
  1265. UI.clickTool("rectangle");
  1266. mouse.down(10, 10);
  1267. mouse.up(100, 100);
  1268. // Select first rectangle while keeping third one selected.
  1269. // Third rectangle is selected because it was the last element to be created.
  1270. mouse.reset();
  1271. Keyboard.withModifierKeys({ shift: true }, () => {
  1272. mouse.click();
  1273. });
  1274. // Create group with first and third rectangle
  1275. Keyboard.withModifierKeys({ ctrl: true }, () => {
  1276. Keyboard.codePress(CODES.G);
  1277. });
  1278. expect(API.getSelectedElements().length).toBe(2);
  1279. const selectedGroupIds = Object.keys(h.state.selectedGroupIds);
  1280. expect(selectedGroupIds.length).toBe(1);
  1281. // Select second rectangle without deselecting group
  1282. Keyboard.withModifierKeys({ shift: true }, () => {
  1283. mouse.click(110, 110);
  1284. });
  1285. expect(API.getSelectedElements().length).toBe(3);
  1286. // Pointer down o first rectangle that is part of the group
  1287. mouse.reset();
  1288. Keyboard.withModifierKeys({ shift: true }, () => {
  1289. mouse.down();
  1290. });
  1291. expect(API.getSelectedElements().length).toBe(3);
  1292. Keyboard.withModifierKeys({ shift: true }, () => {
  1293. mouse.up();
  1294. });
  1295. expect(API.getSelectedElements().length).toBe(1);
  1296. },
  1297. );