1289 lines
34 KiB
TypeScript
1289 lines
34 KiB
TypeScript
import {
|
|
convertToExcalidrawElements,
|
|
Excalidraw,
|
|
} from "@excalidraw/excalidraw";
|
|
import { arrayToMap } from "@excalidraw/common";
|
|
import { degreesToRadians, pointFrom } from "@excalidraw/math";
|
|
|
|
import { API } from "@excalidraw/excalidraw/tests/helpers/api";
|
|
import { Keyboard, Pointer, UI } from "@excalidraw/excalidraw/tests/helpers/ui";
|
|
import { getTextEditor } from "@excalidraw/excalidraw/tests/queries/dom";
|
|
import {
|
|
getCloneByOrigId,
|
|
render,
|
|
} from "@excalidraw/excalidraw/tests/test-utils";
|
|
|
|
import { getSelectedElements } from "@excalidraw/excalidraw/scene";
|
|
|
|
import type { Degrees } from "@excalidraw/math";
|
|
|
|
import { elementOverlapsWithFrame } from "../src/frame";
|
|
|
|
import type {
|
|
ExcalidrawElement,
|
|
ExcalidrawFrameLikeElement,
|
|
} from "../src/types";
|
|
|
|
const { h } = window;
|
|
const mouse = new Pointer("mouse");
|
|
|
|
describe("adding elements to frames", () => {
|
|
type ElementType = string;
|
|
const assertOrder = (
|
|
els: readonly { type: ElementType }[],
|
|
order: ElementType[],
|
|
) => {
|
|
expect(els.map((el) => el.type)).toEqual(order);
|
|
};
|
|
|
|
const reorderElements = <T extends { type: ElementType }>(
|
|
els: readonly T[],
|
|
order: ElementType[],
|
|
) => {
|
|
return order.reduce((acc: T[], el) => {
|
|
acc.push(els.find((e) => e.type === el)!);
|
|
return acc;
|
|
}, []);
|
|
};
|
|
|
|
function resizeFrameOverElement(
|
|
frame: ExcalidrawElement,
|
|
element: ExcalidrawElement,
|
|
) {
|
|
mouse.clickAt(0, 0);
|
|
mouse.downAt(frame.x + frame.width, frame.y + frame.height);
|
|
mouse.moveTo(
|
|
element.x + element.width + 50,
|
|
element.y + element.height + 50,
|
|
);
|
|
mouse.up();
|
|
}
|
|
|
|
function dragElementIntoFrame(
|
|
frame: ExcalidrawElement,
|
|
element: ExcalidrawElement,
|
|
) {
|
|
mouse.clickAt(element.x, element.y);
|
|
mouse.downAt(element.x + element.width / 2, element.y + element.height / 2);
|
|
mouse.moveTo(frame.x + frame.width / 2, frame.y + frame.height / 2);
|
|
mouse.up();
|
|
}
|
|
|
|
function selectElementAndDuplicate(
|
|
element: ExcalidrawElement,
|
|
moveTo: [number, number] = [element.x + 25, element.y + 25],
|
|
) {
|
|
const [x, y] = [
|
|
element.x + element.width / 2,
|
|
element.y + element.height / 2,
|
|
];
|
|
|
|
Keyboard.withModifierKeys({ alt: true }, () => {
|
|
mouse.downAt(x, y);
|
|
mouse.moveTo(moveTo[0], moveTo[1]);
|
|
mouse.up();
|
|
});
|
|
}
|
|
|
|
function expectEqualIds(expected: ExcalidrawElement[]) {
|
|
expect(h.elements.map((x) => x.id)).toEqual(expected.map((x) => x.id));
|
|
}
|
|
|
|
let frame: ExcalidrawElement;
|
|
let rect1: ExcalidrawElement;
|
|
let rect2: ExcalidrawElement;
|
|
let rect3: ExcalidrawElement;
|
|
let rect4: ExcalidrawElement;
|
|
let text: ExcalidrawElement;
|
|
let arrow: ExcalidrawElement;
|
|
|
|
beforeEach(async () => {
|
|
await render(<Excalidraw />);
|
|
|
|
frame = API.createElement({ id: "id0", type: "frame", x: 0, width: 150 });
|
|
rect1 = API.createElement({
|
|
id: "id1",
|
|
type: "rectangle",
|
|
x: -1000,
|
|
});
|
|
rect2 = API.createElement({
|
|
id: "id2",
|
|
type: "rectangle",
|
|
x: 200,
|
|
width: 50,
|
|
});
|
|
rect3 = API.createElement({
|
|
id: "id3",
|
|
type: "rectangle",
|
|
x: 400,
|
|
width: 50,
|
|
});
|
|
rect4 = API.createElement({
|
|
id: "id4",
|
|
type: "rectangle",
|
|
x: 1000,
|
|
width: 50,
|
|
});
|
|
text = API.createElement({
|
|
id: "id5",
|
|
type: "text",
|
|
x: 100,
|
|
});
|
|
arrow = API.createElement({
|
|
id: "id6",
|
|
type: "arrow",
|
|
x: 100,
|
|
boundElements: [{ id: text.id, type: "text" }],
|
|
});
|
|
});
|
|
|
|
it("should treat an element fully containing a frame as overlapping the frame", () => {
|
|
const containingRect = API.createElement({
|
|
type: "rectangle",
|
|
x: -50,
|
|
y: -50,
|
|
width: 250,
|
|
height: 250,
|
|
});
|
|
|
|
API.setElements([containingRect, frame]);
|
|
|
|
expect(
|
|
elementOverlapsWithFrame(
|
|
containingRect,
|
|
frame as ExcalidrawFrameLikeElement,
|
|
arrayToMap(h.elements),
|
|
),
|
|
).toBe(true);
|
|
});
|
|
|
|
it("uses precise geometry (not the bounding box) to detect frame overlap", () => {
|
|
// a 300x200 frame at the origin, with shapes positioned just outside each
|
|
// corner so that their bounding box clips the frame but their actual
|
|
// outline does not. The precise `isElementIntersectingFrame` must report
|
|
// no overlap.
|
|
const overlapFrame = API.createElement({
|
|
type: "frame",
|
|
x: 0,
|
|
y: 0,
|
|
width: 300,
|
|
height: 200,
|
|
});
|
|
// clips the bottom-right corner; the rhombus tip stays out
|
|
const diamond = API.createElement({
|
|
type: "diamond",
|
|
x: 290,
|
|
y: 175,
|
|
width: 100,
|
|
height: 100,
|
|
});
|
|
// clips the bottom-left corner; rotated (NOT 45°, which would degenerate
|
|
// into an axis-aligned square whose shape fills its AABB)
|
|
const diamondRotated = API.createElement({
|
|
type: "diamond",
|
|
x: -50,
|
|
y: 195,
|
|
width: 100,
|
|
height: 100,
|
|
angle: degreesToRadians(45 as Degrees),
|
|
});
|
|
// clips the top-left corner; the curve doesn't reach the corner
|
|
const ellipse = API.createElement({
|
|
type: "ellipse",
|
|
x: -90,
|
|
y: -90,
|
|
width: 100,
|
|
height: 100,
|
|
});
|
|
// clips the top edge; the polyline weaves outside the frame
|
|
const line = API.createElement({
|
|
type: "line",
|
|
x: 130,
|
|
y: -20,
|
|
width: 293,
|
|
height: 183,
|
|
points: [
|
|
pointFrom(0, 0),
|
|
pointFrom(-165, -96),
|
|
pointFrom(-293, -16),
|
|
pointFrom(-162, 87),
|
|
],
|
|
});
|
|
// clips the bottom-left corner; the stroke stays outside
|
|
const freedraw = API.createElement({
|
|
type: "freedraw",
|
|
x: -15,
|
|
y: 180,
|
|
width: 207,
|
|
height: 209,
|
|
points: [
|
|
pointFrom(0, 0),
|
|
pointFrom(-69, 99),
|
|
pointFrom(-33, 183),
|
|
pointFrom(48, 209),
|
|
pointFrom(124, 195),
|
|
pointFrom(138, 145),
|
|
pointFrom(123, 85),
|
|
],
|
|
});
|
|
// clips the top-right corner, but its rounded corner is cut away from
|
|
// exactly that corner — so its actual outline stays outside
|
|
const roundedRect = API.createElement({
|
|
type: "rectangle",
|
|
x: 295,
|
|
y: -100,
|
|
width: 160,
|
|
height: 110,
|
|
roundness: { type: 3 },
|
|
});
|
|
|
|
API.setElements([
|
|
overlapFrame,
|
|
diamond,
|
|
diamondRotated,
|
|
ellipse,
|
|
line,
|
|
freedraw,
|
|
roundedRect,
|
|
]);
|
|
const elementsMap = arrayToMap(h.elements);
|
|
const f = overlapFrame as ExcalidrawFrameLikeElement;
|
|
|
|
expect(elementOverlapsWithFrame(diamond, f, elementsMap)).toBe(false);
|
|
expect(elementOverlapsWithFrame(diamondRotated, f, elementsMap)).toBe(
|
|
false,
|
|
);
|
|
expect(elementOverlapsWithFrame(ellipse, f, elementsMap)).toBe(false);
|
|
expect(elementOverlapsWithFrame(line, f, elementsMap)).toBe(false);
|
|
expect(elementOverlapsWithFrame(freedraw, f, elementsMap)).toBe(false);
|
|
expect(elementOverlapsWithFrame(roundedRect, f, elementsMap)).toBe(false);
|
|
});
|
|
|
|
it("should not add a newly created element to a frame behind a non-frame element", () => {
|
|
const cover = API.createElement({
|
|
id: "cover",
|
|
type: "rectangle",
|
|
x: 10,
|
|
y: 10,
|
|
width: 80,
|
|
height: 80,
|
|
backgroundColor: "#ffc9c9",
|
|
});
|
|
|
|
API.setElements([frame, cover]);
|
|
|
|
UI.clickTool("rectangle");
|
|
mouse.downAt(20, 20);
|
|
mouse.moveTo(40, 40);
|
|
mouse.upAt(40, 40);
|
|
|
|
const createdElement = h.elements.find(
|
|
(element) => element.id !== frame.id && element.id !== cover.id,
|
|
);
|
|
|
|
expect(createdElement?.frameId).toBe(null);
|
|
expect(h.elements.map((element) => element.id)).toEqual([
|
|
frame.id,
|
|
cover.id,
|
|
createdElement?.id,
|
|
]);
|
|
});
|
|
|
|
it("should add a newly created element to a frame over a non-frame element", () => {
|
|
const cover = API.createElement({
|
|
id: "cover",
|
|
type: "rectangle",
|
|
x: 10,
|
|
y: 10,
|
|
width: 80,
|
|
height: 80,
|
|
backgroundColor: "#ffc9c9",
|
|
});
|
|
|
|
API.setElements([cover, frame]);
|
|
|
|
UI.clickTool("rectangle");
|
|
mouse.downAt(20, 20);
|
|
mouse.moveTo(40, 40);
|
|
mouse.upAt(40, 40);
|
|
|
|
const createdElement = h.elements.find(
|
|
(element) => element.id !== frame.id && element.id !== cover.id,
|
|
);
|
|
|
|
expect(createdElement?.frameId).toBe(frame.id);
|
|
});
|
|
|
|
it("should highlight the target frame while creating a new element", () => {
|
|
API.setElements([frame]);
|
|
|
|
UI.clickTool("rectangle");
|
|
mouse.downAt(20, 20);
|
|
mouse.moveTo(40, 40);
|
|
|
|
expect(h.state.frameToHighlight?.id).toBe(frame.id);
|
|
|
|
mouse.upAt(40, 40);
|
|
|
|
expect(h.state.frameToHighlight).toBe(null);
|
|
});
|
|
|
|
it("should highlight the target frame while hovering with a creation tool", () => {
|
|
API.setElements([frame]);
|
|
|
|
UI.clickTool("rectangle");
|
|
mouse.moveTo(20, 20);
|
|
|
|
expect(h.state.frameToHighlight?.id).toBe(frame.id);
|
|
|
|
mouse.moveTo(200, 200);
|
|
|
|
expect(h.state.frameToHighlight).toBe(null);
|
|
});
|
|
|
|
it("should not add grid-snapped text outside the frame to the clicked frame", async () => {
|
|
const offsetFrame = API.createElement({
|
|
id: "offsetFrame",
|
|
type: "frame",
|
|
x: 10,
|
|
y: 0,
|
|
width: 150,
|
|
height: 150,
|
|
});
|
|
|
|
API.setElements([offsetFrame]);
|
|
API.setAppState({
|
|
gridModeEnabled: true,
|
|
});
|
|
|
|
UI.clickTool("text");
|
|
mouse.clickAt(12, 0);
|
|
|
|
await getTextEditor();
|
|
|
|
const createdText = h.elements.find(
|
|
(element) => element.id !== offsetFrame.id,
|
|
);
|
|
|
|
expect(createdText?.x).toBe(0);
|
|
expect(createdText?.y).toBe(0);
|
|
expect(createdText?.frameId).toBe(null);
|
|
});
|
|
|
|
it("should add a newly created element to a frame behind another frame", () => {
|
|
const lockedFrame = API.createElement({
|
|
id: "lockedFrame",
|
|
type: "frame",
|
|
x: 10,
|
|
y: 10,
|
|
width: 80,
|
|
height: 80,
|
|
locked: true,
|
|
});
|
|
|
|
API.setElements([frame, lockedFrame]);
|
|
|
|
UI.clickTool("rectangle");
|
|
mouse.downAt(20, 20);
|
|
mouse.moveTo(40, 40);
|
|
mouse.upAt(40, 40);
|
|
|
|
const createdElement = h.elements.find(
|
|
(element) => element.id !== frame.id && element.id !== lockedFrame.id,
|
|
);
|
|
|
|
expect(createdElement?.frameId).toBe(frame.id);
|
|
});
|
|
|
|
it("should insert a newly created frame child just below its frame", () => {
|
|
const frameChildUnderCursor = API.createElement({
|
|
id: "frameChildUnderCursor",
|
|
type: "rectangle",
|
|
x: 10,
|
|
y: 10,
|
|
width: 80,
|
|
height: 80,
|
|
backgroundColor: "#ffc9c9",
|
|
frameId: frame.id,
|
|
});
|
|
const otherFrameChild = API.createElement({
|
|
id: "otherFrameChild",
|
|
type: "rectangle",
|
|
x: 100,
|
|
y: 20,
|
|
width: 20,
|
|
height: 20,
|
|
frameId: frame.id,
|
|
});
|
|
|
|
API.setElements([frameChildUnderCursor, otherFrameChild, frame]);
|
|
|
|
UI.clickTool("rectangle");
|
|
mouse.downAt(20, 20);
|
|
mouse.moveTo(40, 40);
|
|
mouse.upAt(40, 40);
|
|
|
|
const createdElement = h.elements.find(
|
|
(element) =>
|
|
element.id !== frame.id &&
|
|
element.id !== frameChildUnderCursor.id &&
|
|
element.id !== otherFrameChild.id,
|
|
);
|
|
|
|
expect(createdElement?.frameId).toBe(frame.id);
|
|
expect(h.elements.map((element) => element.id)).toEqual([
|
|
frameChildUnderCursor.id,
|
|
otherFrameChild.id,
|
|
createdElement?.id,
|
|
frame.id,
|
|
]);
|
|
});
|
|
|
|
it("should insert a newly created frame child above the highest frame child", () => {
|
|
const frameChildUnderCursor = API.createElement({
|
|
id: "frameChildUnderCursor",
|
|
type: "rectangle",
|
|
x: 10,
|
|
y: 10,
|
|
width: 80,
|
|
height: 80,
|
|
backgroundColor: "#ffc9c9",
|
|
frameId: frame.id,
|
|
});
|
|
const otherFrameChild = API.createElement({
|
|
id: "otherFrameChild",
|
|
type: "rectangle",
|
|
x: 100,
|
|
y: 20,
|
|
width: 20,
|
|
height: 20,
|
|
frameId: frame.id,
|
|
});
|
|
|
|
API.setElements([frame, frameChildUnderCursor, otherFrameChild]);
|
|
|
|
UI.clickTool("rectangle");
|
|
mouse.downAt(20, 20);
|
|
mouse.moveTo(40, 40);
|
|
mouse.upAt(40, 40);
|
|
|
|
const createdElement = h.elements.find(
|
|
(element) =>
|
|
element.id !== frame.id &&
|
|
element.id !== frameChildUnderCursor.id &&
|
|
element.id !== otherFrameChild.id,
|
|
);
|
|
|
|
expect(createdElement?.frameId).toBe(frame.id);
|
|
expect(h.elements.map((element) => element.id)).toEqual([
|
|
frame.id,
|
|
frameChildUnderCursor.id,
|
|
otherFrameChild.id,
|
|
createdElement?.id,
|
|
]);
|
|
});
|
|
|
|
const commonTestCases = async (
|
|
func: typeof resizeFrameOverElement | typeof dragElementIntoFrame,
|
|
) => {
|
|
describe.skip("when frame is in a layer below", async () => {
|
|
it("should add an element", async () => {
|
|
API.setElements([frame, rect2]);
|
|
|
|
func(frame, rect2);
|
|
|
|
expect(h.elements[0].frameId).toBe(frame.id);
|
|
expectEqualIds([rect2, frame]);
|
|
});
|
|
|
|
it("should add elements", async () => {
|
|
API.setElements([frame, rect2, rect3]);
|
|
|
|
func(frame, rect2);
|
|
func(frame, rect3);
|
|
|
|
expect(rect2.frameId).toBe(frame.id);
|
|
expect(rect3.frameId).toBe(frame.id);
|
|
expectEqualIds([rect2, rect3, frame]);
|
|
});
|
|
|
|
it("should add elements when there are other other elements in between", async () => {
|
|
API.setElements([frame, rect1, rect2, rect4, rect3]);
|
|
|
|
func(frame, rect2);
|
|
func(frame, rect3);
|
|
|
|
expect(rect2.frameId).toBe(frame.id);
|
|
expect(rect3.frameId).toBe(frame.id);
|
|
expectEqualIds([rect2, rect3, frame, rect1, rect4]);
|
|
});
|
|
|
|
it("should add elements when there are other elements in between and the order is reversed", async () => {
|
|
API.setElements([frame, rect3, rect4, rect2, rect1]);
|
|
|
|
func(frame, rect2);
|
|
func(frame, rect3);
|
|
|
|
expect(rect2.frameId).toBe(frame.id);
|
|
expect(rect3.frameId).toBe(frame.id);
|
|
expectEqualIds([rect2, rect3, frame, rect4, rect1]);
|
|
});
|
|
});
|
|
|
|
describe.skip("when frame is in a layer above", async () => {
|
|
it("should add an element", async () => {
|
|
API.setElements([rect2, frame]);
|
|
|
|
func(frame, rect2);
|
|
|
|
expect(h.elements[0].frameId).toBe(frame.id);
|
|
expectEqualIds([rect2, frame]);
|
|
});
|
|
|
|
it("should add elements", async () => {
|
|
API.setElements([rect2, rect3, frame]);
|
|
|
|
func(frame, rect2);
|
|
func(frame, rect3);
|
|
|
|
expect(rect2.frameId).toBe(frame.id);
|
|
expect(rect3.frameId).toBe(frame.id);
|
|
expectEqualIds([rect3, rect2, frame]);
|
|
});
|
|
|
|
it("should add elements when there are other other elements in between", async () => {
|
|
API.setElements([rect1, rect2, rect4, rect3, frame]);
|
|
|
|
func(frame, rect2);
|
|
func(frame, rect3);
|
|
|
|
expect(rect2.frameId).toBe(frame.id);
|
|
expect(rect3.frameId).toBe(frame.id);
|
|
expectEqualIds([rect1, rect4, rect3, rect2, frame]);
|
|
});
|
|
|
|
it("should add elements when there are other elements in between and the order is reversed", async () => {
|
|
API.setElements([rect3, rect4, rect2, rect1, frame]);
|
|
|
|
func(frame, rect2);
|
|
func(frame, rect3);
|
|
|
|
expect(rect2.frameId).toBe(frame.id);
|
|
expect(rect3.frameId).toBe(frame.id);
|
|
expectEqualIds([rect4, rect1, rect3, rect2, frame]);
|
|
});
|
|
});
|
|
|
|
describe("when frame is in an inner layer", async () => {
|
|
it.skip("should add elements", async () => {
|
|
API.setElements([rect2, frame, rect3]);
|
|
|
|
func(frame, rect2);
|
|
func(frame, rect3);
|
|
|
|
expect(rect2.frameId).toBe(frame.id);
|
|
expect(rect3.frameId).toBe(frame.id);
|
|
expectEqualIds([rect2, rect3, frame]);
|
|
});
|
|
|
|
it.skip("should add elements when there are other other elements in between", async () => {
|
|
API.setElements([rect2, rect1, frame, rect4, rect3]);
|
|
|
|
func(frame, rect2);
|
|
func(frame, rect3);
|
|
|
|
expect(rect2.frameId).toBe(frame.id);
|
|
expect(rect3.frameId).toBe(frame.id);
|
|
expectEqualIds([rect1, rect2, rect3, frame, rect4]);
|
|
});
|
|
|
|
it.skip("should add elements when there are other elements in between and the order is reversed", async () => {
|
|
API.setElements([rect3, rect4, frame, rect2, rect1]);
|
|
|
|
func(frame, rect2);
|
|
func(frame, rect3);
|
|
|
|
expect(rect2.frameId).toBe(frame.id);
|
|
expect(rect3.frameId).toBe(frame.id);
|
|
expectEqualIds([rect4, rect3, rect2, frame, rect1]);
|
|
});
|
|
});
|
|
};
|
|
|
|
const resizingTest = async (
|
|
containerType: "arrow" | "rectangle",
|
|
initialOrder: ElementType[],
|
|
expectedOrder: ElementType[],
|
|
) => {
|
|
await render(<Excalidraw />);
|
|
|
|
const frame = API.createElement({ type: "frame", x: 0, y: 0 });
|
|
|
|
API.setElements(
|
|
reorderElements(
|
|
[
|
|
frame,
|
|
...convertToExcalidrawElements([
|
|
{
|
|
type: containerType,
|
|
x: 100,
|
|
y: 100,
|
|
height: 10,
|
|
label: { text: "xx" },
|
|
},
|
|
]),
|
|
],
|
|
initialOrder,
|
|
),
|
|
);
|
|
|
|
assertOrder(h.elements, initialOrder);
|
|
|
|
expect(h.elements[1].frameId).toBe(null);
|
|
expect(h.elements[2].frameId).toBe(null);
|
|
|
|
const container = h.elements[1];
|
|
|
|
resizeFrameOverElement(frame, container);
|
|
assertOrder(h.elements, expectedOrder);
|
|
|
|
expect(h.elements[0].frameId).toBe(frame.id);
|
|
expect(h.elements[1].frameId).toBe(frame.id);
|
|
};
|
|
|
|
describe("resizing frame over elements", async () => {
|
|
await commonTestCases(resizeFrameOverElement);
|
|
|
|
it.skip("resizing over text containers and labelled arrows", async () => {
|
|
await resizingTest(
|
|
"rectangle",
|
|
["frame", "rectangle", "text"],
|
|
["rectangle", "text", "frame"],
|
|
);
|
|
await resizingTest(
|
|
"rectangle",
|
|
["frame", "text", "rectangle"],
|
|
["rectangle", "text", "frame"],
|
|
);
|
|
await resizingTest(
|
|
"rectangle",
|
|
["rectangle", "text", "frame"],
|
|
["rectangle", "text", "frame"],
|
|
);
|
|
await resizingTest(
|
|
"rectangle",
|
|
["text", "rectangle", "frame"],
|
|
["rectangle", "text", "frame"],
|
|
);
|
|
await resizingTest(
|
|
"arrow",
|
|
["frame", "arrow", "text"],
|
|
["arrow", "text", "frame"],
|
|
);
|
|
await resizingTest(
|
|
"arrow",
|
|
["text", "arrow", "frame"],
|
|
["arrow", "text", "frame"],
|
|
);
|
|
await resizingTest(
|
|
"arrow",
|
|
["frame", "arrow", "text"],
|
|
["arrow", "text", "frame"],
|
|
);
|
|
|
|
// FIXME failing in tests (it fails to add elements to frame for some
|
|
// reason) but works in browser. (╯°□°)╯︵ ┻━┻
|
|
//
|
|
// Looks like the `getElementsCompletelyInFrame()` doesn't work
|
|
// in these cases.
|
|
//
|
|
// await testElements(
|
|
// "arrow",
|
|
// ["arrow", "text", "frame"],
|
|
// ["arrow", "text", "frame"],
|
|
// );
|
|
});
|
|
|
|
it.skip("should add arrow bound with text when frame is in a layer below", async () => {
|
|
API.setElements([frame, arrow, text]);
|
|
|
|
resizeFrameOverElement(frame, arrow);
|
|
|
|
expect(arrow.frameId).toBe(frame.id);
|
|
expect(text.frameId).toBe(frame.id);
|
|
expectEqualIds([arrow, text, frame]);
|
|
});
|
|
|
|
it("should add arrow bound with text when frame is in a layer above", async () => {
|
|
API.setElements([arrow, text, frame]);
|
|
|
|
resizeFrameOverElement(frame, arrow);
|
|
|
|
expect(arrow.frameId).toBe(frame.id);
|
|
expect(text.frameId).toBe(frame.id);
|
|
expectEqualIds([arrow, text, frame]);
|
|
});
|
|
|
|
it.skip("should add arrow bound with text when frame is in an inner layer", async () => {
|
|
API.setElements([arrow, frame, text]);
|
|
|
|
resizeFrameOverElement(frame, arrow);
|
|
|
|
expect(arrow.frameId).toBe(frame.id);
|
|
expect(text.frameId).toBe(frame.id);
|
|
expectEqualIds([arrow, text, frame]);
|
|
});
|
|
});
|
|
|
|
describe("resizing frame over elements but downwards", async () => {
|
|
it.skip("should add elements when frame is in a layer below", async () => {
|
|
API.setElements([frame, rect1, rect2, rect3, rect4]);
|
|
|
|
resizeFrameOverElement(frame, rect4);
|
|
resizeFrameOverElement(frame, rect3);
|
|
|
|
expect(rect2.frameId).toBe(frame.id);
|
|
expect(rect3.frameId).toBe(frame.id);
|
|
expectEqualIds([rect2, rect3, frame, rect4, rect1]);
|
|
});
|
|
|
|
it.skip("should add elements when frame is in a layer above", async () => {
|
|
API.setElements([rect1, rect2, rect3, rect4, frame]);
|
|
|
|
resizeFrameOverElement(frame, rect4);
|
|
resizeFrameOverElement(frame, rect3);
|
|
|
|
expect(rect2.frameId).toBe(frame.id);
|
|
expect(rect3.frameId).toBe(frame.id);
|
|
expectEqualIds([rect1, rect2, rect3, frame, rect4]);
|
|
});
|
|
|
|
it.skip("should add elements when frame is in an inner layer", async () => {
|
|
API.setElements([rect1, rect2, frame, rect3, rect4]);
|
|
|
|
resizeFrameOverElement(frame, rect4);
|
|
resizeFrameOverElement(frame, rect3);
|
|
|
|
expect(rect2.frameId).toBe(frame.id);
|
|
expect(rect3.frameId).toBe(frame.id);
|
|
expectEqualIds([rect1, rect2, rect3, frame, rect4]);
|
|
});
|
|
});
|
|
|
|
describe("dragging elements into the frame", async () => {
|
|
await commonTestCases(dragElementIntoFrame);
|
|
|
|
it("should add a dragged element fully containing the frame", () => {
|
|
const containingRect = API.createElement({
|
|
type: "rectangle",
|
|
x: 220,
|
|
y: 20,
|
|
width: 300,
|
|
height: 300,
|
|
});
|
|
|
|
API.setElements([frame, containingRect]);
|
|
|
|
dragElementIntoFrame(frame, containingRect);
|
|
|
|
expect(API.getElement(containingRect).frameId).toBe(frame.id);
|
|
});
|
|
|
|
it("should drag an element into a frame", () => {
|
|
API.setElements([rect2, frame]);
|
|
|
|
dragElementIntoFrame(frame, rect2);
|
|
|
|
expect(rect2.frameId).toBe(frame.id);
|
|
});
|
|
|
|
it("should move an element dragged from one frame into another", () => {
|
|
const otherFrame = API.createElement({
|
|
id: "otherFrame",
|
|
type: "frame",
|
|
x: 300,
|
|
y: 0,
|
|
width: 150,
|
|
height: 150,
|
|
});
|
|
const frameChild = API.createElement({
|
|
id: "frameChild",
|
|
type: "rectangle",
|
|
x: 50,
|
|
y: 50,
|
|
width: 20,
|
|
height: 20,
|
|
frameId: frame.id,
|
|
});
|
|
|
|
API.setElements([frame, frameChild, otherFrame]);
|
|
|
|
expect(frameChild.frameId).toBe(frame.id);
|
|
|
|
dragElementIntoFrame(otherFrame, frameChild);
|
|
|
|
expect(frameChild.frameId).toBe(otherFrame.id);
|
|
});
|
|
|
|
it("should layer a dragged element above the highest frame child", () => {
|
|
const frameChild = API.createElement({
|
|
id: "frameChild",
|
|
type: "rectangle",
|
|
x: 10,
|
|
y: 10,
|
|
width: 20,
|
|
height: 20,
|
|
frameId: frame.id,
|
|
});
|
|
|
|
API.setElements([frame, frameChild, rect2]);
|
|
|
|
dragElementIntoFrame(frame, rect2);
|
|
|
|
expect(rect2.frameId).toBe(frame.id);
|
|
expect(h.elements.map((element) => element.id)).toEqual([
|
|
frame.id,
|
|
frameChild.id,
|
|
rect2.id,
|
|
]);
|
|
expect(rect2.index! > frameChild.index!).toBe(true);
|
|
expect(rect2.index! > frame.index!).toBe(true);
|
|
});
|
|
|
|
it("should preview a dragged element above the highest frame child before pointerup", () => {
|
|
const frameChild = API.createElement({
|
|
id: "frameChild",
|
|
type: "rectangle",
|
|
x: 10,
|
|
y: 10,
|
|
width: 20,
|
|
height: 20,
|
|
frameId: frame.id,
|
|
});
|
|
|
|
API.setElements([rect2, frame, frameChild]);
|
|
API.setSelectedElements([rect2]);
|
|
API.updateElement(rect2, {
|
|
x: 10,
|
|
y: 10,
|
|
});
|
|
|
|
const getRenderableElementIds = (
|
|
selectedElementsAreBeingDragged: boolean,
|
|
) => {
|
|
return h.app.renderer
|
|
.getRenderableElements({
|
|
zoom: h.state.zoom,
|
|
offsetLeft: 0,
|
|
offsetTop: 0,
|
|
scrollX: 0,
|
|
scrollY: 0,
|
|
height: 1000,
|
|
width: 1000,
|
|
editingTextElement: h.state.editingTextElement,
|
|
newElement: h.state.newElement,
|
|
selectedElements: getSelectedElements(h.elements, h.state),
|
|
selectedElementsAreBeingDragged,
|
|
frameToHighlight: frame as ExcalidrawFrameLikeElement,
|
|
})
|
|
.visibleElements.map((element) => element.id);
|
|
};
|
|
|
|
expect(h.elements.map((element) => element.id)).toEqual([
|
|
rect2.id,
|
|
frame.id,
|
|
frameChild.id,
|
|
]);
|
|
expect(getRenderableElementIds(false)).toEqual([
|
|
rect2.id,
|
|
frame.id,
|
|
frameChild.id,
|
|
]);
|
|
expect(getRenderableElementIds(true)).toEqual([
|
|
frame.id,
|
|
frameChild.id,
|
|
rect2.id,
|
|
]);
|
|
expect(h.elements.map((element) => element.id)).toEqual([
|
|
rect2.id,
|
|
frame.id,
|
|
frameChild.id,
|
|
]);
|
|
expect(rect2.frameId).toBe(null);
|
|
});
|
|
|
|
it("should not preview reorder dragged elements already in the highlighted frame", () => {
|
|
const frameChild = API.createElement({
|
|
id: "frameChild",
|
|
type: "rectangle",
|
|
x: 10,
|
|
y: 10,
|
|
width: 20,
|
|
height: 20,
|
|
frameId: frame.id,
|
|
});
|
|
const otherFrameChild = API.createElement({
|
|
id: "otherFrameChild",
|
|
type: "rectangle",
|
|
x: 40,
|
|
y: 10,
|
|
width: 20,
|
|
height: 20,
|
|
frameId: frame.id,
|
|
});
|
|
|
|
API.setElements([frameChild, frame, otherFrameChild]);
|
|
API.setSelectedElements([frameChild]);
|
|
|
|
const renderableElementIds = h.app.renderer
|
|
.getRenderableElements({
|
|
zoom: h.state.zoom,
|
|
offsetLeft: 0,
|
|
offsetTop: 0,
|
|
scrollX: 0,
|
|
scrollY: 0,
|
|
height: 1000,
|
|
width: 1000,
|
|
editingTextElement: h.state.editingTextElement,
|
|
newElement: h.state.newElement,
|
|
selectedElements: getSelectedElements(h.elements, h.state),
|
|
selectedElementsAreBeingDragged: true,
|
|
frameToHighlight: frame as ExcalidrawFrameLikeElement,
|
|
})
|
|
.visibleElements.map((element) => element.id);
|
|
|
|
expect(renderableElementIds).toEqual([
|
|
frameChild.id,
|
|
frame.id,
|
|
otherFrameChild.id,
|
|
]);
|
|
});
|
|
|
|
it("should put a dragged mixed selection above the highest frame child", () => {
|
|
const frameChild = API.createElement({
|
|
id: "frameChild",
|
|
type: "rectangle",
|
|
x: 50,
|
|
y: 10,
|
|
width: 20,
|
|
height: 20,
|
|
frameId: frame.id,
|
|
boundElements: [{ id: "boundText", type: "text" }],
|
|
});
|
|
const boundText = API.createElement({
|
|
id: "boundText",
|
|
type: "text",
|
|
x: 50,
|
|
y: 10,
|
|
width: 20,
|
|
height: 20,
|
|
containerId: frameChild.id,
|
|
frameId: frame.id,
|
|
});
|
|
const otherFrameChild = API.createElement({
|
|
id: "otherFrameChild",
|
|
type: "rectangle",
|
|
x: 80,
|
|
y: 10,
|
|
width: 20,
|
|
height: 20,
|
|
frameId: frame.id,
|
|
});
|
|
const nonFrameElement = API.createElement({
|
|
id: "nonFrameElement",
|
|
type: "rectangle",
|
|
x: 155,
|
|
y: 10,
|
|
width: 20,
|
|
height: 20,
|
|
});
|
|
|
|
API.setElements([
|
|
frame,
|
|
frameChild,
|
|
boundText,
|
|
otherFrameChild,
|
|
nonFrameElement,
|
|
]);
|
|
API.setSelectedElements([frameChild, nonFrameElement]);
|
|
|
|
mouse.downAt(
|
|
nonFrameElement.x + nonFrameElement.width / 2,
|
|
nonFrameElement.y + nonFrameElement.height / 2,
|
|
);
|
|
mouse.moveTo(frame.x + frame.width - 5, nonFrameElement.y + 10);
|
|
mouse.up();
|
|
|
|
expect(frameChild.frameId).toBe(frame.id);
|
|
expect(boundText.frameId).toBe(frame.id);
|
|
expect(nonFrameElement.frameId).toBe(frame.id);
|
|
expect(h.elements.map((element) => element.id)).toEqual([
|
|
frame.id,
|
|
otherFrameChild.id,
|
|
frameChild.id,
|
|
boundText.id,
|
|
nonFrameElement.id,
|
|
]);
|
|
});
|
|
|
|
it("should not reorder dragged elements already in the highlighted frame", () => {
|
|
const frameChild = API.createElement({
|
|
id: "frameChild",
|
|
type: "rectangle",
|
|
x: 50,
|
|
y: 10,
|
|
width: 20,
|
|
height: 20,
|
|
frameId: frame.id,
|
|
});
|
|
const otherFrameChild = API.createElement({
|
|
id: "otherFrameChild",
|
|
type: "rectangle",
|
|
x: 80,
|
|
y: 10,
|
|
width: 20,
|
|
height: 20,
|
|
frameId: frame.id,
|
|
});
|
|
|
|
API.setElements([frame, frameChild, otherFrameChild]);
|
|
API.setSelectedElements([frameChild]);
|
|
|
|
mouse.downAt(
|
|
frameChild.x + frameChild.width / 2,
|
|
frameChild.y + frameChild.height / 2,
|
|
);
|
|
mouse.moveTo(frameChild.x + frameChild.width / 2 + 5, frameChild.y + 10);
|
|
mouse.up();
|
|
|
|
expect(frameChild.frameId).toBe(frame.id);
|
|
expect(h.elements.map((element) => element.id)).toEqual([
|
|
frame.id,
|
|
frameChild.id,
|
|
otherFrameChild.id,
|
|
]);
|
|
});
|
|
|
|
it("should not drag an element into a frame behind a non-frame element", () => {
|
|
const cover = API.createElement({
|
|
id: "cover",
|
|
type: "rectangle",
|
|
x: 10,
|
|
y: 10,
|
|
width: 80,
|
|
height: 80,
|
|
backgroundColor: "#ffc9c9",
|
|
});
|
|
API.setElements([frame, cover, rect2]);
|
|
|
|
mouse.clickAt(rect2.x, rect2.y);
|
|
mouse.downAt(rect2.x + rect2.width / 2, rect2.y + rect2.height / 2);
|
|
mouse.moveTo(20, 20);
|
|
mouse.upAt(20, 20);
|
|
|
|
expect(rect2.frameId).toBe(null);
|
|
});
|
|
|
|
it("should drag an element into a frame over a non-frame element", () => {
|
|
const cover = API.createElement({
|
|
id: "cover",
|
|
type: "rectangle",
|
|
x: 10,
|
|
y: 10,
|
|
width: 80,
|
|
height: 80,
|
|
backgroundColor: "#ffc9c9",
|
|
});
|
|
API.setElements([cover, rect2, frame]);
|
|
|
|
mouse.clickAt(rect2.x, rect2.y);
|
|
mouse.downAt(rect2.x + rect2.width / 2, rect2.y + rect2.height / 2);
|
|
mouse.moveTo(20, 20);
|
|
mouse.upAt(20, 20);
|
|
|
|
expect(rect2.frameId).toBe(frame.id);
|
|
});
|
|
|
|
it("should keep dragging a frame child over a non-frame element above its frame", () => {
|
|
const cover = API.createElement({
|
|
id: "cover",
|
|
type: "rectangle",
|
|
x: 10,
|
|
y: 10,
|
|
width: 80,
|
|
height: 80,
|
|
backgroundColor: "#ffc9c9",
|
|
});
|
|
const frameChild = API.createElement({
|
|
id: "frameChild",
|
|
type: "rectangle",
|
|
x: 100,
|
|
y: 20,
|
|
width: 20,
|
|
height: 20,
|
|
frameId: frame.id,
|
|
});
|
|
|
|
API.setElements([frameChild, frame, cover]);
|
|
API.setSelectedElements([frameChild]);
|
|
|
|
mouse.downAt(
|
|
frameChild.x + frameChild.width / 2,
|
|
frameChild.y + frameChild.height / 2,
|
|
);
|
|
mouse.moveTo(20, 20);
|
|
|
|
expect(h.state.frameToHighlight?.id).toBe(frame.id);
|
|
|
|
mouse.upAt(20, 20);
|
|
|
|
expect(frameChild.frameId).toBe(frame.id);
|
|
});
|
|
|
|
it.skip("should drag element inside, duplicate it and keep it in frame", () => {
|
|
API.setElements([frame, rect2]);
|
|
|
|
dragElementIntoFrame(frame, rect2);
|
|
|
|
selectElementAndDuplicate(rect2);
|
|
|
|
const rect2_copy = getCloneByOrigId(rect2.id);
|
|
|
|
expect(rect2_copy.frameId).toBe(frame.id);
|
|
expect(rect2.frameId).toBe(frame.id);
|
|
expectEqualIds([rect2_copy, rect2, frame]);
|
|
});
|
|
|
|
it.skip("should drag element inside, duplicate it and remove it from frame", () => {
|
|
API.setElements([frame, rect2]);
|
|
|
|
dragElementIntoFrame(frame, rect2);
|
|
|
|
// move the rect2 outside the frame
|
|
selectElementAndDuplicate(rect2, [-1000, -1000]);
|
|
|
|
const rect2_copy = getCloneByOrigId(rect2.id);
|
|
|
|
expect(rect2_copy.frameId).toBe(frame.id);
|
|
expect(rect2.frameId).toBe(null);
|
|
expectEqualIds([rect2_copy, frame, rect2]);
|
|
});
|
|
|
|
it("random order 01", () => {
|
|
const frame1 = API.createElement({
|
|
type: "frame",
|
|
x: 0,
|
|
y: 0,
|
|
width: 100,
|
|
height: 100,
|
|
});
|
|
const frame2 = API.createElement({
|
|
type: "frame",
|
|
x: 200,
|
|
y: 0,
|
|
width: 100,
|
|
height: 100,
|
|
});
|
|
const frame3 = API.createElement({
|
|
type: "frame",
|
|
x: 300,
|
|
y: 0,
|
|
width: 100,
|
|
height: 100,
|
|
});
|
|
|
|
const rectangle1 = API.createElement({
|
|
type: "rectangle",
|
|
x: 25,
|
|
y: 25,
|
|
width: 50,
|
|
height: 50,
|
|
frameId: frame1.id,
|
|
});
|
|
const rectangle2 = API.createElement({
|
|
type: "rectangle",
|
|
x: 225,
|
|
y: 25,
|
|
width: 50,
|
|
height: 50,
|
|
frameId: frame2.id,
|
|
});
|
|
const rectangle3 = API.createElement({
|
|
type: "rectangle",
|
|
x: 325,
|
|
y: 25,
|
|
width: 50,
|
|
height: 50,
|
|
frameId: frame3.id,
|
|
});
|
|
const rectangle4 = API.createElement({
|
|
type: "rectangle",
|
|
x: 350,
|
|
y: 25,
|
|
width: 50,
|
|
height: 50,
|
|
frameId: frame3.id,
|
|
});
|
|
|
|
API.setElements([
|
|
frame1,
|
|
rectangle4,
|
|
rectangle1,
|
|
rectangle3,
|
|
frame3,
|
|
rectangle2,
|
|
frame2,
|
|
]);
|
|
|
|
API.setSelectedElements([rectangle2]);
|
|
|
|
const origSize = h.elements.length;
|
|
|
|
expect(h.elements.length).toBe(origSize);
|
|
dragElementIntoFrame(frame3, rectangle2);
|
|
expect(h.elements.length).toBe(origSize);
|
|
});
|
|
|
|
it("random order 02", () => {
|
|
const frame1 = API.createElement({
|
|
type: "frame",
|
|
x: 0,
|
|
y: 0,
|
|
width: 100,
|
|
height: 100,
|
|
});
|
|
const frame2 = API.createElement({
|
|
type: "frame",
|
|
x: 200,
|
|
y: 0,
|
|
width: 100,
|
|
height: 100,
|
|
});
|
|
const rectangle1 = API.createElement({
|
|
type: "rectangle",
|
|
x: 25,
|
|
y: 25,
|
|
width: 50,
|
|
height: 50,
|
|
frameId: frame1.id,
|
|
});
|
|
const rectangle2 = API.createElement({
|
|
type: "rectangle",
|
|
x: 225,
|
|
y: 25,
|
|
width: 50,
|
|
height: 50,
|
|
frameId: frame2.id,
|
|
});
|
|
|
|
API.setElements([rectangle1, rectangle2, frame1, frame2]);
|
|
|
|
API.setSelectedElements([rectangle2]);
|
|
|
|
expect(h.elements.length).toBe(4);
|
|
dragElementIntoFrame(frame2, rectangle1);
|
|
expect(h.elements.length).toBe(4);
|
|
});
|
|
});
|
|
});
|