Files
kandev/apps/web/hooks/use-app-shortcuts.test.ts
T
José Almeida e626d33a7c fix(web): make toggle-sidebar shortcut work on every route (#1283)
* fix(web): make toggle-sidebar shortcut work on every route

TOGGLE_SIDEBAR (Cmd/Ctrl+B) was handled only inside useEditorKeybinds,
mounted just by the dockview desktop layout, so it was dead on Kanban,
the task list, Office, Settings, etc. Lift it to a global app-root
listener (useAppShortcuts, mounted in GlobalCommands) that honors user
overrides via getShortcut and keeps the isEditableTarget guard, and drop
the now-duplicate branch from useEditorKeybinds. Adds a hook unit test
and a Playwright e2e covering Kanban, Settings, and the dockview session.

* fix(web): hydrate userSettings into the root store on settings routes

Each StateProvider creates its own store; the settings/general page mounts
a nested one that the root-mounted GlobalCommands/AppSidebar can't see. The
settings layout hydrated the root store but omitted userSettings, so
override-driven global shortcuts (TOGGLE_SIDEBAR, Quick Chat) were dead on
every /settings route. Hydrate userSettings there too, matching the
kanban/tasks/office/github routes.

* test(web): cover the macOS Cmd path; note the settings-layout catch trade-off

Address review feedback: add a metaKey (Cmd) variant to the useAppShortcuts
test so both ctrlOrCmd branches are exercised, and document that the settings
layout's outer catch intentionally drops userSettings overrides in the
already-degraded error path.
2026-06-06 23:09:12 +01:00

92 lines
3.0 KiB
TypeScript

import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { renderHook, cleanup } from "@testing-library/react";
import type { StoredShortcutOverrides } from "@/lib/keyboard/shortcut-overrides";
const mockToggleAppSidebar = vi.fn();
let mockShortcuts: StoredShortcutOverrides = {};
function buildState() {
return {
userSettings: { keyboardShortcuts: mockShortcuts },
toggleAppSidebar: mockToggleAppSidebar,
};
}
vi.mock("@/components/state-provider", () => ({
useAppStoreApi: () => ({ getState: () => buildState() }),
}));
import { useAppShortcuts } from "./use-app-shortcuts";
/** Bind TOGGLE_SIDEBAR to Cmd/Ctrl+B (it is unbound by default). */
const CTRL_B_OVERRIDE: StoredShortcutOverrides = {
TOGGLE_SIDEBAR: { key: "b", modifiers: { ctrlOrCmd: true } },
};
function pressB(modifier: "ctrlKey" | "metaKey", target: EventTarget = window): KeyboardEvent {
const init: KeyboardEventInit = { key: "b", bubbles: true, cancelable: true };
init[modifier] = true;
const event = new KeyboardEvent("keydown", init);
target.dispatchEvent(event);
return event;
}
// Cover both `ctrlOrCmd` branches: Ctrl on Windows/Linux, Cmd (metaKey) on macOS.
const pressCtrlB = (target?: EventTarget) => pressB("ctrlKey", target);
const pressMetaB = (target?: EventTarget) => pressB("metaKey", target);
describe("useAppShortcuts", () => {
beforeEach(() => {
mockToggleAppSidebar.mockClear();
mockShortcuts = {};
});
// renderHook attaches a window listener; unmount it between tests so stale
// listeners from earlier tests don't fire on later dispatches.
afterEach(() => cleanup());
it("does nothing by default — TOGGLE_SIDEBAR is unbound", () => {
renderHook(() => useAppShortcuts());
pressCtrlB();
expect(mockToggleAppSidebar).not.toHaveBeenCalled();
});
it("toggles the sidebar when the shortcut is bound to Cmd/Ctrl+B", () => {
mockShortcuts = CTRL_B_OVERRIDE;
renderHook(() => useAppShortcuts());
const event = pressCtrlB();
expect(mockToggleAppSidebar).toHaveBeenCalledTimes(1);
expect(event.defaultPrevented).toBe(true);
});
it("toggles the sidebar with Cmd (metaKey) too — the macOS path", () => {
mockShortcuts = CTRL_B_OVERRIDE;
renderHook(() => useAppShortcuts());
const event = pressMetaB();
expect(mockToggleAppSidebar).toHaveBeenCalledTimes(1);
expect(event.defaultPrevented).toBe(true);
});
it("ignores the shortcut while typing in an editable element", () => {
mockShortcuts = CTRL_B_OVERRIDE;
renderHook(() => useAppShortcuts());
const input = document.createElement("input");
document.body.appendChild(input);
pressCtrlB(input);
expect(mockToggleAppSidebar).not.toHaveBeenCalled();
input.remove();
});
it("removes its listener on unmount", () => {
mockShortcuts = CTRL_B_OVERRIDE;
const { unmount } = renderHook(() => useAppShortcuts());
unmount();
pressCtrlB();
expect(mockToggleAppSidebar).not.toHaveBeenCalled();
});
});