|
@@ -0,0 +1,530 @@
|
|
|
+import { Excalidraw } from "../index";
|
|
|
+import {
|
|
|
+ act,
|
|
|
+ assertElements,
|
|
|
+ getCloneByOrigId,
|
|
|
+ render,
|
|
|
+} from "../tests/test-utils";
|
|
|
+import { API } from "../tests/helpers/api";
|
|
|
+import { actionDuplicateSelection } from "./actionDuplicateSelection";
|
|
|
+import React from "react";
|
|
|
+import { ORIG_ID } from "../constants";
|
|
|
+
|
|
|
+const { h } = window;
|
|
|
+
|
|
|
+describe("actionDuplicateSelection", () => {
|
|
|
+ beforeEach(async () => {
|
|
|
+ await render(<Excalidraw />);
|
|
|
+ });
|
|
|
+
|
|
|
+ describe("duplicating frames", () => {
|
|
|
+ it("frame selected only", async () => {
|
|
|
+ const frame = API.createElement({
|
|
|
+ type: "frame",
|
|
|
+ });
|
|
|
+
|
|
|
+ const rectangle = API.createElement({
|
|
|
+ type: "rectangle",
|
|
|
+ frameId: frame.id,
|
|
|
+ });
|
|
|
+
|
|
|
+ API.setElements([frame, rectangle]);
|
|
|
+ API.setSelectedElements([frame]);
|
|
|
+
|
|
|
+ act(() => {
|
|
|
+ h.app.actionManager.executeAction(actionDuplicateSelection);
|
|
|
+ });
|
|
|
+
|
|
|
+ assertElements(h.elements, [
|
|
|
+ { id: frame.id },
|
|
|
+ { id: rectangle.id, frameId: frame.id },
|
|
|
+ { [ORIG_ID]: rectangle.id, frameId: getCloneByOrigId(frame.id)?.id },
|
|
|
+ { [ORIG_ID]: frame.id, selected: true },
|
|
|
+ ]);
|
|
|
+ });
|
|
|
+
|
|
|
+ it("frame selected only (with text container)", async () => {
|
|
|
+ const frame = API.createElement({
|
|
|
+ type: "frame",
|
|
|
+ });
|
|
|
+
|
|
|
+ const [rectangle, text] = API.createTextContainer({ frameId: frame.id });
|
|
|
+
|
|
|
+ API.setElements([frame, rectangle, text]);
|
|
|
+ API.setSelectedElements([frame]);
|
|
|
+
|
|
|
+ act(() => {
|
|
|
+ h.app.actionManager.executeAction(actionDuplicateSelection);
|
|
|
+ });
|
|
|
+
|
|
|
+ assertElements(h.elements, [
|
|
|
+ { id: frame.id },
|
|
|
+ { id: rectangle.id, frameId: frame.id },
|
|
|
+ { id: text.id, containerId: rectangle.id, frameId: frame.id },
|
|
|
+ { [ORIG_ID]: rectangle.id, frameId: getCloneByOrigId(frame.id)?.id },
|
|
|
+ {
|
|
|
+ [ORIG_ID]: text.id,
|
|
|
+ containerId: getCloneByOrigId(rectangle.id)?.id,
|
|
|
+ frameId: getCloneByOrigId(frame.id)?.id,
|
|
|
+ },
|
|
|
+ { [ORIG_ID]: frame.id, selected: true },
|
|
|
+ ]);
|
|
|
+ });
|
|
|
+
|
|
|
+ it("frame + text container selected (order A)", async () => {
|
|
|
+ const frame = API.createElement({
|
|
|
+ type: "frame",
|
|
|
+ });
|
|
|
+
|
|
|
+ const [rectangle, text] = API.createTextContainer({ frameId: frame.id });
|
|
|
+
|
|
|
+ API.setElements([frame, rectangle, text]);
|
|
|
+ API.setSelectedElements([frame, rectangle]);
|
|
|
+
|
|
|
+ act(() => {
|
|
|
+ h.app.actionManager.executeAction(actionDuplicateSelection);
|
|
|
+ });
|
|
|
+
|
|
|
+ assertElements(h.elements, [
|
|
|
+ { id: frame.id },
|
|
|
+ { id: rectangle.id, frameId: frame.id },
|
|
|
+ { id: text.id, containerId: rectangle.id, frameId: frame.id },
|
|
|
+ {
|
|
|
+ [ORIG_ID]: rectangle.id,
|
|
|
+ frameId: getCloneByOrigId(frame.id)?.id,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ [ORIG_ID]: text.id,
|
|
|
+ containerId: getCloneByOrigId(rectangle.id)?.id,
|
|
|
+ frameId: getCloneByOrigId(frame.id)?.id,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ [ORIG_ID]: frame.id,
|
|
|
+ selected: true,
|
|
|
+ },
|
|
|
+ ]);
|
|
|
+ });
|
|
|
+
|
|
|
+ it("frame + text container selected (order B)", async () => {
|
|
|
+ const frame = API.createElement({
|
|
|
+ type: "frame",
|
|
|
+ });
|
|
|
+
|
|
|
+ const [rectangle, text] = API.createTextContainer({ frameId: frame.id });
|
|
|
+
|
|
|
+ API.setElements([text, rectangle, frame]);
|
|
|
+ API.setSelectedElements([rectangle, frame]);
|
|
|
+
|
|
|
+ act(() => {
|
|
|
+ h.app.actionManager.executeAction(actionDuplicateSelection);
|
|
|
+ });
|
|
|
+
|
|
|
+ assertElements(h.elements, [
|
|
|
+ { id: rectangle.id, frameId: frame.id },
|
|
|
+ { id: text.id, containerId: rectangle.id, frameId: frame.id },
|
|
|
+ { id: frame.id },
|
|
|
+ {
|
|
|
+ type: "rectangle",
|
|
|
+ [ORIG_ID]: `${rectangle.id}`,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ [ORIG_ID]: `${text.id}`,
|
|
|
+ type: "text",
|
|
|
+ containerId: getCloneByOrigId(rectangle.id)?.id,
|
|
|
+ frameId: getCloneByOrigId(frame.id)?.id,
|
|
|
+ },
|
|
|
+ { [ORIG_ID]: `${frame.id}`, type: "frame", selected: true },
|
|
|
+ ]);
|
|
|
+ });
|
|
|
+ });
|
|
|
+
|
|
|
+ describe("duplicating frame children", () => {
|
|
|
+ it("frame child selected", () => {
|
|
|
+ const frame = API.createElement({
|
|
|
+ type: "frame",
|
|
|
+ });
|
|
|
+
|
|
|
+ const rectangle = API.createElement({
|
|
|
+ type: "rectangle",
|
|
|
+ frameId: frame.id,
|
|
|
+ });
|
|
|
+
|
|
|
+ API.setElements([frame, rectangle]);
|
|
|
+ API.setSelectedElements([rectangle]);
|
|
|
+
|
|
|
+ act(() => {
|
|
|
+ h.app.actionManager.executeAction(actionDuplicateSelection);
|
|
|
+ });
|
|
|
+
|
|
|
+ assertElements(h.elements, [
|
|
|
+ { id: frame.id },
|
|
|
+ { id: rectangle.id, frameId: frame.id },
|
|
|
+ { [ORIG_ID]: rectangle.id, frameId: frame.id, selected: true },
|
|
|
+ ]);
|
|
|
+ });
|
|
|
+
|
|
|
+ it("frame text container selected (rectangle selected)", () => {
|
|
|
+ const frame = API.createElement({
|
|
|
+ type: "frame",
|
|
|
+ });
|
|
|
+
|
|
|
+ const [rectangle, text] = API.createTextContainer({ frameId: frame.id });
|
|
|
+
|
|
|
+ API.setElements([frame, rectangle, text]);
|
|
|
+ API.setSelectedElements([rectangle]);
|
|
|
+
|
|
|
+ act(() => {
|
|
|
+ h.app.actionManager.executeAction(actionDuplicateSelection);
|
|
|
+ });
|
|
|
+
|
|
|
+ assertElements(h.elements, [
|
|
|
+ { id: frame.id },
|
|
|
+ { id: rectangle.id, frameId: frame.id },
|
|
|
+ { id: text.id, containerId: rectangle.id, frameId: frame.id },
|
|
|
+ { [ORIG_ID]: rectangle.id, frameId: frame.id, selected: true },
|
|
|
+ {
|
|
|
+ [ORIG_ID]: text.id,
|
|
|
+ containerId: getCloneByOrigId(rectangle.id).id,
|
|
|
+ frameId: frame.id,
|
|
|
+ },
|
|
|
+ ]);
|
|
|
+ });
|
|
|
+
|
|
|
+ it("frame bound text selected (container not selected)", () => {
|
|
|
+ const frame = API.createElement({
|
|
|
+ type: "frame",
|
|
|
+ });
|
|
|
+
|
|
|
+ const [rectangle, text] = API.createTextContainer({ frameId: frame.id });
|
|
|
+
|
|
|
+ API.setElements([frame, rectangle, text]);
|
|
|
+ API.setSelectedElements([text]);
|
|
|
+
|
|
|
+ act(() => {
|
|
|
+ h.app.actionManager.executeAction(actionDuplicateSelection);
|
|
|
+ });
|
|
|
+
|
|
|
+ assertElements(h.elements, [
|
|
|
+ { id: frame.id },
|
|
|
+ { id: rectangle.id, frameId: frame.id },
|
|
|
+ { id: text.id, containerId: rectangle.id, frameId: frame.id },
|
|
|
+ { [ORIG_ID]: rectangle.id, frameId: frame.id, selected: true },
|
|
|
+ {
|
|
|
+ [ORIG_ID]: text.id,
|
|
|
+ containerId: getCloneByOrigId(rectangle.id).id,
|
|
|
+ frameId: frame.id,
|
|
|
+ },
|
|
|
+ ]);
|
|
|
+ });
|
|
|
+
|
|
|
+ it("frame text container selected (text not exists)", () => {
|
|
|
+ const frame = API.createElement({
|
|
|
+ type: "frame",
|
|
|
+ });
|
|
|
+
|
|
|
+ const [rectangle] = API.createTextContainer({ frameId: frame.id });
|
|
|
+
|
|
|
+ API.setElements([frame, rectangle]);
|
|
|
+ API.setSelectedElements([rectangle]);
|
|
|
+
|
|
|
+ act(() => {
|
|
|
+ h.app.actionManager.executeAction(actionDuplicateSelection);
|
|
|
+ });
|
|
|
+
|
|
|
+ assertElements(h.elements, [
|
|
|
+ { id: frame.id },
|
|
|
+ { id: rectangle.id, frameId: frame.id },
|
|
|
+ { [ORIG_ID]: rectangle.id, frameId: frame.id, selected: true },
|
|
|
+ ]);
|
|
|
+ });
|
|
|
+
|
|
|
+ // shouldn't happen
|
|
|
+ it("frame bound text selected (container not exists)", () => {
|
|
|
+ const frame = API.createElement({
|
|
|
+ type: "frame",
|
|
|
+ });
|
|
|
+
|
|
|
+ const [, text] = API.createTextContainer({ frameId: frame.id });
|
|
|
+
|
|
|
+ API.setElements([frame, text]);
|
|
|
+ API.setSelectedElements([text]);
|
|
|
+
|
|
|
+ act(() => {
|
|
|
+ h.app.actionManager.executeAction(actionDuplicateSelection);
|
|
|
+ });
|
|
|
+
|
|
|
+ assertElements(h.elements, [
|
|
|
+ { id: frame.id },
|
|
|
+ { id: text.id, frameId: frame.id },
|
|
|
+ { [ORIG_ID]: text.id, frameId: frame.id },
|
|
|
+ ]);
|
|
|
+ });
|
|
|
+
|
|
|
+ it("frame bound container selected (text has no frameId)", () => {
|
|
|
+ const frame = API.createElement({
|
|
|
+ type: "frame",
|
|
|
+ });
|
|
|
+
|
|
|
+ const [rectangle, text] = API.createTextContainer({
|
|
|
+ frameId: frame.id,
|
|
|
+ label: { frameId: null },
|
|
|
+ });
|
|
|
+
|
|
|
+ API.setElements([frame, rectangle, text]);
|
|
|
+ API.setSelectedElements([rectangle]);
|
|
|
+
|
|
|
+ act(() => {
|
|
|
+ h.app.actionManager.executeAction(actionDuplicateSelection);
|
|
|
+ });
|
|
|
+
|
|
|
+ assertElements(h.elements, [
|
|
|
+ { id: frame.id },
|
|
|
+ { id: rectangle.id, frameId: frame.id },
|
|
|
+ { id: text.id, containerId: rectangle.id },
|
|
|
+ { [ORIG_ID]: rectangle.id, frameId: frame.id, selected: true },
|
|
|
+ {
|
|
|
+ [ORIG_ID]: text.id,
|
|
|
+ containerId: getCloneByOrigId(rectangle.id).id,
|
|
|
+ },
|
|
|
+ ]);
|
|
|
+ });
|
|
|
+ });
|
|
|
+
|
|
|
+ describe("duplicating multiple frames", () => {
|
|
|
+ it("multiple frames selected (no children)", () => {
|
|
|
+ const frame1 = API.createElement({
|
|
|
+ type: "frame",
|
|
|
+ });
|
|
|
+
|
|
|
+ const rect1 = API.createElement({
|
|
|
+ type: "rectangle",
|
|
|
+ frameId: frame1.id,
|
|
|
+ });
|
|
|
+
|
|
|
+ const frame2 = API.createElement({
|
|
|
+ type: "frame",
|
|
|
+ });
|
|
|
+
|
|
|
+ const rect2 = API.createElement({
|
|
|
+ type: "rectangle",
|
|
|
+ frameId: frame2.id,
|
|
|
+ });
|
|
|
+
|
|
|
+ const ellipse = API.createElement({
|
|
|
+ type: "ellipse",
|
|
|
+ });
|
|
|
+
|
|
|
+ API.setElements([rect1, frame1, ellipse, rect2, frame2]);
|
|
|
+ API.setSelectedElements([frame1, frame2]);
|
|
|
+
|
|
|
+ act(() => {
|
|
|
+ h.app.actionManager.executeAction(actionDuplicateSelection);
|
|
|
+ });
|
|
|
+
|
|
|
+ assertElements(h.elements, [
|
|
|
+ { id: rect1.id, frameId: frame1.id },
|
|
|
+ { id: frame1.id },
|
|
|
+ { [ORIG_ID]: rect1.id, frameId: getCloneByOrigId(frame1.id)?.id },
|
|
|
+ { [ORIG_ID]: frame1.id, selected: true },
|
|
|
+ { id: ellipse.id },
|
|
|
+ { id: rect2.id, frameId: frame2.id },
|
|
|
+ { id: frame2.id },
|
|
|
+ { [ORIG_ID]: rect2.id, frameId: getCloneByOrigId(frame2.id)?.id },
|
|
|
+ { [ORIG_ID]: frame2.id, selected: true },
|
|
|
+ ]);
|
|
|
+ });
|
|
|
+
|
|
|
+ it("multiple frames selected (no children) + unrelated element", () => {
|
|
|
+ const frame1 = API.createElement({
|
|
|
+ type: "frame",
|
|
|
+ });
|
|
|
+
|
|
|
+ const rect1 = API.createElement({
|
|
|
+ type: "rectangle",
|
|
|
+ frameId: frame1.id,
|
|
|
+ });
|
|
|
+
|
|
|
+ const frame2 = API.createElement({
|
|
|
+ type: "frame",
|
|
|
+ });
|
|
|
+
|
|
|
+ const rect2 = API.createElement({
|
|
|
+ type: "rectangle",
|
|
|
+ frameId: frame2.id,
|
|
|
+ });
|
|
|
+
|
|
|
+ const ellipse = API.createElement({
|
|
|
+ type: "ellipse",
|
|
|
+ });
|
|
|
+
|
|
|
+ API.setElements([rect1, frame1, ellipse, rect2, frame2]);
|
|
|
+ API.setSelectedElements([frame1, ellipse, frame2]);
|
|
|
+
|
|
|
+ act(() => {
|
|
|
+ h.app.actionManager.executeAction(actionDuplicateSelection);
|
|
|
+ });
|
|
|
+
|
|
|
+ assertElements(h.elements, [
|
|
|
+ { id: rect1.id, frameId: frame1.id },
|
|
|
+ { id: frame1.id },
|
|
|
+ { [ORIG_ID]: rect1.id, frameId: getCloneByOrigId(frame1.id)?.id },
|
|
|
+ { [ORIG_ID]: frame1.id, selected: true },
|
|
|
+ { id: ellipse.id },
|
|
|
+ { [ORIG_ID]: ellipse.id, selected: true },
|
|
|
+ { id: rect2.id, frameId: frame2.id },
|
|
|
+ { id: frame2.id },
|
|
|
+ { [ORIG_ID]: rect2.id, frameId: getCloneByOrigId(frame2.id)?.id },
|
|
|
+ { [ORIG_ID]: frame2.id, selected: true },
|
|
|
+ ]);
|
|
|
+ });
|
|
|
+ });
|
|
|
+
|
|
|
+ describe("duplicating containers/bound elements", () => {
|
|
|
+ it("labeled arrow (arrow selected)", () => {
|
|
|
+ const [arrow, text] = API.createLabeledArrow();
|
|
|
+
|
|
|
+ API.setElements([arrow, text]);
|
|
|
+ API.setSelectedElements([arrow]);
|
|
|
+
|
|
|
+ act(() => {
|
|
|
+ h.app.actionManager.executeAction(actionDuplicateSelection);
|
|
|
+ });
|
|
|
+
|
|
|
+ assertElements(h.elements, [
|
|
|
+ { id: arrow.id },
|
|
|
+ { id: text.id, containerId: arrow.id },
|
|
|
+ { [ORIG_ID]: arrow.id, selected: true },
|
|
|
+ { [ORIG_ID]: text.id, containerId: getCloneByOrigId(arrow.id)?.id },
|
|
|
+ ]);
|
|
|
+ });
|
|
|
+
|
|
|
+ // shouldn't happen
|
|
|
+ it("labeled arrow (text selected)", () => {
|
|
|
+ const [arrow, text] = API.createLabeledArrow();
|
|
|
+
|
|
|
+ API.setElements([arrow, text]);
|
|
|
+ API.setSelectedElements([text]);
|
|
|
+
|
|
|
+ act(() => {
|
|
|
+ h.app.actionManager.executeAction(actionDuplicateSelection);
|
|
|
+ });
|
|
|
+
|
|
|
+ assertElements(h.elements, [
|
|
|
+ { id: arrow.id },
|
|
|
+ { id: text.id, containerId: arrow.id },
|
|
|
+ { [ORIG_ID]: arrow.id, selected: true },
|
|
|
+ { [ORIG_ID]: text.id, containerId: getCloneByOrigId(arrow.id)?.id },
|
|
|
+ ]);
|
|
|
+ });
|
|
|
+ });
|
|
|
+
|
|
|
+ describe("duplicating groups", () => {
|
|
|
+ it("duplicate group containing frame (children don't have groupIds set)", () => {
|
|
|
+ const frame = API.createElement({
|
|
|
+ type: "frame",
|
|
|
+ groupIds: ["A"],
|
|
|
+ });
|
|
|
+
|
|
|
+ const [rectangle, text] = API.createTextContainer({
|
|
|
+ frameId: frame.id,
|
|
|
+ });
|
|
|
+
|
|
|
+ const ellipse = API.createElement({
|
|
|
+ type: "ellipse",
|
|
|
+ groupIds: ["A"],
|
|
|
+ });
|
|
|
+
|
|
|
+ API.setElements([rectangle, text, frame, ellipse]);
|
|
|
+ API.setSelectedElements([frame, ellipse]);
|
|
|
+
|
|
|
+ act(() => {
|
|
|
+ h.app.actionManager.executeAction(actionDuplicateSelection);
|
|
|
+ });
|
|
|
+
|
|
|
+ assertElements(h.elements, [
|
|
|
+ { id: rectangle.id, frameId: frame.id },
|
|
|
+ { id: text.id, frameId: frame.id },
|
|
|
+ { id: frame.id },
|
|
|
+ { id: ellipse.id },
|
|
|
+ { [ORIG_ID]: rectangle.id, frameId: getCloneByOrigId(frame.id)?.id },
|
|
|
+ { [ORIG_ID]: text.id, frameId: getCloneByOrigId(frame.id)?.id },
|
|
|
+ { [ORIG_ID]: frame.id, selected: true },
|
|
|
+ { [ORIG_ID]: ellipse.id, selected: true },
|
|
|
+ ]);
|
|
|
+ });
|
|
|
+
|
|
|
+ it("duplicate group containing frame (children have groupIds)", () => {
|
|
|
+ const frame = API.createElement({
|
|
|
+ type: "frame",
|
|
|
+ groupIds: ["A"],
|
|
|
+ });
|
|
|
+
|
|
|
+ const [rectangle, text] = API.createTextContainer({
|
|
|
+ frameId: frame.id,
|
|
|
+ groupIds: ["A"],
|
|
|
+ });
|
|
|
+
|
|
|
+ const ellipse = API.createElement({
|
|
|
+ type: "ellipse",
|
|
|
+ groupIds: ["A"],
|
|
|
+ });
|
|
|
+
|
|
|
+ API.setElements([rectangle, text, frame, ellipse]);
|
|
|
+ API.setSelectedElements([frame, ellipse]);
|
|
|
+
|
|
|
+ act(() => {
|
|
|
+ h.app.actionManager.executeAction(actionDuplicateSelection);
|
|
|
+ });
|
|
|
+
|
|
|
+ assertElements(h.elements, [
|
|
|
+ { id: rectangle.id, frameId: frame.id },
|
|
|
+ { id: text.id, frameId: frame.id },
|
|
|
+ { id: frame.id },
|
|
|
+ { id: ellipse.id },
|
|
|
+ {
|
|
|
+ [ORIG_ID]: rectangle.id,
|
|
|
+ frameId: getCloneByOrigId(frame.id)?.id,
|
|
|
+ // FIXME shouldn't be selected (in selectGroupsForSelectedElements)
|
|
|
+ selected: true,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ [ORIG_ID]: text.id,
|
|
|
+ frameId: getCloneByOrigId(frame.id)?.id,
|
|
|
+ // FIXME shouldn't be selected (in selectGroupsForSelectedElements)
|
|
|
+ selected: true,
|
|
|
+ },
|
|
|
+ { [ORIG_ID]: frame.id, selected: true },
|
|
|
+ { [ORIG_ID]: ellipse.id, selected: true },
|
|
|
+ ]);
|
|
|
+ });
|
|
|
+
|
|
|
+ it("duplicating element nested in group", () => {
|
|
|
+ const ellipse = API.createElement({
|
|
|
+ type: "ellipse",
|
|
|
+ groupIds: ["B"],
|
|
|
+ });
|
|
|
+ const rect1 = API.createElement({
|
|
|
+ type: "rectangle",
|
|
|
+ groupIds: ["A", "B"],
|
|
|
+ });
|
|
|
+ const rect2 = API.createElement({
|
|
|
+ type: "rectangle",
|
|
|
+ groupIds: ["A", "B"],
|
|
|
+ });
|
|
|
+
|
|
|
+ API.setElements([ellipse, rect1, rect2]);
|
|
|
+ API.setSelectedElements([ellipse], "B");
|
|
|
+
|
|
|
+ act(() => {
|
|
|
+ h.app.actionManager.executeAction(actionDuplicateSelection);
|
|
|
+ });
|
|
|
+
|
|
|
+ assertElements(h.elements, [
|
|
|
+ { id: ellipse.id },
|
|
|
+ { [ORIG_ID]: ellipse.id, groupIds: ["B"], selected: true },
|
|
|
+ { id: rect1.id, groupIds: ["A", "B"] },
|
|
|
+ { id: rect2.id, groupIds: ["A", "B"] },
|
|
|
+ ]);
|
|
|
+ });
|
|
|
+ });
|
|
|
+});
|