diff --git a/src/tests/helpers/api.ts b/src/tests/helpers/api.ts new file mode 100644 index 00000000..dcc2dc87 --- /dev/null +++ b/src/tests/helpers/api.ts @@ -0,0 +1,32 @@ +import { ExcalidrawElement } from "../../element/types"; + +const { h } = window; + +export class API { + static getSelectedElements = (): ExcalidrawElement[] => { + return h.elements.filter( + (element) => h.state.selectedElementIds[element.id], + ); + }; + + static getSelectedElement = (): ExcalidrawElement => { + const selectedElements = API.getSelectedElements(); + if (selectedElements.length !== 1) { + throw new Error( + `expected 1 selected element; got ${selectedElements.length}`, + ); + } + return selectedElements[0]; + }; + + static getStateHistory = () => { + // @ts-ignore + return h.history.stateHistory; + }; + + static clearSelection = () => { + // @ts-ignore + h.app.clearSelection(null); + expect(API.getSelectedElements().length).toBe(0); + }; +} diff --git a/src/tests/helpers/ui.ts b/src/tests/helpers/ui.ts new file mode 100644 index 00000000..7baadd18 --- /dev/null +++ b/src/tests/helpers/ui.ts @@ -0,0 +1,203 @@ +import { ToolName } from "../queries/toolQueries"; +import { fireEvent, GlobalTestState } from "../test-utils"; +import { KEYS, Key } from "../../keys"; +import { ExcalidrawElement } from "../../element/types"; +import { API } from "./api"; + +const { h } = window; + +let altKey = false; +let shiftKey = false; +let ctrlKey = false; + +export class Keyboard { + static withModifierKeys = ( + modifiers: { alt?: boolean; shift?: boolean; ctrl?: boolean }, + cb: () => void, + ) => { + const prevAltKey = altKey; + const prevShiftKey = shiftKey; + const prevCtrlKey = ctrlKey; + + altKey = !!modifiers.alt; + shiftKey = !!modifiers.shift; + ctrlKey = !!modifiers.ctrl; + + try { + cb(); + } finally { + altKey = prevAltKey; + shiftKey = prevShiftKey; + ctrlKey = prevCtrlKey; + } + }; + + static hotkeyDown = (hotkey: Key) => { + const key = KEYS[hotkey]; + if (typeof key !== "string") { + throw new Error("must provide a hotkey, not a key code"); + } + Keyboard.keyDown(key); + }; + + static hotkeyUp = (hotkey: Key) => { + const key = KEYS[hotkey]; + if (typeof key !== "string") { + throw new Error("must provide a hotkey, not a key code"); + } + Keyboard.keyUp(key); + }; + + static keyDown = (key: string) => { + fireEvent.keyDown(document, { + key, + ctrlKey, + shiftKey, + altKey, + keyCode: key.toUpperCase().charCodeAt(0), + which: key.toUpperCase().charCodeAt(0), + }); + }; + + static keyUp = (key: string) => { + fireEvent.keyUp(document, { + key, + ctrlKey, + shiftKey, + altKey, + keyCode: key.toUpperCase().charCodeAt(0), + which: key.toUpperCase().charCodeAt(0), + }); + }; + + static hotkeyPress = (key: Key) => { + Keyboard.hotkeyDown(key); + Keyboard.hotkeyUp(key); + }; + + static keyPress = (key: string) => { + Keyboard.keyDown(key); + Keyboard.keyUp(key); + }; +} + +export class Pointer { + private clientX = 0; + private clientY = 0; + + constructor( + private readonly pointerType: "mouse" | "touch" | "pen", + private readonly pointerId = 1, + ) {} + + reset() { + this.clientX = 0; + this.clientY = 0; + } + + getPosition() { + return [this.clientX, this.clientY]; + } + + restorePosition(x = 0, y = 0) { + this.clientX = x; + this.clientY = y; + fireEvent.pointerMove(GlobalTestState.canvas, this.getEvent()); + } + + private getEvent() { + return { + clientX: this.clientX, + clientY: this.clientY, + pointerType: this.pointerType, + pointerId: this.pointerId, + altKey, + shiftKey, + ctrlKey, + }; + } + + move(dx: number, dy: number) { + if (dx !== 0 || dy !== 0) { + this.clientX += dx; + this.clientY += dy; + fireEvent.pointerMove(GlobalTestState.canvas, this.getEvent()); + } + } + + down(dx = 0, dy = 0) { + this.move(dx, dy); + fireEvent.pointerDown(GlobalTestState.canvas, this.getEvent()); + } + + up(dx = 0, dy = 0) { + this.move(dx, dy); + fireEvent.pointerUp(GlobalTestState.canvas, this.getEvent()); + } + + click(dx = 0, dy = 0) { + this.down(dx, dy); + this.up(); + } + + doubleClick(dx = 0, dy = 0) { + this.move(dx, dy); + fireEvent.doubleClick(GlobalTestState.canvas, this.getEvent()); + } + + select( + /** if multiple elements supplied, they're shift-selected */ + elements: ExcalidrawElement | ExcalidrawElement[], + ) { + API.clearSelection(); + Keyboard.withModifierKeys({ shift: true }, () => { + elements = Array.isArray(elements) ? elements : [elements]; + elements.forEach((element) => { + this.reset(); + this.click(element.x, element.y); + }); + }); + this.reset(); + } + + clickOn(element: ExcalidrawElement) { + this.reset(); + this.click(element.x, element.y); + this.reset(); + } +} + +const mouse = new Pointer("mouse"); + +export class UI { + static clickTool = (toolName: ToolName) => { + fireEvent.click(GlobalTestState.renderResult.getByToolName(toolName)); + }; + + static createElement( + type: ToolName, + { + x = 0, + y = x, + size = 10, + }: { + x?: number; + y?: number; + size?: number; + }, + ) { + UI.clickTool(type); + mouse.reset(); + mouse.down(x, y); + mouse.reset(); + mouse.up(x + size, y + size); + return h.elements[h.elements.length - 1]; + } + + static group(elements: ExcalidrawElement[]) { + mouse.select(elements); + Keyboard.withModifierKeys({ ctrl: true }, () => { + Keyboard.keyPress("g"); + }); + } +} diff --git a/src/tests/regressionTests.test.tsx b/src/tests/regressionTests.test.tsx index 2d6fe661..98e46f66 100644 --- a/src/tests/regressionTests.test.tsx +++ b/src/tests/regressionTests.test.tsx @@ -2,227 +2,35 @@ import { reseed } from "../random"; import React from "react"; import ReactDOM from "react-dom"; import * as Renderer from "../renderer/renderScene"; -import { waitFor, render, screen, fireEvent } from "./test-utils"; +import { + waitFor, + render, + screen, + fireEvent, + GlobalTestState, +} from "./test-utils"; import App from "../components/App"; import { setLanguage } from "../i18n"; -import { ToolName } from "./queries/toolQueries"; -import { KEYS, Key } from "../keys"; import { setDateTimeForTests } from "../utils"; import { ExcalidrawElement } from "../element/types"; import { getTransformHandles as _getTransformHandles } from "../element"; import { queryByText } from "@testing-library/react"; import { copiedStyles } from "../actions/actionStyles"; +import { UI, Pointer, Keyboard } from "./helpers/ui"; +import { API } from "./helpers/api"; const { h } = window; const renderScene = jest.spyOn(Renderer, "renderScene"); -let getByToolName: (name: string) => HTMLElement = null!; -let canvas: HTMLCanvasElement = null!; - -const clickTool = (toolName: ToolName) => { - fireEvent.click(getByToolName(toolName)); -}; - -const createElement = ( - type: ToolName, - { - x = 0, - y = x, - size = 10, - }: { - x?: number; - y?: number; - size?: number; - }, -) => { - clickTool(type); - mouse.reset(); - mouse.down(x, y); - mouse.reset(); - mouse.up(x + size, y + size); - return h.elements[h.elements.length - 1]; -}; - -const group = (elements: ExcalidrawElement[]) => { - mouse.select(elements); - withModifierKeys({ ctrl: true }, () => { - keyPress("g"); - }); -}; const assertSelectedElements = (...elements: ExcalidrawElement[]) => { expect( - getSelectedElements().map((element) => { + API.getSelectedElements().map((element) => { return element.id; }), ).toEqual(expect.arrayContaining(elements.map((element) => element.id))); }; -const clearSelection = () => { - // @ts-ignore - h.app.clearSelection(null); - expect(getSelectedElements().length).toBe(0); -}; - -let altKey = false; -let shiftKey = false; -let ctrlKey = false; - -function withModifierKeys( - modifiers: { alt?: boolean; shift?: boolean; ctrl?: boolean }, - cb: () => void, -) { - const prevAltKey = altKey; - const prevShiftKey = shiftKey; - const prevCtrlKey = ctrlKey; - - altKey = !!modifiers.alt; - shiftKey = !!modifiers.shift; - ctrlKey = !!modifiers.ctrl; - - try { - cb(); - } finally { - altKey = prevAltKey; - shiftKey = prevShiftKey; - ctrlKey = prevCtrlKey; - } -} - -const hotkeyDown = (hotkey: Key) => { - const key = KEYS[hotkey]; - if (typeof key !== "string") { - throw new Error("must provide a hotkey, not a key code"); - } - keyDown(key); -}; - -const hotkeyUp = (hotkey: Key) => { - const key = KEYS[hotkey]; - if (typeof key !== "string") { - throw new Error("must provide a hotkey, not a key code"); - } - keyUp(key); -}; - -const keyDown = (key: string) => { - fireEvent.keyDown(document, { - key, - ctrlKey, - shiftKey, - altKey, - keyCode: key.toUpperCase().charCodeAt(0), - which: key.toUpperCase().charCodeAt(0), - }); -}; - -const keyUp = (key: string) => { - fireEvent.keyUp(document, { - key, - ctrlKey, - shiftKey, - altKey, - keyCode: key.toUpperCase().charCodeAt(0), - which: key.toUpperCase().charCodeAt(0), - }); -}; - -const hotkeyPress = (key: Key) => { - hotkeyDown(key); - hotkeyUp(key); -}; - -const keyPress = (key: string) => { - keyDown(key); - keyUp(key); -}; - -class Pointer { - private clientX = 0; - private clientY = 0; - - constructor( - private readonly pointerType: "mouse" | "touch" | "pen", - private readonly pointerId = 1, - ) {} - - reset() { - this.clientX = 0; - this.clientY = 0; - } - - getPosition() { - return [this.clientX, this.clientY]; - } - - restorePosition(x = 0, y = 0) { - this.clientX = x; - this.clientY = y; - fireEvent.pointerMove(canvas, this.getEvent()); - } - - private getEvent() { - return { - clientX: this.clientX, - clientY: this.clientY, - pointerType: this.pointerType, - pointerId: this.pointerId, - altKey, - shiftKey, - ctrlKey, - }; - } - - move(dx: number, dy: number) { - if (dx !== 0 || dy !== 0) { - this.clientX += dx; - this.clientY += dy; - fireEvent.pointerMove(canvas, this.getEvent()); - } - } - - down(dx = 0, dy = 0) { - this.move(dx, dy); - fireEvent.pointerDown(canvas, this.getEvent()); - } - - up(dx = 0, dy = 0) { - this.move(dx, dy); - fireEvent.pointerUp(canvas, this.getEvent()); - } - - click(dx = 0, dy = 0) { - this.down(dx, dy); - this.up(); - } - - doubleClick(dx = 0, dy = 0) { - this.move(dx, dy); - fireEvent.doubleClick(canvas, this.getEvent()); - } - - select( - /** if multiple elements supplied, they're shift-selected */ - elements: ExcalidrawElement | ExcalidrawElement[], - ) { - clearSelection(); - withModifierKeys({ shift: true }, () => { - elements = Array.isArray(elements) ? elements : [elements]; - elements.forEach((element) => { - mouse.reset(); - mouse.click(element.x, element.y); - }); - }); - mouse.reset(); - } - - clickOn(element: ExcalidrawElement) { - mouse.reset(); - mouse.click(element.x, element.y); - mouse.reset(); - } -} - const mouse = new Pointer("mouse"); const finger1 = new Pointer("touch", 1); const finger2 = new Pointer("touch", 2); @@ -235,29 +43,10 @@ const clickLabeledElement = (label: string) => { fireEvent.click(element); }; -const getSelectedElements = (): ExcalidrawElement[] => { - return h.elements.filter((element) => h.state.selectedElementIds[element.id]); -}; - -const getSelectedElement = (): ExcalidrawElement => { - const selectedElements = getSelectedElements(); - if (selectedElements.length !== 1) { - throw new Error( - `expected 1 selected element; got ${selectedElements.length}`, - ); - } - return selectedElements[0]; -}; - -function getStateHistory() { - // @ts-ignore - return h.history.stateHistory; -} - type HandlerRectanglesRet = keyof ReturnType; const getTransformHandles = (pointerType: "mouse" | "touch" | "pen") => { const rects = _getTransformHandles( - getSelectedElement(), + API.getSelectedElement(), h.state.zoom, pointerType, ) as { @@ -305,13 +94,9 @@ beforeEach(async () => { mouse.reset(); finger1.reset(); finger2.reset(); - altKey = ctrlKey = shiftKey = false; await setLanguage("en.json"); - const renderResult = render(); - - getByToolName = renderResult.getByToolName; - canvas = renderResult.container.querySelector("canvas")!; + render(); }); afterEach(() => { @@ -320,39 +105,39 @@ afterEach(() => { describe("regression tests", () => { it("draw every type of shape", () => { - clickTool("rectangle"); + UI.clickTool("rectangle"); mouse.down(10, -10); mouse.up(20, 10); - clickTool("diamond"); + UI.clickTool("diamond"); mouse.down(10, -10); mouse.up(20, 10); - clickTool("ellipse"); + UI.clickTool("ellipse"); mouse.down(10, -10); mouse.up(20, 10); - clickTool("arrow"); + UI.clickTool("arrow"); mouse.down(40, -10); mouse.up(50, 10); - clickTool("line"); + UI.clickTool("line"); mouse.down(40, -10); mouse.up(50, 10); - clickTool("arrow"); + UI.clickTool("arrow"); mouse.click(40, -10); mouse.click(50, 10); mouse.click(30, 10); - hotkeyPress("ENTER"); + Keyboard.hotkeyPress("ENTER"); - clickTool("line"); + UI.clickTool("line"); mouse.click(40, -20); mouse.click(50, 10); mouse.click(30, 10); - hotkeyPress("ENTER"); + Keyboard.hotkeyPress("ENTER"); - clickTool("draw"); + UI.clickTool("draw"); mouse.down(40, -20); mouse.up(50, 10); @@ -369,21 +154,21 @@ describe("regression tests", () => { }); it("click to select a shape", () => { - clickTool("rectangle"); + UI.clickTool("rectangle"); mouse.down(10, 10); mouse.up(10, 10); const firstRectPos = mouse.getPosition(); - clickTool("rectangle"); + UI.clickTool("rectangle"); mouse.down(10, -10); mouse.up(10, 10); - const prevSelectedId = getSelectedElement().id; + const prevSelectedId = API.getSelectedElement().id; mouse.restorePosition(...firstRectPos); mouse.click(); - expect(getSelectedElement().id).not.toEqual(prevSelectedId); + expect(API.getSelectedElement().id).not.toEqual(prevSelectedId); }); for (const [keys, shape] of [ @@ -396,18 +181,18 @@ describe("regression tests", () => { ] as [string, ExcalidrawElement["type"]][]) { for (const key of keys) { it(`hotkey ${key} selects ${shape} tool`, () => { - keyPress(key); + Keyboard.keyPress(key); mouse.down(10, 10); mouse.up(10, 10); - expect(getSelectedElement().type).toBe(shape); + expect(API.getSelectedElement().type).toBe(shape); }); } } it("change the properties of a shape", () => { - clickTool("rectangle"); + UI.clickTool("rectangle"); mouse.down(10, 10); mouse.up(10, 10); @@ -415,12 +200,12 @@ describe("regression tests", () => { clickLabeledElement("#fa5252"); clickLabeledElement("Stroke"); clickLabeledElement("#5f3dc4"); - expect(getSelectedElement().backgroundColor).toBe("#fa5252"); - expect(getSelectedElement().strokeColor).toBe("#5f3dc4"); + expect(API.getSelectedElement().backgroundColor).toBe("#fa5252"); + expect(API.getSelectedElement().strokeColor).toBe("#5f3dc4"); }); it("resize an element, trying every resize handle", () => { - clickTool("rectangle"); + UI.clickTool("rectangle"); mouse.down(10, 10); mouse.up(10, 10); @@ -430,7 +215,7 @@ describe("regression tests", () => { const [x, y] = transformHandles[ handlePos as keyof typeof transformHandles ]; - const { width: prevWidth, height: prevHeight } = getSelectedElement(); + const { width: prevWidth, height: prevHeight } = API.getSelectedElement(); mouse.restorePosition(x, y); mouse.down(); mouse.up(-5, -5); @@ -438,7 +223,7 @@ describe("regression tests", () => { const { width: nextWidthNegative, height: nextHeightNegative, - } = getSelectedElement(); + } = API.getSelectedElement(); expect( prevWidth !== nextWidthNegative || prevHeight !== nextHeightNegative, ).toBeTruthy(); @@ -447,7 +232,7 @@ describe("regression tests", () => { mouse.down(); mouse.up(5, 5); - const { width, height } = getSelectedElement(); + const { width, height } = API.getSelectedElement(); expect(width).toBe(prevWidth); expect(height).toBe(prevHeight); checkpoint(`unresize handle ${handlePos} (-5, -5)`); @@ -459,7 +244,7 @@ describe("regression tests", () => { const { width: nextWidthPositive, height: nextHeightPositive, - } = getSelectedElement(); + } = API.getSelectedElement(); expect( prevWidth !== nextWidthPositive || prevHeight !== nextHeightPositive, ).toBeTruthy(); @@ -468,7 +253,10 @@ describe("regression tests", () => { mouse.down(); mouse.up(-5, -5); - const { width: finalWidth, height: finalHeight } = getSelectedElement(); + const { + width: finalWidth, + height: finalHeight, + } = API.getSelectedElement(); expect(finalWidth).toBe(prevWidth); expect(finalHeight).toBe(prevHeight); @@ -477,15 +265,15 @@ describe("regression tests", () => { }); it("click on an element and drag it", () => { - clickTool("rectangle"); + UI.clickTool("rectangle"); mouse.down(10, 10); mouse.up(10, 10); - const { x: prevX, y: prevY } = getSelectedElement(); + const { x: prevX, y: prevY } = API.getSelectedElement(); mouse.down(-10, -10); mouse.up(10, 10); - const { x: nextX, y: nextY } = getSelectedElement(); + const { x: nextX, y: nextY } = API.getSelectedElement(); expect(nextX).toBeGreaterThan(prevX); expect(nextY).toBeGreaterThan(prevY); @@ -494,13 +282,13 @@ describe("regression tests", () => { mouse.down(); mouse.up(-10, -10); - const { x, y } = getSelectedElement(); + const { x, y } = API.getSelectedElement(); expect(x).toBe(prevX); expect(y).toBe(prevY); }); it("alt-drag duplicates an element", () => { - clickTool("rectangle"); + UI.clickTool("rectangle"); mouse.down(10, 10); mouse.up(10, 10); @@ -508,7 +296,7 @@ describe("regression tests", () => { h.elements.filter((element) => element.type === "rectangle").length, ).toBe(1); - withModifierKeys({ alt: true }, () => { + Keyboard.withModifierKeys({ alt: true }, () => { mouse.down(-10, -10); mouse.up(10, 10); }); @@ -519,17 +307,17 @@ describe("regression tests", () => { }); it("click-drag to select a group", () => { - clickTool("rectangle"); + UI.clickTool("rectangle"); mouse.down(10, 10); mouse.up(10, 10); - clickTool("rectangle"); + UI.clickTool("rectangle"); mouse.down(10, -10); mouse.up(10, 10); const finalPosition = mouse.getPosition(); - clickTool("rectangle"); + UI.clickTool("rectangle"); mouse.down(10, -10); mouse.up(10, 10); @@ -545,11 +333,11 @@ describe("regression tests", () => { }); it("shift-click to multiselect, then drag", () => { - clickTool("rectangle"); + UI.clickTool("rectangle"); mouse.down(10, 10); mouse.up(10, 10); - clickTool("rectangle"); + UI.clickTool("rectangle"); mouse.down(10, -10); mouse.up(10, 10); @@ -559,7 +347,7 @@ describe("regression tests", () => { mouse.reset(); mouse.click(10, 10); - withModifierKeys({ shift: true }, () => { + Keyboard.withModifierKeys({ shift: true }, () => { mouse.click(20, 0); }); @@ -608,56 +396,56 @@ describe("regression tests", () => { it("spacebar + drag scrolls the canvas", () => { const { scrollX: startScrollX, scrollY: startScrollY } = h.state; - hotkeyDown("SPACE"); + Keyboard.hotkeyDown("SPACE"); mouse.down(50, 50); mouse.up(60, 60); - hotkeyUp("SPACE"); + Keyboard.hotkeyUp("SPACE"); const { scrollX, scrollY } = h.state; expect(scrollX).not.toEqual(startScrollX); expect(scrollY).not.toEqual(startScrollY); }); it("arrow keys", () => { - clickTool("rectangle"); + UI.clickTool("rectangle"); mouse.down(10, 10); mouse.up(10, 10); - hotkeyPress("ARROW_LEFT"); - hotkeyPress("ARROW_LEFT"); - hotkeyPress("ARROW_RIGHT"); - hotkeyPress("ARROW_UP"); - hotkeyPress("ARROW_UP"); - hotkeyPress("ARROW_DOWN"); + Keyboard.hotkeyPress("ARROW_LEFT"); + Keyboard.hotkeyPress("ARROW_LEFT"); + Keyboard.hotkeyPress("ARROW_RIGHT"); + Keyboard.hotkeyPress("ARROW_UP"); + Keyboard.hotkeyPress("ARROW_UP"); + Keyboard.hotkeyPress("ARROW_DOWN"); expect(h.elements[0].x).toBe(9); expect(h.elements[0].y).toBe(9); }); it("undo/redo drawing an element", () => { - clickTool("rectangle"); + UI.clickTool("rectangle"); mouse.down(10, -10); mouse.up(20, 10); - clickTool("rectangle"); + UI.clickTool("rectangle"); mouse.down(10, 0); mouse.up(30, 20); - clickTool("arrow"); + UI.clickTool("arrow"); mouse.click(60, -10); mouse.click(60, 10); mouse.click(40, 10); - hotkeyPress("ENTER"); + Keyboard.hotkeyPress("ENTER"); expect(h.elements.filter((element) => !element.isDeleted).length).toBe(3); - withModifierKeys({ ctrl: true }, () => { - keyPress("z"); - keyPress("z"); + Keyboard.withModifierKeys({ ctrl: true }, () => { + Keyboard.keyPress("z"); + Keyboard.keyPress("z"); }); expect(h.elements.filter((element) => !element.isDeleted).length).toBe(2); - withModifierKeys({ ctrl: true }, () => { - keyPress("z"); + Keyboard.withModifierKeys({ ctrl: true }, () => { + Keyboard.keyPress("z"); }); expect(h.elements.filter((element) => !element.isDeleted).length).toBe(1); - withModifierKeys({ ctrl: true, shift: true }, () => { - keyPress("z"); + Keyboard.withModifierKeys({ ctrl: true, shift: true }, () => { + Keyboard.keyPress("z"); }); expect(h.elements.filter((element) => !element.isDeleted).length).toBe(2); }); @@ -666,51 +454,51 @@ describe("regression tests", () => { // NOTE: this will fail if this test case is run in isolation. There's // some leaking state or race conditions in initialization/teardown // (couldn't figure out) - expect(getStateHistory().length).toBe(0); + expect(API.getStateHistory().length).toBe(0); - clickTool("rectangle"); + UI.clickTool("rectangle"); mouse.down(10, 10); mouse.up(10, 10); const firstElementEndPoint = mouse.getPosition(); - clickTool("rectangle"); + UI.clickTool("rectangle"); mouse.down(10, -10); mouse.up(10, 10); const secondElementEndPoint = mouse.getPosition(); - expect(getStateHistory().length).toBe(2); + expect(API.getStateHistory().length).toBe(2); - withModifierKeys({ ctrl: true }, () => { - keyPress("z"); + Keyboard.withModifierKeys({ ctrl: true }, () => { + Keyboard.keyPress("z"); }); - expect(getStateHistory().length).toBe(1); + expect(API.getStateHistory().length).toBe(1); // clicking an element shouldn't add to history mouse.restorePosition(...firstElementEndPoint); mouse.click(); - expect(getStateHistory().length).toBe(1); + expect(API.getStateHistory().length).toBe(1); - withModifierKeys({ shift: true, ctrl: true }, () => { - keyPress("z"); + Keyboard.withModifierKeys({ shift: true, ctrl: true }, () => { + Keyboard.keyPress("z"); }); - expect(getStateHistory().length).toBe(2); + expect(API.getStateHistory().length).toBe(2); // clicking an element shouldn't add to history mouse.click(); - expect(getStateHistory().length).toBe(2); + expect(API.getStateHistory().length).toBe(2); - const firstSelectedElementId = getSelectedElement().id; + const firstSelectedElementId = API.getSelectedElement().id; // same for clicking the element just redo-ed mouse.restorePosition(...secondElementEndPoint); mouse.click(); - expect(getStateHistory().length).toBe(2); + expect(API.getStateHistory().length).toBe(2); - expect(getSelectedElement().id).not.toEqual(firstSelectedElementId); + expect(API.getSelectedElement().id).not.toEqual(firstSelectedElementId); }); it("zoom hotkeys", () => { @@ -725,7 +513,7 @@ describe("regression tests", () => { it("rerenders UI on language change", async () => { // select rectangle tool to show properties menu - clickTool("rectangle"); + UI.clickTool("rectangle"); // english lang should display `hachure` label expect(screen.queryByText(/hachure/i)).not.toBeNull(); fireEvent.change(document.querySelector(".dropdown-select__language")!, { @@ -742,15 +530,15 @@ describe("regression tests", () => { }); it("make a group and duplicate it", () => { - clickTool("rectangle"); + UI.clickTool("rectangle"); mouse.down(10, 10); mouse.up(10, 10); - clickTool("rectangle"); + UI.clickTool("rectangle"); mouse.down(10, -10); mouse.up(10, 10); - clickTool("rectangle"); + UI.clickTool("rectangle"); mouse.down(10, -10); mouse.up(10, 10); const end = mouse.getPosition(); @@ -766,15 +554,15 @@ describe("regression tests", () => { expect(h.state.selectedElementIds[element.id]).toBe(true); } - withModifierKeys({ ctrl: true }, () => { - keyPress("g"); + Keyboard.withModifierKeys({ ctrl: true }, () => { + Keyboard.keyPress("g"); }); for (const element of h.elements) { expect(element.groupIds.length).toBe(1); } - withModifierKeys({ alt: true }, () => { + Keyboard.withModifierKeys({ alt: true }, () => { mouse.restorePosition(...end); mouse.down(); mouse.up(10, 10); @@ -792,44 +580,44 @@ describe("regression tests", () => { }); it("double click to edit a group", () => { - clickTool("rectangle"); + UI.clickTool("rectangle"); mouse.down(10, 10); mouse.up(10, 10); - clickTool("rectangle"); + UI.clickTool("rectangle"); mouse.down(10, -10); mouse.up(10, 10); - clickTool("rectangle"); + UI.clickTool("rectangle"); mouse.down(10, -10); mouse.up(10, 10); - withModifierKeys({ ctrl: true }, () => { - keyPress("a"); - keyPress("g"); + Keyboard.withModifierKeys({ ctrl: true }, () => { + Keyboard.keyPress("a"); + Keyboard.keyPress("g"); }); - expect(getSelectedElements().length).toBe(3); + expect(API.getSelectedElements().length).toBe(3); expect(h.state.editingGroupId).toBe(null); mouse.doubleClick(); - expect(getSelectedElements().length).toBe(1); + expect(API.getSelectedElements().length).toBe(1); expect(h.state.editingGroupId).not.toBe(null); }); it("adjusts z order when grouping", () => { const positions = []; - clickTool("rectangle"); + UI.clickTool("rectangle"); mouse.down(10, 10); mouse.up(10, 10); positions.push(mouse.getPosition()); - clickTool("rectangle"); + UI.clickTool("rectangle"); mouse.down(10, -10); mouse.up(10, 10); positions.push(mouse.getPosition()); - clickTool("rectangle"); + UI.clickTool("rectangle"); mouse.down(10, -10); mouse.up(10, 10); positions.push(mouse.getPosition()); @@ -839,11 +627,11 @@ describe("regression tests", () => { mouse.restorePosition(...positions[0]); mouse.click(); mouse.restorePosition(...positions[2]); - withModifierKeys({ shift: true }, () => { + Keyboard.withModifierKeys({ shift: true }, () => { mouse.click(); }); - withModifierKeys({ ctrl: true }, () => { - keyPress("g"); + Keyboard.withModifierKeys({ ctrl: true }, () => { + Keyboard.keyPress("g"); }); expect(h.elements.map((element) => element.id)).toEqual([ @@ -856,33 +644,33 @@ describe("regression tests", () => { it("supports nested groups", () => { const positions: number[][] = []; - clickTool("rectangle"); + UI.clickTool("rectangle"); mouse.down(10, 10); mouse.up(10, 10); positions.push(mouse.getPosition()); - clickTool("rectangle"); + UI.clickTool("rectangle"); mouse.down(10, -10); mouse.up(10, 10); positions.push(mouse.getPosition()); - clickTool("rectangle"); + UI.clickTool("rectangle"); mouse.down(10, -10); mouse.up(10, 10); positions.push(mouse.getPosition()); - withModifierKeys({ ctrl: true }, () => { - keyPress("a"); - keyPress("g"); + Keyboard.withModifierKeys({ ctrl: true }, () => { + Keyboard.keyPress("a"); + Keyboard.keyPress("g"); }); mouse.doubleClick(); - withModifierKeys({ shift: true }, () => { + Keyboard.withModifierKeys({ shift: true }, () => { mouse.restorePosition(...positions[0]); mouse.click(); }); - withModifierKeys({ ctrl: true }, () => { - keyPress("g"); + Keyboard.withModifierKeys({ ctrl: true }, () => { + Keyboard.keyPress("g"); }); const groupIds = h.elements[2].groupIds; @@ -891,39 +679,43 @@ describe("regression tests", () => { expect(h.elements[0].groupIds).toEqual(groupIds.slice(1)); mouse.click(50, 50); - expect(getSelectedElements().length).toBe(0); + expect(API.getSelectedElements().length).toBe(0); mouse.restorePosition(...positions[0]); mouse.click(); - expect(getSelectedElements().length).toBe(3); + expect(API.getSelectedElements().length).toBe(3); expect(h.state.editingGroupId).toBe(null); mouse.doubleClick(); - expect(getSelectedElements().length).toBe(2); + expect(API.getSelectedElements().length).toBe(2); expect(h.state.editingGroupId).toBe(groupIds[1]); mouse.doubleClick(); - expect(getSelectedElements().length).toBe(1); + expect(API.getSelectedElements().length).toBe(1); expect(h.state.editingGroupId).toBe(groupIds[0]); // click out of the group mouse.restorePosition(...positions[1]); mouse.click(); - expect(getSelectedElements().length).toBe(0); + expect(API.getSelectedElements().length).toBe(0); mouse.click(); - expect(getSelectedElements().length).toBe(3); + expect(API.getSelectedElements().length).toBe(3); mouse.doubleClick(); - expect(getSelectedElements().length).toBe(1); + expect(API.getSelectedElements().length).toBe(1); }); it("updates fontSize & fontFamily appState", () => { - clickTool("text"); + UI.clickTool("text"); expect(h.state.currentItemFontFamily).toEqual(1); // Virgil fireEvent.click(screen.getByText(/code/i)); expect(h.state.currentItemFontFamily).toEqual(3); // Cascadia }); it("shows context menu for canvas", () => { - fireEvent.contextMenu(canvas, { button: 2, clientX: 1, clientY: 1 }); + fireEvent.contextMenu(GlobalTestState.canvas, { + button: 2, + clientX: 1, + clientY: 1, + }); const contextMenu = document.querySelector(".context-menu"); const options = contextMenu?.querySelectorAll(".context-menu-option"); const expectedOptions = ["Select all", "Toggle grid mode"]; @@ -934,11 +726,15 @@ describe("regression tests", () => { }); it("shows context menu for element", () => { - clickTool("rectangle"); + UI.clickTool("rectangle"); mouse.down(10, 10); mouse.up(20, 20); - fireEvent.contextMenu(canvas, { button: 2, clientX: 1, clientY: 1 }); + fireEvent.contextMenu(GlobalTestState.canvas, { + button: 2, + clientX: 1, + clientY: 1, + }); const contextMenu = document.querySelector(".context-menu"); const options = contextMenu?.querySelectorAll(".context-menu-option"); const expectedOptions = [ @@ -961,21 +757,25 @@ describe("regression tests", () => { }); it("shows 'Group selection' in context menu for multiple selected elements", () => { - clickTool("rectangle"); + UI.clickTool("rectangle"); mouse.down(10, 10); mouse.up(10, 10); - clickTool("rectangle"); + UI.clickTool("rectangle"); mouse.down(10, -10); mouse.up(10, 10); mouse.reset(); mouse.click(10, 10); - withModifierKeys({ shift: true }, () => { + Keyboard.withModifierKeys({ shift: true }, () => { mouse.click(20, 0); }); - fireEvent.contextMenu(canvas, { button: 2, clientX: 1, clientY: 1 }); + fireEvent.contextMenu(GlobalTestState.canvas, { + button: 2, + clientX: 1, + clientY: 1, + }); const contextMenu = document.querySelector(".context-menu"); const options = contextMenu?.querySelectorAll(".context-menu-option"); @@ -1000,25 +800,29 @@ describe("regression tests", () => { }); it("shows 'Ungroup selection' in context menu for group inside selected elements", () => { - clickTool("rectangle"); + UI.clickTool("rectangle"); mouse.down(10, 10); mouse.up(10, 10); - clickTool("rectangle"); + UI.clickTool("rectangle"); mouse.down(10, -10); mouse.up(10, 10); mouse.reset(); mouse.click(10, 10); - withModifierKeys({ shift: true }, () => { + Keyboard.withModifierKeys({ shift: true }, () => { mouse.click(20, 0); }); - withModifierKeys({ ctrl: true }, () => { - keyPress("g"); + Keyboard.withModifierKeys({ ctrl: true }, () => { + Keyboard.keyPress("g"); }); - fireEvent.contextMenu(canvas, { button: 2, clientX: 1, clientY: 1 }); + fireEvent.contextMenu(GlobalTestState.canvas, { + button: 2, + clientX: 1, + clientY: 1, + }); const contextMenu = document.querySelector(".context-menu"); const options = contextMenu?.querySelectorAll(".context-menu-option"); @@ -1043,25 +847,29 @@ describe("regression tests", () => { }); it("selecting 'Copy styles' in context menu copies styles", () => { - clickTool("rectangle"); + UI.clickTool("rectangle"); mouse.down(10, 10); mouse.up(20, 20); - fireEvent.contextMenu(canvas, { button: 2, clientX: 1, clientY: 1 }); + fireEvent.contextMenu(GlobalTestState.canvas, { + button: 2, + clientX: 1, + clientY: 1, + }); const contextMenu = document.querySelector(".context-menu"); expect(copiedStyles).toBe("{}"); fireEvent.click(queryByText(contextMenu as HTMLElement, "Copy styles")!); expect(copiedStyles).not.toBe("{}"); const element = JSON.parse(copiedStyles); - expect(element).toEqual(getSelectedElement()); + expect(element).toEqual(API.getSelectedElement()); }); it("selecting 'Paste styles' in context menu pastes styles", () => { - clickTool("rectangle"); + UI.clickTool("rectangle"); mouse.down(10, 10); mouse.up(20, 20); - clickTool("rectangle"); + UI.clickTool("rectangle"); mouse.down(10, 10); mouse.up(20, 20); @@ -1085,7 +893,11 @@ describe("regression tests", () => { mouse.reset(); // Copy styles of second rectangle - fireEvent.contextMenu(canvas, { button: 2, clientX: 40, clientY: 40 }); + fireEvent.contextMenu(GlobalTestState.canvas, { + button: 2, + clientX: 40, + clientY: 40, + }); let contextMenu = document.querySelector(".context-menu"); fireEvent.click(queryByText(contextMenu as HTMLElement, "Copy styles")!); const secondRect = JSON.parse(copiedStyles); @@ -1093,11 +905,15 @@ describe("regression tests", () => { mouse.reset(); // Paste styles to first rectangle - fireEvent.contextMenu(canvas, { button: 2, clientX: 10, clientY: 10 }); + fireEvent.contextMenu(GlobalTestState.canvas, { + button: 2, + clientX: 10, + clientY: 10, + }); contextMenu = document.querySelector(".context-menu"); fireEvent.click(queryByText(contextMenu as HTMLElement, "Paste styles")!); - const firstRect = getSelectedElement(); + const firstRect = API.getSelectedElement(); expect(firstRect.id).toBe(h.elements[0].id); expect(firstRect.strokeColor).toBe("#c92a2a"); expect(firstRect.backgroundColor).toBe("#e64980"); @@ -1109,23 +925,31 @@ describe("regression tests", () => { }); it("selecting 'Delete' in context menu deletes element", () => { - clickTool("rectangle"); + UI.clickTool("rectangle"); mouse.down(10, 10); mouse.up(20, 20); - fireEvent.contextMenu(canvas, { button: 2, clientX: 1, clientY: 1 }); + fireEvent.contextMenu(GlobalTestState.canvas, { + button: 2, + clientX: 1, + clientY: 1, + }); const contextMenu = document.querySelector(".context-menu"); fireEvent.click(queryByText(contextMenu as HTMLElement, "Delete")!); - expect(getSelectedElements()).toHaveLength(0); + expect(API.getSelectedElements()).toHaveLength(0); expect(h.elements[0].isDeleted).toBe(true); }); it("selecting 'Add to library' in context menu adds element to library", async () => { - clickTool("rectangle"); + UI.clickTool("rectangle"); mouse.down(10, 10); mouse.up(20, 20); - fireEvent.contextMenu(canvas, { button: 2, clientX: 1, clientY: 1 }); + fireEvent.contextMenu(GlobalTestState.canvas, { + button: 2, + clientX: 1, + clientY: 1, + }); const contextMenu = document.querySelector(".context-menu"); fireEvent.click(queryByText(contextMenu as HTMLElement, "Add to library")!); @@ -1138,11 +962,15 @@ describe("regression tests", () => { }); it("selecting 'Duplicate' in context menu duplicates element", () => { - clickTool("rectangle"); + UI.clickTool("rectangle"); mouse.down(10, 10); mouse.up(20, 20); - fireEvent.contextMenu(canvas, { button: 2, clientX: 1, clientY: 1 }); + fireEvent.contextMenu(GlobalTestState.canvas, { + button: 2, + clientX: 1, + clientY: 1, + }); const contextMenu = document.querySelector(".context-menu"); fireEvent.click(queryByText(contextMenu as HTMLElement, "Duplicate")!); expect(h.elements).toHaveLength(2); @@ -1152,16 +980,20 @@ describe("regression tests", () => { }); it("selecting 'Send backward' in context menu sends element backward", () => { - clickTool("rectangle"); + UI.clickTool("rectangle"); mouse.down(10, 10); mouse.up(20, 20); - clickTool("rectangle"); + UI.clickTool("rectangle"); mouse.down(10, 10); mouse.up(20, 20); mouse.reset(); - fireEvent.contextMenu(canvas, { button: 2, clientX: 40, clientY: 40 }); + fireEvent.contextMenu(GlobalTestState.canvas, { + button: 2, + clientX: 40, + clientY: 40, + }); const contextMenu = document.querySelector(".context-menu"); const elementsBefore = h.elements; fireEvent.click(queryByText(contextMenu as HTMLElement, "Send backward")!); @@ -1170,16 +1002,20 @@ describe("regression tests", () => { }); it("selecting 'Bring forward' in context menu brings element forward", () => { - clickTool("rectangle"); + UI.clickTool("rectangle"); mouse.down(10, 10); mouse.up(20, 20); - clickTool("rectangle"); + UI.clickTool("rectangle"); mouse.down(10, 10); mouse.up(20, 20); mouse.reset(); - fireEvent.contextMenu(canvas, { button: 2, clientX: 10, clientY: 10 }); + fireEvent.contextMenu(GlobalTestState.canvas, { + button: 2, + clientX: 10, + clientY: 10, + }); const contextMenu = document.querySelector(".context-menu"); const elementsBefore = h.elements; fireEvent.click(queryByText(contextMenu as HTMLElement, "Bring forward")!); @@ -1188,16 +1024,20 @@ describe("regression tests", () => { }); it("selecting 'Send to back' in context menu sends element to back", () => { - clickTool("rectangle"); + UI.clickTool("rectangle"); mouse.down(10, 10); mouse.up(20, 20); - clickTool("rectangle"); + UI.clickTool("rectangle"); mouse.down(10, 10); mouse.up(20, 20); mouse.reset(); - fireEvent.contextMenu(canvas, { button: 2, clientX: 40, clientY: 40 }); + fireEvent.contextMenu(GlobalTestState.canvas, { + button: 2, + clientX: 40, + clientY: 40, + }); const contextMenu = document.querySelector(".context-menu"); const elementsBefore = h.elements; fireEvent.click(queryByText(contextMenu as HTMLElement, "Send to back")!); @@ -1205,16 +1045,20 @@ describe("regression tests", () => { }); it("selecting 'Bring to front' in context menu brings element to front", () => { - clickTool("rectangle"); + UI.clickTool("rectangle"); mouse.down(10, 10); mouse.up(20, 20); - clickTool("rectangle"); + UI.clickTool("rectangle"); mouse.down(10, 10); mouse.up(20, 20); mouse.reset(); - fireEvent.contextMenu(canvas, { button: 2, clientX: 10, clientY: 10 }); + fireEvent.contextMenu(GlobalTestState.canvas, { + button: 2, + clientX: 10, + clientY: 10, + }); const contextMenu = document.querySelector(".context-menu"); const elementsBefore = h.elements; fireEvent.click(queryByText(contextMenu as HTMLElement, "Bring to front")!); @@ -1222,20 +1066,24 @@ describe("regression tests", () => { }); it("selecting 'Group selection' in context menu groups selected elements", () => { - clickTool("rectangle"); + UI.clickTool("rectangle"); mouse.down(10, 10); mouse.up(20, 20); - clickTool("rectangle"); + UI.clickTool("rectangle"); mouse.down(10, 10); mouse.up(20, 20); mouse.reset(); - withModifierKeys({ shift: true }, () => { + Keyboard.withModifierKeys({ shift: true }, () => { mouse.click(10, 10); }); - fireEvent.contextMenu(canvas, { button: 2, clientX: 1, clientY: 1 }); + fireEvent.contextMenu(GlobalTestState.canvas, { + button: 2, + clientX: 1, + clientY: 1, + }); const contextMenu = document.querySelector(".context-menu"); fireEvent.click( queryByText(contextMenu as HTMLElement, "Group selection")!, @@ -1246,24 +1094,28 @@ describe("regression tests", () => { }); it("selecting 'Ungroup selection' in context menu ungroups selected group", () => { - clickTool("rectangle"); + UI.clickTool("rectangle"); mouse.down(10, 10); mouse.up(20, 20); - clickTool("rectangle"); + UI.clickTool("rectangle"); mouse.down(10, 10); mouse.up(20, 20); mouse.reset(); - withModifierKeys({ shift: true }, () => { + Keyboard.withModifierKeys({ shift: true }, () => { mouse.click(10, 10); }); - withModifierKeys({ ctrl: true }, () => { - keyPress("g"); + Keyboard.withModifierKeys({ ctrl: true }, () => { + Keyboard.keyPress("g"); }); - fireEvent.contextMenu(canvas, { button: 2, clientX: 1, clientY: 1 }); + fireEvent.contextMenu(GlobalTestState.canvas, { + button: 2, + clientX: 1, + clientY: 1, + }); const contextMenu = document.querySelector(".context-menu"); fireEvent.click( queryByText(contextMenu as HTMLElement, "Ungroup selection")!, @@ -1276,87 +1128,87 @@ describe("regression tests", () => { }); it("deselects selected element, on pointer up, when click hits element bounding box but doesn't hit the element", () => { - clickTool("ellipse"); + UI.clickTool("ellipse"); mouse.down(); mouse.up(100, 100); // hits bounding box without hitting element mouse.down(); - expect(getSelectedElements().length).toBe(1); + expect(API.getSelectedElements().length).toBe(1); mouse.up(); - expect(getSelectedElements().length).toBe(0); + expect(API.getSelectedElements().length).toBe(0); }); it("switches selected element on pointer down", () => { - clickTool("rectangle"); + UI.clickTool("rectangle"); mouse.down(); mouse.up(10, 10); - clickTool("ellipse"); + UI.clickTool("ellipse"); mouse.down(10, 10); mouse.up(10, 10); - expect(getSelectedElement().type).toBe("ellipse"); + expect(API.getSelectedElement().type).toBe("ellipse"); // pointer down on rectangle mouse.reset(); mouse.down(); - expect(getSelectedElement().type).toBe("rectangle"); + expect(API.getSelectedElement().type).toBe("rectangle"); }); it("can drag element that covers another element, while another elem is selected", () => { - clickTool("rectangle"); + UI.clickTool("rectangle"); mouse.down(100, 100); mouse.up(200, 200); - clickTool("rectangle"); + UI.clickTool("rectangle"); mouse.reset(); mouse.down(100, 100); mouse.up(200, 200); - clickTool("ellipse"); + UI.clickTool("ellipse"); mouse.reset(); mouse.down(300, 300); mouse.up(350, 350); - expect(getSelectedElement().type).toBe("ellipse"); + expect(API.getSelectedElement().type).toBe("ellipse"); // pointer down on rectangle mouse.reset(); mouse.down(100, 100); mouse.up(200, 200); - expect(getSelectedElement().type).toBe("rectangle"); + expect(API.getSelectedElement().type).toBe("rectangle"); }); it("deselects selected element on pointer down when pointer doesn't hit any element", () => { - clickTool("rectangle"); + UI.clickTool("rectangle"); mouse.down(); mouse.up(10, 10); - expect(getSelectedElements().length).toBe(1); + expect(API.getSelectedElements().length).toBe(1); // pointer down on space without elements mouse.down(100, 100); - expect(getSelectedElements().length).toBe(0); + expect(API.getSelectedElements().length).toBe(0); }); it("Drags selected element when hitting only bounding box and keeps element selected", () => { - clickTool("ellipse"); + UI.clickTool("ellipse"); mouse.down(); mouse.up(10, 10); - const { x: prevX, y: prevY } = getSelectedElement(); + const { x: prevX, y: prevY } = API.getSelectedElement(); // drag element from point on bounding box that doesn't hit element mouse.reset(); mouse.down(); mouse.up(25, 25); - expect(getSelectedElement().x).toEqual(prevX + 25); - expect(getSelectedElement().y).toEqual(prevY + 25); + expect(API.getSelectedElement().x).toEqual(prevX + 25); + expect(API.getSelectedElement().y).toEqual(prevY + 25); }); it( @@ -1364,7 +1216,7 @@ describe("regression tests", () => { "when clicking intersection between A and B " + "B should be selected on pointer up", () => { - clickTool("rectangle"); + UI.clickTool("rectangle"); // change background color since default is transparent // and transparent elements can't be selected by clicking inside of them clickLabeledElement("Background"); @@ -1376,7 +1228,7 @@ describe("regression tests", () => { // 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. - clickTool("ellipse"); + UI.clickTool("ellipse"); mouse.reset(); mouse.down(500, 500); mouse.up(1000, 1000); @@ -1387,10 +1239,10 @@ describe("regression tests", () => { // pointer down on intersection between ellipse and rectangle mouse.down(900, 900); - expect(getSelectedElement().type).toBe("rectangle"); + expect(API.getSelectedElement().type).toBe("rectangle"); mouse.up(); - expect(getSelectedElement().type).toBe("ellipse"); + expect(API.getSelectedElement().type).toBe("ellipse"); }, ); @@ -1399,7 +1251,7 @@ describe("regression tests", () => { "when dragging on intersection between A and B " + "A should be dragged and keep being selected", () => { - clickTool("rectangle"); + UI.clickTool("rectangle"); // change background color since default is transparent // and transparent elements can't be selected by clicking inside of them clickLabeledElement("Background"); @@ -1411,7 +1263,7 @@ describe("regression tests", () => { // 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. - clickTool("ellipse"); + UI.clickTool("ellipse"); mouse.reset(); mouse.down(500, 500); mouse.up(1000, 1000); @@ -1420,139 +1272,139 @@ describe("regression tests", () => { mouse.reset(); mouse.click(); - const { x: prevX, y: prevY } = getSelectedElement(); + const { x: prevX, y: prevY } = API.getSelectedElement(); // pointer down on intersection between ellipse and rectangle mouse.down(900, 900); mouse.up(100, 100); - expect(getSelectedElement().type).toBe("rectangle"); - expect(getSelectedElement().x).toEqual(prevX + 100); - expect(getSelectedElement().y).toEqual(prevY + 100); + expect(API.getSelectedElement().type).toBe("rectangle"); + 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", () => { - clickTool("rectangle"); + UI.clickTool("rectangle"); mouse.down(); mouse.up(10, 10); - clickTool("ellipse"); + 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(); - withModifierKeys({ shift: true }, () => { + Keyboard.withModifierKeys({ shift: true }, () => { mouse.click(5, 5); }); - expect(getSelectedElements().length).toBe(2); + expect(API.getSelectedElements().length).toBe(2); // pointer down on space without elements mouse.reset(); mouse.down(500, 500); - expect(getSelectedElements().length).toBe(0); + expect(API.getSelectedElements().length).toBe(0); }); it("switches from group of selected elements to another element on pointer down", () => { - clickTool("rectangle"); + UI.clickTool("rectangle"); mouse.down(); mouse.up(10, 10); - clickTool("ellipse"); + UI.clickTool("ellipse"); mouse.down(100, 100); mouse.up(100, 100); - clickTool("diamond"); + 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(); - withModifierKeys({ shift: true }, () => { + Keyboard.withModifierKeys({ shift: true }, () => { mouse.click(110, 160); }); - expect(getSelectedElements().length).toBe(2); + expect(API.getSelectedElements().length).toBe(2); // select rectangle mouse.reset(); mouse.down(); - expect(getSelectedElement().type).toBe("rectangle"); + 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", () => { - clickTool("rectangle"); + UI.clickTool("rectangle"); mouse.down(); mouse.up(10, 10); - clickTool("ellipse"); + 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(); - withModifierKeys({ shift: true }, () => { + 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(getSelectedElements().length).toBe(2); + expect(API.getSelectedElements().length).toBe(2); mouse.up(); - expect(getSelectedElements().length).toBe(0); + 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", () => { - clickTool("rectangle"); + UI.clickTool("rectangle"); mouse.down(); mouse.up(10, 10); - clickTool("ellipse"); + 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(); - withModifierKeys({ shift: true }, () => { + Keyboard.withModifierKeys({ shift: true }, () => { mouse.click(5, 5); }); - expect(getSelectedElements().length).toBe(2); + expect(API.getSelectedElements().length).toBe(2); const { x: firstElementPrevX, y: firstElementPrevY, - } = getSelectedElements()[0]; + } = API.getSelectedElements()[0]; const { x: secondElementPrevX, y: secondElementPrevY, - } = getSelectedElements()[1]; + } = 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(getSelectedElements()[0].x).toEqual(firstElementPrevX + 25); - expect(getSelectedElements()[0].y).toEqual(firstElementPrevY + 25); + expect(API.getSelectedElements()[0].x).toEqual(firstElementPrevX + 25); + expect(API.getSelectedElements()[0].y).toEqual(firstElementPrevY + 25); - expect(getSelectedElements()[1].x).toEqual(secondElementPrevX + 25); - expect(getSelectedElements()[1].y).toEqual(secondElementPrevY + 25); + expect(API.getSelectedElements()[1].x).toEqual(secondElementPrevX + 25); + expect(API.getSelectedElements()[1].y).toEqual(secondElementPrevY + 25); - expect(getSelectedElements().length).toBe(2); + expect(API.getSelectedElements().length).toBe(2); }, ); @@ -1561,31 +1413,31 @@ describe("regression tests", () => { "when element that is not selected is clicked " + "should switch selection to not selected element on pointer up", () => { - clickTool("rectangle"); + UI.clickTool("rectangle"); mouse.down(); mouse.up(10, 10); - clickTool("ellipse"); + UI.clickTool("ellipse"); mouse.down(100, 100); mouse.up(100, 100); - clickTool("diamond"); + 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(); - withModifierKeys({ shift: true }, () => { + Keyboard.withModifierKeys({ shift: true }, () => { mouse.click(); }); // pointer down on ellipse mouse.down(110, 160); - expect(getSelectedElements().length).toBe(2); + expect(API.getSelectedElements().length).toBe(2); mouse.up(); - expect(getSelectedElement().type).toBe("ellipse"); + expect(API.getSelectedElement().type).toBe("ellipse"); }, ); @@ -1594,7 +1446,7 @@ describe("regression tests", () => { "and given B partialy overlaps A " + "when there's a shift-click on the overlapped section B is added to the selection", () => { - clickTool("rectangle"); + UI.clickTool("rectangle"); // change background color since default is transparent // and transparent elements can't be selected by clicking inside of them clickLabeledElement("Background"); @@ -1606,7 +1458,7 @@ describe("regression tests", () => { // 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. - clickTool("ellipse"); + UI.clickTool("ellipse"); mouse.reset(); mouse.down(500, 500); mouse.up(1000, 1000); @@ -1616,82 +1468,82 @@ describe("regression tests", () => { mouse.click(); // click on intersection between ellipse and rectangle - withModifierKeys({ shift: true }, () => { + Keyboard.withModifierKeys({ shift: true }, () => { mouse.click(900, 900); }); - expect(getSelectedElements().length).toBe(2); + expect(API.getSelectedElements().length).toBe(2); }, ); it("shift click on selected element should deselect it on pointer up", () => { - clickTool("rectangle"); + UI.clickTool("rectangle"); mouse.down(); mouse.up(10, 10); // Rectangle is already selected since creating // it was our last action - withModifierKeys({ shift: true }, () => { + Keyboard.withModifierKeys({ shift: true }, () => { mouse.down(); }); - expect(getSelectedElements().length).toBe(1); + expect(API.getSelectedElements().length).toBe(1); - withModifierKeys({ shift: true }, () => { + Keyboard.withModifierKeys({ shift: true }, () => { mouse.up(); }); - expect(getSelectedElements().length).toBe(0); + expect(API.getSelectedElements().length).toBe(0); }); it("single-clicking on a subgroup of a selected group should not alter selection", () => { - const rect1 = createElement("rectangle", { x: 10 }); - const rect2 = createElement("rectangle", { x: 50 }); - group([rect1, rect2]); + const rect1 = UI.createElement("rectangle", { x: 10 }); + const rect2 = UI.createElement("rectangle", { x: 50 }); + UI.group([rect1, rect2]); - const rect3 = createElement("rectangle", { x: 10, y: 50 }); - const rect4 = createElement("rectangle", { x: 50, y: 50 }); - group([rect3, rect4]); + const rect3 = UI.createElement("rectangle", { x: 10, y: 50 }); + const rect4 = UI.createElement("rectangle", { x: 50, y: 50 }); + UI.group([rect3, rect4]); - withModifierKeys({ ctrl: true }, () => { - keyPress("a"); - keyPress("g"); + Keyboard.withModifierKeys({ ctrl: true }, () => { + Keyboard.keyPress("a"); + Keyboard.keyPress("g"); }); const selectedGroupIds_prev = h.state.selectedGroupIds; - const selectedElements_prev = getSelectedElements(); + const selectedElements_prev = API.getSelectedElements(); mouse.clickOn(rect3); expect(h.state.selectedGroupIds).toEqual(selectedGroupIds_prev); - expect(getSelectedElements()).toEqual(selectedElements_prev); + expect(API.getSelectedElements()).toEqual(selectedElements_prev); }); it("Cmd/Ctrl-click exclusively select element under pointer", () => { - const rect1 = createElement("rectangle", { x: 0 }); - const rect2 = createElement("rectangle", { x: 30 }); + const rect1 = UI.createElement("rectangle", { x: 0 }); + const rect2 = UI.createElement("rectangle", { x: 30 }); - group([rect1, rect2]); + UI.group([rect1, rect2]); assertSelectedElements(rect1, rect2); - withModifierKeys({ ctrl: true }, () => { + Keyboard.withModifierKeys({ ctrl: true }, () => { mouse.clickOn(rect1); }); assertSelectedElements(rect1); - clearSelection(); - withModifierKeys({ ctrl: true }, () => { + API.clearSelection(); + Keyboard.withModifierKeys({ ctrl: true }, () => { mouse.clickOn(rect1); }); assertSelectedElements(rect1); - const rect3 = createElement("rectangle", { x: 60 }); - group([rect1, rect3]); + const rect3 = UI.createElement("rectangle", { x: 60 }); + UI.group([rect1, rect3]); assertSelectedElements(rect1, rect2, rect3); - withModifierKeys({ ctrl: true }, () => { + Keyboard.withModifierKeys({ ctrl: true }, () => { mouse.clickOn(rect1); }); assertSelectedElements(rect1); - clearSelection(); - withModifierKeys({ ctrl: true }, () => { + API.clearSelection(); + Keyboard.withModifierKeys({ ctrl: true }, () => { mouse.clickOn(rect3); }); assertSelectedElements(rect3); @@ -1703,25 +1555,25 @@ it( "when user clicks on B, on pointer up " + "only elements from B should be selected", () => { - const rect1 = createElement("rectangle", { y: 0 }); - const rect2 = createElement("rectangle", { y: 30 }); - const rect3 = createElement("rectangle", { y: 60 }); + const rect1 = UI.createElement("rectangle", { y: 0 }); + const rect2 = UI.createElement("rectangle", { y: 30 }); + const rect3 = UI.createElement("rectangle", { y: 60 }); - group([rect1, rect3]); + UI.group([rect1, rect3]); - expect(getSelectedElements().length).toBe(2); + expect(API.getSelectedElements().length).toBe(2); expect(Object.keys(h.state.selectedGroupIds).length).toBe(1); // Select second rectangle without deselecting group - withModifierKeys({ shift: true }, () => { + Keyboard.withModifierKeys({ shift: true }, () => { mouse.clickOn(rect2); }); - expect(getSelectedElements().length).toBe(3); + 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(getSelectedElements().length).toBe(2); + expect(API.getSelectedElements().length).toBe(2); expect(Object.keys(h.state.selectedGroupIds).length).toBe(1); }, ); @@ -1731,15 +1583,15 @@ it( "when user shift-clicks on B, on pointer up " + "only element A should be selected", () => { - clickTool("rectangle"); + UI.clickTool("rectangle"); mouse.down(); mouse.up(100, 100); - clickTool("rectangle"); + UI.clickTool("rectangle"); mouse.down(10, 10); mouse.up(100, 100); - clickTool("rectangle"); + UI.clickTool("rectangle"); mouse.down(10, 10); mouse.up(100, 100); @@ -1747,35 +1599,35 @@ it( // Third rectangle is selected because it was the last element // to be created. mouse.reset(); - withModifierKeys({ shift: true }, () => { + Keyboard.withModifierKeys({ shift: true }, () => { mouse.click(); }); // Create group with first and third rectangle - withModifierKeys({ ctrl: true }, () => { - keyPress("g"); + Keyboard.withModifierKeys({ ctrl: true }, () => { + Keyboard.keyPress("g"); }); - expect(getSelectedElements().length).toBe(2); + expect(API.getSelectedElements().length).toBe(2); const selectedGroupIds = Object.keys(h.state.selectedGroupIds); expect(selectedGroupIds.length).toBe(1); // Select second rectangle without deselecting group - withModifierKeys({ shift: true }, () => { + Keyboard.withModifierKeys({ shift: true }, () => { mouse.click(110, 110); }); - expect(getSelectedElements().length).toBe(3); + expect(API.getSelectedElements().length).toBe(3); // pointer down o first rectangle that is // part of the group mouse.reset(); - withModifierKeys({ shift: true }, () => { + Keyboard.withModifierKeys({ shift: true }, () => { mouse.down(); }); - expect(getSelectedElements().length).toBe(3); - withModifierKeys({ shift: true }, () => { + expect(API.getSelectedElements().length).toBe(3); + Keyboard.withModifierKeys({ shift: true }, () => { mouse.up(); }); - expect(getSelectedElements().length).toBe(1); + expect(API.getSelectedElements().length).toBe(1); }, ); diff --git a/src/tests/test-utils.ts b/src/tests/test-utils.ts index 40421844..d3c27366 100644 --- a/src/tests/test-utils.ts +++ b/src/tests/test-utils.ts @@ -19,14 +19,37 @@ type TestRenderFn = ( options?: Omit, ) => RenderResult; -const renderApp: TestRenderFn = (ui, options) => - render(ui, { +const renderApp: TestRenderFn = (ui, options) => { + const renderResult = render(ui, { queries: customQueries, ...options, }); + GlobalTestState.renderResult = renderResult; + GlobalTestState.canvas = renderResult.container.querySelector("canvas")!; + + return renderResult; +}; + // re-export everything export * from "@testing-library/react"; // override render method export { renderApp as render }; + +/** + * For state-sharing across test helpers. + * NOTE: there shouldn't be concurrency issues as each test is running in its + * own process and thus gets its own instance of this module when running + * tests in parallel. + */ +export class GlobalTestState { + /** + * automatically updated on each call to render() + */ + static renderResult: RenderResult = null!; + /** + * automatically updated on each call to render() + */ + static canvas: HTMLCanvasElement = null!; +}