Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions e2e/utils/event-test-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,20 +8,20 @@ const LOCAL_DB_NAME = "compass-local";
const FORM_TIMEOUT = 10000;

/**
* Dispatch a keyboard shortcut to the window.
* Dispatch a keyboard shortcut to the document.
* Uses the same event properties as the app's internal pressKey utility.
*/
const pressShortcut = async (page: Page, key: string) => {
await page.evaluate((shortcut) => {
window.dispatchEvent(
document.dispatchEvent(
Copy link

Copilot AI Mar 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment says "Dispatch a keyboard shortcut to the window" but the code now dispatches events to document instead. Update the JSDoc to say "Dispatch a keyboard shortcut to the document" to match the actual behavior.

Copilot uses AI. Check for mistakes.
new KeyboardEvent("keydown", {
key: shortcut,
bubbles: true,
cancelable: true,
composed: true,
}),
);
window.dispatchEvent(
document.dispatchEvent(
new KeyboardEvent("keyup", {
key: shortcut,
bubbles: true,
Expand Down
2 changes: 1 addition & 1 deletion packages/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
"@react-oauth/google": "^0.7.0",
"@reduxjs/toolkit": "^1.6.1",
"@svgr/webpack": "^6.2.1",
"@tanstack/react-hotkeys": "^0.3.1",
"axios": "^1.2.2",
"classnames": "^2.3.1",
"css-loader": "^6.3.0",
Expand All @@ -34,7 +35,6 @@
"react-cmdk": "^1.3.9",
"react-datepicker": "^4.2.1",
"react-dom": "^18.1.0",
"react-hotkeys-hook": "^4.4.1",
"react-modal": "^3.16.1",
"react-redux": "^8.1.2",
"react-router-dom": "^6.8.1",
Expand Down
156 changes: 84 additions & 72 deletions packages/web/src/common/hooks/useKeyboardEvent.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { fireEvent } from "@testing-library/react";
import { renderHook } from "@web/__tests__/__mocks__/mock.render";
import { renderHook, waitFor } from "@web/__tests__/__mocks__/mock.render";
import { useKeyboardEvent } from "@web/common/hooks/useKeyboardEvent";
import { getModifierKey } from "@web/common/utils/shortcut/shortcut.util";

Expand All @@ -12,15 +11,34 @@ const mockIsEditable = jest.requireMock(
"@web/views/Day/util/day.shortcut.util",
).isEditable;

/**
* Helper function to dispatch a keyboard event to the document
*/
function dispatchKeyEvent(
key: string,
type: "keydown" | "keyup",
options: KeyboardEventInit = {},
) {
const event = new KeyboardEvent(type, {
key,
bubbles: true,
cancelable: true,
composed: true,
...options,
});
document.dispatchEvent(event);
}

describe("useKeyboardEvent", () => {
const mockHandler = jest.fn();

beforeEach(() => {
jest.clearAllMocks();
mockIsEditable.mockReturnValue(false);
document.body.removeAttribute("data-app-locked");
});

it("should call handler when key combination matches (keyup default)", () => {
it("should call handler when key is pressed (keyup)", async () => {
renderHook(() =>
useKeyboardEvent({
combination: ["a"],
Expand All @@ -29,13 +47,16 @@ describe("useKeyboardEvent", () => {
}),
);

fireEvent.keyDown(window, { key: "a" });
fireEvent.keyUp(window, { key: "a" });
dispatchKeyEvent("a", "keydown");
dispatchKeyEvent("a", "keyup");

expect(mockHandler).toHaveBeenCalledTimes(1);
await waitFor(() => {
expect(mockHandler).toHaveBeenCalledTimes(1);
expect(mockHandler).toHaveBeenCalledWith(expect.any(KeyboardEvent));
});
});

it("should call handler when key combination matches (keydown)", () => {
it("should call handler when key is pressed (keydown)", async () => {
renderHook(() =>
useKeyboardEvent({
combination: ["a"],
Expand All @@ -44,91 +65,78 @@ describe("useKeyboardEvent", () => {
}),
);

fireEvent.keyDown(window, { key: "a" });

expect(mockHandler).toHaveBeenCalledTimes(1);
});

it("should not call handler when key combination does not match", () => {
renderHook(() =>
useKeyboardEvent({
combination: ["a"],
handler: mockHandler,
eventType: "keyup",
}),
);

fireEvent.keyDown(window, { key: "b" });
fireEvent.keyUp(window, { key: "b" });

expect(mockHandler).not.toHaveBeenCalled();
});

it("should handle multi-key combinations", () => {
renderHook(() =>
useKeyboardEvent({
combination: [getModifierKey(), "a"],
handler: mockHandler,
eventType: "keyup",
}),
);

fireEvent.keyDown(window, { key: getModifierKey() });
fireEvent.keyDown(window, { key: "a", ctrlKey: true });
fireEvent.keyUp(window, { key: "a", ctrlKey: true });
dispatchKeyEvent("a", "keydown");

expect(mockHandler).toHaveBeenCalledTimes(1);
await waitFor(() => {
expect(mockHandler).toHaveBeenCalledTimes(1);
});
});

it("should respect exactMatch = true (default)", () => {
renderHook(() =>
useKeyboardEvent({
combination: ["a"],
handler: mockHandler,
exactMatch: true,
eventType: "keyup",
}),
);
it("should handle multi-key combinations with modifier keys", async () => {
const modifierKey = getModifierKey();
const isCtrl = modifierKey === "Control";

fireEvent.keyDown(window, { key: "Shift" });
fireEvent.keyDown(window, { key: "a", shiftKey: true });
fireEvent.keyUp(window, { key: "a", shiftKey: true });

expect(mockHandler).not.toHaveBeenCalled();
});

it("should respect exactMatch = false", () => {
renderHook(() =>
useKeyboardEvent({
combination: ["a"],
combination: [modifierKey, "a"],
handler: mockHandler,
exactMatch: false,
eventType: "keyup",
}),
);

// ... (comments)
// Press modifier key first
dispatchKeyEvent(modifierKey, "keydown", {
ctrlKey: isCtrl,
metaKey: !isCtrl,
});

// Then press 'a' while holding modifier
dispatchKeyEvent("a", "keydown", {
ctrlKey: isCtrl,
metaKey: !isCtrl,
});
dispatchKeyEvent("a", "keyup", {
ctrlKey: isCtrl,
metaKey: !isCtrl,
});

await waitFor(() => {
expect(mockHandler).toHaveBeenCalled();
});
});

it("should not call handler when editing if listenWhileEditing is false (default)", () => {
it("should not call handler when editing if listenWhileEditing is false", async () => {
mockIsEditable.mockReturnValue(true);

renderHook(() =>
useKeyboardEvent({
combination: ["a"],
handler: mockHandler,
listenWhileEditing: false,
eventType: "keyup",
}),
);

fireEvent.keyDown(window, { key: "a" });
fireEvent.keyUp(window, { key: "a" });
dispatchKeyEvent("a", "keydown");
dispatchKeyEvent("a", "keyup");

// Wait a bit to ensure handler is not called
await new Promise((resolve) => setTimeout(resolve, 100));

expect(mockHandler).not.toHaveBeenCalled();
});

it("should call handler when editing if listenWhileEditing is true", () => {
it("should call handler and blur element when editing if listenWhileEditing is true", async () => {
mockIsEditable.mockReturnValue(true);
const mockBlur = jest.fn();
const mockElement = document.createElement("input");
mockElement.blur = mockBlur;

Object.defineProperty(document, "activeElement", {
value: mockElement,
writable: true,
configurable: true,
});

renderHook(() =>
useKeyboardEvent({
Expand All @@ -139,13 +147,16 @@ describe("useKeyboardEvent", () => {
}),
);

fireEvent.keyDown(window, { key: "a" });
fireEvent.keyUp(window, { key: "a" });
dispatchKeyEvent("a", "keydown");
dispatchKeyEvent("a", "keyup");

expect(mockHandler).toHaveBeenCalledTimes(1);
await waitFor(() => {
expect(mockHandler).toHaveBeenCalled();
expect(mockBlur).toHaveBeenCalled();
});
});

it("should not call handler when the app is locked", () => {
it("should not call handler when the app is locked", async () => {
document.body.setAttribute("data-app-locked", "true");

renderHook(() =>
Expand All @@ -156,11 +167,12 @@ describe("useKeyboardEvent", () => {
}),
);

fireEvent.keyDown(window, { key: "a" });
fireEvent.keyUp(window, { key: "a" });
dispatchKeyEvent("a", "keydown");
dispatchKeyEvent("a", "keyup");

expect(mockHandler).not.toHaveBeenCalled();
// Wait a bit to ensure handler is not called
await new Promise((resolve) => setTimeout(resolve, 100));

document.body.removeAttribute("data-app-locked");
expect(mockHandler).not.toHaveBeenCalled();
});
});
Loading