Browse Source

factor out test helpers (#2093)

David Luzar 4 years ago
parent
commit
8b9e2a540d
4 changed files with 423 additions and 357 deletions
  1. 32 0
      src/tests/helpers/api.ts
  2. 203 0
      src/tests/helpers/ui.ts
  3. 163 355
      src/tests/regressionTests.test.tsx
  4. 25 2
      src/tests/test-utils.ts

+ 32 - 0
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);
+  };
+}

+ 203 - 0
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");
+    });
+  }
+}

File diff suppressed because it is too large
+ 163 - 355
src/tests/regressionTests.test.tsx


+ 25 - 2
src/tests/test-utils.ts

@@ -19,14 +19,37 @@ type TestRenderFn = (
   options?: Omit<RenderOptions, "queries">,
 ) => RenderResult<typeof customQueries>;
 
-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<typeof customQueries> = null!;
+  /**
+   * automatically updated on each call to render()
+   */
+  static canvas: HTMLCanvasElement = null!;
+}

Some files were not shown because too many files changed in this diff