We want to allow users to click the checkbox and then follow-up with keyboard shortcuts, e.g. for going to the next file. Change-Id: I5722f518150fca27bd66d085d79b0b73dec468b3
453 lines
15 KiB
JavaScript
453 lines
15 KiB
JavaScript
/**
|
||
* @license
|
||
* Copyright (C) 2016 The Android Open Source Project
|
||
*
|
||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||
* you may not use this file except in compliance with the License.
|
||
* You may obtain a copy of the License at
|
||
*
|
||
* http://www.apache.org/licenses/LICENSE-2.0
|
||
*
|
||
* Unless required by applicable law or agreed to in writing, software
|
||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||
* See the License for the specific language governing permissions and
|
||
* limitations under the License.
|
||
*/
|
||
|
||
import '../../test/common-test-setup-karma.js';
|
||
import {
|
||
KeyboardShortcutMixin, Shortcut,
|
||
ShortcutManager, ShortcutSection, SPECIAL_SHORTCUT,
|
||
} from './keyboard-shortcut-mixin.js';
|
||
import {html} from '@polymer/polymer/lib/utils/html-tag.js';
|
||
import {PolymerElement} from '@polymer/polymer/polymer-element.js';
|
||
|
||
const basicFixture =
|
||
fixtureFromElement('keyboard-shortcut-mixin-test-element');
|
||
|
||
const withinOverlayFixture = fixtureFromTemplate(html`
|
||
<gr-overlay>
|
||
<keyboard-shortcut-mixin-test-element>
|
||
</keyboard-shortcut-mixin-test-element>
|
||
</gr-overlay>
|
||
`);
|
||
|
||
class GrKeyboardShortcutMixinTestElement extends
|
||
KeyboardShortcutMixin(PolymerElement) {
|
||
static get is() {
|
||
return 'keyboard-shortcut-mixin-test-element';
|
||
}
|
||
|
||
get keyBindings() {
|
||
return {
|
||
k: '_handleKey',
|
||
enter: '_handleKey',
|
||
};
|
||
}
|
||
|
||
_handleKey() {}
|
||
}
|
||
|
||
customElements.define(GrKeyboardShortcutMixinTestElement.is,
|
||
GrKeyboardShortcutMixinTestElement);
|
||
|
||
suite('keyboard-shortcut-mixin tests', () => {
|
||
let element;
|
||
let overlay;
|
||
|
||
setup(() => {
|
||
element = basicFixture.instantiate();
|
||
overlay = withinOverlayFixture.instantiate();
|
||
});
|
||
|
||
suite('ShortcutManager', () => {
|
||
test('bindings management', () => {
|
||
const mgr = new ShortcutManager();
|
||
const NEXT_FILE = Shortcut.NEXT_FILE;
|
||
|
||
assert.isUndefined(mgr.getBindingsForShortcut(NEXT_FILE));
|
||
mgr.bindShortcut(NEXT_FILE, ']', '}', 'right');
|
||
assert.deepEqual(
|
||
mgr.getBindingsForShortcut(NEXT_FILE),
|
||
[']', '}', 'right']);
|
||
});
|
||
|
||
test('getShortcut', () => {
|
||
const mgr = new ShortcutManager();
|
||
const NEXT_FILE = Shortcut.NEXT_FILE;
|
||
|
||
assert.isUndefined(mgr.getBindingsForShortcut(NEXT_FILE));
|
||
mgr.bindShortcut(NEXT_FILE, ']', '}', 'right');
|
||
assert.equal(mgr.getShortcut(NEXT_FILE), '],},→');
|
||
});
|
||
|
||
test('getShortcut with modifiers', () => {
|
||
const mgr = new ShortcutManager();
|
||
const NEXT_FILE = Shortcut.NEXT_FILE;
|
||
|
||
assert.isUndefined(mgr.getBindingsForShortcut(NEXT_FILE));
|
||
mgr.bindShortcut(NEXT_FILE, 'Shift+a:key');
|
||
assert.equal(mgr.getShortcut(NEXT_FILE), 'Shift+a');
|
||
});
|
||
|
||
suite('binding descriptions', () => {
|
||
function mapToObject(m) {
|
||
const o = {};
|
||
m.forEach((v, k) => o[k] = v);
|
||
return o;
|
||
}
|
||
|
||
test('single combo description', () => {
|
||
const mgr = new ShortcutManager();
|
||
assert.deepEqual(mgr.describeBinding('a'), ['a']);
|
||
assert.deepEqual(mgr.describeBinding('a:keyup'), ['a']);
|
||
assert.deepEqual(mgr.describeBinding('ctrl+a'), ['Ctrl', 'a']);
|
||
assert.deepEqual(
|
||
mgr.describeBinding('ctrl+shift+up:keyup'),
|
||
['Ctrl', 'Shift', '↑']);
|
||
});
|
||
|
||
test('combo set description', () => {
|
||
const mgr = new ShortcutManager();
|
||
assert.isNull(mgr.describeBindings(Shortcut.NEXT_FILE));
|
||
|
||
mgr.bindShortcut(Shortcut.GO_TO_OPENED_CHANGES,
|
||
SPECIAL_SHORTCUT.GO_KEY, 'o');
|
||
assert.deepEqual(
|
||
mgr.describeBindings(Shortcut.GO_TO_OPENED_CHANGES),
|
||
[['g', 'o']]);
|
||
|
||
mgr.bindShortcut(Shortcut.NEXT_FILE, SPECIAL_SHORTCUT.DOC_ONLY,
|
||
']', 'ctrl+shift+right:keyup');
|
||
assert.deepEqual(
|
||
mgr.describeBindings(Shortcut.NEXT_FILE),
|
||
[[']'], ['Ctrl', 'Shift', '→']]);
|
||
|
||
mgr.bindShortcut(Shortcut.PREV_FILE, '[');
|
||
assert.deepEqual(mgr.describeBindings(Shortcut.PREV_FILE), [['[']]);
|
||
});
|
||
|
||
test('combo set description width', () => {
|
||
const mgr = new ShortcutManager();
|
||
assert.strictEqual(mgr.comboSetDisplayWidth([['u']]), 1);
|
||
assert.strictEqual(mgr.comboSetDisplayWidth([['g', 'o']]), 2);
|
||
assert.strictEqual(mgr.comboSetDisplayWidth([['Shift', 'r']]), 6);
|
||
assert.strictEqual(mgr.comboSetDisplayWidth([['x'], ['y']]), 4);
|
||
assert.strictEqual(
|
||
mgr.comboSetDisplayWidth([['x'], ['y'], ['Shift', 'z']]),
|
||
12);
|
||
});
|
||
|
||
test('distribute shortcut help', () => {
|
||
const mgr = new ShortcutManager();
|
||
assert.deepEqual(mgr.distributeBindingDesc([['o']]), [[['o']]]);
|
||
assert.deepEqual(
|
||
mgr.distributeBindingDesc([['g', 'o']]),
|
||
[[['g', 'o']]]);
|
||
assert.deepEqual(
|
||
mgr.distributeBindingDesc([['ctrl', 'shift', 'meta', 'enter']]),
|
||
[[['ctrl', 'shift', 'meta', 'enter']]]);
|
||
assert.deepEqual(
|
||
mgr.distributeBindingDesc([
|
||
['ctrl', 'shift', 'meta', 'enter'],
|
||
['o'],
|
||
]),
|
||
[
|
||
[['ctrl', 'shift', 'meta', 'enter']],
|
||
[['o']],
|
||
]);
|
||
assert.deepEqual(
|
||
mgr.distributeBindingDesc([
|
||
['ctrl', 'enter'],
|
||
['meta', 'enter'],
|
||
['ctrl', 's'],
|
||
['meta', 's'],
|
||
]),
|
||
[
|
||
[['ctrl', 'enter'], ['meta', 'enter']],
|
||
[['ctrl', 's'], ['meta', 's']],
|
||
]);
|
||
});
|
||
|
||
test('active shortcuts by section', () => {
|
||
const mgr = new ShortcutManager();
|
||
mgr.bindShortcut(Shortcut.NEXT_FILE, ']');
|
||
mgr.bindShortcut(Shortcut.NEXT_LINE, 'j');
|
||
mgr.bindShortcut(Shortcut.GO_TO_OPENED_CHANGES, 'g+o');
|
||
mgr.bindShortcut(Shortcut.SEARCH, '/');
|
||
|
||
assert.deepEqual(
|
||
mapToObject(mgr.activeShortcutsBySection()),
|
||
{});
|
||
|
||
mgr.attachHost({
|
||
keyboardShortcuts() {
|
||
return {
|
||
[Shortcut.NEXT_FILE]: null,
|
||
};
|
||
},
|
||
});
|
||
assert.deepEqual(
|
||
mapToObject(mgr.activeShortcutsBySection()),
|
||
{
|
||
[ShortcutSection.NAVIGATION]: [
|
||
{shortcut: Shortcut.NEXT_FILE, text: 'Go to next file'},
|
||
],
|
||
});
|
||
|
||
mgr.attachHost({
|
||
keyboardShortcuts() {
|
||
return {
|
||
[Shortcut.NEXT_LINE]: null,
|
||
};
|
||
},
|
||
});
|
||
assert.deepEqual(
|
||
mapToObject(mgr.activeShortcutsBySection()),
|
||
{
|
||
[ShortcutSection.DIFFS]: [
|
||
{shortcut: Shortcut.NEXT_LINE, text: 'Go to next line'},
|
||
],
|
||
[ShortcutSection.NAVIGATION]: [
|
||
{shortcut: Shortcut.NEXT_FILE, text: 'Go to next file'},
|
||
],
|
||
});
|
||
|
||
mgr.attachHost({
|
||
keyboardShortcuts() {
|
||
return {
|
||
[Shortcut.SEARCH]: null,
|
||
[Shortcut.GO_TO_OPENED_CHANGES]: null,
|
||
};
|
||
},
|
||
});
|
||
assert.deepEqual(
|
||
mapToObject(mgr.activeShortcutsBySection()),
|
||
{
|
||
[ShortcutSection.DIFFS]: [
|
||
{shortcut: Shortcut.NEXT_LINE, text: 'Go to next line'},
|
||
],
|
||
[ShortcutSection.EVERYWHERE]: [
|
||
{shortcut: Shortcut.SEARCH, text: 'Search'},
|
||
{
|
||
shortcut: Shortcut.GO_TO_OPENED_CHANGES,
|
||
text: 'Go to Opened Changes',
|
||
},
|
||
],
|
||
[ShortcutSection.NAVIGATION]: [
|
||
{shortcut: Shortcut.NEXT_FILE, text: 'Go to next file'},
|
||
],
|
||
});
|
||
});
|
||
|
||
test('directory view', () => {
|
||
const mgr = new ShortcutManager();
|
||
mgr.bindShortcut(Shortcut.NEXT_FILE, ']');
|
||
mgr.bindShortcut(Shortcut.NEXT_LINE, 'j');
|
||
mgr.bindShortcut(Shortcut.GO_TO_OPENED_CHANGES,
|
||
SPECIAL_SHORTCUT.GO_KEY, 'o');
|
||
mgr.bindShortcut(Shortcut.SEARCH, '/');
|
||
mgr.bindShortcut(
|
||
Shortcut.SAVE_COMMENT, 'ctrl+enter', 'meta+enter',
|
||
'ctrl+s', 'meta+s');
|
||
|
||
assert.deepEqual(mapToObject(mgr.directoryView()), {});
|
||
|
||
mgr.attachHost({
|
||
keyboardShortcuts() {
|
||
return {
|
||
[Shortcut.GO_TO_OPENED_CHANGES]: null,
|
||
[Shortcut.NEXT_FILE]: null,
|
||
[Shortcut.NEXT_LINE]: null,
|
||
[Shortcut.SAVE_COMMENT]: null,
|
||
[Shortcut.SEARCH]: null,
|
||
};
|
||
},
|
||
});
|
||
assert.deepEqual(
|
||
mapToObject(mgr.directoryView()),
|
||
{
|
||
[ShortcutSection.DIFFS]: [
|
||
{binding: [['j']], text: 'Go to next line'},
|
||
{
|
||
binding: [['Ctrl', 'Enter'], ['Meta', 'Enter']],
|
||
text: 'Save comment',
|
||
},
|
||
{
|
||
binding: [['Ctrl', 's'], ['Meta', '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'},
|
||
],
|
||
});
|
||
});
|
||
});
|
||
});
|
||
|
||
test('doesn’t block kb shortcuts for non-allowed els', done => {
|
||
const divEl = document.createElement('div');
|
||
element.appendChild(divEl);
|
||
element._handleKey = e => {
|
||
assert.isFalse(element.shouldSuppressKeyboardShortcut(e));
|
||
done();
|
||
};
|
||
MockInteractions.keyDownOn(divEl, 75, null, 'k');
|
||
});
|
||
|
||
test('blocks kb shortcuts for input els', done => {
|
||
const inputEl = document.createElement('input');
|
||
element.appendChild(inputEl);
|
||
element._handleKey = e => {
|
||
assert.isTrue(element.shouldSuppressKeyboardShortcut(e));
|
||
done();
|
||
};
|
||
MockInteractions.keyDownOn(inputEl, 75, null, 'k');
|
||
});
|
||
|
||
test('doesn’t block kb shortcuts for checkboxes', done => {
|
||
const inputEl = document.createElement('input');
|
||
inputEl.setAttribute('type', 'checkbox');
|
||
element.appendChild(inputEl);
|
||
element._handleKey = e => {
|
||
assert.isFalse(element.shouldSuppressKeyboardShortcut(e));
|
||
done();
|
||
};
|
||
MockInteractions.keyDownOn(inputEl, 75, null, 'k');
|
||
});
|
||
|
||
test('blocks kb shortcuts for textarea els', done => {
|
||
const textareaEl = document.createElement('textarea');
|
||
element.appendChild(textareaEl);
|
||
element._handleKey = e => {
|
||
assert.isTrue(element.shouldSuppressKeyboardShortcut(e));
|
||
done();
|
||
};
|
||
MockInteractions.keyDownOn(textareaEl, 75, null, 'k');
|
||
});
|
||
|
||
test('blocks kb shortcuts for anything in a gr-overlay', done => {
|
||
const divEl = document.createElement('div');
|
||
const element =
|
||
overlay.querySelector('keyboard-shortcut-mixin-test-element');
|
||
element.appendChild(divEl);
|
||
element._handleKey = e => {
|
||
assert.isTrue(element.shouldSuppressKeyboardShortcut(e));
|
||
done();
|
||
};
|
||
MockInteractions.keyDownOn(divEl, 75, null, 'k');
|
||
});
|
||
|
||
test('blocks enter shortcut on an anchor', done => {
|
||
const anchorEl = document.createElement('a');
|
||
const element =
|
||
overlay.querySelector('keyboard-shortcut-mixin-test-element');
|
||
element.appendChild(anchorEl);
|
||
element._handleKey = e => {
|
||
assert.isTrue(element.shouldSuppressKeyboardShortcut(e));
|
||
done();
|
||
};
|
||
MockInteractions.keyDownOn(anchorEl, 13, null, 'enter');
|
||
});
|
||
|
||
test('modifierPressed returns accurate values', () => {
|
||
const spy = sinon.spy(element, 'modifierPressed');
|
||
element._handleKey = e => {
|
||
element.modifierPressed(e);
|
||
};
|
||
MockInteractions.keyDownOn(element, 75, 'shift', 'k');
|
||
assert.isTrue(spy.lastCall.returnValue);
|
||
MockInteractions.keyDownOn(element, 75, null, 'k');
|
||
assert.isFalse(spy.lastCall.returnValue);
|
||
MockInteractions.keyDownOn(element, 75, 'ctrl', 'k');
|
||
assert.isTrue(spy.lastCall.returnValue);
|
||
MockInteractions.keyDownOn(element, 75, null, 'k');
|
||
assert.isFalse(spy.lastCall.returnValue);
|
||
MockInteractions.keyDownOn(element, 75, 'meta', 'k');
|
||
assert.isTrue(spy.lastCall.returnValue);
|
||
MockInteractions.keyDownOn(element, 75, null, 'k');
|
||
assert.isFalse(spy.lastCall.returnValue);
|
||
MockInteractions.keyDownOn(element, 75, 'alt', 'k');
|
||
assert.isTrue(spy.lastCall.returnValue);
|
||
});
|
||
|
||
test('isModifierPressed returns accurate value', () => {
|
||
const spy = sinon.spy(element, 'isModifierPressed');
|
||
element._handleKey = e => {
|
||
element.isModifierPressed(e, 'shiftKey');
|
||
};
|
||
MockInteractions.keyDownOn(element, 75, 'shift', 'k');
|
||
assert.isTrue(spy.lastCall.returnValue);
|
||
MockInteractions.keyDownOn(element, 75, null, 'k');
|
||
assert.isFalse(spy.lastCall.returnValue);
|
||
MockInteractions.keyDownOn(element, 75, 'ctrl', 'k');
|
||
assert.isFalse(spy.lastCall.returnValue);
|
||
MockInteractions.keyDownOn(element, 75, null, 'k');
|
||
assert.isFalse(spy.lastCall.returnValue);
|
||
MockInteractions.keyDownOn(element, 75, 'meta', 'k');
|
||
assert.isFalse(spy.lastCall.returnValue);
|
||
MockInteractions.keyDownOn(element, 75, null, 'k');
|
||
assert.isFalse(spy.lastCall.returnValue);
|
||
MockInteractions.keyDownOn(element, 75, 'alt', 'k');
|
||
assert.isFalse(spy.lastCall.returnValue);
|
||
});
|
||
|
||
suite('GO_KEY timing', () => {
|
||
let handlerStub;
|
||
|
||
setup(() => {
|
||
element._shortcut_go_table.set('a', '_handleA');
|
||
handlerStub = element._handleA = sinon.stub();
|
||
sinon.stub(Date, 'now').returns(10000);
|
||
});
|
||
|
||
test('success', () => {
|
||
const e = {detail: {key: 'a'}, preventDefault: () => {}};
|
||
sinon.stub(element, 'shouldSuppressKeyboardShortcut').returns(false);
|
||
element._shortcut_go_key_last_pressed = 9000;
|
||
element._handleGoAction(e);
|
||
assert.isTrue(handlerStub.calledOnce);
|
||
assert.strictEqual(handlerStub.lastCall.args[0], e);
|
||
});
|
||
|
||
test('go key not pressed', () => {
|
||
const e = {detail: {key: 'a'}, preventDefault: () => {}};
|
||
sinon.stub(element, 'shouldSuppressKeyboardShortcut').returns(false);
|
||
element._shortcut_go_key_last_pressed = null;
|
||
element._handleGoAction(e);
|
||
assert.isFalse(handlerStub.called);
|
||
});
|
||
|
||
test('go key pressed too long ago', () => {
|
||
const e = {detail: {key: 'a'}, preventDefault: () => {}};
|
||
sinon.stub(element, 'shouldSuppressKeyboardShortcut').returns(false);
|
||
element._shortcut_go_key_last_pressed = 3000;
|
||
element._handleGoAction(e);
|
||
assert.isFalse(handlerStub.called);
|
||
});
|
||
|
||
test('should suppress', () => {
|
||
const e = {detail: {key: 'a'}, preventDefault: () => {}};
|
||
sinon.stub(element, 'shouldSuppressKeyboardShortcut').returns(true);
|
||
element._shortcut_go_key_last_pressed = 9000;
|
||
element._handleGoAction(e);
|
||
assert.isFalse(handlerStub.called);
|
||
});
|
||
|
||
test('unrecognized key', () => {
|
||
const e = {detail: {key: 'f'}, preventDefault: () => {}};
|
||
sinon.stub(element, 'shouldSuppressKeyboardShortcut').returns(false);
|
||
element._shortcut_go_key_last_pressed = 9000;
|
||
element._handleGoAction(e);
|
||
assert.isFalse(handlerStub.called);
|
||
});
|
||
});
|
||
});
|
||
|