返回 DeepSeek-Reasonix
keyboard-shortcuts.test.ts
根目录 / desktop / frontend / src / __tests__ / keyboard-shortcuts.test.ts
1 // Run: tsx src/__tests__/keyboard-shortcuts.test.ts
2
3 import { JSDOM } from "jsdom";
4
5 import {
6 defaultShortcutCombo,
7 formatShortcutCombo,
8 formatShortcutComboParts,
9 isCloseTabShortcut,
10 isReservedComposerHistoryShortcut,
11 matchesShortcut,
12 shortcutAcceptsCombo,
13 shortcutConflict,
14 shortcutDefinition,
15 type ShortcutPlatform,
16 } from "../lib/keyboardShortcuts";
17 import { topicShortcutIndexFromEvent, topicShortcutLabel } from "../lib/topicShortcuts";
18
19 let passed = 0;
20 let failed = 0;
21
22 function eq(a: unknown, b: unknown, label: string) {
23 if (a === b) {
24 process.stdout.write(` PASS ${label}\n`);
25 passed += 1;
26 } else {
27 process.stdout.write(` FAIL ${label}: expected ${JSON.stringify(b)}, got ${JSON.stringify(a)}\n`);
28 failed += 1;
29 }
30 }
31
32 function event(key: string, modifiers: { ctrlKey?: boolean; metaKey?: boolean; altKey?: boolean; shiftKey?: boolean; defaultPrevented?: boolean } = {}) {
33 return {
34 key,
35 ctrlKey: modifiers.ctrlKey ?? false,
36 metaKey: modifiers.metaKey ?? false,
37 altKey: modifiers.altKey ?? false,
38 shiftKey: modifiers.shiftKey ?? false,
39 defaultPrevented: modifiers.defaultPrevented ?? false,
40 target: null,
41 };
42 }
43
44 function eventWithTarget(
45 key: string,
46 target: EventTarget | null,
47 modifiers: { ctrlKey?: boolean; metaKey?: boolean; altKey?: boolean; shiftKey?: boolean; defaultPrevented?: boolean } = {},
48 ) {
49 return {
50 key,
51 ctrlKey: modifiers.ctrlKey ?? false,
52 metaKey: modifiers.metaKey ?? false,
53 altKey: modifiers.altKey ?? false,
54 shiftKey: modifiers.shiftKey ?? false,
55 defaultPrevented: modifiers.defaultPrevented ?? false,
56 target,
57 };
58 }
59
60 console.log("\nkeyboard shortcuts");
61
62 eq(isCloseTabShortcut(event("w", { metaKey: true }), "darwin"), true, "Cmd+W closes tabs on macOS");
63 eq(isCloseTabShortcut(event("W", { metaKey: true }), "darwin"), true, "Cmd+Shift+W key value still matches W on macOS");
64 eq(isCloseTabShortcut(event("w", { ctrlKey: true }), "darwin"), false, "Control+W does not close tabs on macOS");
65 eq(isCloseTabShortcut(event("w", { metaKey: true }), "windows"), false, "Meta+W does not close tabs on Windows");
66 eq(isCloseTabShortcut(event("w", { ctrlKey: true }), "windows"), true, "Ctrl+W closes tabs on Windows");
67 eq(isCloseTabShortcut(event("w", { ctrlKey: true }), "linux"), true, "Ctrl+W closes tabs on Linux");
68
69 for (const platform of ["darwin", "windows", "linux"] satisfies ShortcutPlatform[]) {
70 eq(isCloseTabShortcut(event("k", { ctrlKey: true, metaKey: true }), platform), false, `${platform} ignores non-W keys`);
71 eq(isCloseTabShortcut(event("w"), platform), false, `${platform} requires the platform modifier`);
72 }
73
74 eq(matchesShortcut(event("k", { metaKey: true }), "commandPalette.open", "darwin"), true, "Cmd+K opens the palette on macOS");
75 eq(matchesShortcut(event("k", { ctrlKey: true }), "commandPalette.open", "windows"), true, "Ctrl+K opens the palette on Windows");
76 eq(matchesShortcut({ key: "?", shiftKey: true }, "shortcuts.show", "darwin"), true, "? opens shortcut help");
77 eq(matchesShortcut({ key: "+", metaKey: true, shiftKey: true }, "textSize.increase", "darwin"), true, "Cmd+Plus still increases text size");
78 eq(formatShortcutCombo(defaultShortcutCombo("settings.open", "darwin"), "darwin"), "⌘,", "formats mac settings shortcut");
79 eq(JSON.stringify(formatShortcutComboParts(defaultShortcutCombo("settings.open", "darwin"), "darwin")), JSON.stringify(["⌘", ","]), "splits mac settings shortcut for display");
80 eq(formatShortcutCombo(defaultShortcutCombo("settings.open", "windows"), "windows"), "Ctrl+,", "formats Windows settings shortcut");
81 eq(JSON.stringify(formatShortcutComboParts(defaultShortcutCombo("settings.open", "windows"), "windows")), JSON.stringify(["Ctrl", ","]), "splits Windows settings shortcut for display");
82 eq(shortcutConflict("settings.open", defaultShortcutCombo("commandPalette.open", "darwin"), "darwin")?.action, "commandPalette.open", "detects shortcut conflicts");
83 eq(matchesShortcut(event("l", { metaKey: true }), "selection.addToChat", "darwin"), true, "Cmd+L adds the selection to chat on macOS");
84 eq(matchesShortcut(event("l", { ctrlKey: true }), "selection.addToChat", "windows"), true, "Ctrl+L adds the selection to chat on Windows");
85 eq(matchesShortcut(event("l", { ctrlKey: true, metaKey: true }), "selection.addToChat", "darwin"), false, "extra modifiers do not trigger the selection shortcut");
86 eq(shortcutConflict("app.newSession", { key: "l", ctrl: true }, "linux")?.action, "selection.addToChat", "rebinding another action onto Ctrl+L conflicts with the selection shortcut");
87 eq(topicShortcutIndexFromEvent(event("1", { metaKey: true }), "darwin"), 0, "Cmd+1 maps to the first topic shortcut on macOS");
88 eq(topicShortcutIndexFromEvent(event("1", { ctrlKey: true }), "darwin"), null, "Ctrl+1 is not a topic shortcut on macOS");
89 eq(topicShortcutIndexFromEvent(event("9", { ctrlKey: true }), "windows"), 8, "Ctrl+9 maps to the ninth topic shortcut on Windows");
90 eq(topicShortcutIndexFromEvent(event("9", { metaKey: true }), "windows"), null, "Meta+9 is not a topic shortcut on Windows");
91 eq(topicShortcutIndexFromEvent(event("1", { ctrlKey: true, shiftKey: true }), "linux"), null, "topic shortcuts reject extra modifiers");
92 eq(topicShortcutIndexFromEvent(event("0", { metaKey: true }), "darwin"), null, "Cmd+0 is not a topic shortcut");
93 eq(topicShortcutIndexFromEvent(event("1", { metaKey: true, defaultPrevented: true }), "darwin"), null, "topic shortcuts yield to already-handled custom shortcuts");
94 {
95 const dom = new JSDOM("<!doctype html><html><body><input /><textarea></textarea></body></html>");
96 const previousHTMLElement = globalThis.HTMLElement;
97 globalThis.HTMLElement = dom.window.HTMLElement;
98 const input = dom.window.document.querySelector("input");
99 const textarea = dom.window.document.querySelector("textarea");
100 eq(topicShortcutIndexFromEvent(eventWithTarget("1", input, { metaKey: true }), "darwin"), 0, "Cmd+1 still works when an input has focus");
101 eq(topicShortcutIndexFromEvent(eventWithTarget("9", textarea, { ctrlKey: true }), "windows"), 8, "Ctrl+9 still works when a textarea has focus");
102 globalThis.HTMLElement = previousHTMLElement;
103 }
104 eq(topicShortcutLabel(1, "darwin"), "⌘1", "topic badge uses the macOS command glyph");
105 eq(topicShortcutLabel(1, "windows"), "Ctrl+1", "topic badge uses the Windows control modifier");
106
107 for (const platform of ["darwin", "windows", "linux"] satisfies ShortcutPlatform[]) {
108 eq(matchesShortcut(event("Enter", { shiftKey: true }), "composer.newline", platform), true, `${platform} newline chord defaults to Shift+Enter`);
109 eq(matchesShortcut(event("Enter"), "composer.newline", platform), false, `${platform} plain Enter is not the default newline chord`);
110 eq(matchesShortcut(event("Enter", { ctrlKey: true }), "composer.newline", platform), false, `${platform} Ctrl+Enter is not the default newline chord`);
111 eq(matchesShortcut(event("Enter"), "composer.send", platform), true, `${platform} send chord defaults to plain Enter`);
112 eq(matchesShortcut(event("Enter", { shiftKey: true }), "composer.send", platform), false, `${platform} Shift+Enter is not the default send chord`);
113 }
114 eq(shortcutConflict("app.newSession", { key: "Enter", shift: true }, "darwin")?.action, "composer.newline", "rebinding another action onto Shift+Enter conflicts with the newline chord");
115 eq(shortcutConflict("composer.newline", { key: "Enter" }, "darwin")?.action, "composer.send", "rebinding the newline chord onto plain Enter conflicts with the send chord");
116 eq(shortcutAcceptsCombo("composer.send", { key: "Enter", ctrl: true }), true, "composer send accepts modified Enter");
117 eq(shortcutAcceptsCombo("composer.newline", { key: "Enter", alt: true }), true, "composer newline accepts modified Enter");
118 eq(shortcutAcceptsCombo("composer.send", { key: "s", ctrl: true }), false, "composer send rejects non-Enter keys");
119 eq(shortcutAcceptsCombo("app.newSession", { key: "s", ctrl: true }), true, "unrestricted shortcuts still accept other keys");
120 eq(formatShortcutCombo(defaultShortcutCombo("composer.newline", "darwin"), "darwin"), "⇧Enter", "formats the mac newline chord");
121 eq(formatShortcutCombo(defaultShortcutCombo("composer.newline", "windows"), "windows"), "Shift+Enter", "formats the Windows newline chord");
122 eq(matchesShortcut(event("z", { metaKey: true }), "composer.undo", "darwin"), true, "Cmd+Z matches composer undo on macOS");
123 eq(matchesShortcut(event("z", { ctrlKey: true }), "composer.undo", "windows"), true, "Ctrl+Z matches composer undo on Windows");
124 eq(matchesShortcut(event("z", { metaKey: true, shiftKey: true }), "composer.redo", "darwin"), true, "Cmd+Shift+Z matches composer redo on macOS");
125 eq(matchesShortcut(event("z", { ctrlKey: true, shiftKey: true }), "composer.redo", "linux"), true, "Ctrl+Shift+Z matches composer redo on Linux");
126 eq(matchesShortcut(event("y", { ctrlKey: true }), "composer.redo", "windows"), false, "Ctrl+Y remains a Composer-level conditional fallback");
127 eq(isReservedComposerHistoryShortcut(event("z", { metaKey: true }), "darwin"), true, "macOS undo is reserved while editing");
128 eq(isReservedComposerHistoryShortcut(event("z", { ctrlKey: true, shiftKey: true }), "windows"), true, "Windows redo is reserved while editing");
129 eq(isReservedComposerHistoryShortcut(event("y", { ctrlKey: true }), "windows"), false, "conditional Ctrl+Y redo does not reserve a global shortcut");
130 eq(shortcutDefinition("composer.undo").configurable, false, "composer undo is visible but locked to the native editing chord");
131 eq(shortcutDefinition("composer.redo").configurable, false, "composer redo is visible but locked to the native editing chord");
132 eq(shortcutConflict("app.newSession", { key: "z", ctrl: true }, "windows")?.action, "composer.undo", "custom shortcuts cannot take the Windows undo chord");
133 eq(shortcutConflict("app.newSession", { key: "z", ctrl: true, shift: true }, "windows")?.action, "composer.redo", "custom shortcuts cannot take the Windows redo chord");
134
135 console.log(`\n${passed} passed, ${failed} failed, ${passed + failed} total`);
136 if (failed > 0) process.exit(1);
137
137 lines TYPESCRIPT