216 lines
5.6 KiB
TypeScript
216 lines
5.6 KiB
TypeScript
import React from "react";
|
|
|
|
import { ARROW_TYPE, KEYS } from "@excalidraw/common";
|
|
|
|
import { CURSOR_HINT_COOLDOWN } from "../components/CursorHint";
|
|
import { Excalidraw } from "../index";
|
|
|
|
import { Keyboard, UI } from "./helpers/ui";
|
|
import {
|
|
GlobalTestState,
|
|
fireEvent,
|
|
mockBoundingClientRect,
|
|
render,
|
|
restoreOriginalGetBoundingClientRect,
|
|
waitFor,
|
|
} from "./test-utils";
|
|
|
|
const { h } = window;
|
|
|
|
const getCursorHint = () =>
|
|
document.querySelector<HTMLElement>("[data-testid='cursor-hint']");
|
|
|
|
// dispatch on the canvas so the event propagates to the container's
|
|
// keydown handler (`handleKeyboardGlobally` is disabled by default)
|
|
const pressKey = (key: string) => {
|
|
Keyboard.keyPress(key, GlobalTestState.interactiveCanvas);
|
|
};
|
|
|
|
describe("cursor hint", () => {
|
|
beforeEach(async () => {
|
|
await render(<Excalidraw />);
|
|
// hints are only shown once the pointer position is known
|
|
fireEvent.pointerMove(GlobalTestState.interactiveCanvas, {
|
|
clientX: 100,
|
|
clientY: 50,
|
|
});
|
|
});
|
|
|
|
it("shows hint when cycling arrow types via shortcut, and auto-hides it", async () => {
|
|
UI.clickTool("arrow");
|
|
expect(getCursorHint()).toBeNull();
|
|
|
|
// default arrow type is `round`, so cycling continues to `elbow`
|
|
pressKey(KEYS.A);
|
|
|
|
expect(h.state.currentItemArrowType).toBe(ARROW_TYPE.elbow);
|
|
expect(getCursorHint()).not.toBeNull();
|
|
expect(getCursorHint()!.querySelector("svg")).not.toBeNull();
|
|
|
|
pressKey(KEYS.A);
|
|
|
|
expect(h.state.currentItemArrowType).toBe(ARROW_TYPE.sharp);
|
|
expect(getCursorHint()).not.toBeNull();
|
|
|
|
await waitFor(() => {
|
|
expect(getCursorHint()).toBeNull();
|
|
});
|
|
});
|
|
|
|
it("shows hint when switching to the arrow tool via shortcut", () => {
|
|
UI.clickTool("rectangle");
|
|
|
|
pressKey(KEYS.A);
|
|
|
|
expect(h.state.activeTool.type).toBe("arrow");
|
|
expect(getCursorHint()).not.toBeNull();
|
|
});
|
|
|
|
it("shows hint when picking the line tool via shortcut", () => {
|
|
pressKey(KEYS.L);
|
|
|
|
expect(h.state.activeTool.type).toBe("line");
|
|
expect(getCursorHint()).not.toBeNull();
|
|
});
|
|
|
|
it("shows hint when picking a tool via numeric shortcut", () => {
|
|
pressKey("6");
|
|
|
|
expect(h.state.activeTool.type).toBe("line");
|
|
expect(getCursorHint()).not.toBeNull();
|
|
});
|
|
|
|
it("hides hint immediately on pointerdown", () => {
|
|
UI.clickTool("arrow");
|
|
|
|
pressKey(KEYS.A);
|
|
expect(getCursorHint()).not.toBeNull();
|
|
|
|
fireEvent.pointerDown(GlobalTestState.interactiveCanvas, {
|
|
clientX: 100,
|
|
clientY: 100,
|
|
});
|
|
|
|
expect(getCursorHint()).toBeNull();
|
|
|
|
// complete the gesture so no drag state leaks into other tests
|
|
fireEvent.pointerUp(GlobalTestState.interactiveCanvas, {
|
|
clientX: 100,
|
|
clientY: 100,
|
|
});
|
|
});
|
|
|
|
it("does not show hint when picking a tool via toolbar", () => {
|
|
UI.clickTool("arrow");
|
|
|
|
expect(h.state.activeTool.type).toBe("arrow");
|
|
expect(getCursorHint()).toBeNull();
|
|
});
|
|
|
|
describe("cooldown", () => {
|
|
// dismiss outside the canvas so we don't start drawing an element
|
|
// (which would block subsequent tool-switch shortcuts)
|
|
const dismissHint = () => {
|
|
fireEvent.pointerDown(document.body);
|
|
expect(getCursorHint()).toBeNull();
|
|
};
|
|
|
|
const expireCooldown = () => {
|
|
(h.app.cursorHints as any).lastShownAt =
|
|
Date.now() - CURSOR_HINT_COOLDOWN;
|
|
};
|
|
|
|
it("suppresses tool-switch hint during cooldown", () => {
|
|
pressKey(KEYS.A);
|
|
expect(getCursorHint()).not.toBeNull();
|
|
dismissHint();
|
|
|
|
pressKey("v");
|
|
pressKey(KEYS.A);
|
|
|
|
expect(h.state.activeTool.type).toBe("arrow");
|
|
expect(getCursorHint()).toBeNull();
|
|
});
|
|
|
|
it("shows tool-switch hint again once cooldown expires", () => {
|
|
pressKey(KEYS.A);
|
|
dismissHint();
|
|
pressKey("v");
|
|
|
|
expireCooldown();
|
|
pressKey(KEYS.A);
|
|
|
|
expect(getCursorHint()).not.toBeNull();
|
|
});
|
|
|
|
it("cycling arrow types bypasses cooldown", () => {
|
|
pressKey(KEYS.A);
|
|
dismissHint();
|
|
|
|
// arrow tool already active -> cycles
|
|
pressKey(KEYS.A);
|
|
|
|
expect(getCursorHint()).not.toBeNull();
|
|
});
|
|
|
|
it("numeric shortcuts bypass cooldown", () => {
|
|
pressKey(KEYS.A);
|
|
dismissHint();
|
|
pressKey("v");
|
|
|
|
pressKey("6");
|
|
expect(h.state.activeTool.type).toBe("line");
|
|
expect(getCursorHint()).not.toBeNull();
|
|
dismissHint();
|
|
|
|
pressKey("v");
|
|
pressKey("5");
|
|
expect(h.state.activeTool.type).toBe("arrow");
|
|
expect(getCursorHint()).not.toBeNull();
|
|
});
|
|
|
|
it("letter shortcut to another tool is also suppressed during cooldown", () => {
|
|
pressKey(KEYS.A);
|
|
dismissHint();
|
|
|
|
pressKey(KEYS.L);
|
|
|
|
expect(h.state.activeTool.type).toBe("line");
|
|
expect(getCursorHint()).toBeNull();
|
|
});
|
|
});
|
|
|
|
it("does not show hint when pointer position is unknown", () => {
|
|
// simulate no pointermove having happened yet
|
|
h.app.viewport.lastPosition.x = 0;
|
|
h.app.viewport.lastPosition.y = 0;
|
|
|
|
UI.clickTool("arrow");
|
|
pressKey(KEYS.A);
|
|
|
|
expect(h.state.currentItemArrowType).toBe(ARROW_TYPE.elbow);
|
|
expect(getCursorHint()).toBeNull();
|
|
});
|
|
|
|
it("tracks pointer position while visible", () => {
|
|
mockBoundingClientRect({
|
|
left: 100,
|
|
top: 50,
|
|
width: 1920,
|
|
height: 1080,
|
|
});
|
|
|
|
try {
|
|
UI.clickTool("arrow");
|
|
pressKey(KEYS.A);
|
|
|
|
fireEvent.pointerMove(window, { clientX: 200, clientY: 100 });
|
|
|
|
// container-relative position, 16px offset from the pointer
|
|
expect(getCursorHint()!.style.transform).toBe("translate(116px, 66px)");
|
|
} finally {
|
|
restoreOriginalGetBoundingClientRect();
|
|
}
|
|
});
|
|
});
|