Keyboard shortcuts (#4421)

* Adds shortcut registry and removes action and key map (#4398)

* Adds Shortcut tests and refactored navigation tests (#4412)

* Adds shortcut items (#4408)

* Add shortcuts for navigation (#4409)

* Add final keyboard shortcut cleanup (#4413)
This commit is contained in:
alschmiedt
2020-11-02 13:30:05 -08:00
committed by GitHub
parent 4364bdb18c
commit f1498e7f07
26 changed files with 1782 additions and 1675 deletions

View File

@@ -81,7 +81,7 @@
<script src="input_test.js"></script>
<script src="insertion_marker_test.js"></script>
<script src="json_test.js"></script>
<script src="key_map_test.js"></script>
<script src="shortcut_registry_test.js"></script>
<script src="keydown_test.js"></script>
<script src="logic_ternary_test.js"></script>
<script src="metrics_test.js"></script>

View File

@@ -1,97 +0,0 @@
/**
* @license
* Copyright 2019 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
suite('Key Map Tests', function() {
setup(function() {
sharedTestSetup.call(this);
Blockly.user.keyMap.setKeyMap(Blockly.user.keyMap.createDefaultKeyMap());
});
teardown(function() {
sharedTestTeardown.call(this);
});
test('Test adding a new action to key map', function() {
var newAction = new Blockly.Action('test_action', 'test', function(){
return "test";
});
Blockly.user.keyMap.setActionForKey('65', newAction);
chai.assert.equal(Blockly.user.keyMap.map_['65'].name, 'test_action');
});
test('Test giving an old action a new key', function() {
Blockly.user.keyMap.setActionForKey(Blockly.utils.KeyCodes.F,
Blockly.navigation.ACTION_PREVIOUS);
chai.assert.isUndefined(Blockly.user.keyMap.map_[Blockly.utils.KeyCodes.W]);
chai.assert.equal(Blockly.user.keyMap.map_[Blockly.utils.KeyCodes.F],
Blockly.navigation.ACTION_PREVIOUS);
});
test('Test get key by action defined', function() {
var key = Blockly.user.keyMap.getKeyByAction(Blockly.navigation.ACTION_PREVIOUS);
chai.assert.equal(key, Blockly.utils.KeyCodes.W);
});
test('Test get key by action not defined', function() {
var key = Blockly.user.keyMap.getKeyByAction(new Blockly.Action('something'));
chai.assert.notExists(key);
});
test('Test set key map', function() {
var testKeyMap = Blockly.user.keyMap.createDefaultKeyMap();
testKeyMap['randomKey'] = new Blockly.Action('test','',null);
Blockly.user.keyMap.setKeyMap(testKeyMap);
chai.assert.equal(Blockly.user.keyMap.map_['randomKey'].name, 'test');
});
test('Test get key map returns a clone', function() {
var keyMap = Blockly.user.keyMap.getKeyMap();
keyMap['randomKey'] = new Blockly.Action('test', '', null);
chai.assert.isUndefined(Blockly.user.keyMap.map_['randomKey']);
});
test('Test serialize key code with modifiers', function() {
var mockEvent = {
getModifierState: function(){
return true;
},
keyCode: 65
};
var serializedKey = Blockly.user.keyMap.serializeKeyEvent(mockEvent);
chai.assert.equal(serializedKey, 'ShiftControlAltMeta65');
});
test('Test serialize key code without modifiers', function() {
var mockEvent = {
getModifierState: function(){
return false;
},
keyCode: 65
};
var serializedKey = Blockly.user.keyMap.serializeKeyEvent(mockEvent);
chai.assert.equal(serializedKey, '65');
});
test('Test modifiers in reverse order', function() {
var testKey = Blockly.user.keyMap.createSerializedKey(
Blockly.utils.KeyCodes.K, [Blockly.user.keyMap.modifierKeys.CONTROL,
Blockly.user.keyMap.modifierKeys.SHIFT]);
Blockly.user.keyMap.setActionForKey(testKey, new Blockly.Action('test', '', null));
var action = Blockly.user.keyMap.getActionByKeyCode('ShiftControl75');
chai.assert.isNotNull(action);
chai.assert.equal(action.name, 'test');
});
test('Test report invalid modifiers', function() {
var shouldThrow = function() {
Blockly.user.keyMap.createSerializedKey(Blockly.utils.KeyCodes.K, ['s',
Blockly.user.keyMap.modifierKeys.SHIFT]);
};
chai.assert.throws(shouldThrow, Error, 's is not a valid modifier key.');
});
teardown(function() {});
});

View File

@@ -93,7 +93,7 @@ suite('Key Down', function() {
suite('Copy', function() {
setup(function() {
setSelectedBlock(this.workspace);
this.copySpy = sinon.spy(Blockly, 'copy_');
this.copySpy = sinon.spy(Blockly, 'copy');
this.hideChaffSpy = sinon.spy(Blockly, 'hideChaff');
});
var testCases = [

View File

@@ -12,9 +12,10 @@
suite('Navigation', function() {
function createNavigationWorkspace(enableKeyboardNav) {
function createNavigationWorkspace(enableKeyboardNav, readOnly) {
var toolbox = document.getElementById('toolbox-categories');
var workspace = Blockly.inject('blocklyDiv', {toolbox: toolbox});
var workspace =
Blockly.inject('blocklyDiv', {toolbox: toolbox, readOnly: readOnly});
if (enableKeyboardNav) {
Blockly.navigation.enableKeyboardAccessibility();
Blockly.navigation.currentState_ = Blockly.navigation.STATE_WS;
@@ -45,64 +46,88 @@ suite('Navigation', function() {
]
}]);
this.workspace = createNavigationWorkspace(true);
Blockly.navigation.focusToolbox_();
this.mockEvent = {
getModifierState: function() {
return false;
}
};
Blockly.navigation.focusToolbox_(this.workspace);
});
teardown(function() {
workspaceTeardown.call(this, this.workspace);
});
function testToolboxSelectMethodCalled(ws, mockEvent, keyCode, selectMethodName) {
mockEvent.keyCode = keyCode;
var toolbox = ws.getToolbox();
toolbox.selectedItem_ = toolbox.contents_[0];
var selectNextStub = sinon.stub(toolbox, selectMethodName);
Blockly.navigation.onKeyPress(mockEvent);
sinon.assert.called(selectNextStub);
}
var testCases = [
[
'Calls toolbox selectNext_',
createKeyDownEvent(Blockly.utils.KeyCodes.S, 'NotAField'), 'selectNext_'
],
[
'Calls toolbox selectPrevious_',
createKeyDownEvent(Blockly.utils.KeyCodes.W, 'NotAField'),
'selectPrevious_'
],
[
'Calls toolbox selectParent_',
createKeyDownEvent(Blockly.utils.KeyCodes.D, 'NotAField'),
'selectChild_'
],
[
'Calls toolbox selectChild_',
createKeyDownEvent(Blockly.utils.KeyCodes.A, 'NotAField'),
'selectParent_'
]
];
test('Calls toolbox selectNext_', function() {
testToolboxSelectMethodCalled(this.workspace, this.mockEvent, Blockly.utils.KeyCodes.S, 'selectNext_');
});
test('Calls toolbox selectPrevious_', function() {
testToolboxSelectMethodCalled(this.workspace, this.mockEvent, Blockly.utils.KeyCodes.W, 'selectPrevious_');
});
test('Calls toolbox selectParent_', function() {
testToolboxSelectMethodCalled(this.workspace, this.mockEvent, Blockly.utils.KeyCodes.D, 'selectChild_');
});
test('Calls toolbox selectChild_', function() {
testToolboxSelectMethodCalled(this.workspace, this.mockEvent, Blockly.utils.KeyCodes.A, 'selectParent_');
testCases.forEach(function(testCase) {
var testCaseName = testCase[0];
var mockEvent = testCase[1];
var stubName = testCase[2];
test(testCaseName, function() {
var toolbox = this.workspace.getToolbox();
var selectStub = sinon.stub(toolbox, stubName);
toolbox.selectedItem_ = toolbox.contents_[0];
Blockly.onKeyDown(mockEvent);
sinon.assert.called(selectStub);
});
});
test('Go to flyout', function() {
this.mockEvent.keyCode = Blockly.utils.KeyCodes.D;
chai.assert.isTrue(Blockly.navigation.onKeyPress(this.mockEvent));
chai.assert.equal(Blockly.navigation.currentState_,
Blockly.navigation.STATE_FLYOUT);
var flyoutCursor = Blockly.navigation.getFlyoutCursor_();
var mockEvent = createKeyDownEvent(Blockly.utils.KeyCodes.D, 'NotAField');
var keyDownSpy =
sinon.spy(Blockly.ShortcutRegistry.registry, 'onKeyDown');
Blockly.onKeyDown(mockEvent);
chai.assert.isTrue(keyDownSpy.returned(true));
chai.assert.equal(
Blockly.navigation.currentState_, Blockly.navigation.STATE_FLYOUT);
var flyoutCursor = Blockly.navigation.getFlyoutCursor_();
chai.assert.equal(flyoutCursor.getCurNode().getLocation().getFieldValue("TEXT"),
"FirstCategory-FirstBlock");
});
test('Focuses workspace from toolbox (e)', function() {
Blockly.navigation.currentState_ = Blockly.navigation.STATE_TOOLBOX;
this.mockEvent.keyCode = Blockly.utils.KeyCodes.E;
chai.assert.isTrue(Blockly.navigation.onKeyPress(this.mockEvent));
chai.assert.equal(Blockly.navigation.currentState_,
Blockly.navigation.STATE_WS);
var mockEvent = createKeyDownEvent(Blockly.utils.KeyCodes.E, 'NotAField');
var keyDownSpy =
sinon.spy(Blockly.ShortcutRegistry.registry, 'onKeyDown');
Blockly.onKeyDown(mockEvent);
chai.assert.isTrue(keyDownSpy.returned(true));
chai.assert.equal(
Blockly.navigation.currentState_, Blockly.navigation.STATE_WS);
});
test('Focuses workspace from toolbox (escape)', function() {
Blockly.navigation.currentState_ = Blockly.navigation.STATE_TOOLBOX;
this.mockEvent.keyCode = Blockly.utils.KeyCodes.E;
chai.assert.isTrue(Blockly.navigation.onKeyPress(this.mockEvent));
chai.assert.equal(Blockly.navigation.currentState_,
Blockly.navigation.STATE_WS);
var mockEvent =
createKeyDownEvent(Blockly.utils.KeyCodes.ESC, 'NotAField');
var keyDownSpy =
sinon.spy(Blockly.ShortcutRegistry.registry, 'onKeyDown');
Blockly.onKeyDown(mockEvent);
chai.assert.isTrue(keyDownSpy.returned(true));
chai.assert.equal(
Blockly.navigation.currentState_, Blockly.navigation.STATE_WS);
});
// More tests:
// - nested categories
@@ -124,14 +149,8 @@ suite('Navigation', function() {
]
}]);
this.workspace = createNavigationWorkspace(true);
Blockly.mainWorkspace = this.workspace;
Blockly.navigation.focusToolbox_();
Blockly.navigation.focusFlyout_();
this.mockEvent = {
getModifierState: function() {
return false;
}
};
Blockly.navigation.focusToolbox_(this.workspace);
Blockly.navigation.focusFlyout_(this.workspace);
});
teardown(function() {
@@ -140,10 +159,15 @@ suite('Navigation', function() {
// Should be a no-op
test('Previous at beginning', function() {
this.mockEvent.keyCode = Blockly.utils.KeyCodes.W;
chai.assert.isTrue(Blockly.navigation.onKeyPress(this.mockEvent));
chai.assert.equal(Blockly.navigation.currentState_,
Blockly.navigation.STATE_FLYOUT);
var mockEvent = createKeyDownEvent(Blockly.utils.KeyCodes.W, 'NotAField');
var keyDownSpy =
sinon.spy(Blockly.ShortcutRegistry.registry, 'onKeyDown');
Blockly.onKeyDown(mockEvent);
chai.assert.isTrue(keyDownSpy.returned(true));
chai.assert.equal(
Blockly.navigation.currentState_, Blockly.navigation.STATE_FLYOUT);
chai.assert.equal(Blockly.navigation.getFlyoutCursor_().getCurNode().getLocation().getFieldValue("TEXT"),
"FirstCategory-FirstBlock");
});
@@ -155,45 +179,72 @@ suite('Navigation', function() {
var flyoutBlock = Blockly.navigation.getFlyoutCursor_().getCurNode().getLocation();
chai.assert.equal(flyoutBlock.getFieldValue("TEXT"),
"FirstCategory-SecondBlock");
this.mockEvent.keyCode = Blockly.utils.KeyCodes.W;
chai.assert.isTrue(Blockly.navigation.onKeyPress(this.mockEvent));
chai.assert.equal(Blockly.navigation.currentState_,
Blockly.navigation.STATE_FLYOUT);
var mockEvent = createKeyDownEvent(Blockly.utils.KeyCodes.W, 'NotAField');
var keyDownSpy =
sinon.spy(Blockly.ShortcutRegistry.registry, 'onKeyDown');
Blockly.onKeyDown(mockEvent);
chai.assert.isTrue(keyDownSpy.returned(true));
chai.assert.equal(
Blockly.navigation.currentState_, Blockly.navigation.STATE_FLYOUT);
flyoutBlock = Blockly.navigation.getFlyoutCursor_().getCurNode().getLocation();
chai.assert.equal(flyoutBlock.getFieldValue("TEXT"),
"FirstCategory-FirstBlock");
});
test('Next', function() {
this.mockEvent.keyCode = Blockly.utils.KeyCodes.S;
chai.assert.isTrue(Blockly.navigation.onKeyPress(this.mockEvent));
chai.assert.equal(Blockly.navigation.currentState_,
Blockly.navigation.STATE_FLYOUT);
var mockEvent = createKeyDownEvent(Blockly.utils.KeyCodes.S, 'NotAField');
var keyDownSpy =
sinon.spy(Blockly.ShortcutRegistry.registry, 'onKeyDown');
Blockly.onKeyDown(mockEvent);
chai.assert.isTrue(keyDownSpy.returned(true));
chai.assert.equal(
Blockly.navigation.currentState_, Blockly.navigation.STATE_FLYOUT);
var flyoutBlock = Blockly.navigation.getFlyoutCursor_().getCurNode().getLocation();
chai.assert.equal(flyoutBlock.getFieldValue("TEXT"),
"FirstCategory-SecondBlock");
});
test('Out', function() {
this.mockEvent.keyCode = Blockly.utils.KeyCodes.A;
chai.assert.isTrue(Blockly.navigation.onKeyPress(this.mockEvent));
chai.assert.equal(Blockly.navigation.currentState_,
Blockly.navigation.STATE_TOOLBOX);
var mockEvent = createKeyDownEvent(Blockly.utils.KeyCodes.A, 'NotAField');
var keyDownSpy =
sinon.spy(Blockly.ShortcutRegistry.registry, 'onKeyDown');
Blockly.onKeyDown(mockEvent);
chai.assert.isTrue(keyDownSpy.returned(true));
chai.assert.equal(
Blockly.navigation.currentState_, Blockly.navigation.STATE_TOOLBOX);
});
test('MARK', function() {
this.mockEvent.keyCode = Blockly.utils.KeyCodes.ENTER;
chai.assert.isTrue(Blockly.navigation.onKeyPress(this.mockEvent));
chai.assert.equal(Blockly.navigation.currentState_,
Blockly.navigation.STATE_WS);
test('Mark', function() {
var mockEvent =
createKeyDownEvent(Blockly.utils.KeyCodes.ENTER, 'NotAField');
var keyDownSpy =
sinon.spy(Blockly.ShortcutRegistry.registry, 'onKeyDown');
Blockly.onKeyDown(mockEvent);
chai.assert.isTrue(keyDownSpy.returned(true));
chai.assert.equal(
Blockly.navigation.currentState_, Blockly.navigation.STATE_WS);
chai.assert.equal(this.workspace.getTopBlocks().length, 1);
});
test('EXIT', function() {
this.mockEvent.keyCode = Blockly.utils.KeyCodes.ESC;
chai.assert.isTrue(Blockly.navigation.onKeyPress(this.mockEvent));
chai.assert.equal(Blockly.navigation.currentState_,
Blockly.navigation.STATE_WS);
test('Exit', function() {
var mockEvent =
createKeyDownEvent(Blockly.utils.KeyCodes.ESC, 'NotAField');
var keyDownSpy =
sinon.spy(Blockly.ShortcutRegistry.registry, 'onKeyDown');
Blockly.onKeyDown(mockEvent);
chai.assert.isTrue(keyDownSpy.returned(true));
chai.assert.equal(
Blockly.navigation.currentState_, Blockly.navigation.STATE_WS);
});
});
@@ -216,12 +267,6 @@ suite('Navigation', function() {
}]);
this.workspace = createNavigationWorkspace(true);
this.basicBlock = this.workspace.newBlock('basic_block');
this.firstCategory_ = this.workspace.getToolbox().contents_[0];
this.mockEvent = {
getModifierState: function() {
return false;
}
};
});
teardown(function() {
@@ -229,77 +274,106 @@ suite('Navigation', function() {
});
test('Previous', function() {
sinon.spy(this.workspace.getCursor(), 'prev');
this.mockEvent.keyCode = Blockly.utils.KeyCodes.W;
chai.assert.isTrue(Blockly.navigation.onKeyPress(this.mockEvent));
sinon.assert.calledOnce(this.workspace.getCursor().prev);
chai.assert.equal(Blockly.navigation.currentState_,
Blockly.navigation.STATE_WS);
this.workspace.getCursor().prev.restore();
var prevSpy = sinon.spy(this.workspace.getCursor(), 'prev');
var keyDownSpy =
sinon.spy(Blockly.ShortcutRegistry.registry, 'onKeyDown');
var wEvent = createKeyDownEvent(Blockly.utils.KeyCodes.W, '');
Blockly.onKeyDown(wEvent);
chai.assert.isTrue(keyDownSpy.returned(true));
sinon.assert.calledOnce(prevSpy);
chai.assert.equal(
Blockly.navigation.currentState_, Blockly.navigation.STATE_WS);
});
test('Next', function() {
var cursor = this.workspace.getCursor();
sinon.spy(cursor, 'next');
this.mockEvent.keyCode = Blockly.utils.KeyCodes.S;
chai.assert.isTrue(Blockly.navigation.onKeyPress(this.mockEvent));
sinon.assert.calledOnce(cursor.next);
chai.assert.equal(Blockly.navigation.currentState_,
Blockly.navigation.STATE_WS);
cursor.next.restore();
var nextSpy = sinon.spy(this.workspace.getCursor(), 'next');
var keyDownSpy =
sinon.spy(Blockly.ShortcutRegistry.registry, 'onKeyDown');
var sEvent = createKeyDownEvent(Blockly.utils.KeyCodes.S, '');
Blockly.onKeyDown(sEvent);
chai.assert.isTrue(keyDownSpy.returned(true));
sinon.assert.calledOnce(nextSpy);
chai.assert.equal(
Blockly.navigation.currentState_, Blockly.navigation.STATE_WS);
});
test('Out', function() {
var cursor = this.workspace.getCursor();
sinon.spy(cursor, 'out');
this.mockEvent.keyCode = Blockly.utils.KeyCodes.A;
chai.assert.isTrue(Blockly.navigation.onKeyPress(this.mockEvent));
sinon.assert.calledOnce(cursor.out);
chai.assert.equal(Blockly.navigation.currentState_,
Blockly.navigation.STATE_WS);
cursor.out.restore();
var outSpy = sinon.spy(this.workspace.getCursor(), 'out');
var keyDownSpy =
sinon.spy(Blockly.ShortcutRegistry.registry, 'onKeyDown');
var aEvent = createKeyDownEvent(Blockly.utils.KeyCodes.A, '');
Blockly.onKeyDown(aEvent);
chai.assert.isTrue(keyDownSpy.returned(true));
sinon.assert.calledOnce(outSpy);
chai.assert.equal(
Blockly.navigation.currentState_, Blockly.navigation.STATE_WS);
});
test('In', function() {
var cursor = this.workspace.getCursor();
sinon.spy(cursor, 'in');
this.mockEvent.keyCode = Blockly.utils.KeyCodes.D;
chai.assert.isTrue(Blockly.navigation.onKeyPress(this.mockEvent));
sinon.assert.calledOnce(cursor.in);
chai.assert.equal(Blockly.navigation.currentState_,
Blockly.navigation.STATE_WS);
cursor.in.restore();
var inSpy = sinon.spy(this.workspace.getCursor(), 'in');
var keyDownSpy =
sinon.spy(Blockly.ShortcutRegistry.registry, 'onKeyDown');
var dEvent = createKeyDownEvent(Blockly.utils.KeyCodes.D, '');
Blockly.onKeyDown(dEvent);
chai.assert.isTrue(keyDownSpy.returned(true));
sinon.assert.calledOnce(inSpy);
chai.assert.equal(
Blockly.navigation.currentState_, Blockly.navigation.STATE_WS);
});
test('Insert', function() {
// Stub modify as we are not testing its behavior, only if it was called.
// Otherwise, there is a warning because there is no marked node.
sinon.stub(Blockly.navigation, 'modify_');
this.mockEvent.keyCode = Blockly.utils.KeyCodes.I;
chai.assert.isTrue(Blockly.navigation.onKeyPress(this.mockEvent));
sinon.assert.calledOnce(Blockly.navigation.modify_);
chai.assert.equal(Blockly.navigation.currentState_,
Blockly.navigation.STATE_WS);
Blockly.navigation.modify_.restore();
var modifyStub = sinon.stub(Blockly.navigation, 'modify_').returns(true);
var keyDownSpy =
sinon.spy(Blockly.ShortcutRegistry.registry, 'onKeyDown');
var iEvent = createKeyDownEvent(Blockly.utils.KeyCodes.I, '');
Blockly.onKeyDown(iEvent);
chai.assert.isTrue(keyDownSpy.returned(true));
sinon.assert.calledOnce(modifyStub);
chai.assert.equal(
Blockly.navigation.currentState_, Blockly.navigation.STATE_WS);
});
test('Mark', function() {
this.workspace.getCursor().setCurNode(
Blockly.ASTNode.createConnectionNode(this.basicBlock.previousConnection));
this.mockEvent.keyCode = Blockly.utils.KeyCodes.ENTER;
chai.assert.isTrue(Blockly.navigation.onKeyPress(this.mockEvent));
var keyDownSpy =
sinon.spy(Blockly.ShortcutRegistry.registry, 'onKeyDown');
var enterEvent = createKeyDownEvent(Blockly.utils.KeyCodes.ENTER, '');
Blockly.onKeyDown(enterEvent);
var markedNode = this.workspace.getMarker(Blockly.navigation.MARKER_NAME).getCurNode();
chai.assert.isTrue(keyDownSpy.returned(true));
chai.assert.equal(markedNode.getLocation(), this.basicBlock.previousConnection);
chai.assert.equal(Blockly.navigation.currentState_,
Blockly.navigation.STATE_WS);
chai.assert.equal(
Blockly.navigation.currentState_, Blockly.navigation.STATE_WS);
});
test('Toolbox', function() {
this.mockEvent.keyCode = Blockly.utils.KeyCodes.T;
chai.assert.isTrue(Blockly.navigation.onKeyPress(this.mockEvent));
chai.assert.equal(this.workspace.getToolbox().getSelectedItem(), this.firstCategory_);
chai.assert.equal(Blockly.navigation.currentState_,
Blockly.navigation.STATE_TOOLBOX);
var keyDownSpy =
sinon.spy(Blockly.ShortcutRegistry.registry, 'onKeyDown');
var tEvent = createKeyDownEvent(Blockly.utils.KeyCodes.T, '');
Blockly.onKeyDown(tEvent);
var firstCategory = this.workspace.getToolbox().contents_[0];
chai.assert.isTrue(keyDownSpy.returned(true));
chai.assert.equal(
this.workspace.getToolbox().getSelectedItem(), firstCategory);
chai.assert.equal(
Blockly.navigation.currentState_, Blockly.navigation.STATE_TOOLBOX);
});
});
@@ -325,83 +399,84 @@ suite('Navigation', function() {
this.workspace = createNavigationWorkspace(true);
this.workspace.getCursor().drawer_ = null;
this.basicBlock = this.workspace.newBlock('basic_block');
Blockly.user.keyMap.setKeyMap(Blockly.user.keyMap.createDefaultKeyMap());
Blockly.mainWorkspace = this.workspace;
Blockly.getMainWorkspace().keyboardAccessibilityMode = true;
Blockly.navigation.currentState_ = Blockly.navigation.STATE_WS;
this.mockEvent = {
getModifierState: function() {
return false;
}
};
});
teardown(function() {
workspaceTeardown.call(this, this.workspace);
});
test('Action does not exist', function() {
var block = this.workspace.getTopBlocks()[0];
var field = block.inputList[0].fieldRow[0];
sinon.spy(field, 'onBlocklyAction');
var fieldSpy = sinon.spy(field, 'onBlocklyAction');
var mockEvent = createKeyDownEvent(Blockly.utils.KeyCodes.N, '');
var keyDownSpy =
sinon.spy(Blockly.ShortcutRegistry.registry, 'onKeyDown');
this.workspace.getCursor().setCurNode(Blockly.ASTNode.createFieldNode(field));
this.mockEvent.keyCode = Blockly.utils.KeyCodes.N;
var isHandled = Blockly.navigation.onKeyPress(this.mockEvent);
chai.assert.isFalse(isHandled);
sinon.assert.notCalled(field.onBlocklyAction);
field.onBlocklyAction.restore();
Blockly.onKeyDown(mockEvent);
chai.assert.isFalse(keyDownSpy.returned(true));
sinon.assert.notCalled(fieldSpy);
});
test('Action exists - field handles action', function() {
var block = this.workspace.getTopBlocks()[0];
var field = block.inputList[0].fieldRow[0];
sinon.stub(field, 'onBlocklyAction').returns(true);
var mockEvent = createKeyDownEvent(Blockly.utils.KeyCodes.A, '');
var fieldSpy = sinon.stub(field, 'onBlocklyAction').returns(true);
var keyDownSpy =
sinon.spy(Blockly.ShortcutRegistry.registry, 'onKeyDown');
this.workspace.getCursor().setCurNode(Blockly.ASTNode.createFieldNode(field));
var isHandled = Blockly.navigation.onBlocklyAction(Blockly.navigation.ACTION_OUT);
chai.assert.isTrue(isHandled);
sinon.assert.calledOnce(field.onBlocklyAction);
Blockly.onKeyDown(mockEvent);
chai.assert.isTrue(keyDownSpy.returned(true));
sinon.assert.calledOnce(fieldSpy);
field.onBlocklyAction.restore();
});
test('Action exists - field does not handle action', function() {
var block = this.workspace.getTopBlocks()[0];
var field = block.inputList[0].fieldRow[0];
sinon.spy(field, 'onBlocklyAction');
var mockEvent = createKeyDownEvent(Blockly.utils.KeyCodes.A, '');
var fieldSpy = sinon.spy(field, 'onBlocklyAction');
var keyDownSpy =
sinon.spy(Blockly.ShortcutRegistry.registry, 'onKeyDown');
this.workspace.getCursor().setCurNode(Blockly.ASTNode.createFieldNode(field));
this.mockEvent.keyCode = Blockly.utils.KeyCodes.A;
var isHandled = Blockly.navigation.onBlocklyAction(Blockly.navigation.ACTION_OUT);
chai.assert.isTrue(isHandled);
sinon.assert.calledOnce(field.onBlocklyAction);
Blockly.onKeyDown(mockEvent);
field.onBlocklyAction.restore();
chai.assert.isTrue(keyDownSpy.returned(true));
sinon.assert.calledOnce(fieldSpy);
});
test('Toggle Action Off', function() {
this.mockEvent.keyCode = 'ShiftControl75';
sinon.spy(Blockly.navigation, 'onBlocklyAction');
Blockly.getMainWorkspace().keyboardAccessibilityMode = true;
var mockEvent = createKeyDownEvent(
Blockly.utils.KeyCodes.K, '',
[Blockly.utils.KeyCodes.SHIFT, Blockly.utils.KeyCodes.CTRL]);
var keyDownSpy =
sinon.spy(Blockly.ShortcutRegistry.registry, 'onKeyDown');
this.workspace.keyboardAccessibilityMode = true;
var isHandled = Blockly.navigation.onKeyPress(this.mockEvent);
chai.assert.isTrue(isHandled);
sinon.assert.calledOnce(Blockly.navigation.onBlocklyAction);
chai.assert.isFalse(Blockly.getMainWorkspace().keyboardAccessibilityMode);
Blockly.navigation.onBlocklyAction.restore();
Blockly.onKeyDown(mockEvent);
chai.assert.isTrue(keyDownSpy.returned(true));
chai.assert.isFalse(this.workspace.keyboardAccessibilityMode);
});
test('Toggle Action On', function() {
this.mockEvent.keyCode = 'ShiftControl75';
sinon.stub(Blockly.navigation, 'focusWorkspace_');
Blockly.getMainWorkspace().keyboardAccessibilityMode = false;
var mockEvent = createKeyDownEvent(
Blockly.utils.KeyCodes.K, '',
[Blockly.utils.KeyCodes.SHIFT, Blockly.utils.KeyCodes.CTRL]);
var keyDownSpy =
sinon.spy(Blockly.ShortcutRegistry.registry, 'onKeyDown');
this.workspace.keyboardAccessibilityMode = false;
var isHandled = Blockly.navigation.onKeyPress(this.mockEvent);
chai.assert.isTrue(isHandled);
sinon.assert.calledOnce(Blockly.navigation.focusWorkspace_);
chai.assert.isTrue(Blockly.getMainWorkspace().keyboardAccessibilityMode);
Blockly.navigation.focusWorkspace_.restore();
Blockly.onKeyDown(mockEvent);
chai.assert.isTrue(keyDownSpy.returned(true));
chai.assert.isTrue(this.workspace.keyboardAccessibilityMode);
});
suite('Test key press in read only mode', function() {
@@ -431,19 +506,12 @@ suite('Navigation', function() {
"tooltip": "",
"helpUrl": ""
}]);
this.workspace = Blockly.inject('blocklyDiv', {readOnly: true});
this.workspace = createNavigationWorkspace(true, true);
Blockly.mainWorkspace = this.workspace;
this.workspace.getCursor().drawer_ = null;
Blockly.getMainWorkspace().keyboardAccessibilityMode = true;
Blockly.navigation.currentState_ = Blockly.navigation.STATE_WS;
this.fieldBlock1 = this.workspace.newBlock('field_block');
this.mockEvent = {
getModifierState: function() {
return false;
}
};
});
teardown(function() {
@@ -452,24 +520,39 @@ suite('Navigation', function() {
test('Perform valid action for read only', function() {
var astNode = Blockly.ASTNode.createBlockNode(this.fieldBlock1);
var mockEvent = createKeyDownEvent(Blockly.utils.KeyCodes.S, '');
this.workspace.getCursor().setCurNode(astNode);
this.mockEvent.keyCode = Blockly.utils.KeyCodes.S;
chai.assert.isTrue(Blockly.navigation.onKeyPress(this.mockEvent));
var keyDownSpy =
sinon.spy(Blockly.ShortcutRegistry.registry, 'onKeyDown');
Blockly.onKeyDown(mockEvent);
chai.assert.isTrue(keyDownSpy.returned(true));
});
test('Perform invalid action for read only', function() {
var astNode = Blockly.ASTNode.createBlockNode(this.fieldBlock1);
var mockEvent = createKeyDownEvent(Blockly.utils.KeyCodes.I, '');
this.workspace.getCursor().setCurNode(astNode);
this.mockEvent.keyCode = Blockly.utils.KeyCodes.I;
chai.assert.isFalse(Blockly.navigation.onKeyPress(this.mockEvent));
var keyDownSpy =
sinon.spy(Blockly.ShortcutRegistry.registry, 'onKeyDown');
Blockly.onKeyDown(mockEvent);
chai.assert.isTrue(keyDownSpy.returned(false));
});
test('Try to perform action on a field', function() {
var field = this.fieldBlock1.inputList[0].fieldRow[0];
var astNode = Blockly.ASTNode.createFieldNode(field);
var mockEvent = createKeyDownEvent(Blockly.utils.KeyCodes.ENTER, '');
this.workspace.getCursor().setCurNode(astNode);
this.mockEvent.keyCode = Blockly.utils.KeyCodes.ENTER;
chai.assert.isFalse(Blockly.navigation.onKeyPress(this.mockEvent));
var keyDownSpy =
sinon.spy(Blockly.ShortcutRegistry.registry, 'onKeyDown');
Blockly.onKeyDown(mockEvent);
chai.assert.isTrue(keyDownSpy.returned(false));
});
});
});
@@ -508,9 +591,9 @@ suite('Navigation', function() {
var prevNode = Blockly.ASTNode.createConnectionNode(previousConnection);
this.workspace.getMarker(Blockly.navigation.MARKER_NAME).setCurNode(prevNode);
Blockly.navigation.focusToolbox_();
Blockly.navigation.focusFlyout_();
Blockly.navigation.insertFromFlyout();
Blockly.navigation.focusToolbox_(this.workspace);
Blockly.navigation.focusFlyout_(this.workspace);
Blockly.navigation.insertFromFlyout(this.workspace);
var insertedBlock = this.basicBlock.previousConnection.targetBlock();
@@ -520,9 +603,9 @@ suite('Navigation', function() {
});
test('Insert Block from flyout without marking a connection', function() {
Blockly.navigation.focusToolbox_();
Blockly.navigation.focusFlyout_();
Blockly.navigation.insertFromFlyout();
Blockly.navigation.focusToolbox_(this.workspace);
Blockly.navigation.focusFlyout_(this.workspace);
Blockly.navigation.insertFromFlyout(this.workspace);
var numBlocks = this.workspace.getTopBlocks().length;

View File

@@ -0,0 +1,355 @@
/**
* @license
* Copyright 2020 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
suite('Keyboard Shortcut Registry Test', function() {
setup(function() {
sharedTestSetup.call(this);
this.registry = new Blockly.ShortcutRegistry();
});
teardown(function() {
sharedTestTeardown.call(this);
});
suite('Registering', function() {
test('Registering a shortcut', function() {
var testShortcut = {'name': 'test_shortcut'};
this.registry.register(testShortcut, true);
var shortcut = this.registry.registry_['test_shortcut'];
chai.assert.equal(shortcut.name, 'test_shortcut');
});
test('Registers shortcut with same name', function() {
var registry = this.registry;
var testShortcut = {'name': 'test_shortcut'};
registry.registry_['test_shortcut'] = [testShortcut];
var shouldThrow = function() {
registry.register(testShortcut);
};
chai.assert.throws(
shouldThrow, Error,
'Shortcut with name "test_shortcut" already exists.');
});
test(
'Registers shortcut with same name opt_allowOverrides=true',
function() {
var registry = this.registry;
var testShortcut = {'name': 'test_shortcut'};
var otherShortcut = {
'name': 'test_shortcut',
'callback': function() {}
};
registry.registry_['test_shortcut'] = [testShortcut];
var shouldNotThrow = function() {
registry.register(otherShortcut, true);
};
chai.assert.doesNotThrow(shouldNotThrow);
chai.assert.exists(registry.registry_['test_shortcut'].callback);
});
});
suite('Unregistering', function() {
test('Unregistering a shortcut', function() {
var testShortcut = {'name': 'test_shortcut'};
this.registry.registry_['test'] = [testShortcut];
chai.assert.isOk(this.registry.registry_['test']);
this.registry.unregister('test', 'test_shortcut');
chai.assert.isUndefined(this.registry.registry_['test']);
});
test('Unregistering a nonexistent shortcut', function() {
var consoleStub = sinon.stub(console, 'warn');
chai.assert.isUndefined(this.registry.registry_['test']);
var registry = this.registry;
chai.assert.isFalse(registry.unregister('test', 'test_shortcut'));
sinon.assert.calledOnceWithExactly(consoleStub, 'Keyboard shortcut with name "test" not found.');
});
test('Unregistering a shortcut with key mappings', function() {
var testShortcut = {'name': 'test_shortcut'};
this.registry.keyMap_['keyCode'] = ['test_shortcut'];
this.registry.registry_['test_shortcut'] = testShortcut;
this.registry.unregister('test_shortcut');
var shortcut = this.registry.registry_['test'];
var keyMappings = this.registry.keyMap_['keyCode'];
chai.assert.isUndefined(shortcut);
chai.assert.isUndefined(keyMappings);
});
test('Unregistering a shortcut with colliding key mappings', function() {
var testShortcut = {'name': 'test_shortcut'};
this.registry.keyMap_['keyCode'] = ['test_shortcut', 'other_shortcutt'];
this.registry.registry_['test_shortcut'] = testShortcut;
this.registry.unregister('test_shortcut');
var shortcut = this.registry.registry_['test'];
var keyMappings = this.registry.keyMap_['keyCode'];
chai.assert.lengthOf(keyMappings, 1);
chai.assert.isUndefined(shortcut);
});
});
suite('addKeyMapping', function() {
test('Adds a key mapping', function() {
this.registry.registry_['test_shortcut'] = {'name': 'test_shortcut'};
this.registry.addKeyMapping('keyCode', 'test_shortcut');
var shortcutNames = this.registry.keyMap_['keyCode'];
chai.assert.lengthOf(shortcutNames, 1);
chai.assert.equal(shortcutNames[0], 'test_shortcut');
});
test('Adds a colliding key mapping - opt_allowCollision=true', function() {
this.registry.registry_['test_shortcut'] = {'name': 'test_shortcut'};
this.registry.keyMap_['keyCode'] = ['test_shortcut_2'];
this.registry.addKeyMapping('keyCode', 'test_shortcut', true);
var shortcutNames = this.registry.keyMap_['keyCode'];
chai.assert.lengthOf(shortcutNames, 2);
chai.assert.equal(shortcutNames[0], 'test_shortcut');
chai.assert.equal(shortcutNames[1], 'test_shortcut_2');
});
test('Adds a colliding key mapping - opt_allowCollision=false', function() {
this.registry.registry_['test_shortcut'] = {'name': 'test_shortcut'};
this.registry.keyMap_['keyCode'] = ['test_shortcut_2'];
var registry = this.registry;
var shouldThrow = function() {
registry.addKeyMapping('keyCode', 'test_shortcut');
};
chai.assert.throws(
shouldThrow, Error,
'Shortcut with name "test_shortcut" collides with shortcuts test_shortcut_2');
});
});
suite('removeKeyMapping', function() {
test('Removes a key mapping', function() {
this.registry.registry_['test_shortcut'] = {'name': 'test_shortcut'};
this.registry.keyMap_['keyCode'] = ['test_shortcut', 'test_shortcut_2'];
var isRemoved =
this.registry.removeKeyMapping('keyCode', 'test_shortcut');
var shortcutNames = this.registry.keyMap_['keyCode'];
chai.assert.lengthOf(shortcutNames, 1);
chai.assert.equal(shortcutNames[0], 'test_shortcut_2');
chai.assert.isTrue(isRemoved);
});
test('Removes last key mapping for a key', function() {
this.registry.registry_['test_shortcut'] = {'name': 'test_shortcut'};
this.registry.keyMap_['keyCode'] = ['test_shortcut'];
this.registry.removeKeyMapping('keyCode', 'test_shortcut');
var shortcutNames = this.registry.keyMap_['keyCode'];
chai.assert.isUndefined(shortcutNames);
});
test('Removes a key map that does not exist opt_quiet=false', function() {
var consoleStub = sinon.stub(console, 'warn');
this.registry.keyMap_['keyCode'] = ['test_shortcut_2'];
var isRemoved =
this.registry.removeKeyMapping('keyCode', 'test_shortcut');
chai.assert.isFalse(isRemoved);
sinon.assert.calledOnceWithExactly(
consoleStub,
'No keyboard shortcut with name "test_shortcut" registered with key code "keyCode"');
});
test(
'Removes a key map that does not exist from empty key mapping opt_quiet=false',
function() {
var consoleStub = sinon.stub(console, 'warn');
var isRemoved =
this.registry.removeKeyMapping('keyCode', 'test_shortcut');
chai.assert.isFalse(isRemoved);
sinon.assert.calledOnceWithExactly(
consoleStub,
'No keyboard shortcut with name "test_shortcut" registered with key code "keyCode"');
});
});
suite('Setters/Getters', function() {
test('Sets the key map', function() {
this.registry.setKeyMap({'keyCode': ['test_shortcut']});
chai.assert.lengthOf(Object.keys(this.registry.keyMap_), 1);
chai.assert.equal(this.registry.keyMap_['keyCode'][0], 'test_shortcut');
});
test('Gets a copy of the key map', function() {
this.registry.keyMap_['keyCode'] = ['a'];
var keyMapCopy = this.registry.getKeyMap();
keyMapCopy['keyCode'] = ['b'];
chai.assert.equal(this.registry.keyMap_['keyCode'][0], 'a');
});
test('Gets a copy of the registry', function() {
this.registry.registry_['shortcutName'] = {'name': 'shortcutName'};
var registrycopy = this.registry.getRegistry();
registrycopy['shortcutName']['name'] = 'shortcutName1';
chai.assert.equal(
this.registry.registry_['shortcutName']['name'], 'shortcutName');
});
test('Gets keyboard shortcuts from a key code', function() {
this.registry.keyMap_['keyCode'] = ['shortcutName'];
var shortcutNames = this.registry.getKeyboardShortcuts('keyCode');
chai.assert.equal(shortcutNames[0], 'shortcutName');
});
test('Gets keycodes by shortcut name', function() {
this.registry.keyMap_['keyCode'] = ['shortcutName'];
this.registry.keyMap_['keyCode1'] = ['shortcutName'];
var shortcutNames =
this.registry.getKeyCodeByShortcutName('shortcutName');
chai.assert.lengthOf(shortcutNames, 2);
chai.assert.equal(shortcutNames[0], 'keyCode');
chai.assert.equal(shortcutNames[1], 'keyCode1');
});
});
suite('onKeyDown', function() {
function addShortcut(registry, shortcut, keyCode, returns) {
registry.register(shortcut, true);
registry.addKeyMapping(keyCode, shortcut.name, true);
return sinon.stub(shortcut, 'callback').returns(returns);
}
setup(function() {
this.testShortcut = {
'name': 'test_shortcut',
'callback': function() {
return true;
},
'precondition': function() {
return true;
}
};
this.callBackStub =
addShortcut(this.registry, this.testShortcut, 'keyCode', true);
});
test('Execute a shortcut from event', function() {
var event = createKeyDownEvent('keyCode', '');
chai.assert.isTrue(this.registry.onKeyDown(this.workspace, event));
sinon.assert.calledOnce(this.callBackStub);
});
test('No shortcut executed from event', function() {
var event = createKeyDownEvent('nonExistentKeyCode', '');
chai.assert.isFalse(this.registry.onKeyDown(this.workspace, event));
});
test('No precondition available - execute callback', function() {
delete this.testShortcut['precondition'];
var event = createKeyDownEvent('keyCode', '');
chai.assert.isTrue(this.registry.onKeyDown(this.workspace, event));
sinon.assert.calledOnce(this.callBackStub);
});
test('Execute all shortcuts in list', function() {
var event = createKeyDownEvent('keyCode', '');
var testShortcut2 = {
'name': 'test_shortcut_2',
'callback': function() {
return false;
},
'precondition': function() {
return false;
}
};
var testShortcut2Stub =
addShortcut(this.registry, testShortcut2, 'keyCode', false);
chai.assert.isTrue(this.registry.onKeyDown(this.workspace, event));
sinon.assert.calledOnce(testShortcut2Stub);
sinon.assert.calledOnce(this.callBackStub);
});
test('Stop executing shortcut when event is handled', function() {
var event = createKeyDownEvent('keyCode', '');
var testShortcut2 = {
'name': 'test_shortcut_2',
'callback': function() {
return false;
},
'precondition': function() {
return false;
}
};
var testShortcut2Stub =
addShortcut(this.registry, testShortcut2, 'keyCode', true);
chai.assert.isTrue(this.registry.onKeyDown(this.workspace, event));
sinon.assert.calledOnce(testShortcut2Stub);
sinon.assert.notCalled(this.callBackStub);
});
});
suite('createSerializedKey', function() {
test('Serialize key', function() {
var serializedKey =
this.registry.createSerializedKey(Blockly.utils.KeyCodes.A);
chai.assert.equal(serializedKey, '65');
});
test('Serialize key code and modifier', function() {
var serializedKey = this.registry.createSerializedKey(
Blockly.utils.KeyCodes.A, [Blockly.utils.KeyCodes.CTRL]);
chai.assert.equal(serializedKey, 'Control+65');
});
test('Serialize only a modifier', function() {
var serializedKey = this.registry.createSerializedKey(
null, [Blockly.utils.KeyCodes.CTRL]);
chai.assert.equal(serializedKey, 'Control');
});
test('Serialize multiple modifiers', function() {
var serializedKey = this.registry.createSerializedKey(
null, [Blockly.utils.KeyCodes.CTRL, Blockly.utils.KeyCodes.SHIFT]);
chai.assert.equal(serializedKey, 'Shift+Control');
});
test('Order of modifiers should result in same serialized key', function() {
var serializedKey = this.registry.createSerializedKey(
null, [Blockly.utils.KeyCodes.CTRL, Blockly.utils.KeyCodes.SHIFT]);
chai.assert.equal(serializedKey, 'Shift+Control');
var serializedKeyNewOrder = this.registry.createSerializedKey(
null, [Blockly.utils.KeyCodes.SHIFT, Blockly.utils.KeyCodes.CTRL]);
chai.assert.equal(serializedKeyNewOrder, 'Shift+Control');
});
});
suite('serializeKeyEvent', function() {
test('Serialize key', function() {
var mockEvent = createKeyDownEvent(Blockly.utils.KeyCodes.A, '');
var serializedKey = this.registry.serializeKeyEvent_(mockEvent);
chai.assert.equal(serializedKey, '65');
});
test('Serialize key code and modifier', function() {
var mockEvent = createKeyDownEvent(
Blockly.utils.KeyCodes.A, '', [Blockly.utils.KeyCodes.CTRL]);
var serializedKey = this.registry.serializeKeyEvent_(mockEvent);
chai.assert.equal(serializedKey, 'Control+65');
});
test('Serialize only a modifier', function() {
var mockEvent =
createKeyDownEvent(null, '', [Blockly.utils.KeyCodes.CTRL]);
var serializedKey = this.registry.serializeKeyEvent_(mockEvent);
chai.assert.equal(serializedKey, 'Control');
});
test('Serialize multiple modifiers', function() {
var mockEvent = createKeyDownEvent(
null, '',
[Blockly.utils.KeyCodes.CTRL, Blockly.utils.KeyCodes.SHIFT]);
var serializedKey = this.registry.serializeKeyEvent_(mockEvent);
chai.assert.equal(serializedKey, 'Shift+Control');
});
test('Throw error when incorrect modifier', function() {
var registry = this.registry;
var shouldThrow = function() {
registry.createSerializedKey(Blockly.utils.KeyCodes.K, ['s']);
};
chai.assert.throws(shouldThrow, Error, 's is not a valid modifier key.');
});
});
teardown(function() {});
});

View File

@@ -576,10 +576,17 @@ function dispatchPointerEvent(target, type, properties) {
function createKeyDownEvent(keyCode, type, modifiers) {
var event = {
keyCode: keyCode,
target: {
type: type
},
getModifierState: function() {
target: {type: type},
getModifierState: function(name) {
if (name == 'Shift' && this.shiftKey) {
return true;
} else if (name == 'Control' && this.ctrlKey) {
return true;
} else if (name == 'Meta' && this.metaKey) {
return true;
} else if (name == 'Alt' && this.altKey) {
return true;
}
return false;
},
preventDefault: function() {}