refactor: export everything from @excalidraw/element, don't import from subpaths (#9466)

* Don't import from subpaths

* Fix tests, move related tests to element
This commit is contained in:
Marcel Mraz
2025-05-09 23:01:33 +02:00
committed by GitHub
parent a8ebe514da
commit 7e41026812
129 changed files with 517 additions and 541 deletions

View File

@@ -0,0 +1,57 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`Test Linear Elements > Test bound text element > should bind text to arrow when clicked on arrow and enter pressed 1`] = `
"Online whiteboard
collaboration made
easy"
`;
exports[`Test Linear Elements > Test bound text element > should bind text to arrow when double clicked 1`] = `
"Online whiteboard
collaboration made
easy"
`;
exports[`Test Linear Elements > Test bound text element > should match styles for text editor 1`] = `
<textarea
class="excalidraw-wysiwyg"
data-type="wysiwyg"
dir="auto"
style="position: absolute; display: inline-block; min-height: 1em; backface-visibility: hidden; margin: 0px; padding: 0px; border: 0px; outline: 0; resize: none; background: transparent; overflow: hidden; z-index: var(--zIndex-wysiwyg); word-break: break-word; white-space: pre-wrap; overflow-wrap: break-word; box-sizing: content-box; width: 10.5px; height: 26.25px; left: 35px; top: 7.5px; transform: translate(0px, 0px) scale(1) rotate(0deg); text-align: center; vertical-align: middle; color: rgb(30, 30, 30); opacity: 1; filter: var(--theme-filter); max-height: 992.5px; font: Emoji 20px 20px; line-height: 1.25; font-family: Excalifont, Xiaolai, Segoe UI Emoji;"
tabindex="0"
wrap="off"
/>
`;
exports[`Test Linear Elements > Test bound text element > should resize and position the bound text and bounding box correctly when 3 pointer arrow element resized 2`] = `
"Online whiteboard
collaboration made
easy"
`;
exports[`Test Linear Elements > Test bound text element > should resize and position the bound text and bounding box correctly when 3 pointer arrow element resized 6`] = `
"Online whiteboard
collaboration made easy"
`;
exports[`Test Linear Elements > Test bound text element > should resize and position the bound text correctly when 2 pointer linear element resized 2`] = `
"Online whiteboard
collaboration made
easy"
`;
exports[`Test Linear Elements > Test bound text element > should resize and position the bound text correctly when 2 pointer linear element resized 5`] = `
"Online whiteboard
collaboration made easy"
`;
exports[`Test Linear Elements > Test bound text element > should wrap the bound text when arrow bound container moves 1`] = `
"Online whiteboard
collaboration made easy"
`;
exports[`Test Linear Elements > Test bound text element > should wrap the bound text when arrow bound container moves 2`] = `
"Online whiteboard
collaboration made
easy"
`;

View File

@@ -0,0 +1,359 @@
import React from "react";
import { vi } from "vitest";
import { KEYS, cloneJSON } from "@excalidraw/common";
import {
Excalidraw,
exportToCanvas,
exportToSvg,
} from "@excalidraw/excalidraw";
import {
actionFlipHorizontal,
actionFlipVertical,
} from "@excalidraw/excalidraw/actions";
import { API } from "@excalidraw/excalidraw/tests/helpers/api";
import { Keyboard, Pointer, UI } from "@excalidraw/excalidraw/tests/helpers/ui";
import {
act,
GlobalTestState,
render,
unmountComponent,
} from "@excalidraw/excalidraw/tests/test-utils";
import type { NormalizedZoomValue } from "@excalidraw/excalidraw/types";
import { duplicateElement } from "../src/duplicate";
import type { ExcalidrawImageElement, ImageCrop } from "../src/types";
const { h } = window;
const mouse = new Pointer("mouse");
beforeEach(async () => {
unmountComponent();
mouse.reset();
localStorage.clear();
sessionStorage.clear();
vi.clearAllMocks();
Object.assign(document, {
elementFromPoint: () => GlobalTestState.canvas,
});
await render(<Excalidraw autoFocus={true} handleKeyboardGlobally={true} />);
API.setAppState({
zoom: {
value: 1 as NormalizedZoomValue,
},
});
const image = API.createElement({ type: "image", width: 200, height: 100 });
API.setElements([image]);
API.setAppState({
selectedElementIds: {
[image.id]: true,
},
});
});
const generateRandomNaturalWidthAndHeight = (image: ExcalidrawImageElement) => {
const initialWidth = image.width;
const initialHeight = image.height;
const scale = 1 + Math.random() * 5;
return {
naturalWidth: initialWidth * scale,
naturalHeight: initialHeight * scale,
};
};
const compareCrops = (cropA: ImageCrop, cropB: ImageCrop) => {
(Object.keys(cropA) as [keyof ImageCrop]).forEach((key) => {
const propA = cropA[key];
const propB = cropB[key];
expect(propA as number).toBeCloseTo(propB as number);
});
};
describe("Enter and leave the crop editor", () => {
it("enter the editor by double clicking", () => {
const image = h.elements[0];
expect(h.state.croppingElementId).toBe(null);
mouse.doubleClickOn(image);
expect(h.state.croppingElementId).not.toBe(null);
expect(h.state.croppingElementId).toBe(image.id);
});
it("enter the editor by pressing enter", () => {
const image = h.elements[0];
expect(h.state.croppingElementId).toBe(null);
Keyboard.keyDown(KEYS.ENTER);
expect(h.state.croppingElementId).not.toBe(null);
expect(h.state.croppingElementId).toBe(image.id);
});
it("leave the editor by clicking outside", () => {
const image = h.elements[0];
Keyboard.keyDown(KEYS.ENTER);
expect(h.state.croppingElementId).not.toBe(null);
mouse.click(image.x - 20, image.y - 20);
expect(h.state.croppingElementId).toBe(null);
});
it("leave the editor by pressing escape", () => {
const image = h.elements[0];
mouse.doubleClickOn(image);
expect(h.state.croppingElementId).not.toBe(null);
Keyboard.keyDown(KEYS.ESCAPE);
expect(h.state.croppingElementId).toBe(null);
});
});
describe("Crop an image", () => {
it("Cropping changes the dimension", async () => {
const image = h.elements[0] as ExcalidrawImageElement;
const initialWidth = image.width;
const initialHeight = image.height;
const { naturalWidth, naturalHeight } =
generateRandomNaturalWidthAndHeight(image);
UI.crop(image, "w", naturalWidth, naturalHeight, [initialWidth / 2, 0]);
expect(image.width).toBeLessThan(initialWidth);
UI.crop(image, "n", naturalWidth, naturalHeight, [0, initialHeight / 2]);
expect(image.height).toBeLessThan(initialHeight);
});
it("Cropping has minimal sizes", async () => {
const image = h.elements[0] as ExcalidrawImageElement;
const initialWidth = image.width;
const initialHeight = image.height;
const { naturalWidth, naturalHeight } =
generateRandomNaturalWidthAndHeight(image);
UI.crop(image, "w", naturalWidth, naturalHeight, [initialWidth, 0]);
expect(image.width).toBeLessThan(initialWidth);
expect(image.width).toBeGreaterThan(0);
UI.crop(image, "w", naturalWidth, naturalHeight, [-initialWidth, 0]);
UI.crop(image, "n", naturalWidth, naturalHeight, [0, initialHeight]);
expect(image.height).toBeLessThan(initialHeight);
expect(image.height).toBeGreaterThan(0);
});
it("Preserve aspect ratio", async () => {
let image = h.elements[0] as ExcalidrawImageElement;
const initialWidth = image.width;
const initialHeight = image.height;
const { naturalWidth, naturalHeight } =
generateRandomNaturalWidthAndHeight(image);
UI.crop(image, "w", naturalWidth, naturalHeight, [initialWidth / 3, 0]);
let resizedWidth = image.width;
let resizedHeight = image.height;
// max height, cropping should not change anything
UI.crop(
image,
"w",
naturalWidth,
naturalHeight,
[-initialWidth / 3, 0],
true,
);
expect(image.width).toBeCloseTo(resizedWidth, 10);
expect(image.height).toBeCloseTo(resizedHeight, 10);
// re-crop to initial state
UI.crop(image, "w", naturalWidth, naturalHeight, [-initialWidth / 3, 0]);
// change crop height and width
UI.crop(image, "s", naturalWidth, naturalHeight, [0, -initialHeight / 2]);
UI.crop(image, "e", naturalWidth, naturalHeight, [-initialWidth / 3, 0]);
resizedWidth = image.width;
resizedHeight = image.height;
// test corner handle aspect ratio preserving
UI.crop(image, "se", naturalWidth, naturalHeight, [initialWidth, 0], true);
expect(image.width / image.height).toBe(resizedWidth / resizedHeight);
expect(image.width).toBeLessThanOrEqual(initialWidth + 0.0001);
expect(image.height).toBeLessThanOrEqual(initialHeight + 0.0001);
// reset
image = API.createElement({ type: "image", width: 200, height: 100 });
API.setElements([image]);
API.setAppState({
selectedElementIds: {
[image.id]: true,
},
});
// 50 x 50 square
UI.crop(image, "nw", naturalWidth, naturalHeight, [150, 50]);
UI.crop(image, "n", naturalWidth, naturalHeight, [0, -100], true);
expect(image.width).toBeCloseTo(image.height);
// image is at the corner, not space to its right to expand, should not be able to resize
expect(image.height).toBeCloseTo(50);
UI.crop(image, "nw", naturalWidth, naturalHeight, [-150, -100], true);
expect(image.width).toBeCloseTo(image.height);
// max height should be reached
expect(image.height).toBeCloseTo(initialHeight);
expect(image.width).toBeCloseTo(initialHeight);
});
});
describe("Cropping and other features", async () => {
it("Cropping works independently of duplication", async () => {
const image = h.elements[0] as ExcalidrawImageElement;
const initialWidth = image.width;
const initialHeight = image.height;
const { naturalWidth, naturalHeight } =
generateRandomNaturalWidthAndHeight(image);
UI.crop(image, "nw", naturalWidth, naturalHeight, [
initialWidth / 2,
initialHeight / 2,
]);
Keyboard.keyDown(KEYS.ESCAPE);
const duplicatedImage = duplicateElement(null, new Map(), image);
act(() => {
h.app.scene.insertElement(duplicatedImage);
});
expect(duplicatedImage.width).toBe(image.width);
expect(duplicatedImage.height).toBe(image.height);
UI.crop(duplicatedImage, "nw", naturalWidth, naturalHeight, [
-initialWidth / 2,
-initialHeight / 2,
]);
expect(duplicatedImage.width).toBe(initialWidth);
expect(duplicatedImage.height).toBe(initialHeight);
const resizedWidth = image.width;
const resizedHeight = image.height;
expect(image.width).not.toBe(duplicatedImage.width);
expect(image.height).not.toBe(duplicatedImage.height);
UI.crop(duplicatedImage, "se", naturalWidth, naturalHeight, [
-initialWidth / 1.5,
-initialHeight / 1.5,
]);
expect(duplicatedImage.width).not.toBe(initialWidth);
expect(image.width).toBe(resizedWidth);
expect(duplicatedImage.height).not.toBe(initialHeight);
expect(image.height).toBe(resizedHeight);
});
it("Resizing should not affect crop", async () => {
const image = h.elements[0] as ExcalidrawImageElement;
const initialWidth = image.width;
const initialHeight = image.height;
const { naturalWidth, naturalHeight } =
generateRandomNaturalWidthAndHeight(image);
UI.crop(image, "nw", naturalWidth, naturalHeight, [
initialWidth / 2,
initialHeight / 2,
]);
const cropBeforeResizing = image.crop;
const cropBeforeResizingCloned = cloneJSON(image.crop) as ImageCrop;
expect(cropBeforeResizing).not.toBe(null);
UI.crop(image, "e", naturalWidth, naturalHeight, [200, 0]);
expect(cropBeforeResizing).toBe(image.crop);
compareCrops(cropBeforeResizingCloned, image.crop!);
UI.resize(image, "s", [0, -100]);
expect(cropBeforeResizing).toBe(image.crop);
compareCrops(cropBeforeResizingCloned, image.crop!);
UI.resize(image, "ne", [-50, -50]);
expect(cropBeforeResizing).toBe(image.crop);
compareCrops(cropBeforeResizingCloned, image.crop!);
});
it("Flipping does not change crop", async () => {
const image = h.elements[0] as ExcalidrawImageElement;
const initialWidth = image.width;
const initialHeight = image.height;
const { naturalWidth, naturalHeight } =
generateRandomNaturalWidthAndHeight(image);
mouse.doubleClickOn(image);
expect(h.state.croppingElementId).not.toBe(null);
UI.crop(image, "nw", naturalWidth, naturalHeight, [
initialWidth / 2,
initialHeight / 2,
]);
Keyboard.keyDown(KEYS.ESCAPE);
const cropBeforeResizing = image.crop;
const cropBeforeResizingCloned = cloneJSON(image.crop) as ImageCrop;
API.executeAction(actionFlipHorizontal);
expect(image.crop).toBe(cropBeforeResizing);
compareCrops(cropBeforeResizingCloned, image.crop!);
API.executeAction(actionFlipVertical);
expect(image.crop).toBe(cropBeforeResizing);
compareCrops(cropBeforeResizingCloned, image.crop!);
});
it("Exports should preserve crops", async () => {
const image = h.elements[0] as ExcalidrawImageElement;
const initialWidth = image.width;
const initialHeight = image.height;
const { naturalWidth, naturalHeight } =
generateRandomNaturalWidthAndHeight(image);
mouse.doubleClickOn(image);
expect(h.state.croppingElementId).not.toBe(null);
UI.crop(image, "nw", naturalWidth, naturalHeight, [
initialWidth / 2,
initialHeight / 4,
]);
Keyboard.keyDown(KEYS.ESCAPE);
const widthToHeightRatio = image.width / image.height;
const canvas = await exportToCanvas({
elements: [image],
// @ts-ignore
appState: h.state,
files: h.app.files,
exportPadding: 0,
});
const exportedCanvasRatio = canvas.width / canvas.height;
expect(widthToHeightRatio).toBeCloseTo(exportedCanvasRatio);
const svg = await exportToSvg({
elements: [image],
// @ts-ignore
appState: h.state,
files: h.app.files,
exportPadding: 0,
});
const svgWidth = svg.getAttribute("width");
const svgHeight = svg.getAttribute("height");
expect(svgWidth).toBeDefined();
expect(svgHeight).toBeDefined();
const exportedSvgRatio = Number(svgWidth) / Number(svgHeight);
expect(widthToHeightRatio).toBeCloseTo(exportedSvgRatio);
});
});

View File

@@ -1,5 +1,5 @@
import type { ObservedAppState } from "@excalidraw/excalidraw/types";
import type { LinearElementEditor } from "@excalidraw/element/linearElementEditor";
import type { LinearElementEditor } from "@excalidraw/element";
import { AppStateDelta } from "../src/delta";

View File

@@ -22,7 +22,7 @@ import type { LocalPoint } from "@excalidraw/math";
import { bindLinearElement } from "../src/binding";
import Scene from "../src/Scene";
import { Scene } from "../src/Scene";
import type {
ExcalidrawArrowElement,

View File

@@ -7,9 +7,9 @@ import {
syncInvalidIndices,
syncMovedIndices,
validateFractionalIndices,
} from "@excalidraw/element/fractionalIndex";
} from "@excalidraw/element";
import { deepCopyElement } from "@excalidraw/element/duplicate";
import { deepCopyElement } from "@excalidraw/element";
import { API } from "@excalidraw/excalidraw/tests/helpers/api";

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
import { API } from "@excalidraw/excalidraw/tests/helpers/api";
import { mutateElement } from "@excalidraw/element/mutateElement";
import { mutateElement } from "@excalidraw/element";
import { normalizeElementOrder } from "../src/sortElements";