mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-09-23 17:30:44 +02:00
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:
@@ -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"
|
||||
`;
|
359
packages/element/tests/cropElement.test.tsx
Normal file
359
packages/element/tests/cropElement.test.tsx
Normal 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);
|
||||
});
|
||||
});
|
@@ -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";
|
||||
|
||||
|
@@ -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,
|
||||
|
@@ -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";
|
||||
|
||||
|
1415
packages/element/tests/linearElementEditor.test.tsx
Normal file
1415
packages/element/tests/linearElementEditor.test.tsx
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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";
|
||||
|
||||
|
Reference in New Issue
Block a user