aboutsummaryrefslogtreecommitdiffstats
path: root/packages/excalidraw/tests/regressionTests.test.tsx
diff options
context:
space:
mode:
authorkj_sh6042026-03-15 16:19:35 -0400
committerkj_sh6042026-03-15 16:19:35 -0400
commit6ec259a0e71174651bae95d4628138bf6fd68742 (patch)
tree5e33c6a5ec091ecabfcb257fdc7b6a88ed8754ac /packages/excalidraw/tests/regressionTests.test.tsx
parent16c8578b15c727f22921f8a80a56ee4d4e7f2272 (diff)
refactor: packages/
Diffstat (limited to 'packages/excalidraw/tests/regressionTests.test.tsx')
-rw-r--r--packages/excalidraw/tests/regressionTests.test.tsx1183
1 files changed, 1183 insertions, 0 deletions
diff --git a/packages/excalidraw/tests/regressionTests.test.tsx b/packages/excalidraw/tests/regressionTests.test.tsx
new file mode 100644
index 0000000..a3e45bc
--- /dev/null
+++ b/packages/excalidraw/tests/regressionTests.test.tsx
@@ -0,0 +1,1183 @@
+import React from "react";
+import type { ExcalidrawElement } from "../element/types";
+import { CODES, KEYS } from "../keys";
+import { Excalidraw } from "../index";
+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";
+import {
+ assertSelectedElements,
+ fireEvent,
+ render,
+ screen,
+ togglePopover,
+ unmountComponent,
+} from "./test-utils";
+import { FONT_FAMILY } from "../constants";
+import { vi } from "vitest";
+
+const { h } = window;
+
+const renderStaticScene = vi.spyOn(StaticScene, "renderStaticScene");
+
+const mouse = new Pointer("mouse");
+const finger1 = new Pointer("touch", 1);
+const finger2 = new Pointer("touch", 2);
+
+/**
+ * This is always called at the end of your test, so usually you don't need to call it.
+ * However, if you have a long test, you might want to call it during the test so it's easier
+ * to debug where a test failure came from.
+ */
+const checkpoint = (name: string) => {
+ expect(renderStaticScene.mock.calls.length).toMatchSnapshot(
+ `[${name}] number of renders`,
+ );
+ expect(h.state).toMatchSnapshot(`[${name}] appState`);
+ expect(h.history).toMatchSnapshot(`[${name}] history`);
+ expect(h.elements.length).toMatchSnapshot(`[${name}] number of elements`);
+ h.elements.forEach((element, i) =>
+ expect(element).toMatchSnapshot(`[${name}] element ${i}`),
+ );
+};
+beforeEach(async () => {
+ unmountComponent();
+
+ localStorage.clear();
+ renderStaticScene.mockClear();
+ reseed(7);
+ setDateTimeForTests("201933152653");
+
+ mouse.reset();
+ finger1.reset();
+ finger2.reset();
+
+ await render(<Excalidraw handleKeyboardGlobally={true} />);
+ API.setAppState({ height: 768, width: 1024 });
+});
+
+afterEach(() => {
+ checkpoint("end of test");
+});
+
+describe("regression tests", () => {
+ it("draw every type of shape", () => {
+ UI.clickTool("rectangle");
+ mouse.down(10, -10);
+ mouse.up(20, 10);
+
+ UI.clickTool("diamond");
+ mouse.down(10, -10);
+ mouse.up(20, 10);
+
+ UI.clickTool("ellipse");
+ mouse.down(10, -10);
+ mouse.up(20, 10);
+
+ UI.clickTool("arrow");
+ mouse.down(40, -10);
+ mouse.up(50, 10);
+
+ UI.clickTool("line");
+ mouse.down(40, -10);
+ mouse.up(50, 10);
+
+ UI.clickTool("arrow");
+ mouse.click(40, -10);
+ mouse.click(50, 10);
+ mouse.click(30, 10);
+ Keyboard.keyPress(KEYS.ENTER);
+
+ UI.clickTool("line");
+ mouse.click(40, -20);
+ mouse.click(50, 10);
+ mouse.click(30, 10);
+ Keyboard.keyPress(KEYS.ENTER);
+
+ UI.clickTool("freedraw");
+ mouse.down(40, -20);
+ mouse.up(50, 10);
+
+ expect(h.elements.map((element) => element.type)).toEqual([
+ "rectangle",
+ "diamond",
+ "ellipse",
+ "arrow",
+ "line",
+ "arrow",
+ "line",
+ "freedraw",
+ ]);
+ });
+
+ it("click to select a shape", () => {
+ UI.clickTool("rectangle");
+ mouse.down(10, 10);
+ mouse.up(10, 10);
+
+ const firstRectPos = mouse.getPosition();
+
+ UI.clickTool("rectangle");
+ mouse.down(10, -10);
+ mouse.up(10, 10);
+
+ const prevSelectedId = API.getSelectedElement().id;
+ mouse.restorePosition(...firstRectPos);
+ mouse.click();
+
+ expect(API.getSelectedElement().id).not.toEqual(prevSelectedId);
+ });
+
+ for (const [keys, shape, shouldSelect] of [
+ [`2${KEYS.R}`, "rectangle", true],
+ [`3${KEYS.D}`, "diamond", true],
+ [`4${KEYS.O}`, "ellipse", true],
+ [`5${KEYS.A}`, "arrow", true],
+ [`6${KEYS.L}`, "line", true],
+ [`7${KEYS.P}`, "freedraw", false],
+ ] as [string, ExcalidrawElement["type"], boolean][]) {
+ for (const key of keys) {
+ it(`key ${key} selects ${shape} tool`, () => {
+ Keyboard.keyPress(key);
+
+ expect(h.state.activeTool.type).toBe(shape);
+
+ mouse.down(10, 10);
+ mouse.up(10, 10);
+
+ if (shouldSelect) {
+ expect(API.getSelectedElement().type).toBe(shape);
+ }
+ });
+ }
+ }
+ it("change the properties of a shape", () => {
+ UI.clickTool("rectangle");
+
+ mouse.down(10, 10);
+ mouse.up(10, 10);
+ togglePopover("Background");
+ UI.clickOnTestId("color-yellow");
+ UI.clickOnTestId("color-red");
+
+ togglePopover("Stroke");
+ UI.clickOnTestId("color-blue");
+ expect(API.getSelectedElement().backgroundColor).toBe("#ffc9c9");
+ expect(API.getSelectedElement().strokeColor).toBe("#1971c2");
+ });
+
+ it("click on an element and drag it", () => {
+ UI.clickTool("rectangle");
+ mouse.down(10, 10);
+ mouse.up(10, 10);
+
+ const { x: prevX, y: prevY } = API.getSelectedElement();
+ mouse.down(-8, -8);
+ mouse.up(10, 10);
+
+ const { x: nextX, y: nextY } = API.getSelectedElement();
+ expect(nextX).toBeGreaterThan(prevX);
+ expect(nextY).toBeGreaterThan(prevY);
+
+ checkpoint("dragged");
+
+ mouse.down();
+ mouse.up(-10, -10);
+
+ const { x, y } = API.getSelectedElement();
+ expect(x).toBe(prevX);
+ expect(y).toBe(prevY);
+ });
+
+ it("alt-drag duplicates an element", () => {
+ UI.clickTool("rectangle");
+ mouse.down(10, 10);
+ mouse.up(10, 10);
+
+ expect(
+ h.elements.filter((element) => element.type === "rectangle").length,
+ ).toBe(1);
+ Keyboard.withModifierKeys({ alt: true }, () => {
+ mouse.down(-8, -8);
+ mouse.up(10, 10);
+ });
+
+ expect(
+ h.elements.filter((element) => element.type === "rectangle").length,
+ ).toBe(2);
+ });
+
+ it("click-drag to select a group", () => {
+ UI.clickTool("rectangle");
+ mouse.down(10, 10);
+ mouse.up(10, 10);
+
+ UI.clickTool("rectangle");
+ mouse.down(10, -10);
+ mouse.up(10, 10);
+
+ const finalPosition = mouse.getPosition();
+
+ UI.clickTool("rectangle");
+ mouse.down(10, -10);
+ mouse.up(10, 10);
+
+ mouse.restorePosition(0, 0);
+ mouse.down();
+ mouse.restorePosition(...finalPosition);
+ mouse.up(5, 5);
+
+ expect(
+ h.elements.filter((element) => h.state.selectedElementIds[element.id])
+ .length,
+ ).toBe(2);
+ });
+
+ it("shift-click to multiselect, then drag", () => {
+ UI.clickTool("rectangle");
+ mouse.down(10, 10);
+ mouse.up(10, 10);
+
+ UI.clickTool("rectangle");
+ mouse.down(10, -10);
+ mouse.up(10, 10);
+
+ const prevRectsXY = h.elements
+ .filter((element) => element.type === "rectangle")
+ .map((element) => ({ x: element.x, y: element.y }));
+
+ mouse.reset();
+ mouse.click(10, 10);
+ Keyboard.withModifierKeys({ shift: true }, () => {
+ mouse.click(20, 0);
+ });
+
+ mouse.down();
+ mouse.up(10, 10);
+
+ h.elements
+ .filter((element) => element.type === "rectangle")
+ .forEach((element, i) => {
+ expect(element.x).toBeGreaterThan(prevRectsXY[i].x);
+ expect(element.y).toBeGreaterThan(prevRectsXY[i].y);
+ });
+ });
+
+ it("pinch-to-zoom works", () => {
+ expect(h.state.zoom.value).toBe(1);
+ finger1.down(50, 50);
+ finger2.down(60, 50);
+ finger1.move(-10, 0);
+ expect(h.state.zoom.value).toBeGreaterThan(1);
+ const zoomed = h.state.zoom.value;
+ finger1.move(5, 0);
+ finger2.move(-5, 0);
+ expect(h.state.zoom.value).toBeLessThan(zoomed);
+ });
+
+ it("two-finger scroll works", () => {
+ // scroll horizontally vertically
+
+ const startScrollY = h.state.scrollY;
+
+ finger1.downAt(0, 0);
+ finger2.downAt(10, 0);
+
+ finger1.clientY -= 10;
+ finger2.clientY -= 10;
+
+ finger1.moveTo();
+ finger2.moveTo();
+
+ finger1.upAt();
+ finger2.upAt();
+ expect(h.state.scrollY).toBeLessThan(startScrollY);
+
+ // scroll horizontally
+
+ const startScrollX = h.state.scrollX;
+
+ finger1.downAt();
+ finger2.downAt();
+
+ finger1.clientX += 10;
+ finger2.clientX += 10;
+
+ finger1.moveTo();
+ finger2.moveTo();
+
+ finger1.upAt();
+ finger2.upAt();
+
+ expect(h.state.scrollX).toBeGreaterThan(startScrollX);
+ });
+
+ it("spacebar + drag scrolls the canvas", () => {
+ const { scrollX: startScrollX, scrollY: startScrollY } = h.state;
+ Keyboard.keyDown(KEYS.SPACE);
+ mouse.down(50, 50);
+ mouse.up(60, 60);
+ Keyboard.keyUp(KEYS.SPACE);
+ const { scrollX, scrollY } = h.state;
+ expect(scrollX).not.toEqual(startScrollX);
+ expect(scrollY).not.toEqual(startScrollY);
+ });
+
+ it("arrow keys", () => {
+ UI.clickTool("rectangle");
+ mouse.down(10, 10);
+ mouse.up(10, 10);
+ Keyboard.keyPress(KEYS.ARROW_LEFT);
+ Keyboard.keyPress(KEYS.ARROW_LEFT);
+ Keyboard.keyPress(KEYS.ARROW_RIGHT);
+ Keyboard.keyPress(KEYS.ARROW_UP);
+ Keyboard.keyPress(KEYS.ARROW_UP);
+ Keyboard.keyPress(KEYS.ARROW_DOWN);
+ expect(h.elements[0].x).toBe(9);
+ expect(h.elements[0].y).toBe(9);
+ });
+
+ it("undo/redo drawing an element", () => {
+ UI.clickTool("rectangle");
+ mouse.down(10, -10);
+ mouse.up(20, 10);
+
+ UI.clickTool("rectangle");
+ mouse.down(10, 0);
+ mouse.up(30, 20);
+
+ UI.clickTool("arrow");
+ mouse.click(60, -10);
+ mouse.click(60, 10);
+ mouse.click(40, 10);
+ Keyboard.keyPress(KEYS.ENTER);
+
+ expect(h.elements.filter((element) => !element.isDeleted).length).toBe(3);
+ Keyboard.withModifierKeys({ ctrl: true }, () => {
+ Keyboard.keyPress(KEYS.Z);
+ Keyboard.keyPress(KEYS.Z);
+ Keyboard.keyPress(KEYS.Z);
+ });
+ expect(h.elements.filter((element) => !element.isDeleted).length).toBe(2);
+ Keyboard.withModifierKeys({ ctrl: true }, () => {
+ Keyboard.keyPress(KEYS.Z);
+ });
+ expect(h.elements.filter((element) => !element.isDeleted).length).toBe(1);
+ Keyboard.withModifierKeys({ ctrl: true, shift: true }, () => {
+ Keyboard.keyPress(KEYS.Z);
+ });
+ expect(h.elements.filter((element) => !element.isDeleted).length).toBe(2);
+ });
+
+ it("noop interaction after undo shouldn't create history entry", () => {
+ expect(API.getUndoStack().length).toBe(0);
+
+ UI.clickTool("rectangle");
+ mouse.down(10, 10);
+ mouse.up(10, 10);
+
+ const firstElementEndPoint = mouse.getPosition();
+
+ UI.clickTool("rectangle");
+ mouse.down(10, -10);
+ mouse.up(10, 10);
+
+ const secondElementEndPoint = mouse.getPosition();
+
+ expect(API.getUndoStack().length).toBe(2);
+
+ Keyboard.withModifierKeys({ ctrl: true }, () => {
+ Keyboard.keyPress(KEYS.Z);
+ });
+
+ expect(API.getUndoStack().length).toBe(1);
+
+ // clicking an element shouldn't add to history
+ mouse.restorePosition(...firstElementEndPoint);
+ mouse.click();
+ expect(API.getUndoStack().length).toBe(1);
+
+ Keyboard.withModifierKeys({ shift: true, ctrl: true }, () => {
+ Keyboard.keyPress(KEYS.Z);
+ });
+
+ expect(API.getUndoStack().length).toBe(2);
+
+ // clicking an element should add to history
+ mouse.click();
+ expect(API.getUndoStack().length).toBe(3);
+
+ const firstSelectedElementId = API.getSelectedElement().id;
+
+ // same for clicking the element just redo-ed
+ mouse.restorePosition(...secondElementEndPoint);
+ mouse.click();
+ expect(API.getUndoStack().length).toBe(4);
+
+ expect(API.getSelectedElement().id).not.toEqual(firstSelectedElementId);
+ });
+
+ it("zoom hotkeys", () => {
+ expect(h.state.zoom.value).toBe(1);
+ fireEvent.keyDown(document, {
+ code: CODES.EQUAL,
+ ctrlKey: true,
+ });
+ fireEvent.keyUp(document, {
+ code: CODES.EQUAL,
+ ctrlKey: true,
+ });
+ expect(h.state.zoom.value).toBeGreaterThan(1);
+ fireEvent.keyDown(document, {
+ code: CODES.MINUS,
+ ctrlKey: true,
+ });
+ fireEvent.keyUp(document, {
+ code: CODES.MINUS,
+ ctrlKey: true,
+ });
+ expect(h.state.zoom.value).toBe(1);
+ });
+
+ it("make a group and duplicate it", () => {
+ UI.clickTool("rectangle");
+ mouse.down(10, 10);
+ mouse.up(10, 10);
+
+ UI.clickTool("rectangle");
+ mouse.down(10, -10);
+ mouse.up(10, 10);
+
+ UI.clickTool("rectangle");
+ mouse.down(10, -10);
+ mouse.up(10, 10);
+ const end = mouse.getPosition();
+
+ mouse.reset();
+ mouse.down();
+ mouse.restorePosition(...end);
+ mouse.up();
+
+ expect(h.elements.length).toBe(3);
+ for (const element of h.elements) {
+ expect(element.groupIds.length).toBe(0);
+ expect(h.state.selectedElementIds[element.id]).toBe(true);
+ }
+
+ Keyboard.withModifierKeys({ ctrl: true }, () => {
+ Keyboard.keyPress(KEYS.G);
+ });
+
+ for (const element of h.elements) {
+ expect(element.groupIds.length).toBe(1);
+ }
+
+ Keyboard.withModifierKeys({ alt: true }, () => {
+ mouse.restorePosition(...end);
+ mouse.down();
+ mouse.up(10, 10);
+ });
+
+ expect(h.elements.length).toBe(6);
+ const groups = new Set();
+ for (const element of h.elements) {
+ for (const groupId of element.groupIds) {
+ groups.add(groupId);
+ }
+ }
+
+ expect(groups.size).toBe(2);
+ });
+
+ it("should group elements and ungroup them", () => {
+ UI.clickTool("rectangle");
+ mouse.down(10, 10);
+ mouse.up(10, 10);
+
+ UI.clickTool("rectangle");
+ mouse.down(10, -10);
+ mouse.up(10, 10);
+
+ UI.clickTool("rectangle");
+ mouse.down(10, -10);
+ mouse.up(10, 10);
+ const end = mouse.getPosition();
+
+ mouse.reset();
+ mouse.down();
+ mouse.restorePosition(...end);
+ mouse.up();
+
+ for (const element of h.elements) {
+ expect(element.groupIds.length).toBe(0);
+ }
+
+ Keyboard.withModifierKeys({ ctrl: true }, () => {
+ Keyboard.keyPress(KEYS.G);
+ });
+
+ for (const element of h.elements) {
+ expect(element.groupIds.length).toBe(1);
+ }
+
+ mouse.moveTo(-10, -10); // the NW resizing handle is at [0, 0], so moving further
+ mouse.down();
+ mouse.restorePosition(...end);
+ mouse.up();
+
+ Keyboard.withModifierKeys({ ctrl: true, shift: true }, () => {
+ Keyboard.keyPress(KEYS.G);
+ });
+
+ for (const element of h.elements) {
+ expect(element.groupIds.length).toBe(0);
+ }
+ });
+
+ it("double click to edit a group", () => {
+ UI.clickTool("rectangle");
+ mouse.down(10, 10);
+ mouse.up(10, 10);
+
+ UI.clickTool("rectangle");
+ mouse.down(10, -10);
+ mouse.up(10, 10);
+
+ UI.clickTool("rectangle");
+ mouse.down(10, -10);
+ mouse.up(10, 10);
+
+ Keyboard.withModifierKeys({ ctrl: true }, () => {
+ Keyboard.keyPress(KEYS.A);
+ Keyboard.keyPress(KEYS.G);
+ });
+
+ expect(API.getSelectedElements().length).toBe(3);
+ expect(h.state.editingGroupId).toBe(null);
+ mouse.doubleClick();
+ expect(API.getSelectedElements().length).toBe(1);
+ expect(h.state.editingGroupId).not.toBe(null);
+ });
+
+ it("adjusts z order when grouping", () => {
+ const positions: number[][] = [];
+
+ UI.clickTool("rectangle");
+ mouse.down(10, 10);
+ mouse.up(10, 10);
+ positions.push(mouse.getPosition());
+
+ UI.clickTool("rectangle");
+ mouse.down(10, -10);
+ mouse.up(10, 10);
+ positions.push(mouse.getPosition());
+
+ UI.clickTool("rectangle");
+ mouse.down(10, -10);
+ mouse.up(10, 10);
+ positions.push(mouse.getPosition());
+
+ const ids = h.elements.map((element) => element.id);
+
+ mouse.restorePosition(...positions[0]);
+ mouse.click();
+ mouse.restorePosition(...positions[2]);
+ Keyboard.withModifierKeys({ shift: true }, () => {
+ mouse.click();
+ });
+ Keyboard.withModifierKeys({ ctrl: true }, () => {
+ Keyboard.keyPress(KEYS.G);
+ });
+
+ expect(h.elements.map((element) => element.id)).toEqual([
+ ids[1],
+ ids[0],
+ ids[2],
+ ]);
+ });
+
+ it("supports nested groups", () => {
+ const rectA = UI.createElement("rectangle", { position: 0, size: 50 });
+ const rectB = UI.createElement("rectangle", { position: 100, size: 50 });
+ const rectC = UI.createElement("rectangle", { position: 200, size: 50 });
+
+ Keyboard.withModifierKeys({ ctrl: true }, () => {
+ Keyboard.keyPress(KEYS.A);
+ Keyboard.keyPress(KEYS.G);
+ });
+
+ mouse.doubleClickOn(rectC);
+ Keyboard.withModifierKeys({ shift: true }, () => {
+ mouse.clickOn(rectA);
+ });
+ Keyboard.withModifierKeys({ ctrl: true }, () => {
+ Keyboard.keyPress(KEYS.G);
+ });
+
+ expect(rectC.groupIds.length).toBe(2);
+ expect(rectA.groupIds).toEqual(rectC.groupIds);
+ expect(rectB.groupIds).toEqual(rectA.groupIds.slice(1));
+
+ mouse.click(0, 100);
+ expect(API.getSelectedElements().length).toBe(0);
+
+ mouse.clickOn(rectA);
+ expect(API.getSelectedElements().length).toBe(3);
+ expect(h.state.editingGroupId).toBe(null);
+
+ mouse.doubleClickOn(rectA);
+ expect(API.getSelectedElements().length).toBe(2);
+ expect(h.state.editingGroupId).toBe(rectA.groupIds[1]);
+
+ mouse.doubleClickOn(rectA);
+ expect(API.getSelectedElements().length).toBe(1);
+ expect(h.state.editingGroupId).toBe(rectA.groupIds[0]);
+
+ // click outside current (sub)group
+ mouse.clickOn(rectB);
+ expect(API.getSelectedElements().length).toBe(3);
+ mouse.doubleClickOn(rectB);
+ expect(API.getSelectedElements().length).toBe(1);
+ });
+
+ it("updates fontSize & fontFamily appState", () => {
+ UI.clickTool("text");
+ expect(h.state.currentItemFontFamily).toEqual(FONT_FAMILY.Excalifont);
+ fireEvent.click(screen.getByTitle(/code/i));
+ expect(h.state.currentItemFontFamily).toEqual(FONT_FAMILY["Comic Shanns"]);
+ });
+
+ it("deselects selected element, on pointer up, when click hits element bounding box but doesn't hit the element", () => {
+ UI.clickTool("ellipse");
+ mouse.down();
+ mouse.up(100, 100);
+
+ expect(API.getSelectedElements().length).toBe(1);
+
+ // hits bounding box without hitting element
+ mouse.down(98, 98);
+ mouse.up();
+ expect(API.getSelectedElements().length).toBe(0);
+ });
+
+ it("switches selected element on pointer down", () => {
+ UI.clickTool("rectangle");
+ mouse.down();
+ mouse.up(10, 10);
+
+ UI.clickTool("ellipse");
+ mouse.down(10, 10);
+ mouse.up(10, 10);
+
+ expect(API.getSelectedElement().type).toBe("ellipse");
+
+ // pointer down on rectangle
+ mouse.reset();
+ mouse.down();
+
+ expect(API.getSelectedElement().type).toBe("rectangle");
+ });
+
+ it("can drag element that covers another element, while another elem is selected", () => {
+ UI.clickTool("rectangle");
+ mouse.down(100, 100);
+ mouse.up(200, 200);
+
+ UI.clickTool("rectangle");
+ mouse.reset();
+ mouse.down(100, 100);
+ mouse.up(200, 200);
+
+ UI.clickTool("ellipse");
+ mouse.reset();
+ mouse.down(300, 300);
+ mouse.up(350, 350);
+
+ expect(API.getSelectedElement().type).toBe("ellipse");
+
+ // pointer down on rectangle
+ mouse.reset();
+ mouse.down(100, 100);
+ mouse.up(200, 200);
+
+ expect(API.getSelectedElement().type).toBe("rectangle");
+ });
+
+ it("deselects selected element on pointer down when pointer doesn't hit any element", () => {
+ UI.clickTool("rectangle");
+ mouse.down();
+ mouse.up(10, 10);
+
+ expect(API.getSelectedElements().length).toBe(1);
+
+ // pointer down on space without elements
+ mouse.down(100, 100);
+
+ expect(API.getSelectedElements().length).toBe(0);
+ });
+
+ it("Drags selected element when hitting only bounding box and keeps element selected", () => {
+ UI.clickTool("ellipse");
+ mouse.down();
+ mouse.up(10, 10);
+
+ const { x: prevX, y: prevY } = API.getSelectedElement();
+ API.clearSelection();
+ // drag element from point on bounding box that doesn't hit element
+ mouse.reset();
+ mouse.down(8, 8);
+ mouse.up(25, 25);
+
+ expect(API.getSelectedElement().x).toEqual(prevX + 25);
+ expect(API.getSelectedElement().y).toEqual(prevY + 25);
+ });
+
+ it(
+ "given selected element A with lower z-index than unselected element B and given B is partially over A " +
+ "when clicking intersection between A and B " +
+ "B should be selected on pointer up",
+ () => {
+ // set background color since default is transparent
+ // and transparent elements can't be selected by clicking inside of them
+ const rect1 = API.createElement({
+ type: "rectangle",
+ backgroundColor: "red",
+ x: 0,
+ y: 0,
+ width: 1000,
+ height: 1000,
+ });
+ const rect2 = API.createElement({
+ type: "rectangle",
+ backgroundColor: "red",
+ x: 500,
+ y: 500,
+ width: 500,
+ height: 500,
+ });
+ API.setElements([rect1, rect2]);
+
+ mouse.select(rect1);
+
+ // pointerdown on rect2 covering rect1 while rect1 is selected should
+ // retain rect1 selection
+ mouse.down(900, 900);
+ expect(API.getSelectedElement().id).toBe(rect1.id);
+
+ // pointerup should select rect2
+ mouse.up();
+ expect(API.getSelectedElement().id).toBe(rect2.id);
+ },
+ );
+
+ it(
+ "given selected element A with lower z-index than unselected element B and given B is partially over A " +
+ "when dragging on intersection between A and B " +
+ "A should be dragged and keep being selected",
+ () => {
+ const rect1 = API.createElement({
+ type: "rectangle",
+ backgroundColor: "red",
+ x: 0,
+ y: 0,
+ width: 1000,
+ height: 1000,
+ });
+ const rect2 = API.createElement({
+ type: "rectangle",
+ backgroundColor: "red",
+ x: 500,
+ y: 500,
+ width: 500,
+ height: 500,
+ });
+ API.setElements([rect1, rect2]);
+
+ mouse.select(rect1);
+
+ expect(API.getSelectedElement().id).toBe(rect1.id);
+
+ const { x: prevX, y: prevY } = API.getSelectedElement();
+
+ // pointer down on intersection between ellipse and rectangle
+ mouse.down(900, 900);
+ mouse.up(100, 100);
+
+ expect(API.getSelectedElement().id).toBe(rect1.id);
+ expect(API.getSelectedElement().x).toEqual(prevX + 100);
+ expect(API.getSelectedElement().y).toEqual(prevY + 100);
+ },
+ );
+
+ it("deselects group of selected elements on pointer down when pointer doesn't hit any element", () => {
+ UI.clickTool("rectangle");
+ mouse.down();
+ mouse.up(10, 10);
+
+ UI.clickTool("ellipse");
+ mouse.down(100, 100);
+ mouse.up(10, 10);
+
+ // Selects first element without deselecting the second element
+ // Second element is already selected because creating it was our last action
+ mouse.reset();
+ Keyboard.withModifierKeys({ shift: true }, () => {
+ mouse.click(5, 5);
+ });
+
+ expect(API.getSelectedElements().length).toBe(2);
+
+ // pointer down on space without elements
+ mouse.reset();
+ mouse.down(500, 500);
+
+ expect(API.getSelectedElements().length).toBe(0);
+ });
+
+ it("switches from group of selected elements to another element on pointer down", () => {
+ UI.clickTool("rectangle");
+ mouse.down();
+ mouse.up(10, 10);
+
+ UI.clickTool("ellipse");
+ mouse.down(100, 100);
+ mouse.up(100, 100);
+
+ UI.clickTool("diamond");
+ mouse.down(100, 100);
+ mouse.up(100, 100);
+
+ // Selects ellipse without deselecting the diamond
+ // Diamond is already selected because creating it was our last action
+ mouse.reset();
+ Keyboard.withModifierKeys({ shift: true }, () => {
+ mouse.click(110, 160);
+ });
+
+ expect(API.getSelectedElements().length).toBe(2);
+
+ // select rectangle
+ mouse.reset();
+ mouse.down();
+
+ expect(API.getSelectedElement().type).toBe("rectangle");
+ });
+
+ it("deselects group of selected elements on pointer up when pointer hits common bounding box without hitting any element", () => {
+ UI.clickTool("rectangle");
+ mouse.down();
+ mouse.up(10, 10);
+
+ UI.clickTool("ellipse");
+ mouse.down(100, 100);
+ mouse.up(10, 10);
+
+ // Selects first element without deselecting the second element
+ // Second element is already selected because creating it was our last action
+ mouse.reset();
+ Keyboard.withModifierKeys({ shift: true }, () => {
+ mouse.click(5, 5);
+ });
+
+ // pointer down on common bounding box without hitting any of the elements
+ mouse.reset();
+ mouse.down(50, 50);
+ expect(API.getSelectedElements().length).toBe(2);
+
+ mouse.up();
+ expect(API.getSelectedElements().length).toBe(0);
+ });
+
+ it("drags selected elements from point inside common bounding box that doesn't hit any element and keeps elements selected after dragging", () => {
+ UI.clickTool("rectangle");
+ mouse.down();
+ mouse.up(10, 10);
+
+ UI.clickTool("ellipse");
+ mouse.down(100, 100);
+ mouse.up(10, 10);
+
+ // Selects first element without deselecting the second element
+ // Second element is already selected because creating it was our last action
+ mouse.reset();
+ Keyboard.withModifierKeys({ shift: true }, () => {
+ mouse.click(5, 5);
+ });
+
+ expect(API.getSelectedElements().length).toBe(2);
+
+ const { x: firstElementPrevX, y: firstElementPrevY } =
+ API.getSelectedElements()[0];
+ const { x: secondElementPrevX, y: secondElementPrevY } =
+ API.getSelectedElements()[1];
+
+ // drag elements from point on common bounding box that doesn't hit any of the elements
+ mouse.reset();
+ mouse.down(50, 50);
+ mouse.up(25, 25);
+
+ expect(API.getSelectedElements()[0].x).toEqual(firstElementPrevX + 25);
+ expect(API.getSelectedElements()[0].y).toEqual(firstElementPrevY + 25);
+
+ expect(API.getSelectedElements()[1].x).toEqual(secondElementPrevX + 25);
+ expect(API.getSelectedElements()[1].y).toEqual(secondElementPrevY + 25);
+
+ expect(API.getSelectedElements().length).toBe(2);
+ });
+
+ it(
+ "given a group of selected elements with an element that is not selected inside the group common bounding box " +
+ "when element that is not selected is clicked " +
+ "should switch selection to not selected element on pointer up",
+ () => {
+ UI.clickTool("rectangle");
+ mouse.down();
+ mouse.up(10, 10);
+
+ UI.clickTool("ellipse");
+ mouse.down(100, 100);
+ mouse.up(100, 100);
+
+ UI.clickTool("diamond");
+ mouse.down(100, 100);
+ mouse.up(100, 100);
+
+ // Selects rectangle without deselecting the diamond
+ // Diamond is already selected because creating it was our last action
+ mouse.reset();
+ Keyboard.withModifierKeys({ shift: true }, () => {
+ mouse.click();
+ });
+
+ // pointer down on ellipse
+ mouse.down(110, 160);
+ expect(API.getSelectedElements().length).toBe(2);
+
+ mouse.up();
+ expect(API.getSelectedElement().type).toBe("ellipse");
+ },
+ );
+
+ it(
+ "given a selected element A and a not selected element B with higher z-index than A " +
+ "and given B partially overlaps A " +
+ "when there's a shift-click on the overlapped section B is added to the selection",
+ () => {
+ UI.clickTool("rectangle");
+ // change background color since default is transparent
+ // and transparent elements can't be selected by clicking inside of them
+ togglePopover("Background");
+ UI.clickOnTestId("color-red");
+ mouse.down();
+ mouse.up(1000, 1000);
+
+ // draw ellipse partially over rectangle.
+ // since ellipse was created after rectangle it has an higher z-index.
+ // we don't need to change background color again since change above
+ // affects next drawn elements.
+ UI.clickTool("ellipse");
+ mouse.reset();
+ mouse.down(500, 500);
+ mouse.up(1000, 1000);
+
+ // select rectangle
+ mouse.reset();
+ mouse.click();
+
+ // click on intersection between ellipse and rectangle
+ Keyboard.withModifierKeys({ shift: true }, () => {
+ mouse.click(900, 900);
+ });
+
+ expect(API.getSelectedElements().length).toBe(2);
+ },
+ );
+
+ it("shift click on selected element should deselect it on pointer up", () => {
+ UI.clickTool("rectangle");
+ mouse.down();
+ mouse.up(10, 10);
+
+ // Rectangle is already selected since creating
+ // it was our last action
+ Keyboard.withModifierKeys({ shift: true }, () => {
+ mouse.down(-8, -8);
+ });
+ expect(API.getSelectedElements().length).toBe(1);
+
+ Keyboard.withModifierKeys({ shift: true }, () => {
+ mouse.up();
+ });
+ expect(API.getSelectedElements().length).toBe(0);
+ });
+
+ it("single-clicking on a subgroup of a selected group should not alter selection", () => {
+ const rect1 = UI.createElement("rectangle", {
+ x: 10,
+ });
+ const rect2 = UI.createElement("rectangle", {
+ x: 50,
+ });
+ UI.group([rect1, rect2]);
+
+ const rect3 = UI.createElement("rectangle", {
+ x: 10,
+ y: 50,
+ });
+ const rect4 = UI.createElement("rectangle", {
+ x: 50,
+ y: 50,
+ });
+ UI.group([rect3, rect4]);
+
+ Keyboard.withModifierKeys({ ctrl: true }, () => {
+ Keyboard.keyPress(KEYS.A);
+ Keyboard.keyPress(KEYS.G);
+ });
+
+ const selectedGroupIds_prev = h.state.selectedGroupIds;
+ const selectedElements_prev = API.getSelectedElements();
+ mouse.clickOn(rect3);
+ expect(h.state.selectedGroupIds).toEqual(selectedGroupIds_prev);
+ expect(API.getSelectedElements()).toEqual(selectedElements_prev);
+ });
+
+ it("deleting last but one element in editing group should unselect the group", () => {
+ const rect1 = UI.createElement("rectangle", { x: 10 });
+ const rect2 = UI.createElement("rectangle", { x: 50 });
+
+ UI.group([rect1, rect2]);
+
+ mouse.doubleClickOn(rect1);
+ Keyboard.keyDown(KEYS.DELETE);
+
+ // Clicking on the deleted element, hence in the empty space
+ mouse.clickOn(rect1);
+
+ expect(h.state.selectedGroupIds).toEqual({});
+ expect(API.getSelectedElements()).toEqual([]);
+
+ // Clicking back in and expecting no group selection
+ mouse.clickOn(rect2);
+
+ expect(h.state.selectedGroupIds).toEqual({ [rect2.groupIds[0]]: false });
+ expect(API.getSelectedElements()).toEqual([rect2.get()]);
+ });
+
+ it("Cmd/Ctrl-click exclusively select element under pointer", () => {
+ const rect1 = UI.createElement("rectangle", { x: 0 });
+ const rect2 = UI.createElement("rectangle", { x: 30 });
+
+ UI.group([rect1, rect2]);
+ assertSelectedElements(rect1, rect2);
+
+ Keyboard.withModifierKeys({ ctrl: true }, () => {
+ mouse.clickOn(rect1);
+ });
+ assertSelectedElements(rect1);
+
+ API.clearSelection();
+ Keyboard.withModifierKeys({ ctrl: true }, () => {
+ mouse.clickOn(rect1);
+ });
+ assertSelectedElements(rect1);
+
+ const rect3 = UI.createElement("rectangle", { x: 60 });
+ UI.group([rect1, rect3]);
+ assertSelectedElements(rect1, rect2, rect3);
+
+ mouse.reset();
+ Keyboard.withModifierKeys({ ctrl: true }, () => {
+ mouse.click(10, 5);
+ });
+ assertSelectedElements(rect1);
+
+ API.clearSelection();
+ Keyboard.withModifierKeys({ ctrl: true }, () => {
+ mouse.clickOn(rect3);
+ });
+ assertSelectedElements(rect3);
+ });
+});
+
+it(
+ "given element A and group of elements B and given both are selected " +
+ "when user clicks on B, on pointer up " +
+ "only elements from B should be selected",
+ () => {
+ const rect1 = UI.createElement("rectangle", { y: 0 });
+ const rect2 = UI.createElement("rectangle", { y: 30 });
+ const rect3 = UI.createElement("rectangle", { y: 60 });
+
+ UI.group([rect1, rect3]);
+
+ expect(API.getSelectedElements().length).toBe(2);
+ expect(Object.keys(h.state.selectedGroupIds).length).toBe(1);
+
+ // Select second rectangle without deselecting group
+ Keyboard.withModifierKeys({ shift: true }, () => {
+ mouse.clickOn(rect2);
+ });
+ expect(API.getSelectedElements().length).toBe(3);
+
+ // clicking on first rectangle that is part of the group should select
+ // that group (exclusively)
+ mouse.clickOn(rect1);
+ expect(API.getSelectedElements().length).toBe(2);
+ expect(Object.keys(h.state.selectedGroupIds).length).toBe(1);
+ },
+);
+
+it(
+ "given element A and group of elements B and given both are selected " +
+ "when user shift-clicks on B, on pointer up " +
+ "only element A should be selected",
+ () => {
+ UI.clickTool("rectangle");
+ mouse.down();
+ mouse.up(100, 100);
+
+ UI.clickTool("rectangle");
+ mouse.down(10, 10);
+ mouse.up(100, 100);
+
+ UI.clickTool("rectangle");
+ mouse.down(10, 10);
+ mouse.up(100, 100);
+
+ // Select first rectangle while keeping third one selected.
+ // Third rectangle is selected because it was the last element to be created.
+ mouse.reset();
+ Keyboard.withModifierKeys({ shift: true }, () => {
+ mouse.click();
+ });
+
+ // Create group with first and third rectangle
+ Keyboard.withModifierKeys({ ctrl: true }, () => {
+ Keyboard.keyPress(KEYS.G);
+ });
+
+ expect(API.getSelectedElements().length).toBe(2);
+ const selectedGroupIds = Object.keys(h.state.selectedGroupIds);
+ expect(selectedGroupIds.length).toBe(1);
+
+ // Select second rectangle without deselecting group
+ Keyboard.withModifierKeys({ shift: true }, () => {
+ mouse.click(110, 110);
+ });
+ expect(API.getSelectedElements().length).toBe(3);
+
+ // Pointer down o first rectangle that is part of the group
+ mouse.reset();
+ Keyboard.withModifierKeys({ shift: true }, () => {
+ mouse.down();
+ });
+ expect(API.getSelectedElements().length).toBe(3);
+ Keyboard.withModifierKeys({ shift: true }, () => {
+ mouse.up();
+ });
+ expect(API.getSelectedElements().length).toBe(1);
+ },
+);