diff --git a/packages/excalidraw/actions/actionFlip.test.tsx b/packages/excalidraw/actions/actionFlip.test.tsx
index c8a6239cdf..ef71dbe940 100644
--- a/packages/excalidraw/actions/actionFlip.test.tsx
+++ b/packages/excalidraw/actions/actionFlip.test.tsx
@@ -1,11 +1,897 @@
import React from "react";
-import { Excalidraw } from "../index";
-import { render } from "../tests/test-utils";
+import { Excalidraw, ROUNDNESS } from "../index";
+import {
+ fireEvent,
+ GlobalTestState,
+ render,
+ screen,
+ waitFor,
+} from "../tests/test-utils";
import { API } from "../tests/helpers/api";
-import { point } from "../../math";
+import type { LocalPoint } from "../../math";
+import { point, radians } from "../../math";
import { actionFlipHorizontal, actionFlipVertical } from "./actionFlip";
+import { Keyboard, Pointer, UI } from "../tests/helpers/ui";
+import { vi } from "vitest";
+import type {
+ ExcalidrawElement,
+ ExcalidrawImageElement,
+ ExcalidrawLinearElement,
+ ExcalidrawTextElementWithContainer,
+ FileId,
+} from "../element/types";
+import ReactDOM from "react-dom";
+import type { NormalizedZoomValue } from "../types";
+import { getElementAbsoluteCoords, newLinearElement } from "../element";
+import { arrayToMap, cloneJSON } from "../utils";
+import { createPasteEvent } from "../clipboard";
+import { KEYS } from "../keys";
+import { getBoundTextElementPosition } from "../element/textElement";
const { h } = window;
+const mouse = new Pointer("mouse");
+
+vi.mock("../data/blob", async (actual) => {
+ const orig: Object = await actual();
+ return {
+ ...orig,
+ resizeImageFile: (imageFile: File) => imageFile,
+ generateIdFromFile: () => "fileId" as FileId,
+ };
+});
+
+beforeEach(async () => {
+ // Unmount ReactDOM from root
+ ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
+
+ mouse.reset();
+ localStorage.clear();
+ sessionStorage.clear();
+ vi.clearAllMocks();
+
+ Object.assign(document, {
+ elementFromPoint: () => GlobalTestState.canvas,
+ });
+ await render();
+ API.setAppState({
+ zoom: {
+ value: 1 as NormalizedZoomValue,
+ },
+ });
+});
+
+const createAndSelectOneRectangle = (angle: number = 0) => {
+ UI.createElement("rectangle", {
+ x: 0,
+ y: 0,
+ width: 100,
+ height: 50,
+ angle,
+ });
+};
+
+const createAndSelectOneDiamond = (angle: number = 0) => {
+ UI.createElement("diamond", {
+ x: 0,
+ y: 0,
+ width: 100,
+ height: 50,
+ angle,
+ });
+};
+
+const createAndSelectOneEllipse = (angle: number = 0) => {
+ UI.createElement("ellipse", {
+ x: 0,
+ y: 0,
+ width: 100,
+ height: 50,
+ angle,
+ });
+};
+
+const createAndSelectOneArrow = (angle: number = 0) => {
+ UI.createElement("arrow", {
+ x: 0,
+ y: 0,
+ width: 100,
+ height: 50,
+ angle,
+ });
+};
+
+const createAndSelectOneLine = (angle: number = 0) => {
+ UI.createElement("line", {
+ x: 0,
+ y: 0,
+ width: 100,
+ height: 50,
+ angle,
+ });
+};
+
+const createAndReturnOneDraw = (angle: number = 0) => {
+ return UI.createElement("freedraw", {
+ x: 0,
+ y: 0,
+ width: 50,
+ height: 100,
+ angle,
+ });
+};
+
+const createLinearElementWithCurveInsideMinMaxPoints = (
+ type: "line" | "arrow",
+ extraProps: any = {},
+) => {
+ return newLinearElement({
+ type,
+ x: 2256.910668124894,
+ y: -2412.5069664197654,
+ width: 1750.4888916015625,
+ height: 410.51605224609375,
+ angle: radians(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: [
+ point(0, 0),
+ point(-922.4761962890625, 300.3277587890625),
+ point(828.0126953125, 410.51605224609375),
+ ],
+ });
+};
+
+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],
+ ],
+ ...extraProps,
+ });
+};
+
+const checkElementsBoundingBox = async (
+ element1: ExcalidrawElement,
+ element2: ExcalidrawElement,
+ toleranceInPx: number = 0,
+) => {
+ const elementsMap = arrayToMap([element1, element2]);
+ const [x1, y1, x2, y2] = getElementAbsoluteCoords(element1, elementsMap);
+
+ const [x12, y12, x22, y22] = getElementAbsoluteCoords(element2, elementsMap);
+
+ await waitFor(() => {
+ // Check if width and height did not change
+ expect(x2 - x1).toBeCloseTo(x22 - x12, -1);
+ expect(y2 - y1).toBeCloseTo(y22 - y12, -1);
+ });
+};
+
+const checkHorizontalFlip = async (toleranceInPx: number = 0.00001) => {
+ const originalElement = cloneJSON(h.elements[0]);
+ API.executeAction(actionFlipHorizontal);
+ const newElement = h.elements[0];
+ await checkElementsBoundingBox(originalElement, newElement, toleranceInPx);
+};
+
+const checkTwoPointsLineHorizontalFlip = async () => {
+ const originalElement = cloneJSON(h.elements[0]) as ExcalidrawLinearElement;
+ API.executeAction(actionFlipHorizontal);
+ const newElement = h.elements[0] as ExcalidrawLinearElement;
+ await waitFor(() => {
+ expect(originalElement.points[0][0]).toBeCloseTo(
+ -newElement.points[0][0],
+ 5,
+ );
+ expect(originalElement.points[0][1]).toBeCloseTo(
+ newElement.points[0][1],
+ 5,
+ );
+ expect(originalElement.points[1][0]).toBeCloseTo(
+ -newElement.points[1][0],
+ 5,
+ );
+ expect(originalElement.points[1][1]).toBeCloseTo(
+ newElement.points[1][1],
+ 5,
+ );
+ });
+};
+
+const checkTwoPointsLineVerticalFlip = async () => {
+ const originalElement = cloneJSON(h.elements[0]) as ExcalidrawLinearElement;
+ API.executeAction(actionFlipVertical);
+ const newElement = h.elements[0] as ExcalidrawLinearElement;
+ await waitFor(() => {
+ expect(originalElement.points[0][0]).toBeCloseTo(
+ newElement.points[0][0],
+ 5,
+ );
+ expect(originalElement.points[0][1]).toBeCloseTo(
+ -newElement.points[0][1],
+ 5,
+ );
+ expect(originalElement.points[1][0]).toBeCloseTo(
+ newElement.points[1][0],
+ 5,
+ );
+ expect(originalElement.points[1][1]).toBeCloseTo(
+ -newElement.points[1][1],
+ 5,
+ );
+ });
+};
+
+const checkRotatedHorizontalFlip = async (
+ expectedAngle: number,
+ toleranceInPx: number = 0.00001,
+) => {
+ const originalElement = cloneJSON(h.elements[0]);
+ API.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 = cloneJSON(h.elements[0]);
+ API.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 = cloneJSON(h.elements[0]);
+
+ API.executeAction(actionFlipVertical);
+
+ const newElement = h.elements[0];
+ await checkElementsBoundingBox(originalElement, newElement, toleranceInPx);
+};
+
+const checkVerticalHorizontalFlip = async (toleranceInPx: number = 0.00001) => {
+ const originalElement = cloneJSON(h.elements[0]);
+
+ API.executeAction(actionFlipHorizontal);
+ API.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();
+ await checkHorizontalFlip();
+ });
+
+ it("flips an unrotated rectangle vertically correctly", async () => {
+ createAndSelectOneRectangle();
+
+ await checkVerticalFlip();
+ });
+
+ it("flips a rotated rectangle horizontally correctly", async () => {
+ const originalAngle = (3 * Math.PI) / 4;
+ const expectedAngle = (5 * Math.PI) / 4;
+
+ createAndSelectOneRectangle(originalAngle);
+
+ await checkRotatedHorizontalFlip(expectedAngle);
+ });
+
+ it("flips a rotated rectangle vertically correctly", async () => {
+ const originalAngle = (3 * Math.PI) / 4;
+ const expectedAgnle = (5 * Math.PI) / 4;
+
+ createAndSelectOneRectangle(originalAngle);
+
+ await checkRotatedVerticalFlip(expectedAgnle);
+ });
+});
+
+// Diamond element
+describe("diamond", () => {
+ it("flips an unrotated diamond horizontally correctly", async () => {
+ createAndSelectOneDiamond();
+
+ await checkHorizontalFlip();
+ });
+
+ it("flips an unrotated diamond vertically correctly", async () => {
+ createAndSelectOneDiamond();
+
+ await checkVerticalFlip();
+ });
+
+ it("flips a rotated diamond horizontally correctly", async () => {
+ const originalAngle = (5 * Math.PI) / 4;
+ const expectedAngle = (3 * Math.PI) / 4;
+
+ createAndSelectOneDiamond(originalAngle);
+
+ await checkRotatedHorizontalFlip(expectedAngle);
+ });
+
+ it("flips a rotated diamond vertically correctly", async () => {
+ const originalAngle = (5 * Math.PI) / 4;
+ const expectedAngle = (3 * Math.PI) / 4;
+
+ createAndSelectOneDiamond(originalAngle);
+
+ await checkRotatedVerticalFlip(expectedAngle);
+ });
+});
+
+// Ellipse element
+describe("ellipse", () => {
+ it("flips an unrotated ellipse horizontally correctly", async () => {
+ createAndSelectOneEllipse();
+
+ await checkHorizontalFlip();
+ });
+
+ it("flips an unrotated ellipse vertically correctly", async () => {
+ createAndSelectOneEllipse();
+
+ await checkVerticalFlip();
+ });
+
+ it("flips a rotated ellipse horizontally correctly", async () => {
+ const originalAngle = (7 * Math.PI) / 4;
+ const expectedAngle = Math.PI / 4;
+
+ createAndSelectOneEllipse(originalAngle);
+
+ await checkRotatedHorizontalFlip(expectedAngle);
+ });
+
+ it("flips a rotated ellipse vertically correctly", async () => {
+ const originalAngle = (7 * Math.PI) / 4;
+ const expectedAngle = Math.PI / 4;
+
+ createAndSelectOneEllipse(originalAngle);
+
+ 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");
+ API.setElements([arrow]);
+ API.setAppState({ selectedElementIds: { [arrow.id]: true } });
+ await checkHorizontalFlip(
+ MULTIPOINT_LINEAR_ELEMENT_FLIP_TOLERANCE_IN_PIXELS,
+ );
+ });
+
+ it("flips an unrotated arrow vertically with line inside min/max points bounds", async () => {
+ const arrow = createLinearElementWithCurveInsideMinMaxPoints("arrow");
+ API.setElements([arrow]);
+ API.setAppState({ selectedElementIds: { [arrow.id]: true } });
+
+ await checkVerticalFlip(50);
+ });
+
+ it("flips a rotated arrow horizontally with line inside min/max points bounds", async () => {
+ const originalAngle = radians(Math.PI / 4);
+ const expectedAngle = radians((7 * Math.PI) / 4);
+ const line = createLinearElementWithCurveInsideMinMaxPoints("arrow");
+ API.setElements([line]);
+ API.setAppState({
+ selectedElementIds: {
+ ...h.state.selectedElementIds,
+ [line.id]: true,
+ },
+ });
+ API.updateElement(line, {
+ angle: originalAngle,
+ });
+
+ await checkRotatedHorizontalFlip(
+ expectedAngle,
+ MULTIPOINT_LINEAR_ELEMENT_FLIP_TOLERANCE_IN_PIXELS,
+ );
+ });
+
+ it("flips a rotated arrow vertically with line inside min/max points bounds", async () => {
+ const originalAngle = radians(Math.PI / 4);
+ const expectedAngle = radians((7 * Math.PI) / 4);
+ const line = createLinearElementWithCurveInsideMinMaxPoints("arrow");
+ API.setElements([line]);
+ API.setAppState({
+ selectedElementIds: {
+ ...h.state.selectedElementIds,
+ [line.id]: true,
+ },
+ });
+ API.updateElement(line, {
+ angle: originalAngle,
+ });
+
+ await checkRotatedVerticalFlip(
+ expectedAngle,
+ MULTIPOINT_LINEAR_ELEMENT_FLIP_TOLERANCE_IN_PIXELS,
+ );
+ });
+
+ //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");
+ API.setElements([arrow]);
+ API.setAppState({ selectedElementIds: { [arrow.id]: true } });
+
+ await checkHorizontalFlip(
+ MULTIPOINT_LINEAR_ELEMENT_FLIP_TOLERANCE_IN_PIXELS,
+ );
+ });
+
+ //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 = radians(Math.PI / 4);
+ const expectedAngle = radians((7 * Math.PI) / 4);
+ const line = createLinearElementsWithCurveOutsideMinMaxPoints("arrow");
+ API.updateElement(line, { angle: originalAngle });
+ API.setElements([line]);
+ API.setAppState({ selectedElementIds: { [line.id]: true } });
+
+ await checkRotatedVerticalFlip(
+ expectedAngle,
+ MULTIPOINT_LINEAR_ELEMENT_FLIP_TOLERANCE_IN_PIXELS,
+ );
+ });
+
+ //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");
+ API.setElements([arrow]);
+ API.setAppState({ selectedElementIds: { [arrow.id]: true } });
+
+ await checkVerticalFlip(MULTIPOINT_LINEAR_ELEMENT_FLIP_TOLERANCE_IN_PIXELS);
+ });
+
+ //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 = radians(Math.PI / 4);
+ const expectedAngle = radians((7 * Math.PI) / 4);
+ const line = createLinearElementsWithCurveOutsideMinMaxPoints("arrow");
+ API.updateElement(line, { angle: originalAngle });
+ API.setElements([line]);
+ API.setAppState({ selectedElementIds: { [line.id]: true } });
+
+ await checkRotatedVerticalFlip(
+ expectedAngle,
+ MULTIPOINT_LINEAR_ELEMENT_FLIP_TOLERANCE_IN_PIXELS,
+ );
+ });
+
+ it("flips an unrotated arrow horizontally correctly", async () => {
+ createAndSelectOneArrow();
+ await checkHorizontalFlip(
+ TWO_POINTS_LINEAR_ELEMENT_FLIP_TOLERANCE_IN_PIXELS,
+ );
+ });
+
+ it("flips an unrotated arrow vertically correctly", async () => {
+ createAndSelectOneArrow();
+ await checkVerticalFlip(TWO_POINTS_LINEAR_ELEMENT_FLIP_TOLERANCE_IN_PIXELS);
+ });
+
+ it("flips a two points arrow horizontally correctly", async () => {
+ createAndSelectOneArrow();
+ await checkTwoPointsLineHorizontalFlip();
+ });
+
+ it("flips a two points arrow vertically correctly", async () => {
+ createAndSelectOneArrow();
+ await checkTwoPointsLineVerticalFlip();
+ });
+});
+
+// Line element
+describe("line", () => {
+ it("flips an unrotated line horizontally with line inside min/max points bounds", async () => {
+ const line = createLinearElementWithCurveInsideMinMaxPoints("line");
+ API.setElements([line]);
+ API.setAppState({ selectedElementIds: { [line.id]: true } });
+
+ await checkHorizontalFlip(
+ MULTIPOINT_LINEAR_ELEMENT_FLIP_TOLERANCE_IN_PIXELS,
+ );
+ });
+
+ it("flips an unrotated line vertically with line inside min/max points bounds", async () => {
+ const line = createLinearElementWithCurveInsideMinMaxPoints("line");
+ API.setElements([line]);
+ API.setAppState({ selectedElementIds: { [line.id]: true } });
+
+ await checkVerticalFlip(MULTIPOINT_LINEAR_ELEMENT_FLIP_TOLERANCE_IN_PIXELS);
+ });
+
+ 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");
+ API.setElements([line]);
+ API.setAppState({ selectedElementIds: { [line.id]: true } });
+
+ await checkHorizontalFlip(
+ MULTIPOINT_LINEAR_ELEMENT_FLIP_TOLERANCE_IN_PIXELS,
+ );
+ });
+
+ //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");
+ API.setElements([line]);
+ API.setAppState({ selectedElementIds: { [line.id]: true } });
+
+ await checkVerticalFlip(MULTIPOINT_LINEAR_ELEMENT_FLIP_TOLERANCE_IN_PIXELS);
+ });
+
+ //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 = radians(Math.PI / 4);
+ const expectedAngle = radians((7 * Math.PI) / 4);
+ const line = createLinearElementsWithCurveOutsideMinMaxPoints("line");
+ API.updateElement(line, { angle: originalAngle });
+ API.setElements([line]);
+ API.setAppState({ selectedElementIds: { [line.id]: true } });
+
+ await checkRotatedHorizontalFlip(
+ expectedAngle,
+ MULTIPOINT_LINEAR_ELEMENT_FLIP_TOLERANCE_IN_PIXELS,
+ );
+ });
+
+ //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 = radians(Math.PI / 4);
+ const expectedAngle = radians((7 * Math.PI) / 4);
+ const line = createLinearElementsWithCurveOutsideMinMaxPoints("line");
+ API.updateElement(line, { angle: originalAngle });
+ API.setElements([line]);
+ API.setAppState({ selectedElementIds: { [line.id]: true } });
+
+ await checkRotatedVerticalFlip(
+ expectedAngle,
+ MULTIPOINT_LINEAR_ELEMENT_FLIP_TOLERANCE_IN_PIXELS,
+ );
+ });
+
+ it("flips an unrotated line vertically correctly", async () => {
+ createAndSelectOneLine();
+ await checkVerticalFlip(TWO_POINTS_LINEAR_ELEMENT_FLIP_TOLERANCE_IN_PIXELS);
+ });
+
+ it("flips a rotated line horizontally with line inside min/max points bounds", async () => {
+ const originalAngle = radians(Math.PI / 4);
+ const expectedAngle = radians((7 * Math.PI) / 4);
+ const line = createLinearElementWithCurveInsideMinMaxPoints("line");
+ API.setElements([line]);
+ API.setAppState({
+ selectedElementIds: {
+ ...h.state.selectedElementIds,
+ [line.id]: true,
+ },
+ });
+ API.updateElement(line, {
+ angle: originalAngle,
+ });
+
+ await checkRotatedHorizontalFlip(
+ expectedAngle,
+ MULTIPOINT_LINEAR_ELEMENT_FLIP_TOLERANCE_IN_PIXELS,
+ );
+ });
+
+ it("flips a rotated line vertically with line inside min/max points bounds", async () => {
+ const originalAngle = radians(Math.PI / 4);
+ const expectedAngle = radians((7 * Math.PI) / 4);
+ const line = createLinearElementWithCurveInsideMinMaxPoints("line");
+ API.setElements([line]);
+ API.setAppState({
+ selectedElementIds: {
+ ...h.state.selectedElementIds,
+ [line.id]: true,
+ },
+ });
+ API.updateElement(line, {
+ angle: originalAngle,
+ });
+
+ await checkRotatedVerticalFlip(
+ expectedAngle,
+ MULTIPOINT_LINEAR_ELEMENT_FLIP_TOLERANCE_IN_PIXELS,
+ );
+ });
+
+ it("flips a two points line horizontally correctly", async () => {
+ createAndSelectOneLine();
+ await checkTwoPointsLineHorizontalFlip();
+ });
+
+ 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
+ API.setAppState({
+ selectedElementIds: {
+ ...h.state.selectedElementIds,
+ [draw.id]: true,
+ },
+ });
+ await checkHorizontalFlip();
+ });
+
+ it("flips an unrotated drawing vertically correctly", async () => {
+ const draw = createAndReturnOneDraw();
+ // select draw, since not done automatically
+ API.setAppState({
+ selectedElementIds: {
+ ...h.state.selectedElementIds,
+ [draw.id]: true,
+ },
+ });
+ await checkVerticalFlip();
+ });
+
+ it("flips a rotated drawing horizontally correctly", async () => {
+ const originalAngle = Math.PI / 4;
+ const expectedAngle = (7 * Math.PI) / 4;
+
+ const draw = createAndReturnOneDraw(originalAngle);
+ // select draw, since not done automatically
+ API.setAppState({
+ selectedElementIds: {
+ ...h.state.selectedElementIds,
+ [draw.id]: true,
+ },
+ });
+
+ await checkRotatedHorizontalFlip(expectedAngle);
+ });
+
+ it("flips a rotated drawing vertically correctly", async () => {
+ const originalAngle = Math.PI / 4;
+ const expectedAngle = (7 * Math.PI) / 4;
+
+ const draw = createAndReturnOneDraw(originalAngle);
+ // select draw, since not done automatically
+ API.setAppState({
+ selectedElementIds: {
+ ...h.state.selectedElementIds,
+ [draw.id]: true,
+ },
+ });
+
+ await checkRotatedVerticalFlip(expectedAngle);
+ });
+});
+
+//image
+//TODO: currently there is no test for pixel colors at flipped positions.
+describe("image", () => {
+ const createImage = async () => {
+ const sendPasteEvent = (file?: File) => {
+ const clipboardEvent = createPasteEvent({ files: file ? [file] : [] });
+ document.dispatchEvent(clipboardEvent);
+ };
+
+ sendPasteEvent(await API.loadFile("./fixtures/smiley_embedded_v2.png"));
+ };
+
+ 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(0);
+ });
+
+ it("flips an rotated image horizontally correctly", async () => {
+ const originalAngle = radians(Math.PI / 4);
+ const expectedAngle = radians((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();
+ });
+ API.updateElement(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 = radians(Math.PI / 4);
+ const expectedAngle = radians((7 * 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();
+ });
+ API.updateElement(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(0);
+ });
+});
+
+describe("mutliple elements", () => {
+ it("with bound text flip correctly", async () => {
+ UI.clickTool("arrow");
+ fireEvent.click(screen.getByTitle("Architect"));
+ const arrow = UI.createElement("arrow", {
+ x: 0,
+ y: 0,
+ width: 180,
+ height: 80,
+ });
+
+ Keyboard.keyPress(KEYS.ENTER);
+ let editor = document.querySelector(
+ ".excalidraw-textEditorContainer > textarea",
+ )!;
+ fireEvent.input(editor, { target: { value: "arrow" } });
+ Keyboard.exitTextEditor(editor);
+
+ const rectangle = UI.createElement("rectangle", {
+ x: 0,
+ y: 100,
+ width: 100,
+ height: 100,
+ });
+
+ Keyboard.keyPress(KEYS.ENTER);
+ editor = document.querySelector(
+ ".excalidraw-textEditorContainer > textarea",
+ )!;
+ fireEvent.input(editor, { target: { value: "rect\ntext" } });
+ Keyboard.exitTextEditor(editor);
+
+ mouse.select([arrow, rectangle]);
+ API.executeAction(actionFlipHorizontal);
+ API.executeAction(actionFlipVertical);
+
+ const arrowText = h.elements[1] as ExcalidrawTextElementWithContainer;
+ const arrowTextPos = getBoundTextElementPosition(
+ arrow.get(),
+ arrowText,
+ arrayToMap(h.elements),
+ )!;
+ const rectText = h.elements[3] as ExcalidrawTextElementWithContainer;
+
+ expect(arrow.x).toBeCloseTo(180);
+ expect(arrow.y).toBeCloseTo(200);
+ expect(arrow.points[1][0]).toBeCloseTo(-180);
+ expect(arrow.points[1][1]).toBeCloseTo(-80);
+
+ expect(arrowTextPos.x - (arrow.x - arrow.width)).toBeCloseTo(
+ arrow.x - (arrowTextPos.x + arrowText.width),
+ );
+ expect(arrowTextPos.y - (arrow.y - arrow.height)).toBeCloseTo(
+ arrow.y - (arrowTextPos.y + arrowText.height),
+ );
+
+ expect(rectangle.x).toBeCloseTo(80);
+ expect(rectangle.y).toBeCloseTo(0);
+
+ expect(rectText.x - rectangle.x).toBeCloseTo(
+ rectangle.x + rectangle.width - (rectText.x + rectText.width),
+ );
+ expect(rectText.y - rectangle.y).toBeCloseTo(
+ rectangle.y + rectangle.height - (rectText.y + rectText.height),
+ );
+ });
+});
describe("flipping re-centers selection", () => {
it("elbow arrow touches group selection side yet it remains in place after multiple moves", async () => {
diff --git a/packages/excalidraw/tests/flip.test.tsx b/packages/excalidraw/tests/flip.test.tsx
deleted file mode 100644
index 778f148b5c..0000000000
--- a/packages/excalidraw/tests/flip.test.tsx
+++ /dev/null
@@ -1,895 +0,0 @@
-import ReactDOM from "react-dom";
-import {
- fireEvent,
- GlobalTestState,
- render,
- screen,
- waitFor,
-} from "./test-utils";
-import { UI, Pointer, Keyboard } from "./helpers/ui";
-import { API } from "./helpers/api";
-import { actionFlipHorizontal, actionFlipVertical } from "../actions";
-import { getElementAbsoluteCoords } from "../element";
-import type {
- ExcalidrawElement,
- ExcalidrawImageElement,
- ExcalidrawLinearElement,
- ExcalidrawTextElementWithContainer,
- FileId,
-} from "../element/types";
-import { newLinearElement } from "../element";
-import { Excalidraw } from "../index";
-import type { NormalizedZoomValue } from "../types";
-import { ROUNDNESS } from "../constants";
-import { vi } from "vitest";
-import { KEYS } from "../keys";
-import { getBoundTextElementPosition } from "../element/textElement";
-import { createPasteEvent } from "../clipboard";
-import { arrayToMap, cloneJSON } from "../utils";
-import type { LocalPoint, Radians } from "../../math";
-import { point, radians } from "../../math";
-
-const { h } = window;
-const mouse = new Pointer("mouse");
-
-vi.mock("../data/blob", async (actual) => {
- const orig: Object = await actual();
- return {
- ...orig,
- resizeImageFile: (imageFile: File) => imageFile,
- generateIdFromFile: () => "fileId" as FileId,
- };
-});
-
-beforeEach(async () => {
- // Unmount ReactDOM from root
- ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
-
- mouse.reset();
- localStorage.clear();
- sessionStorage.clear();
- vi.clearAllMocks();
-
- Object.assign(document, {
- elementFromPoint: () => GlobalTestState.canvas,
- });
- await render();
- API.setAppState({
- zoom: {
- value: 1 as NormalizedZoomValue,
- },
- });
-});
-
-const createAndSelectOneRectangle = (angle: number = 0) => {
- UI.createElement("rectangle", {
- x: 0,
- y: 0,
- width: 100,
- height: 50,
- angle,
- });
-};
-
-const createAndSelectOneDiamond = (angle: number = 0) => {
- UI.createElement("diamond", {
- x: 0,
- y: 0,
- width: 100,
- height: 50,
- angle,
- });
-};
-
-const createAndSelectOneEllipse = (angle: number = 0) => {
- UI.createElement("ellipse", {
- x: 0,
- y: 0,
- width: 100,
- height: 50,
- angle,
- });
-};
-
-const createAndSelectOneArrow = (angle: number = 0) => {
- UI.createElement("arrow", {
- x: 0,
- y: 0,
- width: 100,
- height: 50,
- angle,
- });
-};
-
-const createAndSelectOneLine = (angle: number = 0) => {
- UI.createElement("line", {
- x: 0,
- y: 0,
- width: 100,
- height: 50,
- angle,
- });
-};
-
-const createAndReturnOneDraw = (angle: number = 0) => {
- return UI.createElement("freedraw", {
- x: 0,
- y: 0,
- width: 50,
- height: 100,
- angle,
- });
-};
-
-const createLinearElementWithCurveInsideMinMaxPoints = (
- type: "line" | "arrow",
- extraProps: any = {},
-) => {
- return newLinearElement({
- type,
- x: 2256.910668124894,
- y: -2412.5069664197654,
- width: 1750.4888916015625,
- height: 410.51605224609375,
- angle: radians(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: [
- point(0, 0),
- point(-922.4761962890625, 300.3277587890625),
- point(828.0126953125, 410.51605224609375),
- ],
- });
-};
-
-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],
- ],
- ...extraProps,
- });
-};
-
-const checkElementsBoundingBox = async (
- element1: ExcalidrawElement,
- element2: ExcalidrawElement,
- toleranceInPx: number = 0,
-) => {
- const elementsMap = arrayToMap([element1, element2]);
- const [x1, y1, x2, y2] = getElementAbsoluteCoords(element1, elementsMap);
-
- const [x12, y12, x22, y22] = getElementAbsoluteCoords(element2, elementsMap);
-
- await waitFor(() => {
- // Check if width and height did not change
- expect(x2 - x1).toBeCloseTo(x22 - x12, -1);
- expect(y2 - y1).toBeCloseTo(y22 - y12, -1);
- });
-};
-
-const checkHorizontalFlip = async (toleranceInPx: number = 0.00001) => {
- const originalElement = cloneJSON(h.elements[0]);
- API.executeAction(actionFlipHorizontal);
- const newElement = h.elements[0];
- await checkElementsBoundingBox(originalElement, newElement, toleranceInPx);
-};
-
-const checkTwoPointsLineHorizontalFlip = async () => {
- const originalElement = cloneJSON(h.elements[0]) as ExcalidrawLinearElement;
- API.executeAction(actionFlipHorizontal);
- const newElement = h.elements[0] as ExcalidrawLinearElement;
- await waitFor(() => {
- expect(originalElement.points[0][0]).toBeCloseTo(
- -newElement.points[0][0],
- 5,
- );
- expect(originalElement.points[0][1]).toBeCloseTo(
- newElement.points[0][1],
- 5,
- );
- expect(originalElement.points[1][0]).toBeCloseTo(
- -newElement.points[1][0],
- 5,
- );
- expect(originalElement.points[1][1]).toBeCloseTo(
- newElement.points[1][1],
- 5,
- );
- });
-};
-
-const checkTwoPointsLineVerticalFlip = async () => {
- const originalElement = cloneJSON(h.elements[0]) as ExcalidrawLinearElement;
- API.executeAction(actionFlipVertical);
- const newElement = h.elements[0] as ExcalidrawLinearElement;
- await waitFor(() => {
- expect(originalElement.points[0][0]).toBeCloseTo(
- newElement.points[0][0],
- 5,
- );
- expect(originalElement.points[0][1]).toBeCloseTo(
- -newElement.points[0][1],
- 5,
- );
- expect(originalElement.points[1][0]).toBeCloseTo(
- newElement.points[1][0],
- 5,
- );
- expect(originalElement.points[1][1]).toBeCloseTo(
- -newElement.points[1][1],
- 5,
- );
- });
-};
-
-const checkRotatedHorizontalFlip = async (
- expectedAngle: number,
- toleranceInPx: number = 0.00001,
-) => {
- const originalElement = cloneJSON(h.elements[0]);
- API.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 = cloneJSON(h.elements[0]);
- API.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 = cloneJSON(h.elements[0]);
-
- API.executeAction(actionFlipVertical);
-
- const newElement = h.elements[0];
- await checkElementsBoundingBox(originalElement, newElement, toleranceInPx);
-};
-
-const checkVerticalHorizontalFlip = async (toleranceInPx: number = 0.00001) => {
- const originalElement = cloneJSON(h.elements[0]);
-
- API.executeAction(actionFlipHorizontal);
- API.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();
- await checkHorizontalFlip();
- });
-
- it("flips an unrotated rectangle vertically correctly", async () => {
- createAndSelectOneRectangle();
-
- await checkVerticalFlip();
- });
-
- it("flips a rotated rectangle horizontally correctly", async () => {
- const originalAngle = (3 * Math.PI) / 4;
- const expectedAngle = (5 * Math.PI) / 4;
-
- createAndSelectOneRectangle(originalAngle);
-
- await checkRotatedHorizontalFlip(expectedAngle);
- });
-
- it("flips a rotated rectangle vertically correctly", async () => {
- const originalAngle = (3 * Math.PI) / 4;
- const expectedAgnle = (5 * Math.PI) / 4;
-
- createAndSelectOneRectangle(originalAngle);
-
- await checkRotatedVerticalFlip(expectedAgnle);
- });
-});
-
-// Diamond element
-describe("diamond", () => {
- it("flips an unrotated diamond horizontally correctly", async () => {
- createAndSelectOneDiamond();
-
- await checkHorizontalFlip();
- });
-
- it("flips an unrotated diamond vertically correctly", async () => {
- createAndSelectOneDiamond();
-
- await checkVerticalFlip();
- });
-
- it("flips a rotated diamond horizontally correctly", async () => {
- const originalAngle = (5 * Math.PI) / 4;
- const expectedAngle = (3 * Math.PI) / 4;
-
- createAndSelectOneDiamond(originalAngle);
-
- await checkRotatedHorizontalFlip(expectedAngle);
- });
-
- it("flips a rotated diamond vertically correctly", async () => {
- const originalAngle = (5 * Math.PI) / 4;
- const expectedAngle = (3 * Math.PI) / 4;
-
- createAndSelectOneDiamond(originalAngle);
-
- await checkRotatedVerticalFlip(expectedAngle);
- });
-});
-
-// Ellipse element
-describe("ellipse", () => {
- it("flips an unrotated ellipse horizontally correctly", async () => {
- createAndSelectOneEllipse();
-
- await checkHorizontalFlip();
- });
-
- it("flips an unrotated ellipse vertically correctly", async () => {
- createAndSelectOneEllipse();
-
- await checkVerticalFlip();
- });
-
- it("flips a rotated ellipse horizontally correctly", async () => {
- const originalAngle = (7 * Math.PI) / 4;
- const expectedAngle = Math.PI / 4;
-
- createAndSelectOneEllipse(originalAngle);
-
- await checkRotatedHorizontalFlip(expectedAngle);
- });
-
- it("flips a rotated ellipse vertically correctly", async () => {
- const originalAngle = (7 * Math.PI) / 4;
- const expectedAngle = Math.PI / 4;
-
- createAndSelectOneEllipse(originalAngle);
-
- 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");
- API.setElements([arrow]);
- API.setAppState({ selectedElementIds: { [arrow.id]: true } });
- await checkHorizontalFlip(
- MULTIPOINT_LINEAR_ELEMENT_FLIP_TOLERANCE_IN_PIXELS,
- );
- });
-
- it("flips an unrotated arrow vertically with line inside min/max points bounds", async () => {
- const arrow = createLinearElementWithCurveInsideMinMaxPoints("arrow");
- API.setElements([arrow]);
- API.setAppState({ selectedElementIds: { [arrow.id]: true } });
-
- await checkVerticalFlip(50);
- });
-
- it("flips a rotated arrow horizontally with line inside min/max points bounds", async () => {
- const originalAngle = (Math.PI / 4) as Radians;
- const expectedAngle = ((7 * Math.PI) / 4) as Radians;
- const line = createLinearElementWithCurveInsideMinMaxPoints("arrow");
- API.setElements([line]);
- API.setAppState({
- selectedElementIds: {
- ...h.state.selectedElementIds,
- [line.id]: true,
- },
- });
- API.updateElement(line, {
- angle: originalAngle,
- });
-
- await checkRotatedHorizontalFlip(
- expectedAngle,
- MULTIPOINT_LINEAR_ELEMENT_FLIP_TOLERANCE_IN_PIXELS,
- );
- });
-
- it("flips a rotated arrow vertically with line inside min/max points bounds", async () => {
- const originalAngle = (Math.PI / 4) as Radians;
- const expectedAngle = ((7 * Math.PI) / 4) as Radians;
- const line = createLinearElementWithCurveInsideMinMaxPoints("arrow");
- API.setElements([line]);
- API.setAppState({
- selectedElementIds: {
- ...h.state.selectedElementIds,
- [line.id]: true,
- },
- });
- API.updateElement(line, {
- angle: originalAngle,
- });
-
- await checkRotatedVerticalFlip(
- expectedAngle,
- MULTIPOINT_LINEAR_ELEMENT_FLIP_TOLERANCE_IN_PIXELS,
- );
- });
-
- //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");
- API.setElements([arrow]);
- API.setAppState({ selectedElementIds: { [arrow.id]: true } });
-
- await checkHorizontalFlip(
- MULTIPOINT_LINEAR_ELEMENT_FLIP_TOLERANCE_IN_PIXELS,
- );
- });
-
- //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) as Radians;
- const expectedAngle = ((7 * Math.PI) / 4) as Radians;
- const line = createLinearElementsWithCurveOutsideMinMaxPoints("arrow");
- API.updateElement(line, { angle: originalAngle });
- API.setElements([line]);
- API.setAppState({ selectedElementIds: { [line.id]: true } });
-
- await checkRotatedVerticalFlip(
- expectedAngle,
- MULTIPOINT_LINEAR_ELEMENT_FLIP_TOLERANCE_IN_PIXELS,
- );
- });
-
- //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");
- API.setElements([arrow]);
- API.setAppState({ selectedElementIds: { [arrow.id]: true } });
-
- await checkVerticalFlip(MULTIPOINT_LINEAR_ELEMENT_FLIP_TOLERANCE_IN_PIXELS);
- });
-
- //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) as Radians;
- const expectedAngle = ((7 * Math.PI) / 4) as Radians;
- const line = createLinearElementsWithCurveOutsideMinMaxPoints("arrow");
- API.updateElement(line, { angle: originalAngle });
- API.setElements([line]);
- API.setAppState({ selectedElementIds: { [line.id]: true } });
-
- await checkRotatedVerticalFlip(
- expectedAngle,
- MULTIPOINT_LINEAR_ELEMENT_FLIP_TOLERANCE_IN_PIXELS,
- );
- });
-
- it("flips an unrotated arrow horizontally correctly", async () => {
- createAndSelectOneArrow();
- await checkHorizontalFlip(
- TWO_POINTS_LINEAR_ELEMENT_FLIP_TOLERANCE_IN_PIXELS,
- );
- });
-
- it("flips an unrotated arrow vertically correctly", async () => {
- createAndSelectOneArrow();
- await checkVerticalFlip(TWO_POINTS_LINEAR_ELEMENT_FLIP_TOLERANCE_IN_PIXELS);
- });
-
- it("flips a two points arrow horizontally correctly", async () => {
- createAndSelectOneArrow();
- await checkTwoPointsLineHorizontalFlip();
- });
-
- it("flips a two points arrow vertically correctly", async () => {
- createAndSelectOneArrow();
- await checkTwoPointsLineVerticalFlip();
- });
-});
-
-// Line element
-describe("line", () => {
- it("flips an unrotated line horizontally with line inside min/max points bounds", async () => {
- const line = createLinearElementWithCurveInsideMinMaxPoints("line");
- API.setElements([line]);
- API.setAppState({ selectedElementIds: { [line.id]: true } });
-
- await checkHorizontalFlip(
- MULTIPOINT_LINEAR_ELEMENT_FLIP_TOLERANCE_IN_PIXELS,
- );
- });
-
- it("flips an unrotated line vertically with line inside min/max points bounds", async () => {
- const line = createLinearElementWithCurveInsideMinMaxPoints("line");
- API.setElements([line]);
- API.setAppState({ selectedElementIds: { [line.id]: true } });
-
- await checkVerticalFlip(MULTIPOINT_LINEAR_ELEMENT_FLIP_TOLERANCE_IN_PIXELS);
- });
-
- 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");
- API.setElements([line]);
- API.setAppState({ selectedElementIds: { [line.id]: true } });
-
- await checkHorizontalFlip(
- MULTIPOINT_LINEAR_ELEMENT_FLIP_TOLERANCE_IN_PIXELS,
- );
- });
-
- //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");
- API.setElements([line]);
- API.setAppState({ selectedElementIds: { [line.id]: true } });
-
- await checkVerticalFlip(MULTIPOINT_LINEAR_ELEMENT_FLIP_TOLERANCE_IN_PIXELS);
- });
-
- //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) as Radians;
- const expectedAngle = ((7 * Math.PI) / 4) as Radians;
- const line = createLinearElementsWithCurveOutsideMinMaxPoints("line");
- API.updateElement(line, { angle: originalAngle });
- API.setElements([line]);
- API.setAppState({ selectedElementIds: { [line.id]: true } });
-
- await checkRotatedHorizontalFlip(
- expectedAngle,
- MULTIPOINT_LINEAR_ELEMENT_FLIP_TOLERANCE_IN_PIXELS,
- );
- });
-
- //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) as Radians;
- const expectedAngle = ((7 * Math.PI) / 4) as Radians;
- const line = createLinearElementsWithCurveOutsideMinMaxPoints("line");
- API.updateElement(line, { angle: originalAngle });
- API.setElements([line]);
- API.setAppState({ selectedElementIds: { [line.id]: true } });
-
- await checkRotatedVerticalFlip(
- expectedAngle,
- MULTIPOINT_LINEAR_ELEMENT_FLIP_TOLERANCE_IN_PIXELS,
- );
- });
-
- it("flips an unrotated line vertically correctly", async () => {
- createAndSelectOneLine();
- await checkVerticalFlip(TWO_POINTS_LINEAR_ELEMENT_FLIP_TOLERANCE_IN_PIXELS);
- });
-
- it("flips a rotated line horizontally with line inside min/max points bounds", async () => {
- const originalAngle = (Math.PI / 4) as Radians;
- const expectedAngle = ((7 * Math.PI) / 4) as Radians;
- const line = createLinearElementWithCurveInsideMinMaxPoints("line");
- API.setElements([line]);
- API.setAppState({
- selectedElementIds: {
- ...h.state.selectedElementIds,
- [line.id]: true,
- },
- });
- API.updateElement(line, {
- angle: originalAngle,
- });
-
- await checkRotatedHorizontalFlip(
- expectedAngle,
- MULTIPOINT_LINEAR_ELEMENT_FLIP_TOLERANCE_IN_PIXELS,
- );
- });
-
- it("flips a rotated line vertically with line inside min/max points bounds", async () => {
- const originalAngle = (Math.PI / 4) as Radians;
- const expectedAngle = ((7 * Math.PI) / 4) as Radians;
- const line = createLinearElementWithCurveInsideMinMaxPoints("line");
- API.setElements([line]);
- API.setAppState({
- selectedElementIds: {
- ...h.state.selectedElementIds,
- [line.id]: true,
- },
- });
- API.updateElement(line, {
- angle: originalAngle,
- });
-
- await checkRotatedVerticalFlip(
- expectedAngle,
- MULTIPOINT_LINEAR_ELEMENT_FLIP_TOLERANCE_IN_PIXELS,
- );
- });
-
- it("flips a two points line horizontally correctly", async () => {
- createAndSelectOneLine();
- await checkTwoPointsLineHorizontalFlip();
- });
-
- 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
- API.setAppState({
- selectedElementIds: {
- ...h.state.selectedElementIds,
- [draw.id]: true,
- },
- });
- await checkHorizontalFlip();
- });
-
- it("flips an unrotated drawing vertically correctly", async () => {
- const draw = createAndReturnOneDraw();
- // select draw, since not done automatically
- API.setAppState({
- selectedElementIds: {
- ...h.state.selectedElementIds,
- [draw.id]: true,
- },
- });
- await checkVerticalFlip();
- });
-
- it("flips a rotated drawing horizontally correctly", async () => {
- const originalAngle = Math.PI / 4;
- const expectedAngle = (7 * Math.PI) / 4;
-
- const draw = createAndReturnOneDraw(originalAngle);
- // select draw, since not done automatically
- API.setAppState({
- selectedElementIds: {
- ...h.state.selectedElementIds,
- [draw.id]: true,
- },
- });
-
- await checkRotatedHorizontalFlip(expectedAngle);
- });
-
- it("flips a rotated drawing vertically correctly", async () => {
- const originalAngle = Math.PI / 4;
- const expectedAngle = (7 * Math.PI) / 4;
-
- const draw = createAndReturnOneDraw(originalAngle);
- // select draw, since not done automatically
- API.setAppState({
- selectedElementIds: {
- ...h.state.selectedElementIds,
- [draw.id]: true,
- },
- });
-
- await checkRotatedVerticalFlip(expectedAngle);
- });
-});
-
-//image
-//TODO: currently there is no test for pixel colors at flipped positions.
-describe("image", () => {
- const createImage = async () => {
- const sendPasteEvent = (file?: File) => {
- const clipboardEvent = createPasteEvent({ files: file ? [file] : [] });
- document.dispatchEvent(clipboardEvent);
- };
-
- sendPasteEvent(await API.loadFile("./fixtures/smiley_embedded_v2.png"));
- };
-
- 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(0);
- });
-
- it("flips an rotated image horizontally correctly", async () => {
- const originalAngle = (Math.PI / 4) as Radians;
- const expectedAngle = ((7 * Math.PI) / 4) as Radians;
- //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();
- });
- API.updateElement(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) as Radians;
- const expectedAngle = ((7 * Math.PI) / 4) as Radians;
- //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();
- });
- API.updateElement(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(0);
- });
-});
-
-describe("mutliple elements", () => {
- it("with bound text flip correctly", async () => {
- UI.clickTool("arrow");
- fireEvent.click(screen.getByTitle("Architect"));
- const arrow = UI.createElement("arrow", {
- x: 0,
- y: 0,
- width: 180,
- height: 80,
- });
-
- Keyboard.keyPress(KEYS.ENTER);
- let editor = document.querySelector(
- ".excalidraw-textEditorContainer > textarea",
- )!;
- fireEvent.input(editor, { target: { value: "arrow" } });
- Keyboard.exitTextEditor(editor);
-
- const rectangle = UI.createElement("rectangle", {
- x: 0,
- y: 100,
- width: 100,
- height: 100,
- });
-
- Keyboard.keyPress(KEYS.ENTER);
- editor = document.querySelector(
- ".excalidraw-textEditorContainer > textarea",
- )!;
- fireEvent.input(editor, { target: { value: "rect\ntext" } });
- Keyboard.exitTextEditor(editor);
-
- mouse.select([arrow, rectangle]);
- API.executeAction(actionFlipHorizontal);
- API.executeAction(actionFlipVertical);
-
- const arrowText = h.elements[1] as ExcalidrawTextElementWithContainer;
- const arrowTextPos = getBoundTextElementPosition(
- arrow.get(),
- arrowText,
- arrayToMap(h.elements),
- )!;
- const rectText = h.elements[3] as ExcalidrawTextElementWithContainer;
-
- expect(arrow.x).toBeCloseTo(180);
- expect(arrow.y).toBeCloseTo(200);
- expect(arrow.points[1][0]).toBeCloseTo(-180);
- expect(arrow.points[1][1]).toBeCloseTo(-80);
-
- expect(arrowTextPos.x - (arrow.x - arrow.width)).toBeCloseTo(
- arrow.x - (arrowTextPos.x + arrowText.width),
- );
- expect(arrowTextPos.y - (arrow.y - arrow.height)).toBeCloseTo(
- arrow.y - (arrowTextPos.y + arrowText.height),
- );
-
- expect(rectangle.x).toBeCloseTo(80);
- expect(rectangle.y).toBeCloseTo(0);
-
- expect(rectText.x - rectangle.x).toBeCloseTo(
- rectangle.x + rectangle.width - (rectText.x + rectText.width),
- );
- expect(rectText.y - rectangle.y).toBeCloseTo(
- rectangle.y + rectangle.height - (rectText.y + rectText.height),
- );
- });
-});