123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564 |
- import { reseed } from "../random";
- import React from "react";
- import ReactDOM from "react-dom";
- import * as Renderer from "../renderer/renderScene";
- import { render, fireEvent } from "./test-utils";
- import { App } from "../components/App";
- import { ToolName } from "./queries/toolQueries";
- import { KEYS, Key } from "../keys";
- import { setDateTimeForTests } from "../utils";
- import { ExcalidrawElement } from "../element/types";
- import { handlerRectangles } from "../element";
- const { h } = window;
- const renderScene = jest.spyOn(Renderer, "renderScene");
- let getByToolName: (name: string) => HTMLElement = null!;
- let canvas: HTMLCanvasElement = null!;
- function clickTool(toolName: ToolName) {
- fireEvent.click(getByToolName(toolName));
- }
- let lastClientX = 0;
- let lastClientY = 0;
- let pointerType: "mouse" | "pen" | "touch" = "mouse";
- function pointerDown(
- clientX: number = lastClientX,
- clientY: number = lastClientY,
- altKey: boolean = false,
- shiftKey: boolean = false,
- ) {
- lastClientX = clientX;
- lastClientY = clientY;
- fireEvent.pointerDown(canvas, {
- clientX,
- clientY,
- altKey,
- shiftKey,
- pointerId: 1,
- pointerType,
- });
- }
- function pointer2Down(clientX: number, clientY: number) {
- fireEvent.pointerDown(canvas, {
- clientX,
- clientY,
- pointerId: 2,
- pointerType,
- });
- }
- function pointer2Move(clientX: number, clientY: number) {
- fireEvent.pointerMove(canvas, {
- clientX,
- clientY,
- pointerId: 2,
- pointerType,
- });
- }
- function pointer2Up(clientX: number, clientY: number) {
- fireEvent.pointerUp(canvas, {
- clientX,
- clientY,
- pointerId: 2,
- pointerType,
- });
- }
- function pointerMove(
- clientX: number = lastClientX,
- clientY: number = lastClientY,
- altKey: boolean = false,
- shiftKey: boolean = false,
- ) {
- lastClientX = clientX;
- lastClientY = clientY;
- fireEvent.pointerMove(canvas, {
- clientX,
- clientY,
- altKey,
- shiftKey,
- pointerId: 1,
- pointerType,
- });
- }
- function pointerUp(
- clientX: number = lastClientX,
- clientY: number = lastClientY,
- altKey: boolean = false,
- shiftKey: boolean = false,
- ) {
- lastClientX = clientX;
- lastClientY = clientY;
- fireEvent.pointerUp(canvas, { pointerId: 1, pointerType, shiftKey, altKey });
- }
- function hotkeyDown(key: Key) {
- fireEvent.keyDown(document, { key: KEYS[key] });
- }
- function hotkeyUp(key: Key) {
- fireEvent.keyUp(document, {
- key: KEYS[key],
- });
- }
- function keyDown(
- key: string,
- ctrlKey: boolean = false,
- shiftKey: boolean = false,
- ) {
- fireEvent.keyDown(document, { key, ctrlKey, shiftKey });
- }
- function keyUp(
- key: string,
- ctrlKey: boolean = false,
- shiftKey: boolean = false,
- ) {
- fireEvent.keyUp(document, {
- key,
- ctrlKey,
- shiftKey,
- });
- }
- function hotkeyPress(key: Key) {
- hotkeyDown(key);
- hotkeyUp(key);
- }
- function keyPress(
- key: string,
- ctrlKey: boolean = false,
- shiftKey: boolean = false,
- ) {
- keyDown(key, ctrlKey, shiftKey);
- keyUp(key, ctrlKey, shiftKey);
- }
- function clickLabeledElement(label: string) {
- const element = document.querySelector(`[aria-label='${label}']`);
- if (!element) {
- throw new Error(`No labeled element found: ${label}`);
- }
- fireEvent.click(element);
- }
- function getSelectedElement(): ExcalidrawElement {
- const selectedElements = h.elements.filter(
- (element) => h.state.selectedElementIds[element.id],
- );
- if (selectedElements.length !== 1) {
- throw new Error(
- `expected 1 selected element; got ${selectedElements.length}`,
- );
- }
- return selectedElements[0];
- }
- function getResizeHandles() {
- const rects = handlerRectangles(
- getSelectedElement(),
- h.state.zoom,
- pointerType,
- );
- const rv: { [K in keyof typeof rects]: [number, number] } = {} as any;
- for (const handlePos in rects) {
- const [x, y, width, height] = rects[handlePos as keyof typeof rects];
- rv[handlePos as keyof typeof rects] = [x + width / 2, y + height / 2];
- }
- return rv;
- }
- /**
- * 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.
- */
- function checkpoint(name: string) {
- expect(renderScene.mock.calls.length).toMatchSnapshot(
- `[${name}] number of renders`,
- );
- expect(h.state).toMatchSnapshot(`[${name}] appState`);
- expect(h.history.getSnapshotForTest()).toMatchSnapshot(`[${name}] history`);
- expect(h.elements.length).toMatchSnapshot(`[${name}] number of elements`);
- h.elements.forEach((element, i) =>
- expect(element).toMatchSnapshot(`[${name}] element ${i}`),
- );
- }
- beforeEach(() => {
- // Unmount ReactDOM from root
- ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
- localStorage.clear();
- renderScene.mockClear();
- h.history.clear();
- reseed(7);
- setDateTimeForTests("201933152653");
- pointerType = "mouse";
- const renderResult = render(<App />);
- getByToolName = renderResult.getByToolName;
- canvas = renderResult.container.querySelector("canvas")!;
- });
- afterEach(() => {
- checkpoint("end of test");
- });
- describe("regression tests", () => {
- it("draw every type of shape", () => {
- clickTool("rectangle");
- pointerDown(10, 10);
- pointerMove(20, 20);
- pointerUp();
- clickTool("diamond");
- pointerDown(30, 10);
- pointerMove(40, 20);
- pointerUp();
- clickTool("ellipse");
- pointerDown(50, 10);
- pointerMove(60, 20);
- pointerUp();
- clickTool("arrow");
- pointerDown(70, 10);
- pointerMove(80, 20);
- pointerUp();
- clickTool("line");
- pointerDown(90, 10);
- pointerMove(100, 20);
- pointerUp();
- clickTool("arrow");
- pointerDown(10, 30);
- pointerUp();
- pointerMove(20, 40);
- pointerUp();
- pointerMove(10, 50);
- pointerUp();
- hotkeyPress("ENTER");
- clickTool("line");
- pointerDown(30, 30);
- pointerUp();
- pointerMove(40, 40);
- pointerUp();
- pointerMove(30, 50);
- pointerUp();
- hotkeyPress("ENTER");
- });
- it("click to select a shape", () => {
- clickTool("rectangle");
- pointerDown(10, 10);
- pointerMove(20, 20);
- pointerUp();
- clickTool("rectangle");
- pointerDown(30, 10);
- pointerMove(40, 20);
- pointerUp();
- const prevSelectedId = getSelectedElement().id;
- pointerDown(10, 10);
- pointerUp();
- expect(getSelectedElement().id).not.toEqual(prevSelectedId);
- });
- for (const [keys, shape] of [
- ["2r", "rectangle"],
- ["3d", "diamond"],
- ["4e", "ellipse"],
- ["5a", "arrow"],
- ["6l", "line"],
- ] as [string, ExcalidrawElement["type"]][]) {
- for (const key of keys) {
- it(`hotkey ${key} selects ${shape} tool`, () => {
- keyPress(key);
- pointerDown(10, 10);
- pointerMove(20, 20);
- pointerUp();
- expect(getSelectedElement().type).toBe(shape);
- });
- }
- }
- it("change the properties of a shape", () => {
- clickTool("rectangle");
- pointerDown(10, 10);
- pointerMove(20, 20);
- pointerUp();
- clickLabeledElement("Background");
- clickLabeledElement("#fa5252");
- clickLabeledElement("Stroke");
- clickLabeledElement("#5f3dc4");
- expect(getSelectedElement().backgroundColor).toBe("#fa5252");
- expect(getSelectedElement().strokeColor).toBe("#5f3dc4");
- });
- it("resize an element, trying every resize handle", () => {
- clickTool("rectangle");
- pointerDown(10, 10);
- pointerMove(20, 20);
- pointerUp();
- const resizeHandles = getResizeHandles();
- for (const handlePos in resizeHandles) {
- const [x, y] = resizeHandles[handlePos as keyof typeof resizeHandles];
- const { width: prevWidth, height: prevHeight } = getSelectedElement();
- pointerDown(x, y);
- pointerMove(x - 5, y - 5);
- pointerUp();
- const {
- width: nextWidthNegative,
- height: nextHeightNegative,
- } = getSelectedElement();
- expect(
- prevWidth !== nextWidthNegative || prevHeight !== nextHeightNegative,
- ).toBeTruthy();
- checkpoint(`resize handle ${handlePos} (-5, -5)`);
- pointerDown();
- pointerMove(x, y);
- pointerUp();
- const { width, height } = getSelectedElement();
- expect(width).toBe(prevWidth);
- expect(height).toBe(prevHeight);
- checkpoint(`unresize handle ${handlePos} (-5, -5)`);
- pointerDown(x, y);
- pointerMove(x + 5, y + 5);
- pointerUp();
- const {
- width: nextWidthPositive,
- height: nextHeightPositive,
- } = getSelectedElement();
- expect(
- prevWidth !== nextWidthPositive || prevHeight !== nextHeightPositive,
- ).toBeTruthy();
- checkpoint(`resize handle ${handlePos} (+5, +5)`);
- pointerDown();
- pointerMove(x, y);
- pointerUp();
- const { width: finalWidth, height: finalHeight } = getSelectedElement();
- expect(finalWidth).toBe(prevWidth);
- expect(finalHeight).toBe(prevHeight);
- checkpoint(`unresize handle ${handlePos} (+5, +5)`);
- }
- });
- it("click on an element and drag it", () => {
- clickTool("rectangle");
- pointerDown(10, 10);
- pointerMove(20, 20);
- pointerUp();
- const { x: prevX, y: prevY } = getSelectedElement();
- pointerDown(10, 10);
- pointerMove(20, 20);
- pointerUp();
- const { x: nextX, y: nextY } = getSelectedElement();
- expect(nextX).toBeGreaterThan(prevX);
- expect(nextY).toBeGreaterThan(prevY);
- checkpoint("dragged");
- pointerDown();
- pointerMove(10, 10);
- pointerUp();
- const { x, y } = getSelectedElement();
- expect(x).toBe(prevX);
- expect(y).toBe(prevY);
- });
- it("alt-drag duplicates an element", () => {
- clickTool("rectangle");
- pointerDown(10, 10);
- pointerMove(20, 20);
- pointerUp();
- expect(
- h.elements.filter((element) => element.type === "rectangle").length,
- ).toBe(1);
- pointerDown(10, 10, true);
- pointerMove(20, 20, true);
- pointerUp(20, 20, true);
- expect(
- h.elements.filter((element) => element.type === "rectangle").length,
- ).toBe(2);
- });
- it("click-drag to select a group", () => {
- clickTool("rectangle");
- pointerDown(10, 10);
- pointerMove(20, 20);
- pointerUp();
- clickTool("rectangle");
- pointerDown(30, 10);
- pointerMove(40, 20);
- pointerUp();
- clickTool("rectangle");
- pointerDown(50, 10);
- pointerMove(60, 20);
- pointerUp();
- pointerDown(0, 0);
- pointerMove(45, 25);
- pointerUp();
- expect(
- h.elements.filter((element) => h.state.selectedElementIds[element.id])
- .length,
- ).toBe(2);
- });
- it("shift-click to select a group, then drag", () => {
- clickTool("rectangle");
- pointerDown(10, 10);
- pointerMove(20, 20);
- pointerUp();
- clickTool("rectangle");
- pointerDown(30, 10);
- pointerMove(40, 20);
- pointerUp();
- const prevRectsXY = h.elements
- .filter((element) => element.type === "rectangle")
- .map((element) => ({ x: element.x, y: element.y }));
- pointerDown(10, 10);
- pointerUp();
- pointerDown(30, 10, false, true);
- pointerUp();
- pointerDown(30, 10);
- pointerMove(40, 20);
- pointerUp();
- 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).toBe(1);
- pointerType = "touch";
- pointerDown(50, 50);
- pointer2Down(60, 50);
- pointerMove(40, 50);
- pointer2Move(60, 50);
- expect(h.state.zoom).toBeGreaterThan(1);
- const zoomed = h.state.zoom;
- pointerMove(45, 50);
- pointer2Move(55, 50);
- expect(h.state.zoom).toBeLessThan(zoomed);
- pointerUp(45, 50);
- pointer2Up(55, 50);
- });
- it("two-finger scroll works", () => {
- const startScrollY = h.state.scrollY;
- pointerDown(50, 50);
- pointer2Down(60, 50);
- pointerMove(50, 40);
- pointer2Move(60, 40);
- pointerUp(50, 40);
- pointer2Up(60, 40);
- expect(h.state.scrollY).toBeLessThan(startScrollY);
- const startScrollX = h.state.scrollX;
- pointerDown(50, 50);
- pointer2Down(50, 60);
- pointerMove(60, 50);
- pointer2Move(60, 60);
- pointerUp(60, 50);
- pointer2Up(60, 60);
- expect(h.state.scrollX).toBeGreaterThan(startScrollX);
- });
- it("spacebar + drag scrolls the canvas", () => {
- const { scrollX: startScrollX, scrollY: startScrollY } = h.state;
- hotkeyDown("SPACE");
- pointerDown(50, 50);
- pointerMove(60, 60);
- pointerUp();
- hotkeyUp("SPACE");
- const { scrollX, scrollY } = h.state;
- expect(scrollX).not.toEqual(startScrollX);
- expect(scrollY).not.toEqual(startScrollY);
- });
- it("arrow keys", () => {
- clickTool("rectangle");
- pointerDown(10, 10);
- pointerMove(20, 20);
- pointerUp();
- hotkeyPress("ARROW_LEFT");
- hotkeyPress("ARROW_LEFT");
- hotkeyPress("ARROW_RIGHT");
- hotkeyPress("ARROW_UP");
- hotkeyPress("ARROW_UP");
- hotkeyPress("ARROW_DOWN");
- });
- it("undo/redo drawing an element", () => {
- clickTool("rectangle");
- pointerDown(10, 10);
- pointerMove(20, 20);
- pointerUp();
- clickTool("rectangle");
- pointerDown(30, 10);
- pointerMove(40, 20);
- pointerUp();
- clickTool("rectangle");
- pointerDown(50, 10);
- pointerMove(60, 20);
- pointerUp();
- expect(h.elements.filter((element) => !element.isDeleted).length).toBe(3);
- keyPress("z", true);
- expect(h.elements.filter((element) => !element.isDeleted).length).toBe(2);
- keyPress("z", true);
- expect(h.elements.filter((element) => !element.isDeleted).length).toBe(1);
- keyPress("z", true, true);
- expect(h.elements.filter((element) => !element.isDeleted).length).toBe(2);
- });
- it("zoom hotkeys", () => {
- expect(h.state.zoom).toBe(1);
- fireEvent.keyDown(document, { code: "Equal", ctrlKey: true });
- fireEvent.keyUp(document, { code: "Equal", ctrlKey: true });
- expect(h.state.zoom).toBeGreaterThan(1);
- fireEvent.keyDown(document, { code: "Minus", ctrlKey: true });
- fireEvent.keyUp(document, { code: "Minus", ctrlKey: true });
- expect(h.state.zoom).toBe(1);
- });
- });
|