blob: 9f0a34f99fde237f9d6cb3f20d26bcc9ae36df78 [file] [log] [blame]
/**
* @license
* Copyright 2021 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import '../../test/common-test-setup-karma';
import {
COMBO_TIMEOUT_MS,
describeBinding,
ShortcutsService,
} from '../../services/shortcuts/shortcuts-service';
import {Shortcut, ShortcutSection} from './shortcuts-config';
import {SinonFakeTimers, SinonSpy} from 'sinon';
import {Binding, Key, Modifier} from '../../utils/dom-util';
import {getAppContext} from '../app-context';
import {pressKey} from '../../test/test-utils';
const KEY_A: Binding = {key: 'a'};
suite('shortcuts-service tests', () => {
let service: ShortcutsService;
setup(() => {
service = new ShortcutsService(
getAppContext().userModel,
getAppContext().flagsService,
getAppContext().reportingService
);
});
test('getShortcut', () => {
assert.equal(service.getShortcut(Shortcut.NEXT_FILE), ']');
assert.equal(service.getShortcut(Shortcut.TOGGLE_LEFT_PANE), 'A');
assert.equal(
service.getShortcut(Shortcut.SEND_REPLY),
'Ctrl+Enter,Meta/Cmd+Enter'
);
});
suite('addShortcut()', () => {
let el: HTMLElement;
let listener: SinonSpy<[KeyboardEvent], void>;
setup(() => {
el = document.createElement('div');
listener = sinon.spy() as SinonSpy<[KeyboardEvent], void>;
});
test('standard call', () => {
service.addShortcut(el, KEY_A, listener);
assert.isTrue(listener.notCalled);
pressKey(el, KEY_A.key);
assert.isTrue(listener.calledOnce);
});
test('preventDefault option default false', () => {
service.addShortcut(el, KEY_A, listener);
pressKey(el, KEY_A.key);
assert.isTrue(listener.calledOnce);
assert.isTrue(listener.lastCall.firstArg?.defaultPrevented);
});
test('preventDefault option force false', () => {
service.addShortcut(el, KEY_A, listener, {preventDefault: false});
pressKey(el, KEY_A.key);
assert.isTrue(listener.calledOnce);
assert.isFalse(listener.lastCall.firstArg?.defaultPrevented);
});
test('preventDefault option force true', () => {
service.addShortcut(el, KEY_A, listener, {preventDefault: true});
pressKey(el, KEY_A.key);
assert.isTrue(listener.calledOnce);
assert.isTrue(listener.lastCall.firstArg?.defaultPrevented);
});
test('shouldSuppress option default true', () => {
service.shortcutsDisabled = true;
service.addShortcut(el, KEY_A, listener);
pressKey(el, KEY_A.key);
assert.isTrue(listener.notCalled);
});
test('shouldSuppress option force true', () => {
service.shortcutsDisabled = true;
service.addShortcut(el, KEY_A, listener, {shouldSuppress: true});
pressKey(el, KEY_A.key);
assert.isTrue(listener.notCalled);
});
test('shouldSuppress option force false', () => {
service.shortcutsDisabled = true;
service.addShortcut(el, KEY_A, listener, {shouldSuppress: false});
pressKey(el, KEY_A.key);
assert.isTrue(listener.calledOnce);
});
});
suite('binding descriptions', () => {
function mapToObject<K, V>(m: Map<K, V>) {
const o: any = {};
m.forEach((v: V, k: K) => (o[k] = v));
return o;
}
test('single combo description', () => {
assert.deepEqual(describeBinding({key: 'a'}), ['a']);
assert.deepEqual(
describeBinding({key: 'a', modifiers: [Modifier.CTRL_KEY]}),
['Ctrl', 'a']
);
assert.deepEqual(
describeBinding({
key: Key.UP,
modifiers: [Modifier.CTRL_KEY, Modifier.SHIFT_KEY],
}),
['Shift', 'Ctrl', '↑']
);
});
test('combo set description', () => {
assert.deepEqual(
service.describeBindings(Shortcut.GO_TO_OPENED_CHANGES),
[['g', 'o']]
);
assert.deepEqual(service.describeBindings(Shortcut.SAVE_COMMENT), [
['Ctrl', 'Enter'],
['Meta/Cmd', 'Enter'],
['Ctrl', 's'],
['Meta/Cmd', 's'],
]);
assert.deepEqual(service.describeBindings(Shortcut.PREV_FILE), [['[']]);
});
test('combo set description width', () => {
assert.strictEqual(service.comboSetDisplayWidth([['u']]), 1);
assert.strictEqual(service.comboSetDisplayWidth([['g', 'o']]), 2);
assert.strictEqual(service.comboSetDisplayWidth([['Shift', 'r']]), 6);
assert.strictEqual(service.comboSetDisplayWidth([['x'], ['y']]), 4);
assert.strictEqual(
service.comboSetDisplayWidth([['x'], ['y'], ['Shift', 'z']]),
12
);
});
test('distribute shortcut help', () => {
assert.deepEqual(service.distributeBindingDesc([['o']]), [[['o']]]);
assert.deepEqual(service.distributeBindingDesc([['g', 'o']]), [
[['g', 'o']],
]);
assert.deepEqual(
service.distributeBindingDesc([['ctrl', 'shift', 'meta', 'enter']]),
[[['ctrl', 'shift', 'meta', 'enter']]]
);
assert.deepEqual(
service.distributeBindingDesc([
['ctrl', 'shift', 'meta', 'enter'],
['o'],
]),
[[['ctrl', 'shift', 'meta', 'enter']], [['o']]]
);
assert.deepEqual(
service.distributeBindingDesc([
['ctrl', 'enter'],
['meta', 'enter'],
['ctrl', 's'],
['meta', 's'],
]),
[
[
['ctrl', 'enter'],
['meta', 'enter'],
],
[
['ctrl', 's'],
['meta', 's'],
],
]
);
});
test('active shortcuts by section', () => {
assert.deepEqual(mapToObject(service.activeShortcutsBySection()), {});
service.addShortcutListener(Shortcut.NEXT_FILE, _ => {});
assert.deepEqual(mapToObject(service.activeShortcutsBySection()), {
[ShortcutSection.NAVIGATION]: [
{
shortcut: Shortcut.NEXT_FILE,
text: 'Go to next file',
bindings: [{key: ']'}],
},
],
});
service.addShortcutListener(Shortcut.NEXT_LINE, _ => {});
assert.deepEqual(mapToObject(service.activeShortcutsBySection()), {
[ShortcutSection.DIFFS]: [
{
shortcut: Shortcut.NEXT_LINE,
text: 'Go to next line',
bindings: [
{allowRepeat: true, key: 'j'},
{allowRepeat: true, key: 'ArrowDown'},
],
},
],
[ShortcutSection.NAVIGATION]: [
{
shortcut: Shortcut.NEXT_FILE,
text: 'Go to next file',
bindings: [{key: ']'}],
},
],
});
service.addShortcutListener(Shortcut.SEARCH, _ => {});
service.addShortcutListener(Shortcut.GO_TO_OPENED_CHANGES, _ => {});
assert.deepEqual(mapToObject(service.activeShortcutsBySection()), {
[ShortcutSection.DIFFS]: [
{
shortcut: Shortcut.NEXT_LINE,
text: 'Go to next line',
bindings: [
{allowRepeat: true, key: 'j'},
{allowRepeat: true, key: 'ArrowDown'},
],
},
],
[ShortcutSection.EVERYWHERE]: [
{
shortcut: Shortcut.SEARCH,
text: 'Search',
bindings: [{key: '/'}],
},
{
shortcut: Shortcut.GO_TO_OPENED_CHANGES,
text: 'Go to Opened Changes',
bindings: [{key: 'o', combo: 'g'}],
},
],
[ShortcutSection.NAVIGATION]: [
{
shortcut: Shortcut.NEXT_FILE,
text: 'Go to next file',
bindings: [{key: ']'}],
},
],
});
});
test('directory view', () => {
assert.deepEqual(mapToObject(service.directoryView()), {});
service.addShortcutListener(Shortcut.GO_TO_OPENED_CHANGES, _ => {});
service.addShortcutListener(Shortcut.NEXT_FILE, _ => {});
service.addShortcutListener(Shortcut.NEXT_LINE, _ => {});
service.addShortcutListener(Shortcut.SAVE_COMMENT, _ => {});
service.addShortcutListener(Shortcut.SEARCH, _ => {});
assert.deepEqual(mapToObject(service.directoryView()), {
[ShortcutSection.DIFFS]: [
{binding: [['j'], ['↓']], text: 'Go to next line'},
{
binding: [['Ctrl', 'Enter']],
text: 'Save comment',
},
{
binding: [
['Meta/Cmd', 'Enter'],
['Ctrl', 's'],
],
text: 'Save comment',
},
{
binding: [['Meta/Cmd', 's']],
text: 'Save comment',
},
],
[ShortcutSection.EVERYWHERE]: [
{binding: [['/']], text: 'Search'},
{binding: [['g', 'o']], text: 'Go to Opened Changes'},
],
[ShortcutSection.NAVIGATION]: [
{binding: [[']']], text: 'Go to next file'},
],
});
});
});
suite('combo keys', () => {
let clock: SinonFakeTimers;
setup(() => {
clock = sinon.useFakeTimers();
clock.tick(1000);
});
teardown(() => {
clock.restore();
});
test('not in combo key mode initially', () => {
assert.isFalse(service.isInComboKeyMode());
});
test('pressing f does not switch into combo key mode', () => {
const event = new KeyboardEvent('keydown', {key: 'f'});
document.dispatchEvent(event);
assert.isFalse(service.isInComboKeyMode());
});
test('pressing g switches into combo key mode', () => {
const event = new KeyboardEvent('keydown', {key: 'g'});
document.dispatchEvent(event);
assert.isTrue(service.isInComboKeyMode());
});
test('pressing v switches into combo key mode', () => {
const event = new KeyboardEvent('keydown', {key: 'v'});
document.dispatchEvent(event);
assert.isTrue(service.isInComboKeyMode());
});
test('combo key mode timeout', () => {
const event = new KeyboardEvent('keydown', {key: 'g'});
document.dispatchEvent(event);
assert.isTrue(service.isInComboKeyMode());
clock.tick(COMBO_TIMEOUT_MS / 2);
assert.isTrue(service.isInComboKeyMode());
clock.tick(COMBO_TIMEOUT_MS);
assert.isFalse(service.isInComboKeyMode());
});
});
});