b0b968cb22
* feat(web): add reverse direction to recent task switcher Cmd/Ctrl+Shift+Space now cycles the recent task switcher backward (most-recent toward oldest), complementing the forward Cmd/Ctrl+Space. Both directions are user-configurable via Settings > Keyboard Shortcuts. - new TASK_SWITCHER_REVERSE shortcut; Shift keeps it mutually exclusive with the forward binding so both can be held together - thread a cycle direction through openSwitcher/cycleSwitcher; reverse opens on the oldest non-current task - commit on release of either binding's hold modifier - switcher footer shows both Next / Previous bindings * fix(web): commit task switcher only on the active binding's modifier Address PR review on the reverse task switcher: - track the binding that drove the switcher and commit only on ITS hold modifier release, so divergent custom forward/reverse bindings can't commit early (greptile/coderabbit) - move the CycleDirection type below the import block - correct the reverse-shortcut direction comment (oldest -> most-recent) - assert exact task titles in the reverse e2e precondition * test(web): assert task switcher stays open on Shift-release Make explicit that releasing the secondary modifier alone does not commit the reverse switcher — only the hold modifier does.
121 lines
4.0 KiB
TypeScript
121 lines
4.0 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import { KEYS, type KeyboardShortcut, type Platform } from "@/lib/keyboard/constants";
|
|
import {
|
|
getHoldModifier,
|
|
hasHoldModifier,
|
|
isCommitReleaseEvent,
|
|
isCycleShortcutEvent,
|
|
} from "./recent-task-switcher-keys";
|
|
|
|
function event(overrides: Partial<KeyboardEvent>): KeyboardEvent {
|
|
return {
|
|
key: "",
|
|
ctrlKey: false,
|
|
metaKey: false,
|
|
altKey: false,
|
|
shiftKey: false,
|
|
repeat: false,
|
|
...overrides,
|
|
} as KeyboardEvent;
|
|
}
|
|
|
|
const taskSwitcherShortcut: KeyboardShortcut = {
|
|
key: KEYS.SPACE,
|
|
modifiers: { ctrlOrCmd: true },
|
|
};
|
|
|
|
const taskSwitcherReverseShortcut: KeyboardShortcut = {
|
|
key: KEYS.SPACE,
|
|
modifiers: { ctrlOrCmd: true, shift: true },
|
|
};
|
|
|
|
function platform(platform: Platform): Platform {
|
|
return platform;
|
|
}
|
|
|
|
describe("recent task switcher key helpers", () => {
|
|
it("uses Meta as the hold modifier for ctrlOrCmd on macOS", () => {
|
|
expect(getHoldModifier(taskSwitcherShortcut, platform("mac"))).toBe("Meta");
|
|
});
|
|
|
|
it("uses Control as the hold modifier for ctrlOrCmd off macOS", () => {
|
|
expect(getHoldModifier(taskSwitcherShortcut, platform("linux"))).toBe("Control");
|
|
expect(getHoldModifier(taskSwitcherShortcut, platform("windows"))).toBe("Control");
|
|
});
|
|
|
|
it("detects shortcuts without a hold modifier", () => {
|
|
expect(hasHoldModifier({ key: KEYS.SPACE })).toBe(false);
|
|
expect(getHoldModifier({ key: KEYS.SPACE })).toBeNull();
|
|
});
|
|
|
|
it("matches non-repeated cycle keydown events while required modifiers are held", () => {
|
|
expect(
|
|
isCycleShortcutEvent(event({ key: KEYS.SPACE, ctrlKey: true }), taskSwitcherShortcut),
|
|
).toBe(true);
|
|
});
|
|
|
|
it("does not cycle on repeated keydown events", () => {
|
|
expect(
|
|
isCycleShortcutEvent(
|
|
event({ key: KEYS.SPACE, ctrlKey: true, repeat: true }),
|
|
taskSwitcherShortcut,
|
|
),
|
|
).toBe(false);
|
|
});
|
|
|
|
it("matches custom multi-modifier cycle shortcuts", () => {
|
|
const shortcut: KeyboardShortcut = {
|
|
key: KEYS.Y,
|
|
modifiers: { ctrlOrCmd: true, shift: true },
|
|
};
|
|
|
|
expect(isCycleShortcutEvent(event({ key: "y", ctrlKey: true, shiftKey: true }), shortcut)).toBe(
|
|
true,
|
|
);
|
|
expect(isCycleShortcutEvent(event({ key: "y", ctrlKey: true }), shortcut)).toBe(false);
|
|
});
|
|
|
|
it("separates forward and reverse switcher shortcuts by the Shift modifier", () => {
|
|
const forwardEvent = event({ key: KEYS.SPACE, ctrlKey: true });
|
|
const reverseEvent = event({ key: KEYS.SPACE, ctrlKey: true, shiftKey: true });
|
|
|
|
// Forward fires only without Shift; reverse fires only with Shift.
|
|
expect(isCycleShortcutEvent(forwardEvent, taskSwitcherShortcut)).toBe(true);
|
|
expect(isCycleShortcutEvent(forwardEvent, taskSwitcherReverseShortcut)).toBe(false);
|
|
expect(isCycleShortcutEvent(reverseEvent, taskSwitcherShortcut)).toBe(false);
|
|
expect(isCycleShortcutEvent(reverseEvent, taskSwitcherReverseShortcut)).toBe(true);
|
|
});
|
|
|
|
it("commits the reverse shortcut on Ctrl/Cmd release, not Shift release", () => {
|
|
expect(
|
|
isCommitReleaseEvent(
|
|
event({ key: "Control" }),
|
|
taskSwitcherReverseShortcut,
|
|
platform("linux"),
|
|
),
|
|
).toBe(true);
|
|
expect(
|
|
isCommitReleaseEvent(event({ key: "Shift" }), taskSwitcherReverseShortcut, platform("linux")),
|
|
).toBe(false);
|
|
});
|
|
|
|
it("matches the hold modifier release, not secondary modifier release", () => {
|
|
const shortcut: KeyboardShortcut = {
|
|
key: KEYS.Y,
|
|
modifiers: { ctrlOrCmd: true, shift: true },
|
|
};
|
|
|
|
expect(isCommitReleaseEvent(event({ key: "Control" }), shortcut, platform("linux"))).toBe(true);
|
|
expect(isCommitReleaseEvent(event({ key: "Shift" }), shortcut, platform("linux"))).toBe(false);
|
|
});
|
|
|
|
it("matches Meta release for ctrlOrCmd on macOS", () => {
|
|
expect(
|
|
isCommitReleaseEvent(event({ key: "Meta" }), taskSwitcherShortcut, platform("mac")),
|
|
).toBe(true);
|
|
expect(
|
|
isCommitReleaseEvent(event({ key: "Control" }), taskSwitcherShortcut, platform("mac")),
|
|
).toBe(false);
|
|
});
|
|
});
|