blob: 35da84e9ab2524fe650dcadf0609bb345d77c343 [file] [log] [blame]
Ben Rohlfs966586e2021-10-06 16:15:24 +02001/**
2 * @license
Ben Rohlfs94fcbbc2022-05-27 10:45:03 +02003 * Copyright 2021 Google LLC
4 * SPDX-License-Identifier: Apache-2.0
Ben Rohlfs966586e2021-10-06 16:15:24 +02005 */
6import '../../test/common-test-setup-karma';
Ben Rohlfsbe0c0ba2021-10-17 19:36:43 +02007import {
Ben Rohlfsbe0c0ba2021-10-17 19:36:43 +02008 COMBO_TIMEOUT_MS,
Ben Rohlfs410f5222021-10-19 23:44:02 +02009 describeBinding,
10 ShortcutsService,
Ben Rohlfsbe0c0ba2021-10-17 19:36:43 +020011} from '../../services/shortcuts/shortcuts-service';
Ben Rohlfs966586e2021-10-06 16:15:24 +020012import {Shortcut, ShortcutSection} from './shortcuts-config';
Ben Rohlfs1aeb1d32022-06-30 23:02:59 +020013import {SinonFakeTimers, SinonSpy} from 'sinon';
14import {Binding, Key, Modifier} from '../../utils/dom-util';
Chris Poucetefcdb202021-11-22 23:54:26 +010015import {getAppContext} from '../app-context';
Ben Rohlfs1aeb1d32022-06-30 23:02:59 +020016import {pressKey} from '../../test/test-utils';
17
18const KEY_A: Binding = {key: 'a'};
Ben Rohlfs42333cb2021-10-07 07:54:10 +020019
Ben Rohlfs966586e2021-10-06 16:15:24 +020020suite('shortcuts-service tests', () => {
Ben Rohlfs42333cb2021-10-07 07:54:10 +020021 let service: ShortcutsService;
22
23 setup(() => {
Chris Poucetefcdb202021-11-22 23:54:26 +010024 service = new ShortcutsService(
25 getAppContext().userModel,
Chris Poucetb243fa42022-05-24 14:23:19 +020026 getAppContext().flagsService,
Chris Poucetefcdb202021-11-22 23:54:26 +010027 getAppContext().reportingService
28 );
Ben Rohlfs42333cb2021-10-07 07:54:10 +020029 });
30
Ben Rohlfs966586e2021-10-06 16:15:24 +020031 test('getShortcut', () => {
Ben Rohlfs410f5222021-10-19 23:44:02 +020032 assert.equal(service.getShortcut(Shortcut.NEXT_FILE), ']');
33 assert.equal(service.getShortcut(Shortcut.TOGGLE_LEFT_PANE), 'A');
34 assert.equal(
35 service.getShortcut(Shortcut.SEND_REPLY),
36 'Ctrl+Enter,Meta/Cmd+Enter'
37 );
Ben Rohlfs966586e2021-10-06 16:15:24 +020038 });
39
Ben Rohlfs1aeb1d32022-06-30 23:02:59 +020040 suite('addShortcut()', () => {
41 let el: HTMLElement;
42 let listener: SinonSpy<[KeyboardEvent], void>;
43
44 setup(() => {
45 el = document.createElement('div');
46 listener = sinon.spy() as SinonSpy<[KeyboardEvent], void>;
47 });
48
49 test('standard call', () => {
50 service.addShortcut(el, KEY_A, listener);
51 pressKey(el, KEY_A.key);
52 assert.isTrue(listener.calledOnce);
53 });
54
55 test('doNotPrevent option default false', () => {
56 service.addShortcut(el, KEY_A, listener);
57 pressKey(el, KEY_A.key);
58 assert.isTrue(listener.calledOnce);
59 assert.isTrue(listener.lastCall.firstArg?.defaultPrevented);
60 });
61
62 test('doNotPrevent option force false', () => {
63 service.addShortcut(el, KEY_A, listener, {doNotPrevent: false});
64 pressKey(el, KEY_A.key);
65 assert.isTrue(listener.calledOnce);
66 assert.isTrue(listener.lastCall.firstArg?.defaultPrevented);
67 });
68
69 test('doNotPrevent option force true', () => {
70 service.addShortcut(el, KEY_A, listener, {doNotPrevent: true});
71 pressKey(el, KEY_A.key);
72 assert.isTrue(listener.calledOnce);
73 assert.isFalse(listener.lastCall.firstArg?.defaultPrevented);
74 });
75
76 test('shouldSuppress option default true', () => {
77 service.shortcutsDisabled = true;
78 service.addShortcut(el, KEY_A, listener);
79 pressKey(el, KEY_A.key);
80 assert.isTrue(listener.notCalled);
81 });
82
83 test('shouldSuppress option force true', () => {
84 service.shortcutsDisabled = true;
85 service.addShortcut(el, KEY_A, listener, {shouldSuppress: true});
86 pressKey(el, KEY_A.key);
87 assert.isTrue(listener.notCalled);
88 });
89
90 test('shouldSuppress option force false', () => {
91 service.shortcutsDisabled = true;
92 service.addShortcut(el, KEY_A, listener, {shouldSuppress: false});
93 pressKey(el, KEY_A.key);
94 assert.isTrue(listener.calledOnce);
95 });
96 });
97
Ben Rohlfs966586e2021-10-06 16:15:24 +020098 suite('binding descriptions', () => {
99 function mapToObject<K, V>(m: Map<K, V>) {
100 const o: any = {};
101 m.forEach((v: V, k: K) => (o[k] = v));
102 return o;
103 }
104
105 test('single combo description', () => {
Ben Rohlfs410f5222021-10-19 23:44:02 +0200106 assert.deepEqual(describeBinding({key: 'a'}), ['a']);
107 assert.deepEqual(
108 describeBinding({key: 'a', modifiers: [Modifier.CTRL_KEY]}),
109 ['Ctrl', 'a']
110 );
111 assert.deepEqual(
112 describeBinding({
113 key: Key.UP,
114 modifiers: [Modifier.CTRL_KEY, Modifier.SHIFT_KEY],
115 }),
116 ['Shift', 'Ctrl', '↑']
117 );
Ben Rohlfs966586e2021-10-06 16:15:24 +0200118 });
119
120 test('combo set description', () => {
Ben Rohlfs42333cb2021-10-07 07:54:10 +0200121 assert.deepEqual(
122 service.describeBindings(Shortcut.GO_TO_OPENED_CHANGES),
123 [['g', 'o']]
124 );
125 assert.deepEqual(service.describeBindings(Shortcut.SAVE_COMMENT), [
Ben Rohlfsfea82402021-10-06 17:50:47 +0200126 ['Ctrl', 'Enter'],
Ben Rohlfs410f5222021-10-19 23:44:02 +0200127 ['Meta/Cmd', 'Enter'],
Ben Rohlfsfea82402021-10-06 17:50:47 +0200128 ['Ctrl', 's'],
Ben Rohlfs410f5222021-10-19 23:44:02 +0200129 ['Meta/Cmd', 's'],
Ben Rohlfs966586e2021-10-06 16:15:24 +0200130 ]);
Ben Rohlfs42333cb2021-10-07 07:54:10 +0200131 assert.deepEqual(service.describeBindings(Shortcut.PREV_FILE), [['[']]);
Ben Rohlfs966586e2021-10-06 16:15:24 +0200132 });
133
134 test('combo set description width', () => {
Ben Rohlfs42333cb2021-10-07 07:54:10 +0200135 assert.strictEqual(service.comboSetDisplayWidth([['u']]), 1);
136 assert.strictEqual(service.comboSetDisplayWidth([['g', 'o']]), 2);
137 assert.strictEqual(service.comboSetDisplayWidth([['Shift', 'r']]), 6);
138 assert.strictEqual(service.comboSetDisplayWidth([['x'], ['y']]), 4);
Ben Rohlfs966586e2021-10-06 16:15:24 +0200139 assert.strictEqual(
Ben Rohlfs42333cb2021-10-07 07:54:10 +0200140 service.comboSetDisplayWidth([['x'], ['y'], ['Shift', 'z']]),
Ben Rohlfs966586e2021-10-06 16:15:24 +0200141 12
142 );
143 });
144
145 test('distribute shortcut help', () => {
Ben Rohlfs42333cb2021-10-07 07:54:10 +0200146 assert.deepEqual(service.distributeBindingDesc([['o']]), [[['o']]]);
147 assert.deepEqual(service.distributeBindingDesc([['g', 'o']]), [
148 [['g', 'o']],
149 ]);
Ben Rohlfs966586e2021-10-06 16:15:24 +0200150 assert.deepEqual(
Ben Rohlfs42333cb2021-10-07 07:54:10 +0200151 service.distributeBindingDesc([['ctrl', 'shift', 'meta', 'enter']]),
Ben Rohlfs966586e2021-10-06 16:15:24 +0200152 [[['ctrl', 'shift', 'meta', 'enter']]]
153 );
154 assert.deepEqual(
Ben Rohlfs42333cb2021-10-07 07:54:10 +0200155 service.distributeBindingDesc([
156 ['ctrl', 'shift', 'meta', 'enter'],
157 ['o'],
158 ]),
Ben Rohlfs966586e2021-10-06 16:15:24 +0200159 [[['ctrl', 'shift', 'meta', 'enter']], [['o']]]
160 );
161 assert.deepEqual(
Ben Rohlfs42333cb2021-10-07 07:54:10 +0200162 service.distributeBindingDesc([
Ben Rohlfs966586e2021-10-06 16:15:24 +0200163 ['ctrl', 'enter'],
164 ['meta', 'enter'],
165 ['ctrl', 's'],
166 ['meta', 's'],
167 ]),
168 [
169 [
170 ['ctrl', 'enter'],
171 ['meta', 'enter'],
172 ],
173 [
174 ['ctrl', 's'],
175 ['meta', 's'],
176 ],
177 ]
178 );
179 });
180
181 test('active shortcuts by section', () => {
Ben Rohlfs42333cb2021-10-07 07:54:10 +0200182 assert.deepEqual(mapToObject(service.activeShortcutsBySection()), {});
Ben Rohlfs966586e2021-10-06 16:15:24 +0200183
Chris Poucet35764c32022-06-21 09:48:10 +0200184 service.addShortcutListener(Shortcut.NEXT_FILE, _ => {});
Ben Rohlfs42333cb2021-10-07 07:54:10 +0200185 assert.deepEqual(mapToObject(service.activeShortcutsBySection()), {
Ben Rohlfs966586e2021-10-06 16:15:24 +0200186 [ShortcutSection.NAVIGATION]: [
Ben Rohlfsfea82402021-10-06 17:50:47 +0200187 {
188 shortcut: Shortcut.NEXT_FILE,
189 text: 'Go to next file',
Ben Rohlfs410f5222021-10-19 23:44:02 +0200190 bindings: [{key: ']'}],
Ben Rohlfsfea82402021-10-06 17:50:47 +0200191 },
Ben Rohlfs966586e2021-10-06 16:15:24 +0200192 ],
193 });
Chris Poucet35764c32022-06-21 09:48:10 +0200194 service.addShortcutListener(Shortcut.NEXT_LINE, _ => {});
Ben Rohlfs42333cb2021-10-07 07:54:10 +0200195 assert.deepEqual(mapToObject(service.activeShortcutsBySection()), {
Ben Rohlfs966586e2021-10-06 16:15:24 +0200196 [ShortcutSection.DIFFS]: [
Ben Rohlfsfea82402021-10-06 17:50:47 +0200197 {
198 shortcut: Shortcut.NEXT_LINE,
199 text: 'Go to next line',
Ben Rohlfs5f5cb5e2021-11-08 12:10:25 +0100200 bindings: [
201 {allowRepeat: true, key: 'j'},
202 {allowRepeat: true, key: 'ArrowDown'},
203 ],
Ben Rohlfsfea82402021-10-06 17:50:47 +0200204 },
Ben Rohlfs966586e2021-10-06 16:15:24 +0200205 ],
206 [ShortcutSection.NAVIGATION]: [
Ben Rohlfsfea82402021-10-06 17:50:47 +0200207 {
208 shortcut: Shortcut.NEXT_FILE,
209 text: 'Go to next file',
Ben Rohlfs410f5222021-10-19 23:44:02 +0200210 bindings: [{key: ']'}],
Ben Rohlfsfea82402021-10-06 17:50:47 +0200211 },
Ben Rohlfs966586e2021-10-06 16:15:24 +0200212 ],
213 });
214
Chris Poucet35764c32022-06-21 09:48:10 +0200215 service.addShortcutListener(Shortcut.SEARCH, _ => {});
216 service.addShortcutListener(Shortcut.GO_TO_OPENED_CHANGES, _ => {});
Ben Rohlfs42333cb2021-10-07 07:54:10 +0200217 assert.deepEqual(mapToObject(service.activeShortcutsBySection()), {
Ben Rohlfs966586e2021-10-06 16:15:24 +0200218 [ShortcutSection.DIFFS]: [
Ben Rohlfsfea82402021-10-06 17:50:47 +0200219 {
220 shortcut: Shortcut.NEXT_LINE,
221 text: 'Go to next line',
Ben Rohlfs5f5cb5e2021-11-08 12:10:25 +0100222 bindings: [
223 {allowRepeat: true, key: 'j'},
224 {allowRepeat: true, key: 'ArrowDown'},
225 ],
Ben Rohlfsfea82402021-10-06 17:50:47 +0200226 },
Ben Rohlfs966586e2021-10-06 16:15:24 +0200227 ],
228 [ShortcutSection.EVERYWHERE]: [
Ben Rohlfsfea82402021-10-06 17:50:47 +0200229 {
230 shortcut: Shortcut.SEARCH,
231 text: 'Search',
Ben Rohlfs410f5222021-10-19 23:44:02 +0200232 bindings: [{key: '/'}],
Ben Rohlfsfea82402021-10-06 17:50:47 +0200233 },
Ben Rohlfs966586e2021-10-06 16:15:24 +0200234 {
235 shortcut: Shortcut.GO_TO_OPENED_CHANGES,
236 text: 'Go to Opened Changes',
Ben Rohlfs410f5222021-10-19 23:44:02 +0200237 bindings: [{key: 'o', combo: 'g'}],
Ben Rohlfs966586e2021-10-06 16:15:24 +0200238 },
239 ],
240 [ShortcutSection.NAVIGATION]: [
Ben Rohlfsfea82402021-10-06 17:50:47 +0200241 {
242 shortcut: Shortcut.NEXT_FILE,
243 text: 'Go to next file',
Ben Rohlfs410f5222021-10-19 23:44:02 +0200244 bindings: [{key: ']'}],
Ben Rohlfsfea82402021-10-06 17:50:47 +0200245 },
Ben Rohlfs966586e2021-10-06 16:15:24 +0200246 ],
247 });
248 });
249
250 test('directory view', () => {
Ben Rohlfs42333cb2021-10-07 07:54:10 +0200251 assert.deepEqual(mapToObject(service.directoryView()), {});
Ben Rohlfs966586e2021-10-06 16:15:24 +0200252
Chris Poucet35764c32022-06-21 09:48:10 +0200253 service.addShortcutListener(Shortcut.GO_TO_OPENED_CHANGES, _ => {});
254 service.addShortcutListener(Shortcut.NEXT_FILE, _ => {});
255 service.addShortcutListener(Shortcut.NEXT_LINE, _ => {});
256 service.addShortcutListener(Shortcut.SAVE_COMMENT, _ => {});
257 service.addShortcutListener(Shortcut.SEARCH, _ => {});
Ben Rohlfs42333cb2021-10-07 07:54:10 +0200258 assert.deepEqual(mapToObject(service.directoryView()), {
Ben Rohlfs966586e2021-10-06 16:15:24 +0200259 [ShortcutSection.DIFFS]: [
Ben Rohlfsfea82402021-10-06 17:50:47 +0200260 {binding: [['j'], ['↓']], text: 'Go to next line'},
Ben Rohlfs966586e2021-10-06 16:15:24 +0200261 {
Ben Rohlfs410f5222021-10-19 23:44:02 +0200262 binding: [['Ctrl', 'Enter']],
Ben Rohlfs966586e2021-10-06 16:15:24 +0200263 text: 'Save comment',
264 },
265 {
266 binding: [
Ben Rohlfs410f5222021-10-19 23:44:02 +0200267 ['Meta/Cmd', 'Enter'],
Ben Rohlfs966586e2021-10-06 16:15:24 +0200268 ['Ctrl', 's'],
Ben Rohlfs966586e2021-10-06 16:15:24 +0200269 ],
270 text: 'Save comment',
271 },
Ben Rohlfs410f5222021-10-19 23:44:02 +0200272 {
273 binding: [['Meta/Cmd', 's']],
274 text: 'Save comment',
275 },
Ben Rohlfs966586e2021-10-06 16:15:24 +0200276 ],
277 [ShortcutSection.EVERYWHERE]: [
278 {binding: [['/']], text: 'Search'},
279 {binding: [['g', 'o']], text: 'Go to Opened Changes'},
280 ],
281 [ShortcutSection.NAVIGATION]: [
282 {binding: [[']']], text: 'Go to next file'},
283 ],
284 });
285 });
286 });
Ben Rohlfsbe0c0ba2021-10-17 19:36:43 +0200287
288 suite('combo keys', () => {
289 let clock: SinonFakeTimers;
290 setup(() => {
291 clock = sinon.useFakeTimers();
292 clock.tick(1000);
293 });
294
295 teardown(() => {
296 clock.restore();
297 });
298
299 test('not in combo key mode initially', () => {
300 assert.isFalse(service.isInComboKeyMode());
301 });
302
303 test('pressing f does not switch into combo key mode', () => {
304 const event = new KeyboardEvent('keydown', {key: 'f'});
305 document.dispatchEvent(event);
306 assert.isFalse(service.isInComboKeyMode());
307 });
308
309 test('pressing g switches into combo key mode', () => {
310 const event = new KeyboardEvent('keydown', {key: 'g'});
311 document.dispatchEvent(event);
312 assert.isTrue(service.isInComboKeyMode());
313 });
314
315 test('pressing v switches into combo key mode', () => {
316 const event = new KeyboardEvent('keydown', {key: 'v'});
317 document.dispatchEvent(event);
318 assert.isTrue(service.isInComboKeyMode());
319 });
320
321 test('combo key mode timeout', () => {
322 const event = new KeyboardEvent('keydown', {key: 'g'});
323 document.dispatchEvent(event);
324 assert.isTrue(service.isInComboKeyMode());
325 clock.tick(COMBO_TIMEOUT_MS / 2);
326 assert.isTrue(service.isInComboKeyMode());
327 clock.tick(COMBO_TIMEOUT_MS);
328 assert.isFalse(service.isInComboKeyMode());
329 });
330 });
Ben Rohlfs966586e2021-10-06 16:15:24 +0200331});