refactor: separate elements logic into a standalone package (#9285)
Some checks failed
Auto release excalidraw next / Auto-release-excalidraw-next (push) Failing after 2m36s
Build Docker image / build-docker (push) Failing after 6s
Cancel previous runs / cancel (push) Failing after 1s
Publish Docker / publish-docker (push) Failing after 31s
New Sentry production release / sentry (push) Failing after 2m3s

This commit is contained in:
Marcel Mraz 2025-03-26 15:24:59 +01:00 committed by GitHub
parent a18f059188
commit 432a46ef9e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
372 changed files with 3466 additions and 2466 deletions

View file

@ -1,8 +1,9 @@
import React from "react";
import { vi } from "vitest";
import { reseed } from "@excalidraw/common";
import { Excalidraw } from "../index";
import { reseed } from "../random";
import * as StaticScene from "../renderer/staticScene";
import { render, queryByTestId, unmountComponent } from "../tests/test-utils";

View file

@ -1,8 +1,9 @@
import React from "react";
import { CODES } from "@excalidraw/common";
import { copiedStyles } from "../actions/actionStyles";
import { Excalidraw } from "../index";
import { CODES } from "../keys";
import { API } from "../tests/helpers/api";
import { Keyboard, Pointer, UI } from "../tests/helpers/ui";
import {

View file

@ -1,581 +0,0 @@
import React from "react";
import {
actionAlignVerticallyCentered,
actionAlignHorizontallyCentered,
actionGroup,
actionAlignTop,
actionAlignBottom,
actionAlignLeft,
actionAlignRight,
} from "../actions";
import { defaultLang, setLanguage } from "../i18n";
import { Excalidraw } from "../index";
import { KEYS } from "../keys";
import { API } from "./helpers/api";
import { UI, Pointer, Keyboard } from "./helpers/ui";
import { act, unmountComponent, render } from "./test-utils";
const mouse = new Pointer("mouse");
const createAndSelectTwoRectangles = () => {
UI.clickTool("rectangle");
mouse.down();
mouse.up(100, 100);
UI.clickTool("rectangle");
mouse.down(10, 10);
mouse.up(100, 100);
// Select the first element.
// The second rectangle is already reselected because it was the last element created
mouse.reset();
Keyboard.withModifierKeys({ shift: true }, () => {
mouse.click();
});
};
const createAndSelectTwoRectanglesWithDifferentSizes = () => {
UI.clickTool("rectangle");
mouse.down();
mouse.up(100, 100);
UI.clickTool("rectangle");
mouse.down(10, 10);
mouse.up(110, 110);
// Select the first element.
// The second rectangle is already reselected because it was the last element created
mouse.reset();
Keyboard.withModifierKeys({ shift: true }, () => {
mouse.click();
});
};
describe("aligning", () => {
beforeEach(async () => {
unmountComponent();
mouse.reset();
await act(() => {
return setLanguage(defaultLang);
});
await render(<Excalidraw handleKeyboardGlobally={true} />);
});
it("aligns two objects correctly to the top", () => {
createAndSelectTwoRectangles();
expect(API.getSelectedElements()[0].x).toEqual(0);
expect(API.getSelectedElements()[1].x).toEqual(110);
expect(API.getSelectedElements()[0].y).toEqual(0);
expect(API.getSelectedElements()[1].y).toEqual(110);
Keyboard.withModifierKeys({ ctrl: true, shift: true }, () => {
Keyboard.keyPress(KEYS.ARROW_UP);
});
// Check if x position did not change
expect(API.getSelectedElements()[0].x).toEqual(0);
expect(API.getSelectedElements()[1].x).toEqual(110);
expect(API.getSelectedElements()[0].y).toEqual(0);
expect(API.getSelectedElements()[1].y).toEqual(0);
});
it("aligns two objects correctly to the bottom", () => {
createAndSelectTwoRectangles();
expect(API.getSelectedElements()[0].x).toEqual(0);
expect(API.getSelectedElements()[1].x).toEqual(110);
expect(API.getSelectedElements()[0].y).toEqual(0);
expect(API.getSelectedElements()[1].y).toEqual(110);
Keyboard.withModifierKeys({ ctrl: true, shift: true }, () => {
Keyboard.keyPress(KEYS.ARROW_DOWN);
});
// Check if x position did not change
expect(API.getSelectedElements()[0].x).toEqual(0);
expect(API.getSelectedElements()[1].x).toEqual(110);
expect(API.getSelectedElements()[0].y).toEqual(110);
expect(API.getSelectedElements()[1].y).toEqual(110);
});
it("aligns two objects correctly to the left", () => {
createAndSelectTwoRectangles();
expect(API.getSelectedElements()[0].x).toEqual(0);
expect(API.getSelectedElements()[1].x).toEqual(110);
expect(API.getSelectedElements()[0].y).toEqual(0);
expect(API.getSelectedElements()[1].y).toEqual(110);
Keyboard.withModifierKeys({ ctrl: true, shift: true }, () => {
Keyboard.keyPress(KEYS.ARROW_LEFT);
});
expect(API.getSelectedElements()[0].x).toEqual(0);
expect(API.getSelectedElements()[1].x).toEqual(0);
// Check if y position did not change
expect(API.getSelectedElements()[0].y).toEqual(0);
expect(API.getSelectedElements()[1].y).toEqual(110);
});
it("aligns two objects correctly to the right", () => {
createAndSelectTwoRectangles();
expect(API.getSelectedElements()[0].x).toEqual(0);
expect(API.getSelectedElements()[1].x).toEqual(110);
expect(API.getSelectedElements()[0].y).toEqual(0);
expect(API.getSelectedElements()[1].y).toEqual(110);
Keyboard.withModifierKeys({ ctrl: true, shift: true }, () => {
Keyboard.keyPress(KEYS.ARROW_RIGHT);
});
expect(API.getSelectedElements()[0].x).toEqual(110);
expect(API.getSelectedElements()[1].x).toEqual(110);
// Check if y position did not change
expect(API.getSelectedElements()[0].y).toEqual(0);
expect(API.getSelectedElements()[1].y).toEqual(110);
});
it("centers two objects with different sizes correctly vertically", () => {
createAndSelectTwoRectanglesWithDifferentSizes();
expect(API.getSelectedElements()[0].x).toEqual(0);
expect(API.getSelectedElements()[1].x).toEqual(110);
expect(API.getSelectedElements()[0].y).toEqual(0);
expect(API.getSelectedElements()[1].y).toEqual(110);
API.executeAction(actionAlignVerticallyCentered);
// Check if x position did not change
expect(API.getSelectedElements()[0].x).toEqual(0);
expect(API.getSelectedElements()[1].x).toEqual(110);
expect(API.getSelectedElements()[0].y).toEqual(60);
expect(API.getSelectedElements()[1].y).toEqual(55);
});
it("centers two objects with different sizes correctly horizontally", () => {
createAndSelectTwoRectanglesWithDifferentSizes();
expect(API.getSelectedElements()[0].x).toEqual(0);
expect(API.getSelectedElements()[1].x).toEqual(110);
expect(API.getSelectedElements()[0].y).toEqual(0);
expect(API.getSelectedElements()[1].y).toEqual(110);
API.executeAction(actionAlignHorizontallyCentered);
expect(API.getSelectedElements()[0].x).toEqual(60);
expect(API.getSelectedElements()[1].x).toEqual(55);
// Check if y position did not change
expect(API.getSelectedElements()[0].y).toEqual(0);
expect(API.getSelectedElements()[1].y).toEqual(110);
});
const createAndSelectGroupAndRectangle = () => {
UI.clickTool("rectangle");
mouse.down();
mouse.up(100, 100);
UI.clickTool("rectangle");
mouse.down(0, 0);
mouse.up(100, 100);
// Select the first element.
// The second rectangle is already reselected because it was the last element created
mouse.reset();
Keyboard.withModifierKeys({ shift: true }, () => {
mouse.click();
});
API.executeAction(actionGroup);
mouse.reset();
UI.clickTool("rectangle");
mouse.down(200, 200);
mouse.up(100, 100);
// Add the created group to the current selection
mouse.restorePosition(0, 0);
Keyboard.withModifierKeys({ shift: true }, () => {
mouse.click();
});
};
it("aligns a group with another element correctly to the top", () => {
createAndSelectGroupAndRectangle();
expect(API.getSelectedElements()[0].y).toEqual(0);
expect(API.getSelectedElements()[1].y).toEqual(100);
expect(API.getSelectedElements()[2].y).toEqual(200);
API.executeAction(actionAlignTop);
expect(API.getSelectedElements()[0].y).toEqual(0);
expect(API.getSelectedElements()[1].y).toEqual(100);
expect(API.getSelectedElements()[2].y).toEqual(0);
});
it("aligns a group with another element correctly to the bottom", () => {
createAndSelectGroupAndRectangle();
expect(API.getSelectedElements()[0].y).toEqual(0);
expect(API.getSelectedElements()[1].y).toEqual(100);
expect(API.getSelectedElements()[2].y).toEqual(200);
API.executeAction(actionAlignBottom);
expect(API.getSelectedElements()[0].y).toEqual(100);
expect(API.getSelectedElements()[1].y).toEqual(200);
expect(API.getSelectedElements()[2].y).toEqual(200);
});
it("aligns a group with another element correctly to the left", () => {
createAndSelectGroupAndRectangle();
expect(API.getSelectedElements()[0].x).toEqual(0);
expect(API.getSelectedElements()[1].x).toEqual(100);
expect(API.getSelectedElements()[2].x).toEqual(200);
API.executeAction(actionAlignLeft);
expect(API.getSelectedElements()[0].x).toEqual(0);
expect(API.getSelectedElements()[1].x).toEqual(100);
expect(API.getSelectedElements()[2].x).toEqual(0);
});
it("aligns a group with another element correctly to the right", () => {
createAndSelectGroupAndRectangle();
expect(API.getSelectedElements()[0].x).toEqual(0);
expect(API.getSelectedElements()[1].x).toEqual(100);
expect(API.getSelectedElements()[2].x).toEqual(200);
API.executeAction(actionAlignRight);
expect(API.getSelectedElements()[0].x).toEqual(100);
expect(API.getSelectedElements()[1].x).toEqual(200);
expect(API.getSelectedElements()[2].x).toEqual(200);
});
it("centers a group with another element correctly vertically", () => {
createAndSelectGroupAndRectangle();
expect(API.getSelectedElements()[0].y).toEqual(0);
expect(API.getSelectedElements()[1].y).toEqual(100);
expect(API.getSelectedElements()[2].y).toEqual(200);
API.executeAction(actionAlignVerticallyCentered);
expect(API.getSelectedElements()[0].y).toEqual(50);
expect(API.getSelectedElements()[1].y).toEqual(150);
expect(API.getSelectedElements()[2].y).toEqual(100);
});
it("centers a group with another element correctly horizontally", () => {
createAndSelectGroupAndRectangle();
expect(API.getSelectedElements()[0].x).toEqual(0);
expect(API.getSelectedElements()[1].x).toEqual(100);
expect(API.getSelectedElements()[2].x).toEqual(200);
API.executeAction(actionAlignHorizontallyCentered);
expect(API.getSelectedElements()[0].x).toEqual(50);
expect(API.getSelectedElements()[1].x).toEqual(150);
expect(API.getSelectedElements()[2].x).toEqual(100);
});
const createAndSelectTwoGroups = () => {
UI.clickTool("rectangle");
mouse.down();
mouse.up(100, 100);
UI.clickTool("rectangle");
mouse.down(0, 0);
mouse.up(100, 100);
// Select the first element.
// The second rectangle is already selected because it was the last element created
mouse.reset();
Keyboard.withModifierKeys({ shift: true }, () => {
mouse.click();
});
API.executeAction(actionGroup);
mouse.reset();
UI.clickTool("rectangle");
mouse.down(200, 200);
mouse.up(100, 100);
UI.clickTool("rectangle");
mouse.down();
mouse.up(100, 100);
mouse.restorePosition(200, 200);
Keyboard.withModifierKeys({ shift: true }, () => {
mouse.click();
});
API.executeAction(actionGroup);
// Select the first group.
// The second group is already selected because it was the last group created
mouse.reset();
Keyboard.withModifierKeys({ shift: true }, () => {
mouse.click();
});
};
it("aligns two groups correctly to the top", () => {
createAndSelectTwoGroups();
expect(API.getSelectedElements()[0].y).toEqual(0);
expect(API.getSelectedElements()[1].y).toEqual(100);
expect(API.getSelectedElements()[2].y).toEqual(200);
expect(API.getSelectedElements()[3].y).toEqual(300);
API.executeAction(actionAlignTop);
expect(API.getSelectedElements()[0].y).toEqual(0);
expect(API.getSelectedElements()[1].y).toEqual(100);
expect(API.getSelectedElements()[2].y).toEqual(0);
expect(API.getSelectedElements()[3].y).toEqual(100);
});
it("aligns two groups correctly to the bottom", () => {
createAndSelectTwoGroups();
expect(API.getSelectedElements()[0].y).toEqual(0);
expect(API.getSelectedElements()[1].y).toEqual(100);
expect(API.getSelectedElements()[2].y).toEqual(200);
expect(API.getSelectedElements()[3].y).toEqual(300);
API.executeAction(actionAlignBottom);
expect(API.getSelectedElements()[0].y).toEqual(200);
expect(API.getSelectedElements()[1].y).toEqual(300);
expect(API.getSelectedElements()[2].y).toEqual(200);
expect(API.getSelectedElements()[3].y).toEqual(300);
});
it("aligns two groups correctly to the left", () => {
createAndSelectTwoGroups();
expect(API.getSelectedElements()[0].x).toEqual(0);
expect(API.getSelectedElements()[1].x).toEqual(100);
expect(API.getSelectedElements()[2].x).toEqual(200);
expect(API.getSelectedElements()[3].x).toEqual(300);
API.executeAction(actionAlignLeft);
expect(API.getSelectedElements()[0].x).toEqual(0);
expect(API.getSelectedElements()[1].x).toEqual(100);
expect(API.getSelectedElements()[2].x).toEqual(0);
expect(API.getSelectedElements()[3].x).toEqual(100);
});
it("aligns two groups correctly to the right", () => {
createAndSelectTwoGroups();
expect(API.getSelectedElements()[0].x).toEqual(0);
expect(API.getSelectedElements()[1].x).toEqual(100);
expect(API.getSelectedElements()[2].x).toEqual(200);
expect(API.getSelectedElements()[3].x).toEqual(300);
API.executeAction(actionAlignRight);
expect(API.getSelectedElements()[0].x).toEqual(200);
expect(API.getSelectedElements()[1].x).toEqual(300);
expect(API.getSelectedElements()[2].x).toEqual(200);
expect(API.getSelectedElements()[3].x).toEqual(300);
});
it("centers two groups correctly vertically", () => {
createAndSelectTwoGroups();
expect(API.getSelectedElements()[0].y).toEqual(0);
expect(API.getSelectedElements()[1].y).toEqual(100);
expect(API.getSelectedElements()[2].y).toEqual(200);
expect(API.getSelectedElements()[3].y).toEqual(300);
API.executeAction(actionAlignVerticallyCentered);
expect(API.getSelectedElements()[0].y).toEqual(100);
expect(API.getSelectedElements()[1].y).toEqual(200);
expect(API.getSelectedElements()[2].y).toEqual(100);
expect(API.getSelectedElements()[3].y).toEqual(200);
});
it("centers two groups correctly horizontally", () => {
createAndSelectTwoGroups();
expect(API.getSelectedElements()[0].x).toEqual(0);
expect(API.getSelectedElements()[1].x).toEqual(100);
expect(API.getSelectedElements()[2].x).toEqual(200);
expect(API.getSelectedElements()[3].x).toEqual(300);
API.executeAction(actionAlignHorizontallyCentered);
expect(API.getSelectedElements()[0].x).toEqual(100);
expect(API.getSelectedElements()[1].x).toEqual(200);
expect(API.getSelectedElements()[2].x).toEqual(100);
expect(API.getSelectedElements()[3].x).toEqual(200);
});
const createAndSelectNestedGroupAndRectangle = () => {
UI.clickTool("rectangle");
mouse.down();
mouse.up(100, 100);
UI.clickTool("rectangle");
mouse.down(0, 0);
mouse.up(100, 100);
// Select the first element.
// The second rectangle is already reselected because it was the last element created
mouse.reset();
Keyboard.withModifierKeys({ shift: true }, () => {
mouse.click();
});
// Create first group of rectangles
API.executeAction(actionGroup);
mouse.reset();
UI.clickTool("rectangle");
mouse.down(200, 200);
mouse.up(100, 100);
// Add group to current selection
mouse.restorePosition(0, 0);
Keyboard.withModifierKeys({ shift: true }, () => {
mouse.click();
});
// Create the nested group
API.executeAction(actionGroup);
mouse.reset();
UI.clickTool("rectangle");
mouse.down(300, 300);
mouse.up(100, 100);
// Select the nested group, the rectangle is already selected
mouse.reset();
Keyboard.withModifierKeys({ shift: true }, () => {
mouse.click();
});
};
it("aligns nested group and other element correctly to the top", () => {
createAndSelectNestedGroupAndRectangle();
expect(API.getSelectedElements()[0].y).toEqual(0);
expect(API.getSelectedElements()[1].y).toEqual(100);
expect(API.getSelectedElements()[2].y).toEqual(200);
expect(API.getSelectedElements()[3].y).toEqual(300);
API.executeAction(actionAlignTop);
expect(API.getSelectedElements()[0].y).toEqual(0);
expect(API.getSelectedElements()[1].y).toEqual(100);
expect(API.getSelectedElements()[2].y).toEqual(200);
expect(API.getSelectedElements()[3].y).toEqual(0);
});
it("aligns nested group and other element correctly to the bottom", () => {
createAndSelectNestedGroupAndRectangle();
expect(API.getSelectedElements()[0].y).toEqual(0);
expect(API.getSelectedElements()[1].y).toEqual(100);
expect(API.getSelectedElements()[2].y).toEqual(200);
expect(API.getSelectedElements()[3].y).toEqual(300);
API.executeAction(actionAlignBottom);
expect(API.getSelectedElements()[0].y).toEqual(100);
expect(API.getSelectedElements()[1].y).toEqual(200);
expect(API.getSelectedElements()[2].y).toEqual(300);
expect(API.getSelectedElements()[3].y).toEqual(300);
});
it("aligns nested group and other element correctly to the left", () => {
createAndSelectNestedGroupAndRectangle();
expect(API.getSelectedElements()[0].x).toEqual(0);
expect(API.getSelectedElements()[1].x).toEqual(100);
expect(API.getSelectedElements()[2].x).toEqual(200);
expect(API.getSelectedElements()[3].x).toEqual(300);
API.executeAction(actionAlignLeft);
expect(API.getSelectedElements()[0].x).toEqual(0);
expect(API.getSelectedElements()[1].x).toEqual(100);
expect(API.getSelectedElements()[2].x).toEqual(200);
expect(API.getSelectedElements()[3].x).toEqual(0);
});
it("aligns nested group and other element correctly to the right", () => {
createAndSelectNestedGroupAndRectangle();
expect(API.getSelectedElements()[0].x).toEqual(0);
expect(API.getSelectedElements()[1].x).toEqual(100);
expect(API.getSelectedElements()[2].x).toEqual(200);
expect(API.getSelectedElements()[3].x).toEqual(300);
API.executeAction(actionAlignRight);
expect(API.getSelectedElements()[0].x).toEqual(100);
expect(API.getSelectedElements()[1].x).toEqual(200);
expect(API.getSelectedElements()[2].x).toEqual(300);
expect(API.getSelectedElements()[3].x).toEqual(300);
});
it("centers nested group and other element correctly vertically", () => {
createAndSelectNestedGroupAndRectangle();
expect(API.getSelectedElements()[0].y).toEqual(0);
expect(API.getSelectedElements()[1].y).toEqual(100);
expect(API.getSelectedElements()[2].y).toEqual(200);
expect(API.getSelectedElements()[3].y).toEqual(300);
API.executeAction(actionAlignVerticallyCentered);
expect(API.getSelectedElements()[0].y).toEqual(50);
expect(API.getSelectedElements()[1].y).toEqual(150);
expect(API.getSelectedElements()[2].y).toEqual(250);
expect(API.getSelectedElements()[3].y).toEqual(150);
});
it("centers nested group and other element correctly horizontally", () => {
createAndSelectNestedGroupAndRectangle();
expect(API.getSelectedElements()[0].x).toEqual(0);
expect(API.getSelectedElements()[1].x).toEqual(100);
expect(API.getSelectedElements()[2].x).toEqual(200);
expect(API.getSelectedElements()[3].x).toEqual(300);
API.executeAction(actionAlignHorizontallyCentered);
expect(API.getSelectedElements()[0].x).toEqual(50);
expect(API.getSelectedElements()[1].x).toEqual(150);
expect(API.getSelectedElements()[2].x).toEqual(250);
expect(API.getSelectedElements()[3].x).toEqual(150);
});
});

View file

@ -1,15 +1,16 @@
import React from "react";
import { EXPORT_DATA_TYPES, MIME_TYPES } from "@excalidraw/common";
import type { ExcalidrawTextElement } from "@excalidraw/element/types";
import { getDefaultAppState } from "../appState";
import { EXPORT_DATA_TYPES, MIME_TYPES } from "../constants";
import { Excalidraw } from "../index";
import { API } from "./helpers/api";
import { Pointer, UI } from "./helpers/ui";
import { fireEvent, queryByTestId, render, waitFor } from "./test-utils";
import type { ExcalidrawTextElement } from "../element/types";
const { h } = window;
describe("appState", () => {

View file

@ -1,483 +0,0 @@
import { pointFrom } from "@excalidraw/math";
import React from "react";
import { actionWrapTextInContainer } from "../actions/actionBoundText";
import { getTransformHandles } from "../element/transformHandles";
import { Excalidraw, isLinearElement } from "../index";
import { KEYS } from "../keys";
import { arrayToMap } from "../utils";
import { API } from "./helpers/api";
import { UI, Pointer, Keyboard } from "./helpers/ui";
import { fireEvent, render } from "./test-utils";
const { h } = window;
const mouse = new Pointer("mouse");
describe("element binding", () => {
beforeEach(async () => {
await render(<Excalidraw handleKeyboardGlobally={true} />);
});
it("should create valid binding if duplicate start/end points", async () => {
const rect = API.createElement({
type: "rectangle",
x: 0,
y: 0,
width: 50,
height: 50,
});
const arrow = API.createElement({
type: "arrow",
x: 100,
y: 0,
width: 100,
height: 1,
points: [
pointFrom(0, 0),
pointFrom(0, 0),
pointFrom(100, 0),
pointFrom(100, 0),
],
});
API.setElements([rect, arrow]);
expect(arrow.startBinding).toBe(null);
// select arrow
mouse.clickAt(150, 0);
// move arrow start to potential binding position
mouse.downAt(100, 0);
mouse.moveTo(55, 0);
mouse.up(0, 0);
// Point selection is evaluated like the points are rendered,
// from right to left. So clicking on the first point should move the joint,
// not the start point.
expect(arrow.startBinding).toBe(null);
// Now that the start point is free, move it into overlapping position
mouse.downAt(100, 0);
mouse.moveTo(55, 0);
mouse.up(0, 0);
expect(API.getSelectedElements()).toEqual([arrow]);
expect(arrow.startBinding).toEqual({
elementId: rect.id,
focus: expect.toBeNonNaNNumber(),
gap: expect.toBeNonNaNNumber(),
});
// Move the end point to the overlapping binding position
mouse.downAt(200, 0);
mouse.moveTo(55, 0);
mouse.up(0, 0);
// Both the start and the end points should be bound
expect(arrow.startBinding).toEqual({
elementId: rect.id,
focus: expect.toBeNonNaNNumber(),
gap: expect.toBeNonNaNNumber(),
});
expect(arrow.endBinding).toEqual({
elementId: rect.id,
focus: expect.toBeNonNaNNumber(),
gap: expect.toBeNonNaNNumber(),
});
});
//@TODO fix the test with rotation
it.skip("rotation of arrow should rebind both ends", () => {
const rectLeft = UI.createElement("rectangle", {
x: 0,
width: 200,
height: 500,
});
const rectRight = UI.createElement("rectangle", {
x: 400,
width: 200,
height: 500,
});
const arrow = UI.createElement("arrow", {
x: 210,
y: 250,
width: 180,
height: 1,
});
expect(arrow.startBinding?.elementId).toBe(rectLeft.id);
expect(arrow.endBinding?.elementId).toBe(rectRight.id);
const rotation = getTransformHandles(
arrow,
h.state.zoom,
arrayToMap(h.elements),
"mouse",
).rotation!;
const rotationHandleX = rotation[0] + rotation[2] / 2;
const rotationHandleY = rotation[1] + rotation[3] / 2;
mouse.down(rotationHandleX, rotationHandleY);
mouse.move(300, 400);
mouse.up();
expect(arrow.angle).toBeGreaterThan(0.7 * Math.PI);
expect(arrow.angle).toBeLessThan(1.3 * Math.PI);
expect(arrow.startBinding?.elementId).toBe(rectRight.id);
expect(arrow.endBinding?.elementId).toBe(rectLeft.id);
});
// TODO fix & reenable once we rewrite tests to work with concurrency
it.skip(
"editing arrow and moving its head to bind it to element A, finalizing the" +
"editing by clicking on element A should end up selecting A",
async () => {
UI.createElement("rectangle", {
y: 0,
size: 100,
});
// Create arrow bound to rectangle
UI.clickTool("arrow");
mouse.down(50, -100);
mouse.up(0, 80);
// Edit arrow with multi-point
mouse.doubleClick();
// move arrow head
mouse.down();
mouse.up(0, 10);
expect(API.getSelectedElement().type).toBe("arrow");
// NOTE this mouse down/up + await needs to be done in order to repro
// the issue, due to https://github.com/excalidraw/excalidraw/blob/46bff3daceb602accf60c40a84610797260fca94/src/components/App.tsx#L740
mouse.reset();
expect(h.state.editingLinearElement).not.toBe(null);
mouse.down(0, 0);
await new Promise((r) => setTimeout(r, 100));
expect(h.state.editingLinearElement).toBe(null);
expect(API.getSelectedElement().type).toBe("rectangle");
mouse.up();
expect(API.getSelectedElement().type).toBe("rectangle");
},
);
it("should unbind arrow when moving it with keyboard", () => {
const rectangle = UI.createElement("rectangle", {
x: 75,
y: 0,
size: 100,
});
// Creates arrow 1px away from bidding with rectangle
const arrow = UI.createElement("arrow", {
x: 0,
y: 0,
size: 50,
});
expect(arrow.endBinding).toBe(null);
mouse.downAt(50, 50);
mouse.moveTo(51, 0);
mouse.up(0, 0);
// Test sticky connection
expect(API.getSelectedElement().type).toBe("arrow");
Keyboard.keyPress(KEYS.ARROW_RIGHT);
expect(arrow.endBinding?.elementId).toBe(rectangle.id);
Keyboard.keyPress(KEYS.ARROW_LEFT);
expect(arrow.endBinding?.elementId).toBe(rectangle.id);
// Sever connection
expect(API.getSelectedElement().type).toBe("arrow");
Keyboard.keyPress(KEYS.ARROW_LEFT);
expect(arrow.endBinding).toBe(null);
Keyboard.keyPress(KEYS.ARROW_RIGHT);
expect(arrow.endBinding).toBe(null);
});
it("should unbind on bound element deletion", () => {
const rectangle = UI.createElement("rectangle", {
x: 60,
y: 0,
size: 100,
});
const arrow = UI.createElement("arrow", {
x: 0,
y: 0,
size: 50,
});
expect(arrow.endBinding?.elementId).toBe(rectangle.id);
mouse.select(rectangle);
expect(API.getSelectedElement().type).toBe("rectangle");
Keyboard.keyDown(KEYS.DELETE);
expect(arrow.endBinding).toBe(null);
});
it("should unbind on text element deletion by submitting empty text", async () => {
const text = API.createElement({
type: "text",
text: "ola",
x: 60,
y: 0,
width: 100,
height: 100,
});
API.setElements([text]);
const arrow = UI.createElement("arrow", {
x: 0,
y: 0,
size: 50,
});
expect(arrow.endBinding?.elementId).toBe(text.id);
// edit text element and submit
// -------------------------------------------------------------------------
UI.clickTool("text");
mouse.clickAt(text.x + 50, text.y + 50);
const editor = document.querySelector(
".excalidraw-textEditorContainer > textarea",
) as HTMLTextAreaElement;
expect(editor).not.toBe(null);
fireEvent.change(editor, { target: { value: "" } });
fireEvent.keyDown(editor, { key: KEYS.ESCAPE });
expect(
document.querySelector(".excalidraw-textEditorContainer > textarea"),
).toBe(null);
expect(arrow.endBinding).toBe(null);
});
it("should keep binding on text update", async () => {
const text = API.createElement({
type: "text",
text: "ola",
x: 60,
y: 0,
width: 100,
height: 100,
});
API.setElements([text]);
const arrow = UI.createElement("arrow", {
x: 0,
y: 0,
size: 50,
});
expect(arrow.endBinding?.elementId).toBe(text.id);
// delete text element by submitting empty text
// -------------------------------------------------------------------------
UI.clickTool("text");
mouse.clickAt(text.x + 50, text.y + 50);
const editor = document.querySelector(
".excalidraw-textEditorContainer > textarea",
) as HTMLTextAreaElement;
expect(editor).not.toBe(null);
fireEvent.change(editor, { target: { value: "asdasdasdasdas" } });
fireEvent.keyDown(editor, { key: KEYS.ESCAPE });
expect(
document.querySelector(".excalidraw-textEditorContainer > textarea"),
).toBe(null);
expect(arrow.endBinding?.elementId).toBe(text.id);
});
it("should update binding when text containerized", async () => {
const rectangle1 = API.createElement({
type: "rectangle",
id: "rectangle1",
width: 100,
height: 100,
boundElements: [
{ id: "arrow1", type: "arrow" },
{ id: "arrow2", type: "arrow" },
],
});
const arrow1 = API.createElement({
type: "arrow",
id: "arrow1",
points: [pointFrom(0, 0), pointFrom(0, -87.45777932247563)],
startBinding: {
elementId: "rectangle1",
focus: 0.2,
gap: 7,
fixedPoint: [0.5, 1],
},
endBinding: {
elementId: "text1",
focus: 0.2,
gap: 7,
fixedPoint: [1, 0.5],
},
});
const arrow2 = API.createElement({
type: "arrow",
id: "arrow2",
points: [pointFrom(0, 0), pointFrom(0, -87.45777932247563)],
startBinding: {
elementId: "text1",
focus: 0.2,
gap: 7,
fixedPoint: [0.5, 1],
},
endBinding: {
elementId: "rectangle1",
focus: 0.2,
gap: 7,
fixedPoint: [1, 0.5],
},
});
const text1 = API.createElement({
type: "text",
id: "text1",
text: "ola",
boundElements: [
{ id: "arrow1", type: "arrow" },
{ id: "arrow2", type: "arrow" },
],
});
API.setElements([rectangle1, arrow1, arrow2, text1]);
API.setSelectedElements([text1]);
expect(h.state.selectedElementIds[text1.id]).toBe(true);
API.executeAction(actionWrapTextInContainer);
// new text container will be placed before the text element
const container = h.elements.at(-2)!;
expect(container.type).toBe("rectangle");
expect(container.id).not.toBe(rectangle1.id);
expect(container).toEqual(
expect.objectContaining({
boundElements: expect.arrayContaining([
{
type: "text",
id: text1.id,
},
{
type: "arrow",
id: arrow1.id,
},
{
type: "arrow",
id: arrow2.id,
},
]),
}),
);
expect(arrow1.startBinding?.elementId).toBe(rectangle1.id);
expect(arrow1.endBinding?.elementId).toBe(container.id);
expect(arrow2.startBinding?.elementId).toBe(container.id);
expect(arrow2.endBinding?.elementId).toBe(rectangle1.id);
});
// #6459
it("should unbind arrow only from the latest element", () => {
const rectLeft = UI.createElement("rectangle", {
x: 0,
width: 200,
height: 500,
});
const rectRight = UI.createElement("rectangle", {
x: 400,
width: 200,
height: 500,
});
const arrow = UI.createElement("arrow", {
x: 210,
y: 250,
width: 180,
height: 1,
});
expect(arrow.startBinding?.elementId).toBe(rectLeft.id);
expect(arrow.endBinding?.elementId).toBe(rectRight.id);
// Drag arrow off of bound rectangle range
const handles = getTransformHandles(
arrow,
h.state.zoom,
arrayToMap(h.elements),
"mouse",
).se!;
Keyboard.keyDown(KEYS.CTRL_OR_CMD);
const elX = handles[0] + handles[2] / 2;
const elY = handles[1] + handles[3] / 2;
mouse.downAt(elX, elY);
mouse.moveTo(300, 400);
mouse.up();
expect(arrow.startBinding).not.toBe(null);
expect(arrow.endBinding).toBe(null);
});
it("should not unbind when duplicating via selection group", () => {
const rectLeft = UI.createElement("rectangle", {
x: 0,
width: 200,
height: 500,
});
const rectRight = UI.createElement("rectangle", {
x: 400,
y: 200,
width: 200,
height: 500,
});
const arrow = UI.createElement("arrow", {
x: 210,
y: 250,
width: 177,
height: 1,
});
expect(arrow.startBinding?.elementId).toBe(rectLeft.id);
expect(arrow.endBinding?.elementId).toBe(rectRight.id);
mouse.downAt(-100, -100);
mouse.moveTo(650, 750);
mouse.up(0, 0);
expect(API.getSelectedElements().length).toBe(3);
mouse.moveTo(5, 5);
Keyboard.withModifierKeys({ alt: true }, () => {
mouse.downAt(5, 5);
mouse.moveTo(1000, 1000);
mouse.up(0, 0);
expect(window.h.elements.length).toBe(6);
window.h.elements.forEach((element) => {
if (isLinearElement(element)) {
expect(element.startBinding).not.toBe(null);
expect(element.endBinding).not.toBe(null);
} else {
expect(element.boundElements).not.toBe(null);
}
});
});
});
});

View file

@ -1,13 +1,15 @@
import React from "react";
import { vi } from "vitest";
import { getLineHeightInPx } from "@excalidraw/element/textMeasurements";
import { KEYS, arrayToMap, getLineHeight } from "@excalidraw/common";
import { getElementBounds } from "@excalidraw/element/bounds";
import { createPasteEvent, serializeAsClipboardJSON } from "../clipboard";
import { getElementBounds } from "../element";
import { getLineHeightInPx } from "../element/textMeasurements";
import { getLineHeight } from "../fonts";
import { Excalidraw } from "../index";
import { KEYS } from "../keys";
import { arrayToMap } from "../utils";
import { API } from "./helpers/api";
import { mockMermaidToExcalidraw } from "./helpers/mocks";
@ -25,7 +27,7 @@ const { h } = window;
const mouse = new Pointer("mouse");
vi.mock("../keys.ts", async (importOriginal) => {
vi.mock("@excalidraw/common", async (importOriginal) => {
const module: any = await importOriginal();
return {
__esmodule: true,

View file

@ -1,12 +1,13 @@
import React from "react";
import { vi } from "vitest";
import { KEYS, reseed } from "@excalidraw/common";
import { setDateTimeForTests } from "@excalidraw/common";
import { copiedStyles } from "../actions/actionStyles";
import { Excalidraw } from "../index";
import { KEYS } from "../keys";
import { reseed } from "../random";
import * as StaticScene from "../renderer/staticScene";
import { setDateTimeForTests } from "../utils";
import { API } from "./helpers/api";
import { UI, Pointer, Keyboard } from "./helpers/ui";

View file

@ -1,17 +1,22 @@
import React from "react";
import { vi } from "vitest";
import { KEYS, cloneJSON } from "@excalidraw/common";
import { duplicateElement } from "@excalidraw/element/duplicate";
import type {
ExcalidrawImageElement,
ImageCrop,
} from "@excalidraw/element/types";
import { Excalidraw, exportToCanvas, exportToSvg } from "..";
import { actionFlipHorizontal, actionFlipVertical } from "../actions";
import { duplicateElement } from "../element";
import { KEYS } from "../keys";
import { cloneJSON } from "../utils";
import { API } from "./helpers/api";
import { Keyboard, Pointer, UI } from "./helpers/ui";
import { act, GlobalTestState, render, unmountComponent } from "./test-utils";
import type { ExcalidrawImageElement, ImageCrop } from "../element/types";
import type { NormalizedZoomValue } from "../types";
const { h } = window;

View file

@ -1,13 +1,16 @@
import { reconcileElements } from "../../data/reconcile";
import { syncInvalidIndices } from "../../fractionalIndex";
import { randomInteger } from "../../random";
import { cloneJSON } from "../../utils";
import { syncInvalidIndices } from "@excalidraw/element/fractionalIndex";
import { randomInteger, cloneJSON } from "@excalidraw/common";
import type { RemoteExcalidrawElement } from "../../data/reconcile";
import type {
ExcalidrawElement,
OrderedExcalidrawElement,
} from "../../element/types";
} from "@excalidraw/element/types";
import { reconcileElements } from "../../data/reconcile";
import type { RemoteExcalidrawElement } from "../../data/reconcile";
import type { AppState } from "../../types";
type Id = string;

View file

@ -1,21 +1,24 @@
import { pointFrom } from "@excalidraw/math";
import { vi } from "vitest";
import { getDefaultAppState } from "../../appState";
import { DEFAULT_SIDEBAR, FONT_FAMILY, ROUNDNESS } from "../../constants";
import * as restore from "../../data/restore";
import { newElementWith } from "../../element/mutateElement";
import * as sizeHelpers from "../../element/sizeHelpers";
import { API } from "../helpers/api";
import { DEFAULT_SIDEBAR, FONT_FAMILY, ROUNDNESS } from "@excalidraw/common";
import { newElementWith } from "@excalidraw/element/mutateElement";
import * as sizeHelpers from "@excalidraw/element/sizeHelpers";
import type { ImportedDataState } from "../../data/types";
import type {
ExcalidrawElement,
ExcalidrawFreeDrawElement,
ExcalidrawLinearElement,
ExcalidrawTextElement,
} from "../../element/types";
import type { NormalizedZoomValue } from "../../types";
} from "@excalidraw/element/types";
import type { NormalizedZoomValue } from "@excalidraw/excalidraw/types";
import { API } from "../helpers/api";
import * as restore from "../../data/restore";
import { getDefaultAppState } from "../../appState";
import type { ImportedDataState } from "../../data/types";
describe("restoreElements", () => {
const mockSizeHelper = vi.spyOn(sizeHelpers, "isInvisiblySmallElement");

View file

@ -1,9 +1,11 @@
import React from "react";
import { vi } from "vitest";
import { KEYS, reseed } from "@excalidraw/common";
import type { ExcalidrawLinearElement } from "@excalidraw/element/types";
import { Excalidraw } from "../index";
import { KEYS } from "../keys";
import { reseed } from "../random";
import * as InteractiveScene from "../renderer/interactiveScene";
import * as StaticScene from "../renderer/staticScene";
@ -15,8 +17,6 @@ import {
unmountComponent,
} from "./test-utils";
import type { ExcalidrawLinearElement } from "../element/types";
unmountComponent();
const renderInteractiveScene = vi.spyOn(

View file

@ -1,10 +1,13 @@
import React from "react";
import { mutateElement } from "@excalidraw/element/mutateElement";
import { KEYS } from "@excalidraw/common";
import { actionSelectAll } from "../actions";
import { mutateElement } from "../element/mutateElement";
import { t } from "../i18n";
import { Excalidraw } from "../index";
import { KEYS } from "../keys";
import { API } from "../tests/helpers/api";
import { Keyboard, Pointer, UI } from "../tests/helpers/ui";
import { render, unmountComponent } from "../tests/test-utils";

View file

@ -2,7 +2,8 @@ import { queryByText, queryByTestId } from "@testing-library/react";
import React from "react";
import { useMemo } from "react";
import { THEME } from "../constants";
import { THEME } from "@excalidraw/common";
import { t } from "../i18n";
import { Excalidraw, Footer, MainMenu } from "../index";

View file

@ -1,7 +1,10 @@
import React from "react";
import { SVG_NS } from "@excalidraw/common";
import type { FileId } from "@excalidraw/element/types";
import { getDefaultAppState } from "../appState";
import { SVG_NS } from "../constants";
import { getDataURL } from "../data/blob";
import { encodePngMetadata } from "../data/image";
import { serializeAsJSON } from "../data/json";
@ -15,8 +18,6 @@ import {
import { API } from "./helpers/api";
import { render, waitFor } from "./test-utils";
import type { FileId } from "../element/types";
const { h } = window;
const testElements = [

View file

@ -1,4 +1,4 @@
import { VERSIONS } from "../../constants";
import { VERSIONS } from "@excalidraw/common";
import {
diamondFixture,

View file

@ -1,8 +1,8 @@
import { DEFAULT_FONT_FAMILY } from "@excalidraw/common";
import type { Radians } from "@excalidraw/math";
import { DEFAULT_FONT_FAMILY } from "../../constants";
import type { ExcalidrawElement } from "../../element/types";
import type { ExcalidrawElement } from "@excalidraw/element/types";
const elementBase: Omit<ExcalidrawElement, "type"> = {
id: "vWrqOAfkind2qcm7LDAGZ",

View file

@ -1,18 +1,30 @@
import { pointFrom, type Radians } from "@excalidraw/math";
import React from "react";
import { vi } from "vitest";
import { ROUNDNESS, KEYS, arrayToMap, cloneJSON } from "@excalidraw/common";
import { pointFrom, type Radians } from "@excalidraw/math";
import { getBoundTextElementPosition } from "@excalidraw/element/textElement";
import { getElementAbsoluteCoords } from "@excalidraw/element/bounds";
import { newLinearElement } from "@excalidraw/element/newElement";
import type { LocalPoint } from "@excalidraw/math";
import type {
ExcalidrawElement,
ExcalidrawImageElement,
ExcalidrawLinearElement,
ExcalidrawTextElementWithContainer,
FileId,
} from "@excalidraw/element/types";
import { actionFlipHorizontal, actionFlipVertical } from "../actions";
import { createPasteEvent } from "../clipboard";
import { ROUNDNESS } from "../constants";
import { getElementAbsoluteCoords } from "../element";
import { newLinearElement } from "../element";
import { getBoundTextElementPosition } from "../element/textElement";
import { Excalidraw } from "../index";
import { KEYS } from "../keys";
import { arrayToMap, cloneJSON } from "../utils";
// Importing to spy on it and mock the implementation (mocking does not work with simple vi.mock for some reason)
import * as blobModule from "../data/blob";
import { API } from "./helpers/api";
import { UI, Pointer, Keyboard } from "./helpers/ui";
@ -25,25 +37,17 @@ import {
waitFor,
} from "./test-utils";
import type {
ExcalidrawElement,
ExcalidrawImageElement,
ExcalidrawLinearElement,
ExcalidrawTextElementWithContainer,
FileId,
} from "../element/types";
import type { NormalizedZoomValue } from "../types";
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(() => {
const generateIdSpy = vi.spyOn(blobModule, "generateIdFromFile");
const resizeFileSpy = vi.spyOn(blobModule, "resizeImageFile");
generateIdSpy.mockImplementation(() => Promise.resolve("fileId" as FileId));
resizeFileSpy.mockImplementation((file: File) => Promise.resolve(file));
});
beforeEach(async () => {

View file

@ -1,811 +0,0 @@
/* eslint-disable no-lone-blocks */
import { generateKeyBetween } from "fractional-indexing";
import { InvalidFractionalIndexError } from "../errors";
import {
syncInvalidIndices,
syncMovedIndices,
validateFractionalIndices,
} from "../fractionalIndex";
import { arrayToMap } from "../utils";
import { deepCopyElement } from "../element/duplicate";
import { API } from "./helpers/api";
import type { ExcalidrawElement, FractionalIndex } from "../element/types";
describe("sync invalid indices with array order", () => {
describe("should NOT sync empty array", () => {
testMovedIndicesSync({
elements: [],
movedElements: [],
expect: {
unchangedElements: [],
validInput: true,
},
});
testInvalidIndicesSync({
elements: [],
expect: {
unchangedElements: [],
validInput: true,
},
});
});
describe("should NOT sync when index is well defined", () => {
testMovedIndicesSync({
elements: [{ id: "A", index: "a1" }],
movedElements: [],
expect: {
unchangedElements: ["A"],
validInput: true,
},
});
testInvalidIndicesSync({
elements: [{ id: "A", index: "a1" }],
expect: {
unchangedElements: ["A"],
validInput: true,
},
});
});
describe("should NOT sync when indices are well defined", () => {
testMovedIndicesSync({
elements: [
{ id: "A", index: "a1" },
{ id: "B", index: "a2" },
{ id: "C", index: "a3" },
],
movedElements: [],
expect: {
unchangedElements: ["A", "B", "C"],
validInput: true,
},
});
testInvalidIndicesSync({
elements: [
{ id: "A", index: "a1" },
{ id: "B", index: "a2" },
{ id: "C", index: "a3" },
],
expect: {
unchangedElements: ["A", "B", "C"],
validInput: true,
},
});
});
describe("should sync when fractional index is not defined", () => {
testMovedIndicesSync({
elements: [{ id: "A" }],
movedElements: ["A"],
expect: {
unchangedElements: [],
},
});
testInvalidIndicesSync({
elements: [{ id: "A" }],
expect: {
unchangedElements: [],
},
});
});
describe("should sync when fractional indices are duplicated", () => {
testInvalidIndicesSync({
elements: [
{ id: "A", index: "a1" },
{ id: "B", index: "a1" },
],
expect: {
unchangedElements: ["A"],
},
});
testInvalidIndicesSync({
elements: [
{ id: "A", index: "a1" },
{ id: "B", index: "a1" },
],
expect: {
unchangedElements: ["A"],
},
});
});
describe("should sync when a fractional index is out of order", () => {
testMovedIndicesSync({
elements: [
{ id: "A", index: "a2" },
{ id: "B", index: "a1" },
],
movedElements: ["B"],
expect: {
unchangedElements: ["A"],
},
});
testMovedIndicesSync({
elements: [
{ id: "A", index: "a2" },
{ id: "B", index: "a1" },
],
movedElements: ["A"],
expect: {
unchangedElements: ["B"],
},
});
testInvalidIndicesSync({
elements: [
{ id: "A", index: "a2" },
{ id: "B", index: "a1" },
],
expect: {
unchangedElements: ["A"],
},
});
});
describe("should sync when fractional indices are out of order", () => {
testMovedIndicesSync({
elements: [
{ id: "A", index: "a3" },
{ id: "B", index: "a2" },
{ id: "C", index: "a1" },
],
movedElements: ["B", "C"],
expect: {
unchangedElements: ["A"],
},
});
testInvalidIndicesSync({
elements: [
{ id: "A", index: "a3" },
{ id: "B", index: "a2" },
{ id: "C", index: "a1" },
],
expect: {
unchangedElements: ["A"],
},
});
});
describe("should sync when incorrect fractional index is in between correct ones ", () => {
testMovedIndicesSync({
elements: [
{ id: "A", index: "a1" },
{ id: "B", index: "a0" },
{ id: "C", index: "a2" },
],
movedElements: ["B"],
expect: {
unchangedElements: ["A", "C"],
},
});
testInvalidIndicesSync({
elements: [
{ id: "A", index: "a1" },
{ id: "B", index: "a0" },
{ id: "C", index: "a2" },
],
expect: {
unchangedElements: ["A", "C"],
},
});
});
describe("should sync when incorrect fractional index is on top and duplicated below", () => {
testMovedIndicesSync({
elements: [
{ id: "A", index: "a1" },
{ id: "B", index: "a2" },
{ id: "C", index: "a1" },
],
movedElements: ["C"],
expect: {
unchangedElements: ["A", "B"],
},
});
testInvalidIndicesSync({
elements: [
{ id: "A", index: "a1" },
{ id: "B", index: "a2" },
{ id: "C", index: "a1" },
],
expect: {
unchangedElements: ["A", "B"],
},
});
});
describe("should sync when given a mix of duplicate / invalid indices", () => {
testMovedIndicesSync({
elements: [
{ id: "A", index: "a0" },
{ id: "B", index: "a2" },
{ id: "C", index: "a1" },
{ id: "D", index: "a1" },
{ id: "E", index: "a2" },
],
movedElements: ["C", "D", "E"],
expect: {
unchangedElements: ["A", "B"],
},
});
testInvalidIndicesSync({
elements: [
{ id: "A", index: "a0" },
{ id: "B", index: "a2" },
{ id: "C", index: "a1" },
{ id: "D", index: "a1" },
{ id: "E", index: "a2" },
],
expect: {
unchangedElements: ["A", "B"],
},
});
});
describe("should sync when given a mix of undefined / invalid indices", () => {
testMovedIndicesSync({
elements: [
{ id: "A" },
{ id: "B" },
{ id: "C", index: "a0" },
{ id: "D", index: "a2" },
{ id: "E" },
{ id: "F", index: "a3" },
{ id: "G" },
{ id: "H", index: "a1" },
{ id: "I", index: "a2" },
{ id: "J" },
],
movedElements: ["A", "B", "E", "G", "H", "I", "J"],
expect: {
unchangedElements: ["C", "D", "F"],
},
});
testInvalidIndicesSync({
elements: [
{ id: "A" },
{ id: "B" },
{ id: "C", index: "a0" },
{ id: "D", index: "a2" },
{ id: "E" },
{ id: "F", index: "a3" },
{ id: "G" },
{ id: "H", index: "a1" },
{ id: "I", index: "a2" },
{ id: "J" },
],
expect: {
unchangedElements: ["C", "D", "F"],
},
});
});
describe("should sync all moved elements regardless of their validity", () => {
testMovedIndicesSync({
elements: [
{ id: "A", index: "a2" },
{ id: "B", index: "a4" },
],
movedElements: ["A"],
expect: {
validInput: true,
unchangedElements: ["B"],
},
});
testMovedIndicesSync({
elements: [
{ id: "A", index: "a2" },
{ id: "B", index: "a4" },
],
movedElements: ["B"],
expect: {
validInput: true,
unchangedElements: ["A"],
},
});
testMovedIndicesSync({
elements: [
{ id: "C", index: "a2" },
{ id: "D", index: "a3" },
{ id: "A", index: "a0" },
{ id: "B", index: "a1" },
],
movedElements: ["C", "D"],
expect: {
unchangedElements: ["A", "B"],
},
});
testMovedIndicesSync({
elements: [
{ id: "A", index: "a1" },
{ id: "B", index: "a2" },
{ id: "D", index: "a4" },
{ id: "C", index: "a3" },
{ id: "F", index: "a6" },
{ id: "E", index: "a5" },
{ id: "H", index: "a8" },
{ id: "G", index: "a7" },
{ id: "I", index: "a9" },
],
movedElements: ["D", "F", "H"],
expect: {
unchangedElements: ["A", "B", "C", "E", "G", "I"],
},
});
{
testMovedIndicesSync({
elements: [
{ id: "A", index: "a1" },
{ id: "B", index: "a0" },
{ id: "C", index: "a2" },
],
movedElements: ["B", "C"],
expect: {
unchangedElements: ["A"],
},
});
testMovedIndicesSync({
elements: [
{ id: "A", index: "a1" },
{ id: "B", index: "a0" },
{ id: "C", index: "a2" },
],
movedElements: ["A", "B"],
expect: {
unchangedElements: ["C"],
},
});
}
testMovedIndicesSync({
elements: [
{ id: "A", index: "a0" },
{ id: "B", index: "a2" },
{ id: "C", index: "a1" },
{ id: "D", index: "a1" },
{ id: "E", index: "a2" },
],
movedElements: ["B", "D", "E"],
expect: {
unchangedElements: ["A", "C"],
},
});
testMovedIndicesSync({
elements: [
{ id: "A" },
{ id: "B" },
{ id: "C", index: "a0" },
{ id: "D", index: "a2" },
{ id: "E" },
{ id: "F", index: "a3" },
{ id: "G" },
{ id: "H", index: "a1" },
{ id: "I", index: "a2" },
{ id: "J" },
],
movedElements: ["A", "B", "D", "E", "F", "G", "J"],
expect: {
unchangedElements: ["C", "H", "I"],
},
});
});
describe("should generate fractions for explicitly moved elements", () => {
describe("should generate a fraction between 'A' and 'C'", () => {
testMovedIndicesSync({
elements: [
{ id: "A", index: "a1" },
// doing actual fractions, without jitter 'a1' becomes 'a1V'
// as V is taken as the charset's middle-right value
{ id: "B", index: "a1" },
{ id: "C", index: "a2" },
],
movedElements: ["B"],
expect: {
unchangedElements: ["A", "C"],
},
});
testInvalidIndicesSync({
elements: [
{ id: "A", index: "a1" },
{ id: "B", index: "a1" },
{ id: "C", index: "a2" },
],
expect: {
// as above, B will become fractional
unchangedElements: ["A", "C"],
},
});
});
describe("should generate fractions given duplicated indices", () => {
testMovedIndicesSync({
elements: [
{ id: "A", index: "a01" },
{ id: "B", index: "a01" },
{ id: "C", index: "a01" },
{ id: "D", index: "a01" },
{ id: "E", index: "a02" },
{ id: "F", index: "a02" },
{ id: "G", index: "a02" },
],
movedElements: ["B", "C", "D", "E", "F"],
expect: {
unchangedElements: ["A", "G"],
},
});
testMovedIndicesSync({
elements: [
{ id: "A", index: "a01" },
{ id: "B", index: "a01" },
{ id: "C", index: "a01" },
{ id: "D", index: "a01" },
{ id: "E", index: "a02" },
{ id: "F", index: "a02" },
{ id: "G", index: "a02" },
],
movedElements: ["A", "C", "D", "E", "G"],
expect: {
unchangedElements: ["B", "F"],
},
});
testMovedIndicesSync({
elements: [
{ id: "A", index: "a01" },
{ id: "B", index: "a01" },
{ id: "C", index: "a01" },
{ id: "D", index: "a01" },
{ id: "E", index: "a02" },
{ id: "F", index: "a02" },
{ id: "G", index: "a02" },
],
movedElements: ["B", "C", "D", "F", "G"],
expect: {
unchangedElements: ["A", "E"],
},
});
testInvalidIndicesSync({
elements: [
{ id: "A", index: "a01" },
{ id: "B", index: "a01" },
{ id: "C", index: "a01" },
{ id: "D", index: "a01" },
{ id: "E", index: "a02" },
{ id: "F", index: "a02" },
{ id: "G", index: "a02" },
],
expect: {
// notice fallback considers first item (E) as a valid one
unchangedElements: ["A", "E"],
},
});
});
});
describe("should be able to sync 20K invalid indices", () => {
const length = 20_000;
describe("should sync all empty indices", () => {
const elements = Array.from({ length }).map((_, index) => ({
id: `A_${index}`,
}));
testMovedIndicesSync({
// elements without fractional index
elements,
movedElements: Array.from({ length }).map((_, index) => `A_${index}`),
expect: {
unchangedElements: [],
},
});
testInvalidIndicesSync({
// elements without fractional index
elements,
expect: {
unchangedElements: [],
},
});
});
describe("should sync all but last index given a growing array of indices", () => {
let lastIndex: string | null = null;
const elements = Array.from({ length }).map((_, index) => {
// going up from 'a0'
lastIndex = generateKeyBetween(lastIndex, null);
return {
id: `A_${index}`,
// assigning the last generated index, so sync can go down from there
// without jitter lastIndex is 'c4BZ' for 20000th element
index: index === length - 1 ? lastIndex : undefined,
};
});
const movedElements = Array.from({ length }).map(
(_, index) => `A_${index}`,
);
// remove last element
movedElements.pop();
testMovedIndicesSync({
elements,
movedElements,
expect: {
unchangedElements: [`A_${length - 1}`],
},
});
testInvalidIndicesSync({
elements,
expect: {
unchangedElements: [`A_${length - 1}`],
},
});
});
describe("should sync all but first index given a declining array of indices", () => {
let lastIndex: string | null = null;
const elements = Array.from({ length }).map((_, index) => {
// going down from 'a0'
lastIndex = generateKeyBetween(null, lastIndex);
return {
id: `A_${index}`,
// without jitter lastIndex is 'XvoR' for 20000th element
index: lastIndex,
};
});
const movedElements = Array.from({ length }).map(
(_, index) => `A_${index}`,
);
// remove first element
movedElements.shift();
testMovedIndicesSync({
elements,
movedElements,
expect: {
unchangedElements: [`A_0`],
},
});
testInvalidIndicesSync({
elements,
expect: {
unchangedElements: [`A_0`],
},
});
});
});
describe("should automatically fallback to fixing all invalid indices", () => {
describe("should fallback to syncing duplicated indices when moved elements are empty", () => {
testMovedIndicesSync({
elements: [
{ id: "A", index: "a1" },
{ id: "B", index: "a1" },
{ id: "C", index: "a1" },
],
// the validation will throw as nothing was synced
// therefore it will lead to triggering the fallback and fixing all invalid indices
movedElements: [],
expect: {
unchangedElements: ["A"],
},
});
});
describe("should fallback to syncing undefined / invalid indices when moved elements are empty", () => {
testMovedIndicesSync({
elements: [
{ id: "A", index: "a1" },
{ id: "B" },
{ id: "C", index: "a0" },
],
// since elements are invalid, this will fail the validation
// leading to fallback fixing "B" and "C"
movedElements: [],
expect: {
unchangedElements: ["A"],
},
});
});
describe("should fallback to syncing unordered indices when moved element is invalid", () => {
testMovedIndicesSync({
elements: [
{ id: "A", index: "a1" },
{ id: "B", index: "a2" },
{ id: "C", index: "a1" },
],
movedElements: ["A"],
expect: {
unchangedElements: ["A", "B"],
},
});
});
describe("should fallback when trying to generate an index in between unordered elements", () => {
testMovedIndicesSync({
elements: [
{ id: "A", index: "a2" },
{ id: "B" },
{ id: "C", index: "a1" },
],
// 'B' is invalid, but so is 'C', which was not marked as moved
// therefore it will try to generate a key between 'a2' and 'a1'
// which it cannot do, thus will throw during generation and automatically fallback
movedElements: ["B"],
expect: {
unchangedElements: ["A"],
},
});
});
describe("should fallback when trying to generate an index in between duplicate indices", () => {
testMovedIndicesSync({
elements: [
{ id: "A", index: "a01" },
{ id: "B" },
{ id: "C" },
{ id: "D", index: "a01" },
{ id: "E", index: "a01" },
{ id: "F", index: "a01" },
{ id: "G" },
{ id: "I", index: "a03" },
{ id: "H" },
],
// missed "E" therefore upper bound for 'B' is a01, while lower bound is 'a02'
// therefore, similarly to above, it will fail during key generation and lead to fallback
movedElements: ["B", "C", "D", "F", "G", "H"],
expect: {
unchangedElements: ["A", "I"],
},
});
});
});
});
function testMovedIndicesSync(args: {
elements: { id: string; index?: string }[];
movedElements: string[];
expect: {
unchangedElements: string[];
validInput?: true;
};
}) {
const [elements, movedElements] = prepareArguments(
args.elements,
args.movedElements,
);
const expectUnchangedElements = arrayToMap(
args.expect.unchangedElements.map((x) => ({ id: x })),
);
test(
"should sync invalid indices of moved elements or fallback",
elements,
movedElements,
expectUnchangedElements,
args.expect.validInput,
);
}
function testInvalidIndicesSync(args: {
elements: { id: string; index?: string }[];
expect: {
unchangedElements: string[];
validInput?: true;
};
}) {
const [elements] = prepareArguments(args.elements);
const expectUnchangedElements = arrayToMap(
args.expect.unchangedElements.map((x) => ({ id: x })),
);
test(
"should sync invalid indices of all elements",
elements,
undefined,
expectUnchangedElements,
args.expect.validInput,
);
}
function prepareArguments(
elementsLike: { id: string; index?: string }[],
movedElementsIds?: string[],
): [ExcalidrawElement[], Map<string, ExcalidrawElement> | undefined] {
const elements = elementsLike.map((x) =>
API.createElement({ id: x.id, index: x.index as FractionalIndex }),
);
const movedMap = arrayToMap(movedElementsIds || []);
const movedElements = movedElementsIds
? arrayToMap(elements.filter((x) => movedMap.has(x.id)))
: undefined;
return [elements, movedElements];
}
function test(
name: string,
elements: ExcalidrawElement[],
movedElements: Map<string, ExcalidrawElement> | undefined,
expectUnchangedElements: Map<string, { id: string }>,
expectValidInput?: boolean,
) {
it(name, () => {
// ensure the input is invalid (unless the flag is on)
if (!expectValidInput) {
expect(() =>
validateFractionalIndices(elements, {
shouldThrow: true,
includeBoundTextValidation: true,
ignoreLogs: true,
}),
).toThrowError(InvalidFractionalIndexError);
}
// clone due to mutation
const clonedElements = elements.map((x) => deepCopyElement(x));
// act
const syncedElements = movedElements
? syncMovedIndices(clonedElements, movedElements)
: syncInvalidIndices(clonedElements);
expect(syncedElements.length).toBe(elements.length);
expect(() =>
validateFractionalIndices(syncedElements, {
shouldThrow: true,
includeBoundTextValidation: true,
ignoreLogs: true,
}),
).not.toThrowError(InvalidFractionalIndexError);
syncedElements.forEach((synced, index) => {
const element = elements[index];
// ensure the order hasn't changed
expect(synced.id).toBe(element.id);
if (expectUnchangedElements.has(synced.id)) {
// ensure we didn't mutate where we didn't want to mutate
expect(synced.index).toBe(elements[index].index);
expect(synced.version).toBe(elements[index].version);
} else {
expect(synced.index).not.toBe(elements[index].index);
// ensure we mutated just once, even with fallback triggered
expect(synced.version).toBe(elements[index].version + 1);
}
});
});
}

View file

@ -4,29 +4,26 @@ import util from "util";
import { pointFrom, type LocalPoint, type Radians } from "@excalidraw/math";
import { getDefaultAppState } from "../../appState";
import { createTestHook } from "../../components/App";
import { DEFAULT_VERTICAL_ALIGN, ROUNDNESS } from "../../constants";
import { getMimeType } from "../../data/blob";
import { newElement, newTextElement, newLinearElement } from "../../element";
import { mutateElement } from "../../element/mutateElement";
import { DEFAULT_VERTICAL_ALIGN, ROUNDNESS, assertNever } from "@excalidraw/common";
import { mutateElement } from "@excalidraw/element/mutateElement";
import {
newArrowElement,
newElement,
newEmbeddableElement,
newFrameElement,
newFreeDrawElement,
newIframeElement,
newImageElement,
newLinearElement,
newMagicFrameElement,
} from "../../element/newElement";
import { isLinearElementType } from "../../element/typeChecks";
import { selectGroupsForSelectedElements } from "../../groups";
import { getSelectedElements } from "../../scene/selection";
import { assertNever } from "../../utils";
import { GlobalTestState, createEvent, fireEvent, act } from "../test-utils";
newTextElement,
} from "@excalidraw/element/newElement";
import { isLinearElementType } from "@excalidraw/element/typeChecks";
import { getSelectedElements } from "@excalidraw/element/selection";
import { selectGroupsForSelectedElements } from "@excalidraw/element/groups";
import type { Action } from "../../actions/types";
import type App from "../../components/App";
import type {
ExcalidrawElement,
ExcalidrawGenericElement,
@ -41,9 +38,19 @@ import type {
ExcalidrawElbowArrowElement,
ExcalidrawArrowElement,
FixedSegment,
} from "../../element/types";
} from "@excalidraw/element/types";
import type { Mutable } from "@excalidraw/common/utility-types";
import { getMimeType } from "../../data/blob";
import { createTestHook } from "../../components/App";
import { getDefaultAppState } from "../../appState";
import { GlobalTestState, createEvent, fireEvent, act } from "../test-utils";
import type { Action } from "../../actions/types";
import type App from "../../components/App";
import type { AppState } from "../../types";
import type { Mutable } from "../../utility-types";
const readFile = util.promisify(fs.readFile);
// so that window.h is available when App.tsx is not imported as well.

View file

@ -1,11 +1,11 @@
import { pointFrom, pointRotateRads } from "@excalidraw/math";
import type { GlobalPoint, LocalPoint, Radians } from "@excalidraw/math";
import { createTestHook } from "../../components/App";
import { getCommonBounds, getElementPointsCoords } from "../../element/bounds";
import { cropElement } from "../../element/cropElement";
import { mutateElement } from "../../element/mutateElement";
import {
getCommonBounds,
getElementPointsCoords,
} from "@excalidraw/element/bounds";
import { cropElement } from "@excalidraw/element/cropElement";
import { mutateElement } from "@excalidraw/element/mutateElement";
import {
getTransformHandles,
getTransformHandlesFromCoords,
@ -13,21 +13,18 @@ import {
OMIT_SIDES_FOR_MULTIPLE_ELEMENTS,
type TransformHandle,
type TransformHandleDirection,
} from "../../element/transformHandles";
} from "@excalidraw/element/transformHandles";
import {
isLinearElement,
isFreeDrawElement,
isTextElement,
isFrameLikeElement,
} from "../../element/typeChecks";
import { KEYS } from "../../keys";
import { arrayToMap } from "../../utils";
import { getTextEditor } from "../queries/dom";
import { act, fireEvent, GlobalTestState, screen } from "../test-utils";
} from "@excalidraw/element/typeChecks";
import { KEYS, arrayToMap } from "@excalidraw/common";
import { API } from "./api";
import type { GlobalPoint, LocalPoint, Radians } from "@excalidraw/math";
import type { TransformHandleType } from "../../element/transformHandles";
import type { TransformHandleType } from "@excalidraw/element/transformHandles";
import type {
ExcalidrawElement,
ExcalidrawLinearElement,
@ -39,7 +36,14 @@ import type {
ExcalidrawTextContainer,
ExcalidrawTextElementWithContainer,
ExcalidrawImageElement,
} from "../../element/types";
} from "@excalidraw/element/types";
import { createTestHook } from "../../components/App";
import { getTextEditor } from "../queries/dom";
import { act, fireEvent, GlobalTestState, screen } from "../test-utils";
import { API } from "./api";
import type { ToolType } from "../../types";
// so that window.h is available when App.tsx is not imported as well.

View file

@ -8,10 +8,35 @@ import {
import { vi } from "vitest";
import { pointFrom } from "@excalidraw/math";
import { newElementWith } from "@excalidraw/element/mutateElement";
import {
EXPORT_DATA_TYPES,
MIME_TYPES,
ORIG_ID,
KEYS,
arrayToMap,
COLOR_PALETTE,
DEFAULT_ELEMENT_BACKGROUND_COLOR_INDEX,
DEFAULT_ELEMENT_STROKE_COLOR_INDEX,
} from "@excalidraw/common";
import "@excalidraw/utils/test-utils";
import type { LocalPoint, Radians } from "@excalidraw/math";
import type {
ExcalidrawElbowArrowElement,
ExcalidrawFrameElement,
ExcalidrawGenericElement,
ExcalidrawLinearElement,
ExcalidrawTextElement,
FixedPointBinding,
FractionalIndex,
SceneElementsMap,
} from "@excalidraw/element/types";
import "../global.d.ts";
import "../../utils/test-utils";
import {
actionSendBackward,
@ -23,17 +48,8 @@ import { actionToggleViewMode } from "../actions/actionToggleViewMode";
import { getDefaultAppState } from "../appState";
import { HistoryEntry } from "../history";
import { Excalidraw } from "../index";
import { KEYS } from "../keys";
import * as StaticScene from "../renderer/staticScene";
import { EXPORT_DATA_TYPES, MIME_TYPES, ORIG_ID } from "../constants";
import { Snapshot, CaptureUpdateAction } from "../store";
import { arrayToMap } from "../utils";
import {
COLOR_PALETTE,
DEFAULT_ELEMENT_BACKGROUND_COLOR_INDEX,
DEFAULT_ELEMENT_STROKE_COLOR_INDEX,
} from "../colors";
import { newElementWith } from "../element/mutateElement";
import { AppStateChange, ElementsChange } from "../change";
import { API } from "./helpers/api";
@ -47,16 +63,6 @@ import {
getCloneByOrigId,
} from "./test-utils";
import type {
ExcalidrawElbowArrowElement,
ExcalidrawFrameElement,
ExcalidrawGenericElement,
ExcalidrawLinearElement,
ExcalidrawTextElement,
FixedPointBinding,
FractionalIndex,
SceneElementsMap,
} from "../element/types";
import type { AppState } from "../types";
const { h } = window;

View file

@ -2,18 +2,21 @@ import { act, queryByTestId } from "@testing-library/react";
import React from "react";
import { vi } from "vitest";
import { MIME_TYPES, ORIG_ID } from "../constants";
import { MIME_TYPES, ORIG_ID } from "@excalidraw/common";
import { getCommonBoundingBox } from "@excalidraw/element/bounds";
import type { ExcalidrawGenericElement } from "@excalidraw/element/types";
import { parseLibraryJSON } from "../data/blob";
import { serializeLibraryAsJSON } from "../data/json";
import { distributeLibraryItemsOnSquareGrid } from "../data/library";
import { getCommonBoundingBox } from "../element/bounds";
import { Excalidraw } from "../index";
import { API } from "./helpers/api";
import { UI } from "./helpers/ui";
import { fireEvent, getCloneByOrigId, render, waitFor } from "./test-utils";
import type { ExcalidrawGenericElement } from "../element/types";
import type { LibraryItem, LibraryItems } from "../types";
const { h } = window;

View file

@ -3,23 +3,35 @@ import { act, queryByTestId, queryByText } from "@testing-library/react";
import React from "react";
import { vi } from "vitest";
import type { GlobalPoint } from "@excalidraw/math";
import {
ROUNDNESS,
VERTICAL_ALIGN,
KEYS,
reseed,
arrayToMap,
} from "@excalidraw/common";
import { ROUNDNESS, VERTICAL_ALIGN } from "../constants";
import { LinearElementEditor } from "../element/linearElementEditor";
import { LinearElementEditor } from "@excalidraw/element/linearElementEditor";
import {
getBoundTextElementPosition,
getBoundTextMaxWidth,
} from "../element/textElement";
import * as textElementUtils from "../element/textElement";
import { wrapText } from "../element/textWrapping";
} from "@excalidraw/element/textElement";
import * as textElementUtils from "@excalidraw/element/textElement";
import { wrapText } from "@excalidraw/element/textWrapping";
import type { GlobalPoint } from "@excalidraw/math";
import type {
ExcalidrawElement,
ExcalidrawLinearElement,
ExcalidrawTextElementWithContainer,
FontString,
} from "@excalidraw/element/types";
import { Excalidraw, mutateElement } from "../index";
import { KEYS } from "../keys";
import { reseed } from "../random";
import * as InteractiveCanvas from "../renderer/interactiveScene";
import * as StaticScene from "../renderer/staticScene";
import { API } from "../tests/helpers/api";
import { arrayToMap } from "../utils";
import { Keyboard, Pointer, UI } from "./helpers/ui";
import {
@ -30,13 +42,6 @@ import {
unmountComponent,
} from "./test-utils";
import type {
ExcalidrawElement,
ExcalidrawLinearElement,
ExcalidrawTextElementWithContainer,
FontString,
} from "../element/types";
const renderInteractiveScene = vi.spyOn(
InteractiveCanvas,
"renderInteractiveScene",

View file

@ -1,23 +1,26 @@
import React from "react";
import { vi } from "vitest";
import "../../utils/test-utils";
import { bindOrUnbindLinearElement } from "../element/binding";
import { Excalidraw } from "../index";
import { KEYS } from "../keys";
import { reseed } from "../random";
import * as InteractiveCanvas from "../renderer/interactiveScene";
import * as StaticScene from "../renderer/staticScene";
import { bindOrUnbindLinearElement } from "@excalidraw/element/binding";
import { UI, Pointer, Keyboard } from "./helpers/ui";
import { render, fireEvent, act, unmountComponent } from "./test-utils";
import { KEYS, reseed } from "@excalidraw/common";
import "@excalidraw/utils/test-utils";
import type {
ExcalidrawLinearElement,
NonDeleted,
ExcalidrawRectangleElement,
} from "../element/types";
import type Scene from "../scene/Scene";
} from "@excalidraw/element/types";
import type Scene from "@excalidraw/excalidraw/scene/Scene";
import { Excalidraw } from "../index";
import * as InteractiveCanvas from "../renderer/interactiveScene";
import * as StaticScene from "../renderer/staticScene";
import { UI, Pointer, Keyboard } from "./helpers/ui";
import { render, fireEvent, act, unmountComponent } from "./test-utils";
unmountComponent();

View file

@ -1,9 +1,12 @@
import React from "react";
import { vi } from "vitest";
import { KEYS, reseed } from "@excalidraw/common";
import type { ExcalidrawLinearElement } from "@excalidraw/element/types";
import { Excalidraw } from "../index";
import { KEYS } from "../keys";
import { reseed } from "../random";
import * as InteractiveCanvas from "../renderer/interactiveScene";
import * as StaticScene from "../renderer/staticScene";
@ -15,8 +18,6 @@ import {
unmountComponent,
} from "./test-utils";
import type { ExcalidrawLinearElement } from "../element/types";
unmountComponent();
const renderInteractiveScene = vi.spyOn(

View file

@ -1,8 +1,9 @@
import React from "react";
import { vi } from "vitest";
import { resolvablePromise } from "@excalidraw/common";
import { Excalidraw, CaptureUpdateAction } from "../../index";
import { resolvablePromise } from "../../utils";
import { API } from "../helpers/api";
import { Pointer } from "../helpers/ui";
import { render } from "../test-utils";

View file

@ -1,8 +1,8 @@
import { queries, buildQueries } from "@testing-library/react";
import { TOOL_TYPE } from "../../constants";
import { TOOL_TYPE } from "@excalidraw/common";
import type { ToolType } from "../../types";
import type { ToolType } from "@excalidraw/excalidraw/types";
const _getAllByToolName = (container: HTMLElement, tool: ToolType | "lock") => {
const toolTitle = tool === "lock" ? "lock" : TOOL_TYPE[tool];

View file

@ -1,12 +1,14 @@
import React from "react";
import { vi } from "vitest";
import { FONT_FAMILY } from "../constants";
import { FONT_FAMILY, CODES, KEYS, reseed } from "@excalidraw/common";
import { setDateTimeForTests } from "@excalidraw/common";
import type { ExcalidrawElement } from "@excalidraw/element/types";
import { Excalidraw } from "../index";
import { CODES, KEYS } from "../keys";
import { reseed } from "../random";
import * as StaticScene from "../renderer/staticScene";
import { setDateTimeForTests } from "../utils";
import { API } from "./helpers/api";
import { Keyboard, Pointer, UI } from "./helpers/ui";
@ -19,8 +21,6 @@ import {
unmountComponent,
} from "./test-utils";
import type { ExcalidrawElement } from "../element/types";
const { h } = window;
const renderStaticScene = vi.spyOn(StaticScene, "renderStaticScene");

File diff suppressed because it is too large Load diff

View file

@ -1,8 +1,9 @@
import React from "react";
import { expect } from "vitest";
import { reseed } from "@excalidraw/common";
import { Excalidraw } from "../index";
import { reseed } from "../random";
import { UI } from "./helpers/ui";
import { render, unmountComponent } from "./test-utils";

View file

@ -1,6 +1,13 @@
import { exportToCanvas, exportToSvg } from "@excalidraw/utils";
import { FONT_FAMILY, FRAME_STYLE } from "../../constants";
import { FONT_FAMILY, FRAME_STYLE } from "@excalidraw/common";
import type {
ExcalidrawTextElement,
FractionalIndex,
NonDeletedExcalidrawElement,
} from "@excalidraw/element/types";
import { prepareElementsForExport } from "../../data";
import * as exportUtils from "../../scene/export";
import {
@ -11,12 +18,6 @@ import {
} from "../fixtures/elementFixture";
import { API } from "../helpers/api";
import type {
ExcalidrawTextElement,
FractionalIndex,
NonDeletedExcalidrawElement,
} from "../../element/types";
describe("exportToSvg", () => {
const ELEMENT_HEIGHT = 100;
const ELEMENT_WIDTH = 100;

View file

@ -1,7 +1,8 @@
import React from "react";
import { KEYS } from "@excalidraw/common";
import { Excalidraw } from "../index";
import { KEYS } from "../keys";
import { API } from "./helpers/api";
import { Keyboard } from "./helpers/ui";

View file

@ -1,16 +1,21 @@
import React from "react";
import { CANVAS_SEARCH_TAB, CLASSES, DEFAULT_SIDEBAR } from "../constants";
import {
CANVAS_SEARCH_TAB,
CLASSES,
DEFAULT_SIDEBAR,
KEYS,
} from "@excalidraw/common";
import type { ExcalidrawTextElement } from "@excalidraw/element/types";
import { Excalidraw } from "../index";
import { KEYS } from "../keys";
import { API } from "./helpers/api";
import { Keyboard } from "./helpers/ui";
import { updateTextEditor } from "./queries/dom";
import { act, render, waitFor } from "./test-utils";
import type { ExcalidrawTextElement } from "../element/types";
const { h } = window;
const querySearchInput = async () => {

View file

@ -1,12 +1,13 @@
import React from "react";
import { vi } from "vitest";
import { KEYS, reseed } from "@excalidraw/common";
import { SHAPES } from "../components/shapes";
import { Excalidraw } from "../index";
import { KEYS } from "../keys";
import { reseed } from "../random";
import * as InteractiveCanvas from "../renderer/interactiveScene";
import * as StaticScene from "../renderer/staticScene";
import { SHAPES } from "../shapes";
import { API } from "./helpers/api";
import { Keyboard, Pointer, UI } from "./helpers/ui";

View file

@ -1,7 +1,8 @@
import React from "react";
import { KEYS } from "@excalidraw/common";
import { Excalidraw } from "../index";
import { KEYS } from "../keys";
import { API } from "./helpers/api";
import { Keyboard } from "./helpers/ui";

View file

@ -9,10 +9,15 @@ import {
} from "@testing-library/react";
import ansi from "ansicolor";
import { ORIG_ID, arrayToMap } from "@excalidraw/common";
import { getSelectedElements } from "@excalidraw/element/selection";
import type { ExcalidrawElement } from "@excalidraw/element/types";
import type { AllPossibleKeys } from "@excalidraw/common/utility-types";
import { STORAGE_KEYS } from "../../../excalidraw-app/app_constants";
import { ORIG_ID } from "../constants";
import { getSelectedElements } from "../scene/selection";
import { arrayToMap } from "../utils";
import { UI } from "./helpers/ui";
import * as toolQueries from "./queries/toolQueries";
@ -20,8 +25,6 @@ import * as toolQueries from "./queries/toolQueries";
import type { RenderResult, RenderOptions } from "@testing-library/react";
import type { ImportedDataState } from "../data/types";
import type { ExcalidrawElement } from "../element/types";
import type { AllPossibleKeys } from "../utility-types";
export { cleanup as unmountComponent };

View file

@ -1,7 +1,8 @@
import React from "react";
import { resolvablePromise } from "@excalidraw/common";
import { Excalidraw } from "../index";
import { resolvablePromise } from "../utils";
import { Pointer } from "./helpers/ui";
import { act, render } from "./test-utils";

View file

@ -1,4 +1,4 @@
import { isTransparent } from "../utils";
import { isTransparent } from "@excalidraw/common";
describe("Test isTransparent", () => {
it("should return true when color is rgb transparent", () => {

View file

@ -1,8 +1,8 @@
import React from "react";
import { CURSOR_TYPE } from "../constants";
import { CURSOR_TYPE, KEYS } from "@excalidraw/common";
import { Excalidraw } from "../index";
import { KEYS } from "../keys";
import { API } from "./helpers/api";
import { Keyboard, Pointer, UI } from "./helpers/ui";

File diff suppressed because it is too large Load diff