From 06b45e0cfc9bc93b3565acee2932d643169fb298 Mon Sep 17 00:00:00 2001 From: Antonio Della Fortuna <50418432+adarkforce@users.noreply.github.com> Date: Sun, 8 Jan 2023 17:19:13 +0100 Subject: [PATCH] fix: image horizontal flip fix + improved tests (#5799) Co-authored-by: Antonio Della Fortuna Co-authored-by: dwelle fixes https://github.com/excalidraw/excalidraw/issues/5784 --- src/element/resizeElements.ts | 8 +- src/tests/flip.test.tsx | 1120 ++++++++++++++++++--------------- 2 files changed, 621 insertions(+), 507 deletions(-) diff --git a/src/element/resizeElements.ts b/src/element/resizeElements.ts index 3782c232..605ab0c2 100644 --- a/src/element/resizeElements.ts +++ b/src/element/resizeElements.ts @@ -557,10 +557,10 @@ export const resizeSingleElement = ( mutateElement(element, { scale: [ // defaulting because scaleX/Y can be 0/-0 - (Math.sign(scaleX) || stateAtResizeStart.scale[0]) * - stateAtResizeStart.scale[0], - (Math.sign(scaleY) || stateAtResizeStart.scale[1]) * - stateAtResizeStart.scale[1], + (Math.sign(newBoundsX2 - stateAtResizeStart.x) || + stateAtResizeStart.scale[0]) * stateAtResizeStart.scale[0], + (Math.sign(newBoundsY2 - stateAtResizeStart.y) || + stateAtResizeStart.scale[1]) * stateAtResizeStart.scale[1], ], }); } diff --git a/src/tests/flip.test.tsx b/src/tests/flip.test.tsx index a388ad4e..45a5e147 100644 --- a/src/tests/flip.test.tsx +++ b/src/tests/flip.test.tsx @@ -1,22 +1,53 @@ import ReactDOM from "react-dom"; -import { render } from "./test-utils"; -import App from "../components/App"; -import { defaultLang, setLanguage } from "../i18n"; +import { GlobalTestState, render, waitFor } from "./test-utils"; import { UI, Pointer } from "./helpers/ui"; import { API } from "./helpers/api"; import { actionFlipHorizontal, actionFlipVertical } from "../actions"; +import { getElementAbsoluteCoords } from "../element"; +import { + ExcalidrawElement, + ExcalidrawImageElement, + ExcalidrawLinearElement, + FileId, +} from "../element/types"; +import { newLinearElement } from "../element"; +import ExcalidrawApp from "../excalidraw-app"; +import { mutateElement } from "../element/mutateElement"; +import { NormalizedZoomValue } from "../types"; +import { ROUNDNESS } from "../constants"; const { h } = window; const mouse = new Pointer("mouse"); +jest.mock("../data/blob", () => { + const originalModule = jest.requireActual("../data/blob"); + //Prevent Node.js modules errors (document is not defined etc...) + return { + __esModule: true, + ...originalModule, + resizeImageFile: (imageFile: File) => imageFile, + generateIdFromFile: () => "fileId" as FileId, + }; +}); beforeEach(async () => { // Unmount ReactDOM from root ReactDOM.unmountComponentAtNode(document.getElementById("root")!); - mouse.reset(); - await setLanguage(defaultLang); - await render(); + mouse.reset(); + localStorage.clear(); + sessionStorage.clear(); + jest.clearAllMocks(); + + Object.assign(document, { + elementFromPoint: () => GlobalTestState.canvas, + }); + await render(); + h.setState({ + zoom: { + value: 1 as NormalizedZoomValue, + }, + }); }); const createAndSelectOneRectangle = (angle: number = 0) => { @@ -79,593 +110,676 @@ const createAndReturnOneDraw = (angle: number = 0) => { }); }; -const FLIP_PRECISION_DECIMALS = 7; +const createLinearElementWithCurveInsideMinMaxPoints = ( + type: "line" | "arrow", + extraProps: any = {}, +) => { + return newLinearElement({ + type, + x: 2256.910668124894, + y: -2412.5069664197654, + width: 1750.4888916015625, + height: 410.51605224609375, + angle: 0, + strokeColor: "#000000", + backgroundColor: "#fa5252", + fillStyle: "hachure", + strokeWidth: 1, + strokeStyle: "solid", + roughness: 1, + opacity: 100, + groupIds: [], + roundness: { type: ROUNDNESS.PROPORTIONAL_RADIUS }, + boundElements: null, + link: null, + locked: false, + points: [ + [0, 0], + [-922.4761962890625, 300.3277587890625], + [828.0126953125, 410.51605224609375], + ], + startArrowhead: null, + endArrowhead: null, + }); +}; + +const createLinearElementsWithCurveOutsideMinMaxPoints = ( + type: "line" | "arrow", + extraProps: any = {}, +) => { + return newLinearElement({ + type, + x: -1388.6555370382996, + y: 1037.698247710191, + width: 591.2804897585779, + height: 69.32871961377737, + angle: 0, + strokeColor: "#000000", + backgroundColor: "transparent", + fillStyle: "hachure", + strokeWidth: 1, + strokeStyle: "solid", + roughness: 1, + opacity: 100, + groupIds: [], + roundness: { type: ROUNDNESS.PROPORTIONAL_RADIUS }, + boundElements: null, + link: null, + locked: false, + points: [ + [0, 0], + [-584.1485186423079, -15.365636022723947], + [-591.2804897585779, 36.09360810181511], + [-148.56510566829502, 53.96308359105342], + ], + startArrowhead: null, + endArrowhead: null, + ...extraProps, + }); +}; + +const checkElementsBoundingBox = async ( + element1: ExcalidrawElement, + element2: ExcalidrawElement, + toleranceInPx: number = 0, +) => { + const [x1, y1, x2, y2] = getElementAbsoluteCoords(element1); + + const [x12, y12, x22, y22] = getElementAbsoluteCoords(element2); + + debugger; + await waitFor(() => { + // Check if width and height did not change + expect(x1 - toleranceInPx <= x12 && x12 <= x1 + toleranceInPx).toBeTruthy(); + expect(y1 - toleranceInPx <= y12 && y12 <= y1 + toleranceInPx).toBeTruthy(); + expect(x2 - toleranceInPx <= x22 && x22 <= x2 + toleranceInPx).toBeTruthy(); + expect(y2 - toleranceInPx <= y22 && y22 <= y2 + toleranceInPx).toBeTruthy(); + }); +}; + +const checkHorizontalFlip = async (toleranceInPx: number = 0.00001) => { + const originalElement = JSON.parse(JSON.stringify(h.elements[0])); + h.app.actionManager.executeAction(actionFlipHorizontal); + const newElement = h.elements[0]; + await checkElementsBoundingBox(originalElement, newElement, toleranceInPx); +}; + +const checkTwoPointsLineHorizontalFlip = async () => { + const originalElement = JSON.parse( + JSON.stringify(h.elements[0]), + ) as ExcalidrawLinearElement; + h.app.actionManager.executeAction(actionFlipHorizontal); + const newElement = h.elements[0] as ExcalidrawLinearElement; + await waitFor(() => { + expect(originalElement.points[0][0]).toEqual( + newElement.points[0][0] !== 0 ? -newElement.points[0][0] : 0, + ); + expect(originalElement.points[0][1]).toEqual(newElement.points[0][1]); + expect(originalElement.points[1][0]).toEqual( + newElement.points[1][0] !== 0 ? -newElement.points[1][0] : 0, + ); + expect(originalElement.points[1][1]).toEqual(newElement.points[1][1]); + }); +}; + +const checkTwoPointsLineVerticalFlip = async () => { + const originalElement = JSON.parse( + JSON.stringify(h.elements[0]), + ) as ExcalidrawLinearElement; + h.app.actionManager.executeAction(actionFlipVertical); + const newElement = h.elements[0] as ExcalidrawLinearElement; + await waitFor(() => { + expect(originalElement.points[0][0]).toEqual( + newElement.points[0][0] !== 0 ? -newElement.points[0][0] : 0, + ); + expect(originalElement.points[0][1]).toEqual(newElement.points[0][1]); + expect(originalElement.points[1][0]).toEqual( + newElement.points[1][0] !== 0 ? -newElement.points[1][0] : 0, + ); + expect(originalElement.points[1][1]).toEqual(newElement.points[1][1]); + }); +}; + +const checkRotatedHorizontalFlip = async ( + expectedAngle: number, + toleranceInPx: number = 0.00001, +) => { + const originalElement = JSON.parse(JSON.stringify(h.elements[0])); + h.app.actionManager.executeAction(actionFlipHorizontal); + const newElement = h.elements[0]; + await waitFor(() => { + expect(newElement.angle).toBeCloseTo(expectedAngle); + }); + await checkElementsBoundingBox(originalElement, newElement, toleranceInPx); +}; + +const checkRotatedVerticalFlip = async ( + expectedAngle: number, + toleranceInPx: number = 0.00001, +) => { + const originalElement = JSON.parse(JSON.stringify(h.elements[0])); + h.app.actionManager.executeAction(actionFlipVertical); + const newElement = h.elements[0]; + await waitFor(() => { + expect(newElement.angle).toBeCloseTo(expectedAngle); + }); + await checkElementsBoundingBox(originalElement, newElement, toleranceInPx); +}; + +const checkVerticalFlip = async (toleranceInPx: number = 0.00001) => { + const originalElement = JSON.parse(JSON.stringify(h.elements[0])); + + h.app.actionManager.executeAction(actionFlipVertical); + + const newElement = h.elements[0]; + await checkElementsBoundingBox(originalElement, newElement, toleranceInPx); +}; + +const checkVerticalHorizontalFlip = async (toleranceInPx: number = 0.00001) => { + const originalElement = JSON.parse(JSON.stringify(h.elements[0])); + + h.app.actionManager.executeAction(actionFlipHorizontal); + h.app.actionManager.executeAction(actionFlipVertical); + + const newElement = h.elements[0]; + await checkElementsBoundingBox(originalElement, newElement, toleranceInPx); +}; + +const TWO_POINTS_LINEAR_ELEMENT_FLIP_TOLERANCE_IN_PIXELS = 5; +const MULTIPOINT_LINEAR_ELEMENT_FLIP_TOLERANCE_IN_PIXELS = 20; // Rectangle element +describe("rectangle", () => { + it("flips an unrotated rectangle horizontally correctly", async () => { + createAndSelectOneRectangle(); -it("flips an unrotated rectangle horizontally correctly", () => { - createAndSelectOneRectangle(); + await checkHorizontalFlip(); + }); - expect(API.getSelectedElements()[0].x).toEqual(0); + it("flips an unrotated rectangle vertically correctly", async () => { + createAndSelectOneRectangle(); - expect(API.getSelectedElements()[0].y).toEqual(0); + await checkVerticalFlip(); + }); - const originalWidth = API.getSelectedElements()[0].width; - const originalHeight = API.getSelectedElements()[0].height; + it("flips a rotated rectangle horizontally correctly", async () => { + const originalAngle = (3 * Math.PI) / 4; + const expectedAngle = (5 * Math.PI) / 4; - h.app.actionManager.executeAction(actionFlipHorizontal); + createAndSelectOneRectangle(originalAngle); - // Check if x position did not change - expect(API.getSelectedElements()[0].x).toEqual(0); + await checkRotatedHorizontalFlip(expectedAngle); + }); - expect(API.getSelectedElements()[0].y).toEqual(0); + it("flips a rotated rectangle vertically correctly", async () => { + const originalAngle = (3 * Math.PI) / 4; + const expectedAgnle = Math.PI / 4; - // Check if width and height did not change - expect(API.getSelectedElements()[0].width).toEqual(originalWidth); + createAndSelectOneRectangle(originalAngle); - expect(API.getSelectedElements()[0].height).toEqual(originalHeight); -}); - -it("flips an unrotated rectangle vertically correctly", () => { - createAndSelectOneRectangle(); - - expect(API.getSelectedElements()[0].x).toEqual(0); - - expect(API.getSelectedElements()[0].y).toEqual(0); - - const originalWidth = API.getSelectedElements()[0].width; - const originalHeight = API.getSelectedElements()[0].height; - - h.app.actionManager.executeAction(actionFlipVertical); - - // Check if x position did not change - expect(API.getSelectedElements()[0].x).toEqual(0); - - expect(API.getSelectedElements()[0].y).toEqual(0); - - // Check if width and height did not change - expect(API.getSelectedElements()[0].width).toEqual(originalWidth); - - expect(API.getSelectedElements()[0].height).toEqual(originalHeight); -}); - -it("flips a rotated rectangle horizontally correctly", () => { - const originalAngle = (3 * Math.PI) / 4; - const expectedAngle = (5 * Math.PI) / 4; - - createAndSelectOneRectangle(originalAngle); - - expect(API.getSelectedElements()[0].x).toEqual(0); - - expect(API.getSelectedElements()[0].y).toEqual(0); - - const originalWidth = API.getSelectedElements()[0].width; - const originalHeight = API.getSelectedElements()[0].height; - - h.app.actionManager.executeAction(actionFlipHorizontal); - - // Check if x position did not change - expect(API.getSelectedElements()[0].x).toEqual(0); - - expect(API.getSelectedElements()[0].y).toEqual(0); - - // Check if width and height did not change - expect(API.getSelectedElements()[0].width).toEqual(originalWidth); - - expect(API.getSelectedElements()[0].height).toEqual(originalHeight); - - // Check angle - expect(API.getSelectedElements()[0].angle).toBeCloseTo(expectedAngle); -}); - -it("flips a rotated rectangle vertically correctly", () => { - const originalAngle = (3 * Math.PI) / 4; - const expectedAgnle = Math.PI / 4; - - createAndSelectOneRectangle(originalAngle); - - expect(API.getSelectedElements()[0].x).toEqual(0); - - expect(API.getSelectedElements()[0].y).toEqual(0); - - const originalWidth = API.getSelectedElements()[0].width; - const originalHeight = API.getSelectedElements()[0].height; - - h.app.actionManager.executeAction(actionFlipVertical); - - // Check if x position did not change - expect(API.getSelectedElements()[0].x).toEqual(0); - - expect(API.getSelectedElements()[0].y).toEqual(0); - - // Check if width and height did not change - expect(API.getSelectedElements()[0].width).toEqual(originalWidth); - - expect(API.getSelectedElements()[0].height).toEqual(originalHeight); - - // Check angle - expect(API.getSelectedElements()[0].angle).toBeCloseTo(expectedAgnle); + await checkRotatedVerticalFlip(expectedAgnle); + }); }); // Diamond element +describe("diamond", () => { + it("flips an unrotated diamond horizontally correctly", async () => { + createAndSelectOneDiamond(); -it("flips an unrotated diamond horizontally correctly", () => { - createAndSelectOneDiamond(); + await checkHorizontalFlip(); + }); - expect(API.getSelectedElements()[0].x).toEqual(0); + it("flips an unrotated diamond vertically correctly", async () => { + createAndSelectOneDiamond(); - expect(API.getSelectedElements()[0].y).toEqual(0); + await checkVerticalFlip(); + }); - const originalWidth = API.getSelectedElements()[0].width; - const originalHeight = API.getSelectedElements()[0].height; + it("flips a rotated diamond horizontally correctly", async () => { + const originalAngle = (5 * Math.PI) / 4; + const expectedAngle = (3 * Math.PI) / 4; - h.app.actionManager.executeAction(actionFlipHorizontal); + createAndSelectOneDiamond(originalAngle); - // Check if x position did not change - expect(API.getSelectedElements()[0].x).toEqual(0); + await checkRotatedHorizontalFlip(expectedAngle); + }); - expect(API.getSelectedElements()[0].y).toEqual(0); + it("flips a rotated diamond vertically correctly", async () => { + const originalAngle = (5 * Math.PI) / 4; + const expectedAngle = (7 * Math.PI) / 4; - // Check if width and height did not change - expect(API.getSelectedElements()[0].width).toEqual(originalWidth); + createAndSelectOneDiamond(originalAngle); - expect(API.getSelectedElements()[0].height).toEqual(originalHeight); -}); - -it("flips an unrotated diamond vertically correctly", () => { - createAndSelectOneDiamond(); - - expect(API.getSelectedElements()[0].x).toEqual(0); - - expect(API.getSelectedElements()[0].y).toEqual(0); - - const originalWidth = API.getSelectedElements()[0].width; - const originalHeight = API.getSelectedElements()[0].height; - - h.app.actionManager.executeAction(actionFlipVertical); - - // Check if x position did not change - expect(API.getSelectedElements()[0].x).toEqual(0); - - expect(API.getSelectedElements()[0].y).toEqual(0); - - // Check if width and height did not change - expect(API.getSelectedElements()[0].width).toEqual(originalWidth); - - expect(API.getSelectedElements()[0].height).toEqual(originalHeight); -}); - -it("flips a rotated diamond horizontally correctly", () => { - const originalAngle = (5 * Math.PI) / 4; - const expectedAngle = (3 * Math.PI) / 4; - - createAndSelectOneDiamond(originalAngle); - - expect(API.getSelectedElements()[0].x).toEqual(0); - - expect(API.getSelectedElements()[0].y).toEqual(0); - - const originalWidth = API.getSelectedElements()[0].width; - const originalHeight = API.getSelectedElements()[0].height; - - h.app.actionManager.executeAction(actionFlipHorizontal); - - // Check if x position did not change - expect(API.getSelectedElements()[0].x).toEqual(0); - - expect(API.getSelectedElements()[0].y).toEqual(0); - - // Check if width and height did not change - expect(API.getSelectedElements()[0].width).toEqual(originalWidth); - - expect(API.getSelectedElements()[0].height).toEqual(originalHeight); - - // Check angle - expect(API.getSelectedElements()[0].angle).toBeCloseTo(expectedAngle); -}); - -it("flips a rotated diamond vertically correctly", () => { - const originalAngle = (5 * Math.PI) / 4; - const expectedAngle = (7 * Math.PI) / 4; - - createAndSelectOneDiamond(originalAngle); - - expect(API.getSelectedElements()[0].x).toEqual(0); - - expect(API.getSelectedElements()[0].y).toEqual(0); - - const originalWidth = API.getSelectedElements()[0].width; - const originalHeight = API.getSelectedElements()[0].height; - - h.app.actionManager.executeAction(actionFlipVertical); - - // Check if x position did not change - expect(API.getSelectedElements()[0].x).toEqual(0); - - expect(API.getSelectedElements()[0].y).toEqual(0); - - // Check if width and height did not change - expect(API.getSelectedElements()[0].width).toEqual(originalWidth); - - expect(API.getSelectedElements()[0].height).toEqual(originalHeight); - - // Check angle - expect(API.getSelectedElements()[0].angle).toBeCloseTo(expectedAngle); + await checkRotatedVerticalFlip(expectedAngle); + }); }); // Ellipse element +describe("ellipse", () => { + it("flips an unrotated ellipse horizontally correctly", async () => { + createAndSelectOneEllipse(); -it("flips an unrotated ellipse horizontally correctly", () => { - createAndSelectOneEllipse(); + await checkHorizontalFlip(); + }); - expect(API.getSelectedElements()[0].x).toEqual(0); + it("flips an unrotated ellipse vertically correctly", async () => { + createAndSelectOneEllipse(); - expect(API.getSelectedElements()[0].y).toEqual(0); + await checkVerticalFlip(); + }); - const originalWidth = API.getSelectedElements()[0].width; - const originalHeight = API.getSelectedElements()[0].height; + it("flips a rotated ellipse horizontally correctly", async () => { + const originalAngle = (7 * Math.PI) / 4; + const expectedAngle = Math.PI / 4; - h.app.actionManager.executeAction(actionFlipHorizontal); + createAndSelectOneEllipse(originalAngle); - // Check if x position did not change - expect(API.getSelectedElements()[0].x).toEqual(0); + await checkRotatedHorizontalFlip(expectedAngle); + }); - expect(API.getSelectedElements()[0].y).toEqual(0); + it("flips a rotated ellipse vertically correctly", async () => { + const originalAngle = (7 * Math.PI) / 4; + const expectedAngle = (5 * Math.PI) / 4; - // Check if width and height did not change - expect(API.getSelectedElements()[0].width).toEqual(originalWidth); + createAndSelectOneEllipse(originalAngle); - expect(API.getSelectedElements()[0].height).toEqual(originalHeight); -}); - -it("flips an unrotated ellipse vertically correctly", () => { - createAndSelectOneEllipse(); - - expect(API.getSelectedElements()[0].x).toEqual(0); - - expect(API.getSelectedElements()[0].y).toEqual(0); - - const originalWidth = API.getSelectedElements()[0].width; - const originalHeight = API.getSelectedElements()[0].height; - - h.app.actionManager.executeAction(actionFlipVertical); - - // Check if x position did not change - expect(API.getSelectedElements()[0].x).toEqual(0); - - expect(API.getSelectedElements()[0].y).toEqual(0); - - // Check if width and height did not change - expect(API.getSelectedElements()[0].width).toEqual(originalWidth); - - expect(API.getSelectedElements()[0].height).toEqual(originalHeight); -}); - -it("flips a rotated ellipse horizontally correctly", () => { - const originalAngle = (7 * Math.PI) / 4; - const expectedAngle = Math.PI / 4; - - createAndSelectOneEllipse(originalAngle); - - expect(API.getSelectedElements()[0].x).toEqual(0); - - expect(API.getSelectedElements()[0].y).toEqual(0); - - const originalWidth = API.getSelectedElements()[0].width; - const originalHeight = API.getSelectedElements()[0].height; - - h.app.actionManager.executeAction(actionFlipHorizontal); - - // Check if x position did not change - expect(API.getSelectedElements()[0].x).toEqual(0); - - expect(API.getSelectedElements()[0].y).toEqual(0); - - // Check if width and height did not change - expect(API.getSelectedElements()[0].width).toEqual(originalWidth); - - expect(API.getSelectedElements()[0].height).toEqual(originalHeight); - - // Check angle - expect(API.getSelectedElements()[0].angle).toBeCloseTo(expectedAngle); -}); - -it("flips a rotated ellipse vertically correctly", () => { - const originalAngle = (7 * Math.PI) / 4; - const expectedAngle = (5 * Math.PI) / 4; - - createAndSelectOneEllipse(originalAngle); - - expect(API.getSelectedElements()[0].x).toEqual(0); - - expect(API.getSelectedElements()[0].y).toEqual(0); - - const originalWidth = API.getSelectedElements()[0].width; - const originalHeight = API.getSelectedElements()[0].height; - - h.app.actionManager.executeAction(actionFlipVertical); - - // Check if x position did not change - expect(API.getSelectedElements()[0].x).toEqual(0); - - expect(API.getSelectedElements()[0].y).toEqual(0); - - // Check if width and height did not change - expect(API.getSelectedElements()[0].width).toEqual(originalWidth); - - expect(API.getSelectedElements()[0].height).toEqual(originalHeight); - - // Check angle - expect(API.getSelectedElements()[0].angle).toBeCloseTo(expectedAngle); + await checkRotatedVerticalFlip(expectedAngle); + }); }); // Arrow element +describe("arrow", () => { + it("flips an unrotated arrow horizontally with line inside min/max points bounds", async () => { + const arrow = createLinearElementWithCurveInsideMinMaxPoints("arrow"); + h.app.scene.replaceAllElements([arrow]); + h.app.setState({ selectedElementIds: { [arrow.id]: true } }); + await checkHorizontalFlip( + MULTIPOINT_LINEAR_ELEMENT_FLIP_TOLERANCE_IN_PIXELS, + ); + }); -it("flips an unrotated arrow horizontally correctly", () => { - createAndSelectOneArrow(); + it("flips an unrotated arrow vertically with line inside min/max points bounds", async () => { + const arrow = createLinearElementWithCurveInsideMinMaxPoints("arrow"); + h.app.scene.replaceAllElements([arrow]); + h.app.setState({ selectedElementIds: { [arrow.id]: true } }); - const originalWidth = API.getSelectedElements()[0].width; - const originalHeight = API.getSelectedElements()[0].height; + await checkVerticalFlip(50); + }); - h.app.actionManager.executeAction(actionFlipHorizontal); + it("flips a rotated arrow horizontally with line inside min/max points bounds", async () => { + const originalAngle = Math.PI / 4; + const expectedAngle = (7 * Math.PI) / 4; + const line = createLinearElementWithCurveInsideMinMaxPoints("arrow"); + h.app.scene.replaceAllElements([line]); + h.app.state.selectedElementIds[line.id] = true; + mutateElement(line, { + angle: originalAngle, + }); - // Check if width and height did not change - expect(API.getSelectedElements()[0].width).toBeCloseTo( - originalWidth, - FLIP_PRECISION_DECIMALS, - ); + await checkRotatedHorizontalFlip( + expectedAngle, + MULTIPOINT_LINEAR_ELEMENT_FLIP_TOLERANCE_IN_PIXELS, + ); + }); - expect(API.getSelectedElements()[0].height).toBeCloseTo( - originalHeight, - FLIP_PRECISION_DECIMALS, - ); -}); + it("flips a rotated arrow vertically with line inside min/max points bounds", async () => { + const originalAngle = Math.PI / 4; + const expectedAngle = (3 * Math.PI) / 4; + const line = createLinearElementWithCurveInsideMinMaxPoints("arrow"); + h.app.scene.replaceAllElements([line]); + h.app.state.selectedElementIds[line.id] = true; + mutateElement(line, { + angle: originalAngle, + }); -it("flips an unrotated arrow vertically correctly", () => { - createAndSelectOneArrow(); + await checkRotatedVerticalFlip( + expectedAngle, + MULTIPOINT_LINEAR_ELEMENT_FLIP_TOLERANCE_IN_PIXELS, + ); + }); - const originalWidth = API.getSelectedElements()[0].width; - const originalHeight = API.getSelectedElements()[0].height; + //TODO: elements with curve outside minMax points have a wrong bounding box!!! + it.skip("flips an unrotated arrow horizontally with line outside min/max points bounds", async () => { + const arrow = createLinearElementsWithCurveOutsideMinMaxPoints("arrow"); + h.app.scene.replaceAllElements([arrow]); + h.app.setState({ selectedElementIds: { [arrow.id]: true } }); - h.app.actionManager.executeAction(actionFlipVertical); + await checkHorizontalFlip( + MULTIPOINT_LINEAR_ELEMENT_FLIP_TOLERANCE_IN_PIXELS, + ); + }); - // Check if width and height did not change - expect(API.getSelectedElements()[0].width).toBeCloseTo( - originalWidth, - FLIP_PRECISION_DECIMALS, - ); + //TODO: elements with curve outside minMax points have a wrong bounding box!!! + it.skip("flips a rotated arrow horizontally with line outside min/max points bounds", async () => { + const originalAngle = Math.PI / 4; + const expectedAngle = (7 * Math.PI) / 4; + const line = createLinearElementsWithCurveOutsideMinMaxPoints("arrow"); + mutateElement(line, { angle: originalAngle }); + h.app.scene.replaceAllElements([line]); + h.app.setState({ selectedElementIds: { [line.id]: true } }); - expect(API.getSelectedElements()[0].height).toBeCloseTo( - originalHeight, - FLIP_PRECISION_DECIMALS, - ); -}); + await checkRotatedVerticalFlip( + expectedAngle, + MULTIPOINT_LINEAR_ELEMENT_FLIP_TOLERANCE_IN_PIXELS, + ); + }); -//@TODO fix the tests with rotation -it.skip("flips a rotated arrow horizontally correctly", () => { - const originalAngle = Math.PI / 4; - const expectedAngle = (7 * Math.PI) / 4; - createAndSelectOneArrow(originalAngle); + //TODO: elements with curve outside minMax points have a wrong bounding box!!! + it.skip("flips an unrotated arrow vertically with line outside min/max points bounds", async () => { + const arrow = createLinearElementsWithCurveOutsideMinMaxPoints("arrow"); + h.app.scene.replaceAllElements([arrow]); + h.app.setState({ selectedElementIds: { [arrow.id]: true } }); - const originalWidth = API.getSelectedElements()[0].width; - const originalHeight = API.getSelectedElements()[0].height; + await checkVerticalFlip(MULTIPOINT_LINEAR_ELEMENT_FLIP_TOLERANCE_IN_PIXELS); + }); - h.app.actionManager.executeAction(actionFlipHorizontal); + //TODO: elements with curve outside minMax points have a wrong bounding box!!! + it.skip("flips a rotated arrow vertically with line outside min/max points bounds", async () => { + const originalAngle = Math.PI / 4; + const expectedAngle = (3 * Math.PI) / 4; + const line = createLinearElementsWithCurveOutsideMinMaxPoints("arrow"); + mutateElement(line, { angle: originalAngle }); + h.app.scene.replaceAllElements([line]); + h.app.setState({ selectedElementIds: { [line.id]: true } }); - // Check if width and height did not change - expect(API.getSelectedElements()[0].width).toBeCloseTo( - originalWidth, - FLIP_PRECISION_DECIMALS, - ); + await checkRotatedVerticalFlip( + expectedAngle, + MULTIPOINT_LINEAR_ELEMENT_FLIP_TOLERANCE_IN_PIXELS, + ); + }); - expect(API.getSelectedElements()[0].height).toBeCloseTo( - originalHeight, - FLIP_PRECISION_DECIMALS, - ); - // Check angle - expect(API.getSelectedElements()[0].angle).toBeCloseTo(expectedAngle); -}); + it("flips an unrotated arrow horizontally correctly", async () => { + createAndSelectOneArrow(); + await checkHorizontalFlip( + TWO_POINTS_LINEAR_ELEMENT_FLIP_TOLERANCE_IN_PIXELS, + ); + }); -it.skip("flips a rotated arrow vertically correctly", () => { - const originalAngle = Math.PI / 4; - const expectedAngle = (3 * Math.PI) / 4; - createAndSelectOneArrow(originalAngle); + it("flips an unrotated arrow vertically correctly", async () => { + createAndSelectOneArrow(); + await checkVerticalFlip(TWO_POINTS_LINEAR_ELEMENT_FLIP_TOLERANCE_IN_PIXELS); + }); - const originalWidth = API.getSelectedElements()[0].width; - const originalHeight = API.getSelectedElements()[0].height; + it("flips a two points arrow horizontally correctly", async () => { + createAndSelectOneArrow(); + await checkTwoPointsLineHorizontalFlip(); + }); - h.app.actionManager.executeAction(actionFlipVertical); + it("flips a two points arrow vertically correctly", async () => { + createAndSelectOneArrow(); - // Check if width and height did not change - expect(API.getSelectedElements()[0].width).toBeCloseTo( - originalWidth, - FLIP_PRECISION_DECIMALS, - ); - - expect(API.getSelectedElements()[0].height).toBeCloseTo( - originalHeight, - FLIP_PRECISION_DECIMALS, - ); - - // Check angle - expect(API.getSelectedElements()[0].angle).toBeCloseTo(expectedAngle); + await checkTwoPointsLineVerticalFlip(); + }); }); // Line element +describe("line", () => { + it("flips an unrotated line horizontally with line inside min/max points bounds", async () => { + const line = createLinearElementWithCurveInsideMinMaxPoints("line"); + h.app.scene.replaceAllElements([line]); + h.app.setState({ selectedElementIds: { [line.id]: true } }); -it("flips an unrotated line horizontally correctly", () => { - createAndSelectOneLine(); + await checkHorizontalFlip( + MULTIPOINT_LINEAR_ELEMENT_FLIP_TOLERANCE_IN_PIXELS, + ); + }); - const originalWidth = API.getSelectedElements()[0].width; - const originalHeight = API.getSelectedElements()[0].height; + it("flips an unrotated line vertically with line inside min/max points bounds", async () => { + const line = createLinearElementWithCurveInsideMinMaxPoints("line"); + h.app.scene.replaceAllElements([line]); + h.app.setState({ selectedElementIds: { [line.id]: true } }); - h.app.actionManager.executeAction(actionFlipHorizontal); + await checkVerticalFlip(MULTIPOINT_LINEAR_ELEMENT_FLIP_TOLERANCE_IN_PIXELS); + }); - // Check if width and height did not change - expect(API.getSelectedElements()[0].width).toBeCloseTo( - originalWidth, - FLIP_PRECISION_DECIMALS, - ); + it("flips an unrotated line horizontally correctly", async () => { + createAndSelectOneLine(); + await checkHorizontalFlip( + TWO_POINTS_LINEAR_ELEMENT_FLIP_TOLERANCE_IN_PIXELS, + ); + }); + //TODO: elements with curve outside minMax points have a wrong bounding box + it.skip("flips an unrotated line horizontally with line outside min/max points bounds", async () => { + const line = createLinearElementsWithCurveOutsideMinMaxPoints("line"); + h.app.scene.replaceAllElements([line]); + h.app.setState({ selectedElementIds: { [line.id]: true } }); - expect(API.getSelectedElements()[0].height).toBeCloseTo( - originalHeight, - FLIP_PRECISION_DECIMALS, - ); -}); + await checkHorizontalFlip( + MULTIPOINT_LINEAR_ELEMENT_FLIP_TOLERANCE_IN_PIXELS, + ); + }); -it("flips an unrotated line vertically correctly", () => { - createAndSelectOneLine(); + //TODO: elements with curve outside minMax points have a wrong bounding box + it.skip("flips an unrotated line vertically with line outside min/max points bounds", async () => { + const line = createLinearElementsWithCurveOutsideMinMaxPoints("line"); + h.app.scene.replaceAllElements([line]); + h.app.setState({ selectedElementIds: { [line.id]: true } }); - const originalWidth = API.getSelectedElements()[0].width; - const originalHeight = API.getSelectedElements()[0].height; + await checkVerticalFlip(MULTIPOINT_LINEAR_ELEMENT_FLIP_TOLERANCE_IN_PIXELS); + }); - h.app.actionManager.executeAction(actionFlipVertical); + //TODO: elements with curve outside minMax points have a wrong bounding box + it.skip("flips a rotated line horizontally with line outside min/max points bounds", async () => { + const originalAngle = Math.PI / 4; + const expectedAngle = (7 * Math.PI) / 4; + const line = createLinearElementsWithCurveOutsideMinMaxPoints("line"); + mutateElement(line, { angle: originalAngle }); + h.app.scene.replaceAllElements([line]); + h.app.setState({ selectedElementIds: { [line.id]: true } }); - // Check if width and height did not change - expect(API.getSelectedElements()[0].width).toBeCloseTo( - originalWidth, - FLIP_PRECISION_DECIMALS, - ); + await checkRotatedHorizontalFlip( + expectedAngle, + MULTIPOINT_LINEAR_ELEMENT_FLIP_TOLERANCE_IN_PIXELS, + ); + }); - expect(API.getSelectedElements()[0].height).toBeCloseTo( - originalHeight, - FLIP_PRECISION_DECIMALS, - ); -}); + //TODO: elements with curve outside minMax points have a wrong bounding box + it.skip("flips a rotated line vertically with line outside min/max points bounds", async () => { + const originalAngle = Math.PI / 4; + const expectedAngle = (3 * Math.PI) / 4; + const line = createLinearElementsWithCurveOutsideMinMaxPoints("line"); + mutateElement(line, { angle: originalAngle }); + h.app.scene.replaceAllElements([line]); + h.app.setState({ selectedElementIds: { [line.id]: true } }); -it.skip("flips a rotated line horizontally correctly", () => { - const originalAngle = Math.PI / 4; - const expectedAngle = (7 * Math.PI) / 4; + await checkRotatedVerticalFlip( + expectedAngle, + MULTIPOINT_LINEAR_ELEMENT_FLIP_TOLERANCE_IN_PIXELS, + ); + }); - createAndSelectOneLine(originalAngle); + it("flips an unrotated line vertically correctly", async () => { + createAndSelectOneLine(); + await checkVerticalFlip(TWO_POINTS_LINEAR_ELEMENT_FLIP_TOLERANCE_IN_PIXELS); + }); - const originalWidth = API.getSelectedElements()[0].width; - const originalHeight = API.getSelectedElements()[0].height; + it("flips a rotated line horizontally with line inside min/max points bounds", async () => { + const originalAngle = Math.PI / 4; + const expectedAngle = (7 * Math.PI) / 4; + const line = createLinearElementWithCurveInsideMinMaxPoints("line"); + h.app.scene.replaceAllElements([line]); + h.app.state.selectedElementIds[line.id] = true; + mutateElement(line, { + angle: originalAngle, + }); - h.app.actionManager.executeAction(actionFlipHorizontal); + await checkRotatedHorizontalFlip( + expectedAngle, + MULTIPOINT_LINEAR_ELEMENT_FLIP_TOLERANCE_IN_PIXELS, + ); + }); - // Check if width and height did not change - expect(API.getSelectedElements()[0].width).toBeCloseTo( - originalWidth, - FLIP_PRECISION_DECIMALS, - ); + it("flips a rotated line vertically with line inside min/max points bounds", async () => { + const originalAngle = Math.PI / 4; + const expectedAngle = (3 * Math.PI) / 4; + const line = createLinearElementWithCurveInsideMinMaxPoints("line"); + h.app.scene.replaceAllElements([line]); + h.app.state.selectedElementIds[line.id] = true; + mutateElement(line, { + angle: originalAngle, + }); - expect(API.getSelectedElements()[0].height).toBeCloseTo( - originalHeight, - FLIP_PRECISION_DECIMALS, - ); + await checkRotatedVerticalFlip( + expectedAngle, + MULTIPOINT_LINEAR_ELEMENT_FLIP_TOLERANCE_IN_PIXELS, + ); + }); - // Check angle - expect(API.getSelectedElements()[0].angle).toBeCloseTo(expectedAngle); -}); + it("flips a two points line horizontally correctly", async () => { + createAndSelectOneLine(); + await checkTwoPointsLineHorizontalFlip(); + }); -it.skip("flips a rotated line vertically correctly", () => { - const originalAngle = Math.PI / 4; - const expectedAngle = (3 * Math.PI) / 4; - - createAndSelectOneLine(originalAngle); - - const originalWidth = API.getSelectedElements()[0].width; - const originalHeight = API.getSelectedElements()[0].height; - - h.app.actionManager.executeAction(actionFlipVertical); - - // Check if width and height did not change - expect(API.getSelectedElements()[0].width).toBeCloseTo( - originalWidth, - FLIP_PRECISION_DECIMALS, - ); - - expect(API.getSelectedElements()[0].height).toBeCloseTo( - originalHeight, - FLIP_PRECISION_DECIMALS, - ); - - // Check angle - expect(API.getSelectedElements()[0].angle).toBeCloseTo(expectedAngle); + it("flips a two points line vertically correctly", async () => { + createAndSelectOneLine(); + await checkTwoPointsLineVerticalFlip(); + }); }); // Draw element +describe("freedraw", () => { + it("flips an unrotated drawing horizontally correctly", async () => { + const draw = createAndReturnOneDraw(); + // select draw, since not done automatically + h.state.selectedElementIds[draw.id] = true; + await checkHorizontalFlip(); + }); -it("flips an unrotated drawing horizontally correctly", () => { - const draw = createAndReturnOneDraw(); - // select draw, since not done automatically - h.state.selectedElementIds[draw.id] = true; + it("flips an unrotated drawing vertically correctly", async () => { + const draw = createAndReturnOneDraw(); + // select draw, since not done automatically + h.state.selectedElementIds[draw.id] = true; + await checkVerticalFlip(); + }); - const originalWidth = draw.width; - const originalHeight = draw.height; + it("flips a rotated drawing horizontally correctly", async () => { + const originalAngle = Math.PI / 4; + const expectedAngle = (7 * Math.PI) / 4; - h.app.actionManager.executeAction(actionFlipHorizontal); + const draw = createAndReturnOneDraw(originalAngle); + // select draw, since not done automatically + h.state.selectedElementIds[draw.id] = true; - // Check if width and height did not change - expect(draw.width).toBeCloseTo(originalWidth, FLIP_PRECISION_DECIMALS); + await checkRotatedHorizontalFlip(expectedAngle); + }); - expect(draw.height).toBeCloseTo(originalHeight, FLIP_PRECISION_DECIMALS); + it("flips a rotated drawing vertically correctly", async () => { + const originalAngle = Math.PI / 4; + const expectedAngle = (3 * Math.PI) / 4; + + const draw = createAndReturnOneDraw(originalAngle); + // select draw, since not done automatically + h.state.selectedElementIds[draw.id] = true; + + await checkRotatedVerticalFlip(expectedAngle); + }); }); -it("flips an unrotated drawing vertically correctly", () => { - const draw = createAndReturnOneDraw(); - // select draw, since not done automatically - h.state.selectedElementIds[draw.id] = true; +//image +//TODO: currently there is no test for pixel colors at flipped positions. +describe("image", () => { + const createImage = async () => { + const sendPasteEvent = (file?: File) => { + const clipboardEvent = new Event("paste", { + bubbles: true, + cancelable: true, + composed: true, + }); - const originalWidth = draw.width; - const originalHeight = draw.height; + // set `clipboardData` properties. + // @ts-ignore + clipboardEvent.clipboardData = { + getData: () => window.navigator.clipboard.readText(), + files: [file], + }; - h.app.actionManager.executeAction(actionFlipVertical); + document.dispatchEvent(clipboardEvent); + }; - // Check if width and height did not change - expect(draw.width).toBeCloseTo(originalWidth, FLIP_PRECISION_DECIMALS); + sendPasteEvent(await API.loadFile("./fixtures/smiley_embedded_v2.png")); + }; - expect(draw.height).toBeCloseTo(originalHeight, FLIP_PRECISION_DECIMALS); -}); - -it("flips a rotated drawing horizontally correctly", () => { - const originalAngle = Math.PI / 4; - const expectedAngle = (7 * Math.PI) / 4; - - const draw = createAndReturnOneDraw(originalAngle); - // select draw, since not done automatically - h.state.selectedElementIds[draw.id] = true; - - const originalWidth = draw.width; - const originalHeight = draw.height; - - h.app.actionManager.executeAction(actionFlipHorizontal); - - // Check if width and height did not change - expect(draw.width).toBeCloseTo(originalWidth, FLIP_PRECISION_DECIMALS); - - expect(draw.height).toBeCloseTo(originalHeight, FLIP_PRECISION_DECIMALS); - - // Check angle - expect(draw.angle).toBeCloseTo(expectedAngle); -}); - -it("flips a rotated drawing vertically correctly", () => { - const originalAngle = Math.PI / 4; - const expectedAngle = (3 * Math.PI) / 4; - - const draw = createAndReturnOneDraw(originalAngle); - // select draw, since not done automatically - h.state.selectedElementIds[draw.id] = true; - - const originalWidth = draw.width; - const originalHeight = draw.height; - - h.app.actionManager.executeAction(actionFlipVertical); - - // Check if width and height did not change - - expect(API.getSelectedElement().width).toBeCloseTo( - originalWidth, - FLIP_PRECISION_DECIMALS, - ); - - expect(API.getSelectedElement().height).toBeCloseTo( - originalHeight, - FLIP_PRECISION_DECIMALS, - ); - - // Check angle - expect(API.getSelectedElement().angle).toBeCloseTo(expectedAngle); + it("flips an unrotated image horizontally correctly", async () => { + //paste image + await createImage(); + + await waitFor(() => { + expect((h.elements[0] as ExcalidrawImageElement).scale).toEqual([1, 1]); + expect(API.getSelectedElements().length).toBeGreaterThan(0); + expect(API.getSelectedElements()[0].type).toEqual("image"); + expect(h.app.files.fileId).toBeDefined(); + }); + await checkHorizontalFlip(); + expect((h.elements[0] as ExcalidrawImageElement).scale).toEqual([-1, 1]); + expect(h.elements[0].angle).toBeCloseTo(0); + }); + + it("flips an unrotated image vertically correctly", async () => { + //paste image + await createImage(); + await waitFor(() => { + expect((h.elements[0] as ExcalidrawImageElement).scale).toEqual([1, 1]); + expect(API.getSelectedElements().length).toBeGreaterThan(0); + expect(API.getSelectedElements()[0].type).toEqual("image"); + expect(h.app.files.fileId).toBeDefined(); + }); + + await checkVerticalFlip(); + expect((h.elements[0] as ExcalidrawImageElement).scale).toEqual([-1, 1]); + expect(h.elements[0].angle).toBeCloseTo(Math.PI); + }); + + it("flips an rotated image horizontally correctly", async () => { + const originalAngle = Math.PI / 4; + const expectedAngle = (7 * Math.PI) / 4; + //paste image + await createImage(); + await waitFor(() => { + expect((h.elements[0] as ExcalidrawImageElement).scale).toEqual([1, 1]); + expect(API.getSelectedElements().length).toBeGreaterThan(0); + expect(API.getSelectedElements()[0].type).toEqual("image"); + expect(h.app.files.fileId).toBeDefined(); + }); + mutateElement(h.elements[0], { + angle: originalAngle, + }); + await checkRotatedHorizontalFlip(expectedAngle); + expect((h.elements[0] as ExcalidrawImageElement).scale).toEqual([-1, 1]); + }); + + it("flips an rotated image vertically correctly", async () => { + const originalAngle = Math.PI / 4; + const expectedAngle = (3 * Math.PI) / 4; + //paste image + await createImage(); + await waitFor(() => { + expect((h.elements[0] as ExcalidrawImageElement).scale).toEqual([1, 1]); + expect(h.elements[0].angle).toEqual(0); + expect(API.getSelectedElements().length).toBeGreaterThan(0); + expect(API.getSelectedElements()[0].type).toEqual("image"); + expect(h.app.files.fileId).toBeDefined(); + }); + mutateElement(h.elements[0], { + angle: originalAngle, + }); + + await checkRotatedVerticalFlip(expectedAngle); + expect((h.elements[0] as ExcalidrawImageElement).scale).toEqual([-1, 1]); + expect(h.elements[0].angle).toBeCloseTo(expectedAngle); + }); + + it("flips an image both vertically & horizontally", async () => { + //paste image + await createImage(); + await waitFor(() => { + expect((h.elements[0] as ExcalidrawImageElement).scale).toEqual([1, 1]); + expect(API.getSelectedElements().length).toBeGreaterThan(0); + expect(API.getSelectedElements()[0].type).toEqual("image"); + expect(h.app.files.fileId).toBeDefined(); + }); + + await checkVerticalHorizontalFlip(); + expect((h.elements[0] as ExcalidrawImageElement).scale).toEqual([1, 1]); + expect(h.elements[0].angle).toBeCloseTo(Math.PI); + }); });