From c966eb71a7939cc138c4a09ae38e335808c93fac Mon Sep 17 00:00:00 2001 From: taohe Date: Thu, 11 Jun 2026 13:57:56 +0800 Subject: [PATCH] Guard global shortcut key handling --- frontend/src/hooks/use-global-shortcuts.ts | 10 ++- .../unit/hooks/use-global-shortcuts.test.ts | 61 +++++++++++++++++++ 2 files changed, 69 insertions(+), 2 deletions(-) create mode 100644 frontend/tests/unit/hooks/use-global-shortcuts.test.ts diff --git a/frontend/src/hooks/use-global-shortcuts.ts b/frontend/src/hooks/use-global-shortcuts.ts index f04235828..0f267cf6f 100644 --- a/frontend/src/hooks/use-global-shortcuts.ts +++ b/frontend/src/hooks/use-global-shortcuts.ts @@ -19,16 +19,22 @@ interface Shortcut { export function useGlobalShortcuts(shortcuts: Shortcut[]) { useEffect(() => { function handleKeyDown(event: KeyboardEvent) { + if (typeof event.key !== "string" || event.key.length === 0) { + return; + } + const meta = event.metaKey || event.ctrlKey; + const eventKey = event.key.toLowerCase(); for (const shortcut of shortcuts) { + const shortcutKey = shortcut.key.toLowerCase(); if ( - event.key.toLowerCase() === shortcut.key.toLowerCase() && + eventKey === shortcutKey && meta === shortcut.meta && (shortcut.shift ?? false) === event.shiftKey ) { // Allow Cmd+K even in inputs (standard command palette behavior) - if (shortcut.key !== "k") { + if (shortcutKey !== "k") { const target = event.target as HTMLElement; const tag = target.tagName; if ( diff --git a/frontend/tests/unit/hooks/use-global-shortcuts.test.ts b/frontend/tests/unit/hooks/use-global-shortcuts.test.ts new file mode 100644 index 000000000..1c5028dfd --- /dev/null +++ b/frontend/tests/unit/hooks/use-global-shortcuts.test.ts @@ -0,0 +1,61 @@ +import { afterEach, describe, expect, test, vi } from "vitest"; + +type KeydownHandler = (event: KeyboardEvent) => void; + +async function loadHookWithCapturedHandler() { + let cleanup: (() => void) | undefined; + let keydownHandler: KeydownHandler | undefined; + + const addEventListener = vi.fn( + (type: string, listener: EventListenerOrEventListenerObject) => { + if (type === "keydown" && typeof listener === "function") { + keydownHandler = listener as KeydownHandler; + } + }, + ); + const removeEventListener = vi.fn(); + + vi.resetModules(); + vi.doMock("react", () => ({ + useEffect: (effect: () => void | (() => void)) => { + const result = effect(); + cleanup = typeof result === "function" ? result : undefined; + }, + })); + vi.stubGlobal("window", { addEventListener, removeEventListener }); + + const { useGlobalShortcuts } = await import("@/hooks/use-global-shortcuts"); + + return { + cleanup: () => cleanup?.(), + getKeydownHandler: () => keydownHandler, + useGlobalShortcuts, + }; +} + +afterEach(() => { + vi.doUnmock("react"); + vi.unstubAllGlobals(); + vi.resetModules(); +}); + +describe("useGlobalShortcuts", () => { + test("ignores keydown events without a key", async () => { + const action = vi.fn(); + const { getKeydownHandler, useGlobalShortcuts } = + await loadHookWithCapturedHandler(); + + useGlobalShortcuts([{ key: "k", meta: true, action }]); + + const keydownHandler = getKeydownHandler(); + expect(keydownHandler).toBeDefined(); + expect(() => + keydownHandler?.({ + ctrlKey: false, + metaKey: true, + shiftKey: false, + } as KeyboardEvent), + ).not.toThrow(); + expect(action).not.toHaveBeenCalled(); + }); +});