e626d33a7c
* 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.
92 lines
3.0 KiB
TypeScript
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();
|
|
});
|
|
});
|