Files
multica/packages/core/shortcuts/store.test.ts
Jiayuan Zhang b72bd55d16 feat(shortcuts): make browser-reserved accelerators recordable on desktop (#5327)
* feat(shortcuts): make browser-reserved accelerators recordable on desktop

Cmd/Ctrl+P (and L/T/N/D/U) were rejected by the shortcut recorder on every
platform because a browser tab cannot reliably own them. The Electron
renderer receives these as plain keydowns — neither Electron's default menu
nor the desktop shell binds any of them — so the reservation now only
applies to the web runtime.

Adds a ShortcutRuntime dimension (configured by CoreProvider from the
client identity, with a preload-global fallback that is already correct at
module-eval time so store hydration sanitizes with the right runtime).
App-owned accelerators (W/R/Q, editing keys, zoom row) stay reserved
everywhere.

Closes MUL-4457

Co-authored-by: multica-agent <github@multica.ai>

* fix(shortcuts): limit desktop unlock to bare primary browser accelerators

Review follow-up (MUL-4457): the desktop skip matched primary+key with any
extra modifiers, which would also unreserve OS-owned combos such as
Option+Cmd+D (macOS Dock toggle) and Ctrl+Alt+T (Linux terminal). Only the
bare primary chord is now recordable on desktop; every extra-modifier
variant keeps the historical reservation on both runtimes. Adds regression
tests for the OS combos.

Co-authored-by: multica-agent <github@multica.ai>

---------

Co-authored-by: Lambda <lambda@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
2026-07-13 16:05:30 +08:00

147 lines
4.4 KiB
TypeScript

import { afterEach, describe, expect, it } from "vitest";
import { createShortcutChord } from "./definitions";
import { configureShortcutRuntime } from "./platform";
import {
findShortcutConflict,
getShortcut,
migrateShortcutState,
sanitizeShortcutOverrides,
useShortcutStore,
} from "./store";
afterEach(() => {
useShortcutStore.getState().resetAll();
configureShortcutRuntime(null);
});
describe("shortcut store", () => {
it("uses defaults, persists overrides, and can disable an action", () => {
expect(getShortcut("openSearch")).toEqual(
createShortcutChord("K", { primary: true }),
);
expect(getShortcut("send")).toEqual(
createShortcutChord("Enter", { primary: true }),
);
const custom = createShortcutChord("J", { primary: true });
useShortcutStore.getState().setShortcut("openSearch", custom);
expect(getShortcut("openSearch")).toEqual(custom);
useShortcutStore.getState().setShortcut("openSearch", null);
expect(getShortcut("openSearch")).toBeNull();
});
it("removes structurally equal overrides when restoring a default", () => {
useShortcutStore.getState().setShortcut(
"createIssue",
createShortcutChord("I", { primary: true }),
);
useShortcutStore.getState().setShortcut(
"createIssue",
createShortcutChord("C"),
);
expect(useShortcutStore.getState().overrides.createIssue).toBeUndefined();
});
it("rejects unsafe programmatic overrides as a store invariant", () => {
useShortcutStore.getState().setShortcut(
"send",
createShortcutChord("C"),
);
useShortcutStore.getState().setShortcut(
"send",
createShortcutChord("Enter", { shift: true }),
);
useShortcutStore.getState().setShortcut(
"send",
createShortcutChord("Enter", { primary: true, shift: true }),
);
expect(getShortcut("send")).toEqual(
createShortcutChord("Enter", { primary: true }),
);
});
it("drops persisted Send overrides outside Enter and Mod+Enter", () => {
expect(
sanitizeShortcutOverrides({
send: createShortcutChord("Enter", { shift: true }),
}),
).toEqual({});
});
it("keeps browser-only bindings when the runtime is desktop", () => {
const cmdP = createShortcutChord("P", { primary: true });
configureShortcutRuntime("desktop");
expect(sanitizeShortcutOverrides({ openSearch: cmdP })).toEqual({
openSearch: cmdP,
});
useShortcutStore.getState().setShortcut("openSearch", cmdP);
expect(getShortcut("openSearch")).toEqual(cmdP);
// The same persisted value hydrating in a browser falls back to default.
configureShortcutRuntime("web");
expect(sanitizeShortcutOverrides({ openSearch: cmdP })).toEqual({});
});
it("finds conflicts against defaults and overrides", () => {
expect(
findShortcutConflict(
"createIssue",
createShortcutChord("K", { primary: true }),
),
).toBe("openSearch");
const custom = createShortcutChord("1", { primary: true });
useShortcutStore.getState().setShortcut("goInbox", custom);
expect(findShortcutConflict("goChat", custom)).toBe("goInbox");
});
it("migrates v1 string overrides without losing disabled actions", () => {
expect(
migrateShortcutState({
overrides: { openSearch: "Mod+Shift+J", createIssue: null },
}, 1),
).toEqual({
overrides: {
openSearch: createShortcutChord("J", { primary: true, shift: true }),
createIssue: null,
},
});
});
it("drops malformed current-version storage and unknown actions", () => {
expect(
sanitizeShortcutOverrides({
openSearch: { key: "J", modifiers: { primary: true } },
findInIssue: createShortcutChord("J"),
send: createShortcutChord("Enter"),
goInbox: null,
goChat: createShortcutChord("Meta"),
unknownAction: createShortcutChord("X"),
}),
).toEqual({
send: createShortcutChord("Enter"),
goInbox: null,
});
expect(
migrateShortcutState({ overrides: "corrupt" }, 2),
).toEqual({ overrides: {} });
});
it("falls back to defaults instead of disabling when legacy data is invalid", () => {
expect(
migrateShortcutState({
overrides: {
openSearch: "",
createIssue: 42,
goInbox: null,
unknownAction: "Mod+X",
},
}, 1),
).toEqual({ overrides: { goInbox: null } });
});
});