flip.test.tsx 25 KB

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