You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1249 lines
38 KiB
TypeScript
1249 lines
38 KiB
TypeScript
import ReactDOM from "react-dom";
|
|
import {
|
|
ExcalidrawElement,
|
|
ExcalidrawLinearElement,
|
|
ExcalidrawTextElementWithContainer,
|
|
FontString,
|
|
} from "../element/types";
|
|
import { Excalidraw } from "../index";
|
|
import { centerPoint } from "../math";
|
|
import { reseed } from "../random";
|
|
import * as Renderer from "../renderer/renderScene";
|
|
import { Keyboard, Pointer, UI } from "./helpers/ui";
|
|
import { screen, render, fireEvent, GlobalTestState } from "./test-utils";
|
|
import { API } from "../tests/helpers/api";
|
|
import { Point } from "../types";
|
|
import { KEYS } from "../keys";
|
|
import { LinearElementEditor } from "../element/linearElementEditor";
|
|
import { queryByTestId, queryByText } from "@testing-library/react";
|
|
import {
|
|
getBoundTextElementPosition,
|
|
wrapText,
|
|
getBoundTextMaxWidth,
|
|
} from "../element/textElement";
|
|
import * as textElementUtils from "../element/textElement";
|
|
import { ROUNDNESS, VERTICAL_ALIGN } from "../constants";
|
|
import { vi } from "vitest";
|
|
|
|
const renderInteractiveScene = vi.spyOn(Renderer, "renderInteractiveScene");
|
|
const renderStaticScene = vi.spyOn(Renderer, "renderStaticScene");
|
|
|
|
const { h } = window;
|
|
const font = "20px Cascadia, width: Segoe UI Emoji" as FontString;
|
|
|
|
describe("Test Linear Elements", () => {
|
|
let container: HTMLElement;
|
|
let interactiveCanvas: HTMLCanvasElement;
|
|
|
|
beforeEach(async () => {
|
|
// Unmount ReactDOM from root
|
|
ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
|
|
localStorage.clear();
|
|
renderInteractiveScene.mockClear();
|
|
renderStaticScene.mockClear();
|
|
reseed(7);
|
|
const comp = await render(<Excalidraw handleKeyboardGlobally={true} />);
|
|
h.state.width = 1000;
|
|
h.state.height = 1000;
|
|
container = comp.container;
|
|
interactiveCanvas = container.querySelector("canvas.interactive")!;
|
|
});
|
|
|
|
const p1: Point = [20, 20];
|
|
const p2: Point = [60, 20];
|
|
const midpoint = centerPoint(p1, p2);
|
|
const delta = 50;
|
|
const mouse = new Pointer("mouse");
|
|
|
|
const createTwoPointerLinearElement = (
|
|
type: ExcalidrawLinearElement["type"],
|
|
roundness: ExcalidrawElement["roundness"] = null,
|
|
roughness: ExcalidrawLinearElement["roughness"] = 0,
|
|
) => {
|
|
const line = API.createElement({
|
|
x: p1[0],
|
|
y: p1[1],
|
|
width: p2[0] - p1[0],
|
|
height: 0,
|
|
type,
|
|
roughness,
|
|
points: [
|
|
[0, 0],
|
|
[p2[0] - p1[0], p2[1] - p1[1]],
|
|
],
|
|
roundness,
|
|
});
|
|
h.elements = [line];
|
|
|
|
mouse.clickAt(p1[0], p1[1]);
|
|
return line;
|
|
};
|
|
|
|
const createThreePointerLinearElement = (
|
|
type: ExcalidrawLinearElement["type"],
|
|
roundness: ExcalidrawElement["roundness"] = null,
|
|
roughness: ExcalidrawLinearElement["roughness"] = 0,
|
|
) => {
|
|
//dragging line from midpoint
|
|
const p3 = [midpoint[0] + delta - p1[0], midpoint[1] + delta - p1[1]];
|
|
const line = API.createElement({
|
|
x: p1[0],
|
|
y: p1[1],
|
|
width: p3[0] - p1[0],
|
|
height: 0,
|
|
type,
|
|
roughness,
|
|
points: [
|
|
[0, 0],
|
|
[p3[0], p3[1]],
|
|
[p2[0] - p1[0], p2[1] - p1[1]],
|
|
],
|
|
roundness,
|
|
});
|
|
h.elements = [line];
|
|
mouse.clickAt(p1[0], p1[1]);
|
|
return line;
|
|
};
|
|
|
|
const enterLineEditingMode = (
|
|
line: ExcalidrawLinearElement,
|
|
selectProgrammatically = false,
|
|
) => {
|
|
if (selectProgrammatically) {
|
|
API.setSelectedElements([line]);
|
|
} else {
|
|
mouse.clickAt(p1[0], p1[1]);
|
|
}
|
|
Keyboard.withModifierKeys({ ctrl: true }, () => {
|
|
Keyboard.keyPress(KEYS.ENTER);
|
|
});
|
|
expect(h.state.editingLinearElement?.elementId).toEqual(line.id);
|
|
};
|
|
|
|
const drag = (startPoint: Point, endPoint: Point) => {
|
|
fireEvent.pointerDown(interactiveCanvas, {
|
|
clientX: startPoint[0],
|
|
clientY: startPoint[1],
|
|
});
|
|
fireEvent.pointerMove(interactiveCanvas, {
|
|
clientX: endPoint[0],
|
|
clientY: endPoint[1],
|
|
});
|
|
fireEvent.pointerUp(interactiveCanvas, {
|
|
clientX: endPoint[0],
|
|
clientY: endPoint[1],
|
|
});
|
|
};
|
|
|
|
const deletePoint = (point: Point) => {
|
|
fireEvent.pointerDown(interactiveCanvas, {
|
|
clientX: point[0],
|
|
clientY: point[1],
|
|
});
|
|
fireEvent.pointerUp(interactiveCanvas, {
|
|
clientX: point[0],
|
|
clientY: point[1],
|
|
});
|
|
Keyboard.keyPress(KEYS.DELETE);
|
|
};
|
|
|
|
it("should not drag line and add midpoint until dragged beyond a threshold", () => {
|
|
createTwoPointerLinearElement("line");
|
|
const line = h.elements[0] as ExcalidrawLinearElement;
|
|
const originalX = line.x;
|
|
const originalY = line.y;
|
|
expect(line.points.length).toEqual(2);
|
|
|
|
mouse.clickAt(midpoint[0], midpoint[1]);
|
|
drag(midpoint, [midpoint[0] + 1, midpoint[1] + 1]);
|
|
|
|
expect(line.points.length).toEqual(2);
|
|
|
|
expect(line.x).toBe(originalX);
|
|
expect(line.y).toBe(originalY);
|
|
expect(line.points.length).toEqual(2);
|
|
|
|
drag(midpoint, [midpoint[0] + delta, midpoint[1] + delta]);
|
|
expect(line.x).toBe(originalX);
|
|
expect(line.y).toBe(originalY);
|
|
expect(line.points.length).toEqual(3);
|
|
});
|
|
|
|
it("should allow dragging line from midpoint in 2 pointer lines outside editor", async () => {
|
|
createTwoPointerLinearElement("line");
|
|
const line = h.elements[0] as ExcalidrawLinearElement;
|
|
|
|
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(`5`);
|
|
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`5`);
|
|
expect((h.elements[0] as ExcalidrawLinearElement).points.length).toEqual(2);
|
|
|
|
// drag line from midpoint
|
|
drag(midpoint, [midpoint[0] + delta, midpoint[1] + delta]);
|
|
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(`9`);
|
|
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`7`);
|
|
expect(line.points.length).toEqual(3);
|
|
expect(line.points).toMatchInlineSnapshot(`
|
|
[
|
|
[
|
|
0,
|
|
0,
|
|
],
|
|
[
|
|
70,
|
|
50,
|
|
],
|
|
[
|
|
40,
|
|
0,
|
|
],
|
|
]
|
|
`);
|
|
});
|
|
|
|
it("should allow entering and exiting line editor via context menu", () => {
|
|
createTwoPointerLinearElement("line");
|
|
fireEvent.contextMenu(GlobalTestState.interactiveCanvas, {
|
|
button: 2,
|
|
clientX: midpoint[0],
|
|
clientY: midpoint[1],
|
|
});
|
|
// Enter line editor
|
|
let contextMenu = document.querySelector(".context-menu");
|
|
fireEvent.contextMenu(GlobalTestState.interactiveCanvas, {
|
|
button: 2,
|
|
clientX: midpoint[0],
|
|
clientY: midpoint[1],
|
|
});
|
|
fireEvent.click(queryByText(contextMenu as HTMLElement, "Edit line")!);
|
|
|
|
expect(h.state.editingLinearElement?.elementId).toEqual(h.elements[0].id);
|
|
|
|
// Exiting line editor
|
|
fireEvent.contextMenu(GlobalTestState.interactiveCanvas, {
|
|
button: 2,
|
|
clientX: midpoint[0],
|
|
clientY: midpoint[1],
|
|
});
|
|
contextMenu = document.querySelector(".context-menu");
|
|
fireEvent.contextMenu(GlobalTestState.interactiveCanvas, {
|
|
button: 2,
|
|
clientX: midpoint[0],
|
|
clientY: midpoint[1],
|
|
});
|
|
fireEvent.click(
|
|
queryByText(contextMenu as HTMLElement, "Exit line editor")!,
|
|
);
|
|
expect(h.state.editingLinearElement?.elementId).toBeUndefined();
|
|
});
|
|
|
|
it("should enter line editor when using double clicked with ctrl key", () => {
|
|
createTwoPointerLinearElement("line");
|
|
expect(h.state.editingLinearElement?.elementId).toBeUndefined();
|
|
|
|
Keyboard.withModifierKeys({ ctrl: true }, () => {
|
|
mouse.doubleClick();
|
|
});
|
|
expect(h.state.editingLinearElement?.elementId).toEqual(h.elements[0].id);
|
|
});
|
|
|
|
describe("Inside editor", () => {
|
|
it("should not drag line and add midpoint when dragged irrespective of threshold", () => {
|
|
createTwoPointerLinearElement("line");
|
|
const line = h.elements[0] as ExcalidrawLinearElement;
|
|
const originalX = line.x;
|
|
const originalY = line.y;
|
|
enterLineEditingMode(line);
|
|
|
|
expect(line.points.length).toEqual(2);
|
|
|
|
mouse.clickAt(midpoint[0], midpoint[1]);
|
|
expect(line.points.length).toEqual(2);
|
|
|
|
drag(midpoint, [midpoint[0] + 1, midpoint[1] + 1]);
|
|
expect(line.x).toBe(originalX);
|
|
expect(line.y).toBe(originalY);
|
|
expect(line.points.length).toEqual(3);
|
|
});
|
|
|
|
it("should allow dragging line from midpoint in 2 pointer lines", async () => {
|
|
createTwoPointerLinearElement("line");
|
|
|
|
const line = h.elements[0] as ExcalidrawLinearElement;
|
|
enterLineEditingMode(line);
|
|
|
|
// drag line from midpoint
|
|
drag(midpoint, [midpoint[0] + delta, midpoint[1] + delta]);
|
|
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
|
|
`12`,
|
|
);
|
|
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`6`);
|
|
|
|
expect(line.points.length).toEqual(3);
|
|
expect(line.points).toMatchInlineSnapshot(`
|
|
[
|
|
[
|
|
0,
|
|
0,
|
|
],
|
|
[
|
|
70,
|
|
50,
|
|
],
|
|
[
|
|
40,
|
|
0,
|
|
],
|
|
]
|
|
`);
|
|
});
|
|
|
|
it("should update the midpoints when element roundness changed", async () => {
|
|
createThreePointerLinearElement("line");
|
|
|
|
const line = h.elements[0] as ExcalidrawLinearElement;
|
|
expect(line.points.length).toEqual(3);
|
|
|
|
enterLineEditingMode(line);
|
|
|
|
const midPointsWithSharpEdge = LinearElementEditor.getEditorMidPoints(
|
|
line,
|
|
h.state,
|
|
);
|
|
|
|
// update roundness
|
|
fireEvent.click(screen.getByTitle("Round"));
|
|
|
|
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
|
|
`10`,
|
|
);
|
|
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`8`);
|
|
|
|
const midPointsWithRoundEdge = LinearElementEditor.getEditorMidPoints(
|
|
h.elements[0] as ExcalidrawLinearElement,
|
|
h.state,
|
|
);
|
|
expect(midPointsWithRoundEdge[0]).not.toEqual(midPointsWithSharpEdge[0]);
|
|
expect(midPointsWithRoundEdge[1]).not.toEqual(midPointsWithSharpEdge[1]);
|
|
|
|
expect(midPointsWithRoundEdge).toMatchInlineSnapshot(`
|
|
[
|
|
[
|
|
55.9697848965255,
|
|
47.442326230998205,
|
|
],
|
|
[
|
|
76.08587175006699,
|
|
43.294165939653226,
|
|
],
|
|
]
|
|
`);
|
|
});
|
|
|
|
it("should update all the midpoints when element position changed", async () => {
|
|
createThreePointerLinearElement("line", {
|
|
type: ROUNDNESS.PROPORTIONAL_RADIUS,
|
|
});
|
|
|
|
const line = h.elements[0] as ExcalidrawLinearElement;
|
|
expect(line.points.length).toEqual(3);
|
|
enterLineEditingMode(line);
|
|
|
|
const points = LinearElementEditor.getPointsGlobalCoordinates(line);
|
|
expect([line.x, line.y]).toEqual(points[0]);
|
|
|
|
const midPoints = LinearElementEditor.getEditorMidPoints(line, h.state);
|
|
|
|
const startPoint = centerPoint(points[0], midPoints[0] as Point);
|
|
const deltaX = 50;
|
|
const deltaY = 20;
|
|
const endPoint: Point = [startPoint[0] + deltaX, startPoint[1] + deltaY];
|
|
|
|
// Move the element
|
|
drag(startPoint, endPoint);
|
|
|
|
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
|
|
`13`,
|
|
);
|
|
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`9`);
|
|
|
|
expect([line.x, line.y]).toEqual([
|
|
points[0][0] + deltaX,
|
|
points[0][1] + deltaY,
|
|
]);
|
|
|
|
const newMidPoints = LinearElementEditor.getEditorMidPoints(
|
|
line,
|
|
h.state,
|
|
);
|
|
expect(midPoints[0]).not.toEqual(newMidPoints[0]);
|
|
expect(midPoints[1]).not.toEqual(newMidPoints[1]);
|
|
expect(newMidPoints).toMatchInlineSnapshot(`
|
|
[
|
|
[
|
|
105.96978489652551,
|
|
67.4423262309982,
|
|
],
|
|
[
|
|
126.08587175006699,
|
|
63.294165939653226,
|
|
],
|
|
]
|
|
`);
|
|
});
|
|
|
|
describe("When edges are round", () => {
|
|
// This is the expected midpoint for line with round edge
|
|
// hence hardcoding it so if later some bug is introduced
|
|
// this will fail and we can fix it
|
|
const firstSegmentMidpoint: Point = [55, 45];
|
|
const lastSegmentMidpoint: Point = [75, 40];
|
|
|
|
let line: ExcalidrawLinearElement;
|
|
|
|
beforeEach(() => {
|
|
line = createThreePointerLinearElement("line");
|
|
|
|
expect(line.points.length).toEqual(3);
|
|
|
|
enterLineEditingMode(line);
|
|
});
|
|
|
|
it("should allow dragging lines from midpoints in between segments", async () => {
|
|
// drag line via first segment midpoint
|
|
drag(firstSegmentMidpoint, [
|
|
firstSegmentMidpoint[0] + delta,
|
|
firstSegmentMidpoint[1] + delta,
|
|
]);
|
|
expect(line.points.length).toEqual(4);
|
|
|
|
// drag line from last segment midpoint
|
|
drag(lastSegmentMidpoint, [
|
|
lastSegmentMidpoint[0] + delta,
|
|
lastSegmentMidpoint[1] + delta,
|
|
]);
|
|
|
|
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
|
|
`17`,
|
|
);
|
|
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`9`);
|
|
|
|
expect(line.points.length).toEqual(5);
|
|
|
|
expect((h.elements[0] as ExcalidrawLinearElement).points)
|
|
.toMatchInlineSnapshot(`
|
|
[
|
|
[
|
|
0,
|
|
0,
|
|
],
|
|
[
|
|
85,
|
|
75,
|
|
],
|
|
[
|
|
70,
|
|
50,
|
|
],
|
|
[
|
|
105,
|
|
70,
|
|
],
|
|
[
|
|
40,
|
|
0,
|
|
],
|
|
]
|
|
`);
|
|
});
|
|
|
|
it("should update only the first segment midpoint when its point is dragged", async () => {
|
|
const points = LinearElementEditor.getPointsGlobalCoordinates(line);
|
|
const midPoints = LinearElementEditor.getEditorMidPoints(line, h.state);
|
|
|
|
const hitCoords: Point = [points[0][0], points[0][1]];
|
|
|
|
// Drag from first point
|
|
drag(hitCoords, [hitCoords[0] - delta, hitCoords[1] - delta]);
|
|
|
|
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
|
|
`13`,
|
|
);
|
|
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`8`);
|
|
|
|
const newPoints = LinearElementEditor.getPointsGlobalCoordinates(line);
|
|
expect([newPoints[0][0], newPoints[0][1]]).toEqual([
|
|
points[0][0] - delta,
|
|
points[0][1] - delta,
|
|
]);
|
|
|
|
const newMidPoints = LinearElementEditor.getEditorMidPoints(
|
|
line,
|
|
h.state,
|
|
);
|
|
|
|
expect(midPoints[0]).not.toEqual(newMidPoints[0]);
|
|
expect(midPoints[1]).toEqual(newMidPoints[1]);
|
|
});
|
|
|
|
it("should hide midpoints in the segment when points moved close", async () => {
|
|
const points = LinearElementEditor.getPointsGlobalCoordinates(line);
|
|
const midPoints = LinearElementEditor.getEditorMidPoints(line, h.state);
|
|
|
|
const hitCoords: Point = [points[0][0], points[0][1]];
|
|
|
|
// Drag from first point
|
|
drag(hitCoords, [hitCoords[0] + delta, hitCoords[1] + delta]);
|
|
|
|
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
|
|
`13`,
|
|
);
|
|
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`8`);
|
|
|
|
const newPoints = LinearElementEditor.getPointsGlobalCoordinates(line);
|
|
expect([newPoints[0][0], newPoints[0][1]]).toEqual([
|
|
points[0][0] + delta,
|
|
points[0][1] + delta,
|
|
]);
|
|
|
|
const newMidPoints = LinearElementEditor.getEditorMidPoints(
|
|
line,
|
|
h.state,
|
|
);
|
|
// This midpoint is hidden since the points are too close
|
|
expect(newMidPoints[0]).toBeNull();
|
|
expect(midPoints[1]).toEqual(newMidPoints[1]);
|
|
});
|
|
|
|
it("should remove the midpoint when one of the points in the segment is deleted", async () => {
|
|
const line = h.elements[0] as ExcalidrawLinearElement;
|
|
enterLineEditingMode(line);
|
|
const points = LinearElementEditor.getPointsGlobalCoordinates(line);
|
|
|
|
// dragging line from last segment midpoint
|
|
drag(lastSegmentMidpoint, [
|
|
lastSegmentMidpoint[0] + 50,
|
|
lastSegmentMidpoint[1] + 50,
|
|
]);
|
|
expect(line.points.length).toEqual(4);
|
|
|
|
const midPoints = LinearElementEditor.getEditorMidPoints(line, h.state);
|
|
|
|
// delete 3rd point
|
|
deletePoint(points[2]);
|
|
expect(line.points.length).toEqual(3);
|
|
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
|
|
`19`,
|
|
);
|
|
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`9`);
|
|
|
|
const newMidPoints = LinearElementEditor.getEditorMidPoints(
|
|
line,
|
|
h.state,
|
|
);
|
|
expect(newMidPoints.length).toEqual(2);
|
|
expect(midPoints[0]).toEqual(newMidPoints[0]);
|
|
expect(midPoints[1]).toEqual(newMidPoints[1]);
|
|
});
|
|
});
|
|
|
|
describe("When edges are round", () => {
|
|
// This is the expected midpoint for line with round edge
|
|
// hence hardcoding it so if later some bug is introduced
|
|
// this will fail and we can fix it
|
|
const firstSegmentMidpoint: Point = [
|
|
55.9697848965255, 47.442326230998205,
|
|
];
|
|
const lastSegmentMidpoint: Point = [
|
|
76.08587175006699, 43.294165939653226,
|
|
];
|
|
let line: ExcalidrawLinearElement;
|
|
|
|
beforeEach(() => {
|
|
line = createThreePointerLinearElement("line", {
|
|
type: ROUNDNESS.PROPORTIONAL_RADIUS,
|
|
});
|
|
expect(line.points.length).toEqual(3);
|
|
|
|
enterLineEditingMode(line);
|
|
});
|
|
|
|
it("should allow dragging lines from midpoints in between segments", async () => {
|
|
// drag line from first segment midpoint
|
|
drag(firstSegmentMidpoint, [
|
|
firstSegmentMidpoint[0] + delta,
|
|
firstSegmentMidpoint[1] + delta,
|
|
]);
|
|
expect(line.points.length).toEqual(4);
|
|
|
|
// drag line from last segment midpoint
|
|
drag(lastSegmentMidpoint, [
|
|
lastSegmentMidpoint[0] + delta,
|
|
lastSegmentMidpoint[1] + delta,
|
|
]);
|
|
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
|
|
`17`,
|
|
);
|
|
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`9`);
|
|
expect(line.points.length).toEqual(5);
|
|
|
|
expect((h.elements[0] as ExcalidrawLinearElement).points)
|
|
.toMatchInlineSnapshot(`
|
|
[
|
|
[
|
|
0,
|
|
0,
|
|
],
|
|
[
|
|
85.96978489652551,
|
|
77.4423262309982,
|
|
],
|
|
[
|
|
70,
|
|
50,
|
|
],
|
|
[
|
|
106.08587175006699,
|
|
73.29416593965323,
|
|
],
|
|
[
|
|
40,
|
|
0,
|
|
],
|
|
]
|
|
`);
|
|
});
|
|
|
|
it("should update all the midpoints when its point is dragged", async () => {
|
|
const points = LinearElementEditor.getPointsGlobalCoordinates(line);
|
|
const midPoints = LinearElementEditor.getEditorMidPoints(line, h.state);
|
|
|
|
const hitCoords: Point = [points[0][0], points[0][1]];
|
|
|
|
// Drag from first point
|
|
drag(hitCoords, [hitCoords[0] - delta, hitCoords[1] - delta]);
|
|
|
|
const newPoints = LinearElementEditor.getPointsGlobalCoordinates(line);
|
|
expect([newPoints[0][0], newPoints[0][1]]).toEqual([
|
|
points[0][0] - delta,
|
|
points[0][1] - delta,
|
|
]);
|
|
|
|
const newMidPoints = LinearElementEditor.getEditorMidPoints(
|
|
line,
|
|
h.state,
|
|
);
|
|
|
|
expect(midPoints[0]).not.toEqual(newMidPoints[0]);
|
|
expect(midPoints[1]).not.toEqual(newMidPoints[1]);
|
|
expect(newMidPoints).toMatchInlineSnapshot(`
|
|
[
|
|
[
|
|
31.884084517616053,
|
|
23.13275505472383,
|
|
],
|
|
[
|
|
77.74792546875662,
|
|
44.57840982272327,
|
|
],
|
|
]
|
|
`);
|
|
});
|
|
|
|
it("should hide midpoints in the segment when points moved close", async () => {
|
|
const points = LinearElementEditor.getPointsGlobalCoordinates(line);
|
|
const midPoints = LinearElementEditor.getEditorMidPoints(line, h.state);
|
|
|
|
const hitCoords: Point = [points[0][0], points[0][1]];
|
|
|
|
// Drag from first point
|
|
drag(hitCoords, [hitCoords[0] + delta, hitCoords[1] + delta]);
|
|
|
|
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
|
|
`13`,
|
|
);
|
|
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`8`);
|
|
|
|
const newPoints = LinearElementEditor.getPointsGlobalCoordinates(line);
|
|
expect([newPoints[0][0], newPoints[0][1]]).toEqual([
|
|
points[0][0] + delta,
|
|
points[0][1] + delta,
|
|
]);
|
|
|
|
const newMidPoints = LinearElementEditor.getEditorMidPoints(
|
|
line,
|
|
h.state,
|
|
);
|
|
// This mid point is hidden due to point being too close
|
|
expect(newMidPoints[0]).toBeNull();
|
|
expect(newMidPoints[1]).not.toEqual(midPoints[1]);
|
|
});
|
|
|
|
it("should update all the midpoints when a point is deleted", async () => {
|
|
drag(lastSegmentMidpoint, [
|
|
lastSegmentMidpoint[0] + delta,
|
|
lastSegmentMidpoint[1] + delta,
|
|
]);
|
|
expect(line.points.length).toEqual(4);
|
|
|
|
const midPoints = LinearElementEditor.getEditorMidPoints(line, h.state);
|
|
const points = LinearElementEditor.getPointsGlobalCoordinates(line);
|
|
|
|
// delete 3rd point
|
|
deletePoint(points[2]);
|
|
expect(line.points.length).toEqual(3);
|
|
|
|
const newMidPoints = LinearElementEditor.getEditorMidPoints(
|
|
line,
|
|
h.state,
|
|
);
|
|
expect(newMidPoints.length).toEqual(2);
|
|
expect(midPoints[0]).not.toEqual(newMidPoints[0]);
|
|
expect(midPoints[1]).not.toEqual(newMidPoints[1]);
|
|
expect(newMidPoints).toMatchInlineSnapshot(`
|
|
[
|
|
[
|
|
55.9697848965255,
|
|
47.442326230998205,
|
|
],
|
|
[
|
|
76.08587175006699,
|
|
43.294165939653226,
|
|
],
|
|
]
|
|
`);
|
|
});
|
|
});
|
|
|
|
it("in-editor dragging a line point covered by another element", () => {
|
|
createTwoPointerLinearElement("line");
|
|
const line = h.elements[0] as ExcalidrawLinearElement;
|
|
h.elements = [
|
|
line,
|
|
API.createElement({
|
|
type: "rectangle",
|
|
x: line.x - 50,
|
|
y: line.y - 50,
|
|
width: 100,
|
|
height: 100,
|
|
backgroundColor: "red",
|
|
fillStyle: "solid",
|
|
}),
|
|
];
|
|
const dragEndPositionOffset = [100, 100] as const;
|
|
API.setSelectedElements([line]);
|
|
enterLineEditingMode(line, true);
|
|
drag(
|
|
[line.points[0][0] + line.x, line.points[0][1] + line.y],
|
|
[dragEndPositionOffset[0] + line.x, dragEndPositionOffset[1] + line.y],
|
|
);
|
|
expect(line.points).toMatchInlineSnapshot(`
|
|
[
|
|
[
|
|
0,
|
|
0,
|
|
],
|
|
[
|
|
-60,
|
|
-100,
|
|
],
|
|
]
|
|
`);
|
|
});
|
|
});
|
|
|
|
describe("Test bound text element", () => {
|
|
const DEFAULT_TEXT = "Online whiteboard collaboration made easy";
|
|
|
|
const createBoundTextElement = (
|
|
text: string,
|
|
container: ExcalidrawLinearElement,
|
|
) => {
|
|
const textElement = API.createElement({
|
|
type: "text",
|
|
x: 0,
|
|
y: 0,
|
|
text: wrapText(text, font, getBoundTextMaxWidth(container)),
|
|
containerId: container.id,
|
|
width: 30,
|
|
height: 20,
|
|
}) as ExcalidrawTextElementWithContainer;
|
|
|
|
container = {
|
|
...container,
|
|
boundElements: (container.boundElements || []).concat({
|
|
type: "text",
|
|
id: textElement.id,
|
|
}),
|
|
};
|
|
const elements: ExcalidrawElement[] = [];
|
|
h.elements.forEach((element) => {
|
|
if (element.id === container.id) {
|
|
elements.push(container);
|
|
} else {
|
|
elements.push(element);
|
|
}
|
|
});
|
|
const updatedTextElement = { ...textElement, originalText: text };
|
|
h.elements = [...elements, updatedTextElement];
|
|
return { textElement: updatedTextElement, container };
|
|
};
|
|
|
|
describe("Test getBoundTextElementPosition", () => {
|
|
it("should return correct position for 2 pointer arrow", () => {
|
|
createTwoPointerLinearElement("arrow");
|
|
const arrow = h.elements[0] as ExcalidrawLinearElement;
|
|
const { textElement, container } = createBoundTextElement(
|
|
DEFAULT_TEXT,
|
|
arrow,
|
|
);
|
|
const position = LinearElementEditor.getBoundTextElementPosition(
|
|
container,
|
|
textElement,
|
|
);
|
|
expect(position).toMatchInlineSnapshot(`
|
|
{
|
|
"x": 25,
|
|
"y": 10,
|
|
}
|
|
`);
|
|
});
|
|
|
|
it("should return correct position for arrow with odd points", () => {
|
|
createThreePointerLinearElement("arrow", {
|
|
type: ROUNDNESS.PROPORTIONAL_RADIUS,
|
|
});
|
|
const arrow = h.elements[0] as ExcalidrawLinearElement;
|
|
const { textElement, container } = createBoundTextElement(
|
|
DEFAULT_TEXT,
|
|
arrow,
|
|
);
|
|
|
|
const position = LinearElementEditor.getBoundTextElementPosition(
|
|
container,
|
|
textElement,
|
|
);
|
|
expect(position).toMatchInlineSnapshot(`
|
|
{
|
|
"x": 75,
|
|
"y": 60,
|
|
}
|
|
`);
|
|
});
|
|
|
|
it("should return correct position for arrow with even points", () => {
|
|
createThreePointerLinearElement("arrow", {
|
|
type: ROUNDNESS.PROPORTIONAL_RADIUS,
|
|
});
|
|
const arrow = h.elements[0] as ExcalidrawLinearElement;
|
|
const { textElement, container } = createBoundTextElement(
|
|
DEFAULT_TEXT,
|
|
arrow,
|
|
);
|
|
enterLineEditingMode(container);
|
|
// This is the expected midpoint for line with round edge
|
|
// hence hardcoding it so if later some bug is introduced
|
|
// this will fail and we can fix it
|
|
const firstSegmentMidpoint: Point = [
|
|
55.9697848965255, 47.442326230998205,
|
|
];
|
|
// drag line from first segment midpoint
|
|
drag(firstSegmentMidpoint, [
|
|
firstSegmentMidpoint[0] + delta,
|
|
firstSegmentMidpoint[1] + delta,
|
|
]);
|
|
|
|
const position = LinearElementEditor.getBoundTextElementPosition(
|
|
container,
|
|
textElement,
|
|
);
|
|
expect(position).toMatchInlineSnapshot(`
|
|
{
|
|
"x": 85.82201843191861,
|
|
"y": 75.63461309860818,
|
|
}
|
|
`);
|
|
});
|
|
});
|
|
|
|
it("should match styles for text editor", () => {
|
|
createTwoPointerLinearElement("arrow");
|
|
Keyboard.keyPress(KEYS.ENTER);
|
|
const editor = document.querySelector(
|
|
".excalidraw-textEditorContainer > textarea",
|
|
) as HTMLTextAreaElement;
|
|
expect(editor).toMatchSnapshot();
|
|
});
|
|
|
|
it("should bind text to arrow when double clicked", async () => {
|
|
createTwoPointerLinearElement("arrow");
|
|
const arrow = h.elements[0] as ExcalidrawLinearElement;
|
|
|
|
expect(h.elements.length).toBe(1);
|
|
expect(h.elements[0].id).toBe(arrow.id);
|
|
mouse.doubleClickAt(arrow.x, arrow.y);
|
|
expect(h.elements.length).toBe(2);
|
|
|
|
const text = h.elements[1] as ExcalidrawTextElementWithContainer;
|
|
expect(text.type).toBe("text");
|
|
expect(text.containerId).toBe(arrow.id);
|
|
mouse.down();
|
|
const editor = document.querySelector(
|
|
".excalidraw-textEditorContainer > textarea",
|
|
) as HTMLTextAreaElement;
|
|
|
|
fireEvent.change(editor, {
|
|
target: { value: DEFAULT_TEXT },
|
|
});
|
|
|
|
await new Promise((r) => setTimeout(r, 0));
|
|
editor.blur();
|
|
expect(arrow.boundElements).toStrictEqual([
|
|
{ id: text.id, type: "text" },
|
|
]);
|
|
expect((h.elements[1] as ExcalidrawTextElementWithContainer).text)
|
|
.toMatchInlineSnapshot(`
|
|
"Online whiteboard
|
|
collaboration made
|
|
easy"
|
|
`);
|
|
});
|
|
|
|
it("should bind text to arrow when clicked on arrow and enter pressed", async () => {
|
|
const arrow = createTwoPointerLinearElement("arrow");
|
|
|
|
expect(h.elements.length).toBe(1);
|
|
expect(h.elements[0].id).toBe(arrow.id);
|
|
|
|
Keyboard.keyPress(KEYS.ENTER);
|
|
|
|
expect(h.elements.length).toBe(2);
|
|
|
|
const textElement = h.elements[1] as ExcalidrawTextElementWithContainer;
|
|
expect(textElement.type).toBe("text");
|
|
expect(textElement.containerId).toBe(arrow.id);
|
|
const editor = document.querySelector(
|
|
".excalidraw-textEditorContainer > textarea",
|
|
) as HTMLTextAreaElement;
|
|
|
|
await new Promise((r) => setTimeout(r, 0));
|
|
|
|
fireEvent.change(editor, {
|
|
target: { value: DEFAULT_TEXT },
|
|
});
|
|
editor.blur();
|
|
expect(arrow.boundElements).toStrictEqual([
|
|
{ id: textElement.id, type: "text" },
|
|
]);
|
|
expect((h.elements[1] as ExcalidrawTextElementWithContainer).text)
|
|
.toMatchInlineSnapshot(`
|
|
"Online whiteboard
|
|
collaboration made
|
|
easy"
|
|
`);
|
|
});
|
|
|
|
it("should not bind text to line when double clicked", async () => {
|
|
const line = createTwoPointerLinearElement("line");
|
|
|
|
expect(h.elements.length).toBe(1);
|
|
mouse.doubleClickAt(line.x, line.y);
|
|
|
|
expect(h.elements.length).toBe(2);
|
|
|
|
const text = h.elements[1] as ExcalidrawTextElementWithContainer;
|
|
expect(text.type).toBe("text");
|
|
expect(text.containerId).toBeNull();
|
|
expect(line.boundElements).toBeNull();
|
|
});
|
|
|
|
// TODO fix #7029 and rewrite this test
|
|
it.todo(
|
|
"should not rotate the bound text and update position of bound text and bounding box correctly when arrow rotated",
|
|
);
|
|
|
|
it("should resize and position the bound text and bounding box correctly when 3 pointer arrow element resized", () => {
|
|
createThreePointerLinearElement("arrow", {
|
|
type: ROUNDNESS.PROPORTIONAL_RADIUS,
|
|
});
|
|
|
|
const arrow = h.elements[0] as ExcalidrawLinearElement;
|
|
|
|
const { textElement, container } = createBoundTextElement(
|
|
DEFAULT_TEXT,
|
|
arrow,
|
|
);
|
|
expect(container.width).toBe(70);
|
|
expect(container.height).toBe(50);
|
|
expect(getBoundTextElementPosition(container, textElement))
|
|
.toMatchInlineSnapshot(`
|
|
{
|
|
"x": 75,
|
|
"y": 60,
|
|
}
|
|
`);
|
|
expect(textElement.text).toMatchInlineSnapshot(`
|
|
"Online whiteboard
|
|
collaboration made
|
|
easy"
|
|
`);
|
|
expect(LinearElementEditor.getElementAbsoluteCoords(container, true))
|
|
.toMatchInlineSnapshot(`
|
|
[
|
|
20,
|
|
20,
|
|
105,
|
|
80,
|
|
55.45893770831013,
|
|
45,
|
|
]
|
|
`);
|
|
|
|
UI.resize(container, "ne", [300, 200]);
|
|
|
|
expect({ width: container.width, height: container.height })
|
|
.toMatchInlineSnapshot(`
|
|
{
|
|
"height": 130,
|
|
"width": 366.11716195150507,
|
|
}
|
|
`);
|
|
|
|
expect(getBoundTextElementPosition(container, textElement))
|
|
.toMatchInlineSnapshot(`
|
|
{
|
|
"x": 271.11716195150507,
|
|
"y": 45,
|
|
}
|
|
`);
|
|
expect((h.elements[1] as ExcalidrawTextElementWithContainer).text)
|
|
.toMatchInlineSnapshot(`
|
|
"Online whiteboard
|
|
collaboration made easy"
|
|
`);
|
|
expect(LinearElementEditor.getElementAbsoluteCoords(container, true))
|
|
.toMatchInlineSnapshot(`
|
|
[
|
|
20,
|
|
35,
|
|
501.11716195150507,
|
|
95,
|
|
205.4589377083102,
|
|
52.5,
|
|
]
|
|
`);
|
|
});
|
|
|
|
it("should resize and position the bound text correctly when 2 pointer linear element resized", () => {
|
|
createTwoPointerLinearElement("arrow");
|
|
|
|
const arrow = h.elements[0] as ExcalidrawLinearElement;
|
|
const { textElement, container } = createBoundTextElement(
|
|
DEFAULT_TEXT,
|
|
arrow,
|
|
);
|
|
expect(container.width).toBe(40);
|
|
expect(getBoundTextElementPosition(container, textElement))
|
|
.toMatchInlineSnapshot(`
|
|
{
|
|
"x": 25,
|
|
"y": 10,
|
|
}
|
|
`);
|
|
expect(textElement.text).toMatchInlineSnapshot(`
|
|
"Online whiteboard
|
|
collaboration made
|
|
easy"
|
|
`);
|
|
const points = LinearElementEditor.getPointsGlobalCoordinates(container);
|
|
|
|
// Drag from last point
|
|
drag(points[1], [points[1][0] + 300, points[1][1]]);
|
|
|
|
expect({ width: container.width, height: container.height })
|
|
.toMatchInlineSnapshot(`
|
|
{
|
|
"height": 130,
|
|
"width": 340,
|
|
}
|
|
`);
|
|
|
|
expect(getBoundTextElementPosition(container, textElement))
|
|
.toMatchInlineSnapshot(`
|
|
{
|
|
"x": 75,
|
|
"y": -5,
|
|
}
|
|
`);
|
|
expect(textElement.text).toMatchInlineSnapshot(`
|
|
"Online whiteboard
|
|
collaboration made easy"
|
|
`);
|
|
});
|
|
|
|
it("should not render vertical align tool when element selected", () => {
|
|
createTwoPointerLinearElement("arrow");
|
|
const arrow = h.elements[0] as ExcalidrawLinearElement;
|
|
|
|
createBoundTextElement(DEFAULT_TEXT, arrow);
|
|
API.setSelectedElements([arrow]);
|
|
|
|
expect(queryByTestId(container, "align-top")).toBeNull();
|
|
expect(queryByTestId(container, "align-middle")).toBeNull();
|
|
expect(queryByTestId(container, "align-bottom")).toBeNull();
|
|
});
|
|
|
|
it("should wrap the bound text when arrow bound container moves", async () => {
|
|
const rect = UI.createElement("rectangle", {
|
|
x: 400,
|
|
width: 200,
|
|
height: 500,
|
|
});
|
|
const arrow = UI.createElement("arrow", {
|
|
x: -10,
|
|
y: 250,
|
|
width: 400,
|
|
height: 1,
|
|
});
|
|
|
|
mouse.select(arrow);
|
|
Keyboard.keyPress(KEYS.ENTER);
|
|
const editor = document.querySelector(
|
|
".excalidraw-textEditorContainer > textarea",
|
|
) as HTMLTextAreaElement;
|
|
await new Promise((r) => setTimeout(r, 0));
|
|
fireEvent.change(editor, { target: { value: DEFAULT_TEXT } });
|
|
editor.blur();
|
|
|
|
const textElement = h.elements[2] as ExcalidrawTextElementWithContainer;
|
|
|
|
expect(arrow.endBinding?.elementId).toBe(rect.id);
|
|
expect(arrow.width).toBe(400);
|
|
expect(rect.x).toBe(400);
|
|
expect(rect.y).toBe(0);
|
|
expect(
|
|
wrapText(textElement.originalText, font, getBoundTextMaxWidth(arrow)),
|
|
).toMatchInlineSnapshot(`
|
|
"Online whiteboard
|
|
collaboration made easy"
|
|
`);
|
|
const handleBindTextResizeSpy = vi.spyOn(
|
|
textElementUtils,
|
|
"handleBindTextResize",
|
|
);
|
|
|
|
mouse.select(rect);
|
|
mouse.downAt(rect.x, rect.y);
|
|
mouse.moveTo(200, 0);
|
|
mouse.upAt(200, 0);
|
|
|
|
expect(arrow.width).toBe(200);
|
|
expect(rect.x).toBe(200);
|
|
expect(rect.y).toBe(0);
|
|
expect(handleBindTextResizeSpy).toHaveBeenCalledWith(
|
|
h.elements[1],
|
|
false,
|
|
);
|
|
expect(
|
|
wrapText(textElement.originalText, font, getBoundTextMaxWidth(arrow)),
|
|
).toMatchInlineSnapshot(`
|
|
"Online whiteboard
|
|
collaboration made
|
|
easy"
|
|
`);
|
|
});
|
|
|
|
it("should not render horizontal align tool when element selected", () => {
|
|
createTwoPointerLinearElement("arrow");
|
|
const arrow = h.elements[0] as ExcalidrawLinearElement;
|
|
|
|
createBoundTextElement(DEFAULT_TEXT, arrow);
|
|
API.setSelectedElements([arrow]);
|
|
|
|
expect(queryByTestId(container, "align-left")).toBeNull();
|
|
expect(queryByTestId(container, "align-horizontal-center")).toBeNull();
|
|
expect(queryByTestId(container, "align-right")).toBeNull();
|
|
});
|
|
|
|
it("should update label coords when a label binded via context menu is unbinded", async () => {
|
|
createTwoPointerLinearElement("arrow");
|
|
const text = API.createElement({
|
|
type: "text",
|
|
text: "Hello Excalidraw",
|
|
});
|
|
expect(text.x).toBe(0);
|
|
expect(text.y).toBe(0);
|
|
|
|
h.elements = [h.elements[0], text];
|
|
|
|
const container = h.elements[0];
|
|
API.setSelectedElements([container, text]);
|
|
fireEvent.contextMenu(GlobalTestState.interactiveCanvas, {
|
|
button: 2,
|
|
clientX: 20,
|
|
clientY: 30,
|
|
});
|
|
let contextMenu = document.querySelector(".context-menu");
|
|
|
|
fireEvent.click(
|
|
queryByText(contextMenu as HTMLElement, "Bind text to the container")!,
|
|
);
|
|
expect(container.boundElements).toStrictEqual([
|
|
{ id: h.elements[1].id, type: "text" },
|
|
]);
|
|
expect(text.containerId).toBe(container.id);
|
|
expect(text.verticalAlign).toBe(VERTICAL_ALIGN.MIDDLE);
|
|
|
|
mouse.reset();
|
|
mouse.clickAt(
|
|
container.x + container.width / 2,
|
|
container.y + container.height / 2,
|
|
);
|
|
mouse.down();
|
|
mouse.up();
|
|
API.setSelectedElements([h.elements[0], h.elements[1]]);
|
|
|
|
fireEvent.contextMenu(GlobalTestState.interactiveCanvas, {
|
|
button: 2,
|
|
clientX: 20,
|
|
clientY: 30,
|
|
});
|
|
contextMenu = document.querySelector(".context-menu");
|
|
fireEvent.click(queryByText(contextMenu as HTMLElement, "Unbind text")!);
|
|
expect(container.boundElements).toEqual([]);
|
|
expect(text).toEqual(
|
|
expect.objectContaining({
|
|
containerId: null,
|
|
width: 160,
|
|
height: 25,
|
|
x: -40,
|
|
y: 7.5,
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("should not update label position when arrow dragged", () => {
|
|
createTwoPointerLinearElement("arrow");
|
|
let arrow = h.elements[0] as ExcalidrawLinearElement;
|
|
createBoundTextElement(DEFAULT_TEXT, arrow);
|
|
let label = h.elements[1] as ExcalidrawTextElementWithContainer;
|
|
expect(arrow.x).toBe(20);
|
|
expect(arrow.y).toBe(20);
|
|
expect(label.x).toBe(0);
|
|
expect(label.y).toBe(0);
|
|
mouse.reset();
|
|
mouse.select(arrow);
|
|
mouse.select(label);
|
|
mouse.downAt(arrow.x, arrow.y);
|
|
mouse.moveTo(arrow.x + 20, arrow.y + 30);
|
|
mouse.up(arrow.x + 20, arrow.y + 30);
|
|
|
|
arrow = h.elements[0] as ExcalidrawLinearElement;
|
|
label = h.elements[1] as ExcalidrawTextElementWithContainer;
|
|
expect(arrow.x).toBe(80);
|
|
expect(arrow.y).toBe(100);
|
|
expect(label.x).toBe(0);
|
|
expect(label.y).toBe(0);
|
|
});
|
|
});
|
|
});
|