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("[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(); // 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(); } }); });