From 8e9ca0498a3b150a5ee5b932fd5cfa94c1c8b63b Mon Sep 17 00:00:00 2001 From: John Coffey Date: Tue, 15 Sep 2026 07:27:12 -0700 Subject: [PATCH] Ignore a keydown that carries no key Picking a saved login from Chrome's password autofill dispatches a plain Event named "keydown", with no key on it. The shortcut listener passed it to comboOf, which read the key's length and threw -- an uncaught TypeError in the console on every sign-in. Harmless, since nothing was bound to it, but it was noise that looks like a real fault. comboOf now returns null for an event with no key, as it already does for a bare modifier, so the listener stops there. --- web/src/lib/__tests__/keyboardKeyless.test.ts | 42 +++++++++++++++++++ web/src/lib/keyboard.ts | 4 ++ 2 files changed, 46 insertions(+) create mode 100644 web/src/lib/__tests__/keyboardKeyless.test.ts diff --git a/web/src/lib/__tests__/keyboardKeyless.test.ts b/web/src/lib/__tests__/keyboardKeyless.test.ts new file mode 100644 index 0000000..be127f3 --- /dev/null +++ b/web/src/lib/__tests__/keyboardKeyless.test.ts @@ -0,0 +1,42 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; +import { comboOf, keyboard } from "@/lib/keyboard"; + +/* + * A "keydown" that carries no key. Chrome's password autofill dispatches one + * as a plain Event when a saved login is picked, and comboOf read `key.length` + * off it -- an uncaught TypeError in the console on every sign-in. + */ + +let pop: (() => void) | null = null; + +afterEach(() => { + pop?.(); + pop = null; +}); + +describe("a keydown with no key", () => { + it("has no combo", () => { + expect(comboOf(new Event("keydown") as KeyboardEvent)).toBeNull(); + }); + + it("reaches no binding and throws nothing", () => { + const handler = vi.fn(); + pop = keyboard.pushScope("test", [{ keys: "e", description: "Archive", group: "Mail", handler }]); + const errors: unknown[] = []; + const onError = (ev: ErrorEvent) => errors.push(ev.error); + window.addEventListener("error", onError); + try { + window.dispatchEvent(new Event("keydown", { bubbles: true })); + } finally { + window.removeEventListener("error", onError); + } + expect(errors).toEqual([]); + expect(handler).not.toHaveBeenCalled(); + }); + + it("leaves real keys alone", () => { + expect(comboOf(new KeyboardEvent("keydown", { key: "e" }))).toBe("e"); + expect(comboOf(new KeyboardEvent("keydown", { key: "E", shiftKey: true }))).toBe("E"); + expect(comboOf(new KeyboardEvent("keydown", { key: "Enter", ctrlKey: true }))).toMatch(/enter$/); + }); +}); diff --git a/web/src/lib/keyboard.ts b/web/src/lib/keyboard.ts index 93f973b..aa81f53 100644 --- a/web/src/lib/keyboard.ts +++ b/web/src/lib/keyboard.ts @@ -136,6 +136,10 @@ const isMac = typeof navigator !== "undefined" && /Mac|iPhone|iPad/.test(navigat export function comboOf(e: KeyboardEvent): string | null { const key = e.key; + // Chrome's password autofill dispatches a plain Event named "keydown" when a + // saved login is picked: no key, nothing to match, and reading its length + // threw on every sign-in. + if (!key) return null; if (key === "Shift" || key === "Control" || key === "Alt" || key === "Meta") return null; const parts: string[] = []; const mod = isMac ? e.metaKey : e.ctrlKey;