flip.test.tsx 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886
  1. import ReactDOM from "react-dom";
  2. import {
  3. fireEvent,
  4. GlobalTestState,
  5. render,
  6. screen,
  7. waitFor,
  8. } from "./test-utils";
  9. import { UI, Pointer, Keyboard } from "./helpers/ui";
  10. import { API } from "./helpers/api";
  11. import { actionFlipHorizontal, actionFlipVertical } from "../actions";
  12. import { getElementAbsoluteCoords } from "../element";
  13. import type {
  14. ExcalidrawElement,
  15. ExcalidrawImageElement,
  16. ExcalidrawLinearElement,
  17. ExcalidrawTextElementWithContainer,
  18. FileId,
  19. } from "../element/types";
  20. import { newLinearElement } from "../element";
  21. import { Excalidraw } from "../index";
  22. import { mutateElement } from "../element/mutateElement";
  23. import type { NormalizedZoomValue } from "../types";
  24. import { ROUNDNESS } from "../constants";
  25. import { vi } from "vitest";
  26. import * as blob from "../data/blob";
  27. import { KEYS } from "../keys";
  28. import { getBoundTextElementPosition } from "../element/textElement";
  29. import { createPasteEvent } from "../clipboard";
  30. import { arrayToMap, cloneJSON } from "../utils";
  31. const { h } = window;
  32. const mouse = new Pointer("mouse");
  33. // This needs to fixed in vitest mock, as when importActual used with mock
  34. // the tests hangs - https://github.com/vitest-dev/vitest/issues/546.
  35. // But fortunately spying and mocking the return value of spy works :p
  36. const resizeImageFileSpy = vi.spyOn(blob, "resizeImageFile");
  37. const generateIdFromFileSpy = vi.spyOn(blob, "generateIdFromFile");
  38. resizeImageFileSpy.mockImplementation(async (imageFile: File) => imageFile);
  39. generateIdFromFileSpy.mockImplementation(async () => "fileId" as FileId);
  40. beforeEach(async () => {
  41. // Unmount ReactDOM from root
  42. ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
  43. mouse.reset();
  44. localStorage.clear();
  45. sessionStorage.clear();
  46. vi.clearAllMocks();
  47. Object.assign(document, {
  48. elementFromPoint: () => GlobalTestState.canvas,
  49. });
  50. await render(<Excalidraw autoFocus={true} handleKeyboardGlobally={true} />);
  51. h.setState({
  52. zoom: {
  53. value: 1 as NormalizedZoomValue,
  54. },
  55. });
  56. });
  57. const createAndSelectOneRectangle = (angle: number = 0) => {
  58. UI.createElement("rectangle", {
  59. x: 0,
  60. y: 0,
  61. width: 100,
  62. height: 50,
  63. angle,
  64. });
  65. };
  66. const createAndSelectOneDiamond = (angle: number = 0) => {
  67. UI.createElement("diamond", {
  68. x: 0,
  69. y: 0,
  70. width: 100,
  71. height: 50,
  72. angle,
  73. });
  74. };
  75. const createAndSelectOneEllipse = (angle: number = 0) => {
  76. UI.createElement("ellipse", {
  77. x: 0,
  78. y: 0,
  79. width: 100,
  80. height: 50,
  81. angle,
  82. });
  83. };
  84. const createAndSelectOneArrow = (angle: number = 0) => {
  85. UI.createElement("arrow", {
  86. x: 0,
  87. y: 0,
  88. width: 100,
  89. height: 50,
  90. angle,
  91. });
  92. };
  93. const createAndSelectOneLine = (angle: number = 0) => {
  94. UI.createElement("line", {
  95. x: 0,
  96. y: 0,
  97. width: 100,
  98. height: 50,
  99. angle,
  100. });
  101. };
  102. const createAndReturnOneDraw = (angle: number = 0) => {
  103. return UI.createElement("freedraw", {
  104. x: 0,
  105. y: 0,
  106. width: 50,
  107. height: 100,
  108. angle,
  109. });
  110. };
  111. const createLinearElementWithCurveInsideMinMaxPoints = (
  112. type: "line" | "arrow",
  113. extraProps: any = {},
  114. ) => {
  115. return newLinearElement({
  116. type,
  117. x: 2256.910668124894,
  118. y: -2412.5069664197654,
  119. width: 1750.4888916015625,
  120. height: 410.51605224609375,
  121. angle: 0,
  122. strokeColor: "#000000",
  123. backgroundColor: "#fa5252",
  124. fillStyle: "hachure",
  125. strokeWidth: 1,
  126. strokeStyle: "solid",
  127. roughness: 1,
  128. opacity: 100,
  129. groupIds: [],
  130. roundness: { type: ROUNDNESS.PROPORTIONAL_RADIUS },
  131. boundElements: null,
  132. link: null,
  133. locked: false,
  134. points: [
  135. [0, 0],
  136. [-922.4761962890625, 300.3277587890625],
  137. [828.0126953125, 410.51605224609375],
  138. ],
  139. startArrowhead: null,
  140. endArrowhead: null,
  141. });
  142. };
  143. const createLinearElementsWithCurveOutsideMinMaxPoints = (
  144. type: "line" | "arrow",
  145. extraProps: any = {},
  146. ) => {
  147. return newLinearElement({
  148. type,
  149. x: -1388.6555370382996,
  150. y: 1037.698247710191,
  151. width: 591.2804897585779,
  152. height: 69.32871961377737,
  153. angle: 0,
  154. strokeColor: "#000000",
  155. backgroundColor: "transparent",
  156. fillStyle: "hachure",
  157. strokeWidth: 1,
  158. strokeStyle: "solid",
  159. roughness: 1,
  160. opacity: 100,
  161. groupIds: [],
  162. roundness: { type: ROUNDNESS.PROPORTIONAL_RADIUS },
  163. boundElements: null,
  164. link: null,
  165. locked: false,
  166. points: [
  167. [0, 0],
  168. [-584.1485186423079, -15.365636022723947],
  169. [-591.2804897585779, 36.09360810181511],
  170. [-148.56510566829502, 53.96308359105342],
  171. ],
  172. startArrowhead: null,
  173. endArrowhead: null,
  174. ...extraProps,
  175. });
  176. };
  177. const checkElementsBoundingBox = async (
  178. element1: ExcalidrawElement,
  179. element2: ExcalidrawElement,
  180. toleranceInPx: number = 0,
  181. ) => {
  182. const elementsMap = arrayToMap([element1, element2]);
  183. const [x1, y1, x2, y2] = getElementAbsoluteCoords(element1, elementsMap);
  184. const [x12, y12, x22, y22] = getElementAbsoluteCoords(element2, elementsMap);
  185. await waitFor(() => {
  186. // Check if width and height did not change
  187. expect(x2 - x1).toBeCloseTo(x22 - x12, -1);
  188. expect(y2 - y1).toBeCloseTo(y22 - y12, -1);
  189. });
  190. };
  191. const checkHorizontalFlip = async (toleranceInPx: number = 0.00001) => {
  192. const originalElement = cloneJSON(h.elements[0]);
  193. h.app.actionManager.executeAction(actionFlipHorizontal);
  194. const newElement = h.elements[0];
  195. await checkElementsBoundingBox(originalElement, newElement, toleranceInPx);
  196. };
  197. const checkTwoPointsLineHorizontalFlip = async () => {
  198. const originalElement = cloneJSON(h.elements[0]) as ExcalidrawLinearElement;
  199. h.app.actionManager.executeAction(actionFlipHorizontal);
  200. const newElement = h.elements[0] as ExcalidrawLinearElement;
  201. await waitFor(() => {
  202. expect(originalElement.points[0][0]).toBeCloseTo(
  203. -newElement.points[0][0],
  204. 5,
  205. );
  206. expect(originalElement.points[0][1]).toBeCloseTo(
  207. newElement.points[0][1],
  208. 5,
  209. );
  210. expect(originalElement.points[1][0]).toBeCloseTo(
  211. -newElement.points[1][0],
  212. 5,
  213. );
  214. expect(originalElement.points[1][1]).toBeCloseTo(
  215. newElement.points[1][1],
  216. 5,
  217. );
  218. });
  219. };
  220. const checkTwoPointsLineVerticalFlip = async () => {
  221. const originalElement = cloneJSON(h.elements[0]) as ExcalidrawLinearElement;
  222. h.app.actionManager.executeAction(actionFlipVertical);
  223. const newElement = h.elements[0] as ExcalidrawLinearElement;
  224. await waitFor(() => {
  225. expect(originalElement.points[0][0]).toBeCloseTo(
  226. newElement.points[0][0],
  227. 5,
  228. );
  229. expect(originalElement.points[0][1]).toBeCloseTo(
  230. -newElement.points[0][1],
  231. 5,
  232. );
  233. expect(originalElement.points[1][0]).toBeCloseTo(
  234. newElement.points[1][0],
  235. 5,
  236. );
  237. expect(originalElement.points[1][1]).toBeCloseTo(
  238. -newElement.points[1][1],
  239. 5,
  240. );
  241. });
  242. };
  243. const checkRotatedHorizontalFlip = async (
  244. expectedAngle: number,
  245. toleranceInPx: number = 0.00001,
  246. ) => {
  247. const originalElement = cloneJSON(h.elements[0]);
  248. h.app.actionManager.executeAction(actionFlipHorizontal);
  249. const newElement = h.elements[0];
  250. await waitFor(() => {
  251. expect(newElement.angle).toBeCloseTo(expectedAngle);
  252. });
  253. await checkElementsBoundingBox(originalElement, newElement, toleranceInPx);
  254. };
  255. const checkRotatedVerticalFlip = async (
  256. expectedAngle: number,
  257. toleranceInPx: number = 0.00001,
  258. ) => {
  259. const originalElement = cloneJSON(h.elements[0]);
  260. h.app.actionManager.executeAction(actionFlipVertical);
  261. const newElement = h.elements[0];
  262. await waitFor(() => {
  263. expect(newElement.angle).toBeCloseTo(expectedAngle);
  264. });
  265. await checkElementsBoundingBox(originalElement, newElement, toleranceInPx);
  266. };
  267. const checkVerticalFlip = async (toleranceInPx: number = 0.00001) => {
  268. const originalElement = cloneJSON(h.elements[0]);
  269. h.app.actionManager.executeAction(actionFlipVertical);
  270. const newElement = h.elements[0];
  271. await checkElementsBoundingBox(originalElement, newElement, toleranceInPx);
  272. };
  273. const checkVerticalHorizontalFlip = async (toleranceInPx: number = 0.00001) => {
  274. const originalElement = cloneJSON(h.elements[0]);
  275. h.app.actionManager.executeAction(actionFlipHorizontal);
  276. h.app.actionManager.executeAction(actionFlipVertical);
  277. const newElement = h.elements[0];
  278. await checkElementsBoundingBox(originalElement, newElement, toleranceInPx);
  279. };
  280. const TWO_POINTS_LINEAR_ELEMENT_FLIP_TOLERANCE_IN_PIXELS = 5;
  281. const MULTIPOINT_LINEAR_ELEMENT_FLIP_TOLERANCE_IN_PIXELS = 20;
  282. // Rectangle element
  283. describe("rectangle", () => {
  284. it("flips an unrotated rectangle horizontally correctly", async () => {
  285. createAndSelectOneRectangle();
  286. await checkHorizontalFlip();
  287. });
  288. it("flips an unrotated rectangle vertically correctly", async () => {
  289. createAndSelectOneRectangle();
  290. await checkVerticalFlip();
  291. });
  292. it("flips a rotated rectangle horizontally correctly", async () => {
  293. const originalAngle = (3 * Math.PI) / 4;
  294. const expectedAngle = (5 * Math.PI) / 4;
  295. createAndSelectOneRectangle(originalAngle);
  296. await checkRotatedHorizontalFlip(expectedAngle);
  297. });
  298. it("flips a rotated rectangle vertically correctly", async () => {
  299. const originalAngle = (3 * Math.PI) / 4;
  300. const expectedAgnle = (5 * Math.PI) / 4;
  301. createAndSelectOneRectangle(originalAngle);
  302. await checkRotatedVerticalFlip(expectedAgnle);
  303. });
  304. });
  305. // Diamond element
  306. describe("diamond", () => {
  307. it("flips an unrotated diamond horizontally correctly", async () => {
  308. createAndSelectOneDiamond();
  309. await checkHorizontalFlip();
  310. });
  311. it("flips an unrotated diamond vertically correctly", async () => {
  312. createAndSelectOneDiamond();
  313. await checkVerticalFlip();
  314. });
  315. it("flips a rotated diamond horizontally correctly", async () => {
  316. const originalAngle = (5 * Math.PI) / 4;
  317. const expectedAngle = (3 * Math.PI) / 4;
  318. createAndSelectOneDiamond(originalAngle);
  319. await checkRotatedHorizontalFlip(expectedAngle);
  320. });
  321. it("flips a rotated diamond vertically correctly", async () => {
  322. const originalAngle = (5 * Math.PI) / 4;
  323. const expectedAngle = (3 * Math.PI) / 4;
  324. createAndSelectOneDiamond(originalAngle);
  325. await checkRotatedVerticalFlip(expectedAngle);
  326. });
  327. });
  328. // Ellipse element
  329. describe("ellipse", () => {
  330. it("flips an unrotated ellipse horizontally correctly", async () => {
  331. createAndSelectOneEllipse();
  332. await checkHorizontalFlip();
  333. });
  334. it("flips an unrotated ellipse vertically correctly", async () => {
  335. createAndSelectOneEllipse();
  336. await checkVerticalFlip();
  337. });
  338. it("flips a rotated ellipse horizontally correctly", async () => {
  339. const originalAngle = (7 * Math.PI) / 4;
  340. const expectedAngle = Math.PI / 4;
  341. createAndSelectOneEllipse(originalAngle);
  342. await checkRotatedHorizontalFlip(expectedAngle);
  343. });
  344. it("flips a rotated ellipse vertically correctly", async () => {
  345. const originalAngle = (7 * Math.PI) / 4;
  346. const expectedAngle = Math.PI / 4;
  347. createAndSelectOneEllipse(originalAngle);
  348. await checkRotatedVerticalFlip(expectedAngle);
  349. });
  350. });
  351. // Arrow element
  352. describe("arrow", () => {
  353. it("flips an unrotated arrow horizontally with line inside min/max points bounds", async () => {
  354. const arrow = createLinearElementWithCurveInsideMinMaxPoints("arrow");
  355. h.elements = [arrow];
  356. h.app.setState({ selectedElementIds: { [arrow.id]: true } });
  357. await checkHorizontalFlip(
  358. MULTIPOINT_LINEAR_ELEMENT_FLIP_TOLERANCE_IN_PIXELS,
  359. );
  360. });
  361. it("flips an unrotated arrow vertically with line inside min/max points bounds", async () => {
  362. const arrow = createLinearElementWithCurveInsideMinMaxPoints("arrow");
  363. h.elements = [arrow];
  364. h.app.setState({ selectedElementIds: { [arrow.id]: true } });
  365. await checkVerticalFlip(50);
  366. });
  367. it("flips a rotated arrow horizontally with line inside min/max points bounds", async () => {
  368. const originalAngle = Math.PI / 4;
  369. const expectedAngle = (7 * Math.PI) / 4;
  370. const line = createLinearElementWithCurveInsideMinMaxPoints("arrow");
  371. h.elements = [line];
  372. h.state.selectedElementIds = {
  373. ...h.state.selectedElementIds,
  374. [line.id]: true,
  375. };
  376. mutateElement(line, {
  377. angle: originalAngle,
  378. });
  379. await checkRotatedHorizontalFlip(
  380. expectedAngle,
  381. MULTIPOINT_LINEAR_ELEMENT_FLIP_TOLERANCE_IN_PIXELS,
  382. );
  383. });
  384. it("flips a rotated arrow vertically with line inside min/max points bounds", async () => {
  385. const originalAngle = Math.PI / 4;
  386. const expectedAngle = (7 * Math.PI) / 4;
  387. const line = createLinearElementWithCurveInsideMinMaxPoints("arrow");
  388. h.elements = [line];
  389. h.state.selectedElementIds = {
  390. ...h.state.selectedElementIds,
  391. [line.id]: true,
  392. };
  393. mutateElement(line, {
  394. angle: originalAngle,
  395. });
  396. await checkRotatedVerticalFlip(
  397. expectedAngle,
  398. MULTIPOINT_LINEAR_ELEMENT_FLIP_TOLERANCE_IN_PIXELS,
  399. );
  400. });
  401. //TODO: elements with curve outside minMax points have a wrong bounding box!!!
  402. it.skip("flips an unrotated arrow horizontally with line outside min/max points bounds", async () => {
  403. const arrow = createLinearElementsWithCurveOutsideMinMaxPoints("arrow");
  404. h.elements = [arrow];
  405. h.app.setState({ selectedElementIds: { [arrow.id]: true } });
  406. await checkHorizontalFlip(
  407. MULTIPOINT_LINEAR_ELEMENT_FLIP_TOLERANCE_IN_PIXELS,
  408. );
  409. });
  410. //TODO: elements with curve outside minMax points have a wrong bounding box!!!
  411. it.skip("flips a rotated arrow horizontally with line outside min/max points bounds", async () => {
  412. const originalAngle = Math.PI / 4;
  413. const expectedAngle = (7 * Math.PI) / 4;
  414. const line = createLinearElementsWithCurveOutsideMinMaxPoints("arrow");
  415. mutateElement(line, { angle: originalAngle });
  416. h.elements = [line];
  417. h.app.setState({ selectedElementIds: { [line.id]: true } });
  418. await checkRotatedVerticalFlip(
  419. expectedAngle,
  420. MULTIPOINT_LINEAR_ELEMENT_FLIP_TOLERANCE_IN_PIXELS,
  421. );
  422. });
  423. //TODO: elements with curve outside minMax points have a wrong bounding box!!!
  424. it.skip("flips an unrotated arrow vertically with line outside min/max points bounds", async () => {
  425. const arrow = createLinearElementsWithCurveOutsideMinMaxPoints("arrow");
  426. h.elements = [arrow];
  427. h.app.setState({ selectedElementIds: { [arrow.id]: true } });
  428. await checkVerticalFlip(MULTIPOINT_LINEAR_ELEMENT_FLIP_TOLERANCE_IN_PIXELS);
  429. });
  430. //TODO: elements with curve outside minMax points have a wrong bounding box!!!
  431. it.skip("flips a rotated arrow vertically with line outside min/max points bounds", async () => {
  432. const originalAngle = Math.PI / 4;
  433. const expectedAngle = (7 * Math.PI) / 4;
  434. const line = createLinearElementsWithCurveOutsideMinMaxPoints("arrow");
  435. mutateElement(line, { angle: originalAngle });
  436. h.elements = [line];
  437. h.app.setState({ selectedElementIds: { [line.id]: true } });
  438. await checkRotatedVerticalFlip(
  439. expectedAngle,
  440. MULTIPOINT_LINEAR_ELEMENT_FLIP_TOLERANCE_IN_PIXELS,
  441. );
  442. });
  443. it("flips an unrotated arrow horizontally correctly", async () => {
  444. createAndSelectOneArrow();
  445. await checkHorizontalFlip(
  446. TWO_POINTS_LINEAR_ELEMENT_FLIP_TOLERANCE_IN_PIXELS,
  447. );
  448. });
  449. it("flips an unrotated arrow vertically correctly", async () => {
  450. createAndSelectOneArrow();
  451. await checkVerticalFlip(TWO_POINTS_LINEAR_ELEMENT_FLIP_TOLERANCE_IN_PIXELS);
  452. });
  453. it("flips a two points arrow horizontally correctly", async () => {
  454. createAndSelectOneArrow();
  455. await checkTwoPointsLineHorizontalFlip();
  456. });
  457. it("flips a two points arrow vertically correctly", async () => {
  458. createAndSelectOneArrow();
  459. await checkTwoPointsLineVerticalFlip();
  460. });
  461. });
  462. // Line element
  463. describe("line", () => {
  464. it("flips an unrotated line horizontally with line inside min/max points bounds", async () => {
  465. const line = createLinearElementWithCurveInsideMinMaxPoints("line");
  466. h.elements = [line];
  467. h.app.setState({ selectedElementIds: { [line.id]: true } });
  468. await checkHorizontalFlip(
  469. MULTIPOINT_LINEAR_ELEMENT_FLIP_TOLERANCE_IN_PIXELS,
  470. );
  471. });
  472. it("flips an unrotated line vertically with line inside min/max points bounds", async () => {
  473. const line = createLinearElementWithCurveInsideMinMaxPoints("line");
  474. h.elements = [line];
  475. h.app.setState({ selectedElementIds: { [line.id]: true } });
  476. await checkVerticalFlip(MULTIPOINT_LINEAR_ELEMENT_FLIP_TOLERANCE_IN_PIXELS);
  477. });
  478. it("flips an unrotated line horizontally correctly", async () => {
  479. createAndSelectOneLine();
  480. await checkHorizontalFlip(
  481. TWO_POINTS_LINEAR_ELEMENT_FLIP_TOLERANCE_IN_PIXELS,
  482. );
  483. });
  484. //TODO: elements with curve outside minMax points have a wrong bounding box
  485. it.skip("flips an unrotated line horizontally with line outside min/max points bounds", async () => {
  486. const line = createLinearElementsWithCurveOutsideMinMaxPoints("line");
  487. h.elements = [line];
  488. h.app.setState({ selectedElementIds: { [line.id]: true } });
  489. await checkHorizontalFlip(
  490. MULTIPOINT_LINEAR_ELEMENT_FLIP_TOLERANCE_IN_PIXELS,
  491. );
  492. });
  493. //TODO: elements with curve outside minMax points have a wrong bounding box
  494. it.skip("flips an unrotated line vertically with line outside min/max points bounds", async () => {
  495. const line = createLinearElementsWithCurveOutsideMinMaxPoints("line");
  496. h.elements = [line];
  497. h.app.setState({ selectedElementIds: { [line.id]: true } });
  498. await checkVerticalFlip(MULTIPOINT_LINEAR_ELEMENT_FLIP_TOLERANCE_IN_PIXELS);
  499. });
  500. //TODO: elements with curve outside minMax points have a wrong bounding box
  501. it.skip("flips a rotated line horizontally with line outside min/max points bounds", async () => {
  502. const originalAngle = Math.PI / 4;
  503. const expectedAngle = (7 * Math.PI) / 4;
  504. const line = createLinearElementsWithCurveOutsideMinMaxPoints("line");
  505. mutateElement(line, { angle: originalAngle });
  506. h.elements = [line];
  507. h.app.setState({ selectedElementIds: { [line.id]: true } });
  508. await checkRotatedHorizontalFlip(
  509. expectedAngle,
  510. MULTIPOINT_LINEAR_ELEMENT_FLIP_TOLERANCE_IN_PIXELS,
  511. );
  512. });
  513. //TODO: elements with curve outside minMax points have a wrong bounding box
  514. it.skip("flips a rotated line vertically with line outside min/max points bounds", async () => {
  515. const originalAngle = Math.PI / 4;
  516. const expectedAngle = (7 * Math.PI) / 4;
  517. const line = createLinearElementsWithCurveOutsideMinMaxPoints("line");
  518. mutateElement(line, { angle: originalAngle });
  519. h.elements = [line];
  520. h.app.setState({ selectedElementIds: { [line.id]: true } });
  521. await checkRotatedVerticalFlip(
  522. expectedAngle,
  523. MULTIPOINT_LINEAR_ELEMENT_FLIP_TOLERANCE_IN_PIXELS,
  524. );
  525. });
  526. it("flips an unrotated line vertically correctly", async () => {
  527. createAndSelectOneLine();
  528. await checkVerticalFlip(TWO_POINTS_LINEAR_ELEMENT_FLIP_TOLERANCE_IN_PIXELS);
  529. });
  530. it("flips a rotated line horizontally with line inside min/max points bounds", async () => {
  531. const originalAngle = Math.PI / 4;
  532. const expectedAngle = (7 * Math.PI) / 4;
  533. const line = createLinearElementWithCurveInsideMinMaxPoints("line");
  534. h.elements = [line];
  535. h.state.selectedElementIds = {
  536. ...h.state.selectedElementIds,
  537. [line.id]: true,
  538. };
  539. mutateElement(line, {
  540. angle: originalAngle,
  541. });
  542. await checkRotatedHorizontalFlip(
  543. expectedAngle,
  544. MULTIPOINT_LINEAR_ELEMENT_FLIP_TOLERANCE_IN_PIXELS,
  545. );
  546. });
  547. it("flips a rotated line vertically with line inside min/max points bounds", async () => {
  548. const originalAngle = Math.PI / 4;
  549. const expectedAngle = (7 * Math.PI) / 4;
  550. const line = createLinearElementWithCurveInsideMinMaxPoints("line");
  551. h.elements = [line];
  552. h.state.selectedElementIds = {
  553. ...h.state.selectedElementIds,
  554. [line.id]: true,
  555. };
  556. mutateElement(line, {
  557. angle: originalAngle,
  558. });
  559. await checkRotatedVerticalFlip(
  560. expectedAngle,
  561. MULTIPOINT_LINEAR_ELEMENT_FLIP_TOLERANCE_IN_PIXELS,
  562. );
  563. });
  564. it("flips a two points line horizontally correctly", async () => {
  565. createAndSelectOneLine();
  566. await checkTwoPointsLineHorizontalFlip();
  567. });
  568. it("flips a two points line vertically correctly", async () => {
  569. createAndSelectOneLine();
  570. await checkTwoPointsLineVerticalFlip();
  571. });
  572. });
  573. // Draw element
  574. describe("freedraw", () => {
  575. it("flips an unrotated drawing horizontally correctly", async () => {
  576. const draw = createAndReturnOneDraw();
  577. // select draw, since not done automatically
  578. h.state.selectedElementIds = {
  579. ...h.state.selectedElementIds,
  580. [draw.id]: true,
  581. };
  582. await checkHorizontalFlip();
  583. });
  584. it("flips an unrotated drawing vertically correctly", async () => {
  585. const draw = createAndReturnOneDraw();
  586. // select draw, since not done automatically
  587. h.state.selectedElementIds = {
  588. ...h.state.selectedElementIds,
  589. [draw.id]: true,
  590. };
  591. await checkVerticalFlip();
  592. });
  593. it("flips a rotated drawing horizontally correctly", async () => {
  594. const originalAngle = Math.PI / 4;
  595. const expectedAngle = (7 * Math.PI) / 4;
  596. const draw = createAndReturnOneDraw(originalAngle);
  597. // select draw, since not done automatically
  598. h.state.selectedElementIds = {
  599. ...h.state.selectedElementIds,
  600. [draw.id]: true,
  601. };
  602. await checkRotatedHorizontalFlip(expectedAngle);
  603. });
  604. it("flips a rotated drawing vertically correctly", async () => {
  605. const originalAngle = Math.PI / 4;
  606. const expectedAngle = (7 * Math.PI) / 4;
  607. const draw = createAndReturnOneDraw(originalAngle);
  608. // select draw, since not done automatically
  609. h.state.selectedElementIds = {
  610. ...h.state.selectedElementIds,
  611. [draw.id]: true,
  612. };
  613. await checkRotatedVerticalFlip(expectedAngle);
  614. });
  615. });
  616. //image
  617. //TODO: currently there is no test for pixel colors at flipped positions.
  618. describe("image", () => {
  619. const createImage = async () => {
  620. const sendPasteEvent = (file?: File) => {
  621. const clipboardEvent = createPasteEvent({ files: file ? [file] : [] });
  622. document.dispatchEvent(clipboardEvent);
  623. };
  624. sendPasteEvent(await API.loadFile("./fixtures/smiley_embedded_v2.png"));
  625. };
  626. it("flips an unrotated image horizontally correctly", async () => {
  627. //paste image
  628. await createImage();
  629. await waitFor(() => {
  630. expect((h.elements[0] as ExcalidrawImageElement).scale).toEqual([1, 1]);
  631. expect(API.getSelectedElements().length).toBeGreaterThan(0);
  632. expect(API.getSelectedElements()[0].type).toEqual("image");
  633. expect(h.app.files.fileId).toBeDefined();
  634. });
  635. await checkHorizontalFlip();
  636. expect((h.elements[0] as ExcalidrawImageElement).scale).toEqual([-1, 1]);
  637. expect(h.elements[0].angle).toBeCloseTo(0);
  638. });
  639. it("flips an unrotated image vertically correctly", async () => {
  640. //paste image
  641. await createImage();
  642. await waitFor(() => {
  643. expect((h.elements[0] as ExcalidrawImageElement).scale).toEqual([1, 1]);
  644. expect(API.getSelectedElements().length).toBeGreaterThan(0);
  645. expect(API.getSelectedElements()[0].type).toEqual("image");
  646. expect(h.app.files.fileId).toBeDefined();
  647. });
  648. await checkVerticalFlip();
  649. expect((h.elements[0] as ExcalidrawImageElement).scale).toEqual([1, -1]);
  650. expect(h.elements[0].angle).toBeCloseTo(0);
  651. });
  652. it("flips an rotated image horizontally correctly", async () => {
  653. const originalAngle = Math.PI / 4;
  654. const expectedAngle = (7 * Math.PI) / 4;
  655. //paste image
  656. await createImage();
  657. await waitFor(() => {
  658. expect((h.elements[0] as ExcalidrawImageElement).scale).toEqual([1, 1]);
  659. expect(API.getSelectedElements().length).toBeGreaterThan(0);
  660. expect(API.getSelectedElements()[0].type).toEqual("image");
  661. expect(h.app.files.fileId).toBeDefined();
  662. });
  663. mutateElement(h.elements[0], {
  664. angle: originalAngle,
  665. });
  666. await checkRotatedHorizontalFlip(expectedAngle);
  667. expect((h.elements[0] as ExcalidrawImageElement).scale).toEqual([-1, 1]);
  668. });
  669. it("flips an rotated image vertically correctly", async () => {
  670. const originalAngle = Math.PI / 4;
  671. const expectedAngle = (7 * Math.PI) / 4;
  672. //paste image
  673. await createImage();
  674. await waitFor(() => {
  675. expect((h.elements[0] as ExcalidrawImageElement).scale).toEqual([1, 1]);
  676. expect(h.elements[0].angle).toEqual(0);
  677. expect(API.getSelectedElements().length).toBeGreaterThan(0);
  678. expect(API.getSelectedElements()[0].type).toEqual("image");
  679. expect(h.app.files.fileId).toBeDefined();
  680. });
  681. mutateElement(h.elements[0], {
  682. angle: originalAngle,
  683. });
  684. await checkRotatedVerticalFlip(expectedAngle);
  685. expect((h.elements[0] as ExcalidrawImageElement).scale).toEqual([1, -1]);
  686. expect(h.elements[0].angle).toBeCloseTo(expectedAngle);
  687. });
  688. it("flips an image both vertically & horizontally", async () => {
  689. //paste image
  690. await createImage();
  691. await waitFor(() => {
  692. expect((h.elements[0] as ExcalidrawImageElement).scale).toEqual([1, 1]);
  693. expect(API.getSelectedElements().length).toBeGreaterThan(0);
  694. expect(API.getSelectedElements()[0].type).toEqual("image");
  695. expect(h.app.files.fileId).toBeDefined();
  696. });
  697. await checkVerticalHorizontalFlip();
  698. expect((h.elements[0] as ExcalidrawImageElement).scale).toEqual([-1, -1]);
  699. expect(h.elements[0].angle).toBeCloseTo(0);
  700. });
  701. });
  702. describe("mutliple elements", () => {
  703. it("with bound text flip correctly", async () => {
  704. UI.clickTool("arrow");
  705. fireEvent.click(screen.getByTitle("Architect"));
  706. const arrow = UI.createElement("arrow", {
  707. x: 0,
  708. y: 0,
  709. width: 180,
  710. height: 80,
  711. });
  712. Keyboard.keyPress(KEYS.ENTER);
  713. let editor = document.querySelector<HTMLTextAreaElement>(
  714. ".excalidraw-textEditorContainer > textarea",
  715. )!;
  716. fireEvent.input(editor, { target: { value: "arrow" } });
  717. await new Promise((resolve) => setTimeout(resolve, 0));
  718. Keyboard.keyPress(KEYS.ESCAPE);
  719. const rectangle = UI.createElement("rectangle", {
  720. x: 0,
  721. y: 100,
  722. width: 100,
  723. height: 100,
  724. });
  725. Keyboard.keyPress(KEYS.ENTER);
  726. editor = document.querySelector<HTMLTextAreaElement>(
  727. ".excalidraw-textEditorContainer > textarea",
  728. )!;
  729. fireEvent.input(editor, { target: { value: "rect\ntext" } });
  730. await new Promise((resolve) => setTimeout(resolve, 0));
  731. Keyboard.keyPress(KEYS.ESCAPE);
  732. mouse.select([arrow, rectangle]);
  733. h.app.actionManager.executeAction(actionFlipHorizontal);
  734. h.app.actionManager.executeAction(actionFlipVertical);
  735. const arrowText = h.elements[1] as ExcalidrawTextElementWithContainer;
  736. const arrowTextPos = getBoundTextElementPosition(
  737. arrow.get(),
  738. arrowText,
  739. arrayToMap(h.elements),
  740. )!;
  741. const rectText = h.elements[3] as ExcalidrawTextElementWithContainer;
  742. expect(arrow.x).toBeCloseTo(180);
  743. expect(arrow.y).toBeCloseTo(200);
  744. expect(arrow.points[1][0]).toBeCloseTo(-180);
  745. expect(arrow.points[1][1]).toBeCloseTo(-80);
  746. expect(arrowTextPos.x - (arrow.x - arrow.width)).toBeCloseTo(
  747. arrow.x - (arrowTextPos.x + arrowText.width),
  748. );
  749. expect(arrowTextPos.y - (arrow.y - arrow.height)).toBeCloseTo(
  750. arrow.y - (arrowTextPos.y + arrowText.height),
  751. );
  752. expect(rectangle.x).toBeCloseTo(80);
  753. expect(rectangle.y).toBeCloseTo(0);
  754. expect(rectText.x - rectangle.x).toBeCloseTo(
  755. rectangle.x + rectangle.width - (rectText.x + rectText.width),
  756. );
  757. expect(rectText.y - rectangle.y).toBeCloseTo(
  758. rectangle.y + rectangle.height - (rectText.y + rectText.height),
  759. );
  760. });
  761. });