diff --git a/tests/mocha/.eslintrc.json b/tests/mocha/.eslintrc.json index 2b1d412ba..33f4bcc42 100644 --- a/tests/mocha/.eslintrc.json +++ b/tests/mocha/.eslintrc.json @@ -21,6 +21,7 @@ "assertWarnings": true, "captureWarnings": true, "createDeprecationWarningStub": true, + "createKeyDownEvent": true, "createRenderedBlock": true, "createTestBlock": true, "defineBasicBlockWithField": true, diff --git a/tests/mocha/index.html b/tests/mocha/index.html index 9266a4179..2ee094490 100644 --- a/tests/mocha/index.html +++ b/tests/mocha/index.html @@ -82,6 +82,7 @@ + diff --git a/tests/mocha/keydown_test.js b/tests/mocha/keydown_test.js new file mode 100644 index 000000000..d41517c30 --- /dev/null +++ b/tests/mocha/keydown_test.js @@ -0,0 +1,277 @@ +/** + * @license + * Copyright 2020 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +suite('Key Down', function() { + setup(function() { + sharedTestSetup.call(this); + this.workspace = Blockly.inject('blocklyDiv', {}); + }); + teardown(function() { + sharedTestTeardown.call(this); + }); + + /** + * Creates a block and sets it as Blockly.selected. + * @param {Blockly.Workspace} workspace The workspace to create a new block on. + */ + function setSelectedBlock(workspace) { + defineStackBlock(this.sharedCleanup); + Blockly.selected = workspace.newBlock('stack_block'); + } + + /** + * Creates a test for not running keyDown events when the workspace is in read only mode. + * @param {Object} keyEvent Mocked key down event. Use createKeyDownEvent. + * @param {string=} opt_name An optional name for the test case. + */ + function runReadOnlyTest(keyEvent, opt_name) { + var name = opt_name ? opt_name : 'Not called when readOnly is true'; + test(name, function() { + this.workspace.options.readOnly = true; + Blockly.onKeyDown(keyEvent); + sinon.assert.notCalled(this.hideChaffSpy); + }); + } + + suite('Escape', function() { + setup(function() { + this.event = createKeyDownEvent(Blockly.utils.KeyCodes.ESC, 'NotAField'); + this.hideChaffSpy = sinon.spy(Blockly, 'hideChaff'); + }); + test('Simple', function() { + Blockly.onKeyDown(this.event); + sinon.assert.calledOnce(this.hideChaffSpy); + }); + runReadOnlyTest(createKeyDownEvent(Blockly.utils.KeyCodes.ESC, 'NotAField')); + test('Not called when focus is on an HTML input', function() { + var event = createKeyDownEvent(this.event, 'textarea'); + Blockly.onKeyDown(event); + sinon.assert.notCalled(this.hideChaffSpy); + }); + test('Not called on hidden workspaces', function() { + this.workspace.isVisible_ = false; + Blockly.onKeyDown(this.event); + sinon.assert.notCalled(this.hideChaffSpy); + }); + }); + + suite('Delete Block', function() { + setup(function() { + this.hideChaffSpy = sinon.spy(Blockly, 'hideChaff'); + setSelectedBlock(this.workspace); + this.deleteSpy = sinon.spy(Blockly.selected, 'dispose'); + }); + var testCases = [ + ['Delete', createKeyDownEvent(Blockly.utils.KeyCodes.DELETE, 'NotAField')], + ['Backspace', createKeyDownEvent(Blockly.utils.KeyCodes.BACKSPACE, 'NotAField')] + ]; + // Delete a block. + suite('Simple', function() { + testCases.forEach(function(testCase) { + var testCaseName = testCase[0]; + var keyEvent = testCase[1]; + test(testCaseName, function() { + Blockly.onKeyDown(keyEvent); + sinon.assert.calledOnce(this.hideChaffSpy); + sinon.assert.calledOnce(this.deleteSpy); + }); + }); + }); + // Do not delete a block if workspace is in readOnly mode. + suite('Not called when readOnly is true', function() { + testCases.forEach(function(testCase) { + var testCaseName = testCase[0]; + var keyEvent = testCase[1]; + runReadOnlyTest(keyEvent, testCaseName); + }); + }); + }); + + suite('Copy', function() { + setup(function() { + setSelectedBlock(this.workspace); + this.copySpy = sinon.spy(Blockly, 'copy_'); + this.hideChaffSpy = sinon.spy(Blockly, 'hideChaff'); + }); + var testCases = [ + ['Control C', createKeyDownEvent(Blockly.utils.KeyCodes.C, 'NotAField', [Blockly.utils.KeyCodes.CTRL])], + ['Meta C', createKeyDownEvent(Blockly.utils.KeyCodes.C, 'NotAField', [Blockly.utils.KeyCodes.META])], + ['Alt C', createKeyDownEvent(Blockly.utils.KeyCodes.C, 'NotAField', [Blockly.utils.KeyCodes.ALT])] + ]; + // Copy a block. + suite('Simple', function() { + testCases.forEach(function(testCase) { + var testCaseName = testCase[0]; + var keyEvent = testCase[1]; + test(testCaseName, function() { + Blockly.onKeyDown(keyEvent); + sinon.assert.calledOnce(this.copySpy); + sinon.assert.calledOnce(this.hideChaffSpy); + }); + }); + }); + // Do not copy a block if a workspace is in readonly mode. + suite('Not called when readOnly is true', function() { + testCases.forEach(function(testCase) { + var testCaseName = testCase[0]; + var keyEvent = testCase[1]; + runReadOnlyTest(keyEvent, testCaseName); + }); + }); + // Do not copy a block if a gesture is in progress. + suite('Gesture in progress', function() { + testCases.forEach(function(testCase) { + var testCaseName = testCase[0]; + var keyEvent = testCase[1]; + test(testCaseName, function() { + sinon.stub(Blockly.Gesture, 'inProgress').returns(true); + Blockly.onKeyDown(keyEvent); + sinon.assert.notCalled(this.copySpy); + sinon.assert.notCalled(this.hideChaffSpy); + }); + }); + }); + // Do not copy a block if is is not deletable. + suite('Block is not deletable', function() { + testCases.forEach(function(testCase) { + var testCaseName = testCase[0]; + var keyEvent = testCase[1]; + test(testCaseName, function() { + sinon.stub(Blockly.selected, 'isDeletable').returns(false); + Blockly.onKeyDown(keyEvent); + sinon.assert.notCalled(this.copySpy); + sinon.assert.notCalled(this.hideChaffSpy); + }); + }); + }); + // Do not copy a block if it is not movable. + suite('Block is not movable', function() { + testCases.forEach(function(testCase) { + var testCaseName = testCase[0]; + var keyEvent = testCase[1]; + test(testCaseName, function() { + sinon.stub(Blockly.selected, 'isMovable').returns(false); + Blockly.onKeyDown(keyEvent); + sinon.assert.notCalled(this.copySpy); + sinon.assert.notCalled(this.hideChaffSpy); + }); + }); + }); + }); + + suite('Undo', function() { + setup(function() { + this.undoSpy = sinon.spy(this.workspace, 'undo'); + this.hideChaffSpy = sinon.spy(Blockly, 'hideChaff'); + }); + var testCases = [ + ['Control Z', createKeyDownEvent(Blockly.utils.KeyCodes.Z, 'NotAField', [Blockly.utils.KeyCodes.CTRL])], + ['Meta Z', createKeyDownEvent(Blockly.utils.KeyCodes.Z, 'NotAField', [Blockly.utils.KeyCodes.META])], + ['Alt Z', createKeyDownEvent(Blockly.utils.KeyCodes.Z, 'NotAField', [Blockly.utils.KeyCodes.ALT])] + ]; + // Undo. + suite('Simple', function() { + testCases.forEach(function(testCase) { + var testCaseName = testCase[0]; + var keyEvent = testCase[1]; + test(testCaseName, function() { + Blockly.onKeyDown(keyEvent); + sinon.assert.calledOnce(this.undoSpy); + sinon.assert.calledWith(this.undoSpy, false); + sinon.assert.calledOnce(this.hideChaffSpy); + }); + }); + }); + // Do not undo if a gesture is in progress. + suite('Gesture in progress', function() { + testCases.forEach(function(testCase) { + var testCaseName = testCase[0]; + var keyEvent = testCase[1]; + test(testCaseName, function() { + sinon.stub(Blockly.Gesture, 'inProgress').returns(true); + Blockly.onKeyDown(keyEvent); + sinon.assert.notCalled(this.undoSpy); + sinon.assert.notCalled(this.hideChaffSpy); + }); + }); + }); + // Do not undo if the workspace is in readOnly mode. + suite('Not called when readOnly is true', function() { + testCases.forEach(function(testCase) { + var testCaseName = testCase[0]; + var keyEvent = testCase[1]; + runReadOnlyTest(keyEvent, testCaseName); + }); + }); + }); + + suite('Redo', function() { + setup(function() { + this.redoSpy = sinon.spy(this.workspace, 'undo'); + this.hideChaffSpy = sinon.spy(Blockly, 'hideChaff'); + }); + var testCases = [ + ['Control Shift Z', createKeyDownEvent(Blockly.utils.KeyCodes.Z, 'NotAField', [Blockly.utils.KeyCodes.CTRL, Blockly.utils.KeyCodes.SHIFT])], + ['Meta Shift Z', createKeyDownEvent(Blockly.utils.KeyCodes.Z, 'NotAField', [Blockly.utils.KeyCodes.META, Blockly.utils.KeyCodes.SHIFT])], + ['Alt Shift Z', createKeyDownEvent(Blockly.utils.KeyCodes.Z, 'NotAField', [Blockly.utils.KeyCodes.ALT, Blockly.utils.KeyCodes.SHIFT])] + ]; + // Undo. + suite('Simple', function() { + testCases.forEach(function(testCase) { + var testCaseName = testCase[0]; + var keyEvent = testCase[1]; + test(testCaseName, function() { + Blockly.onKeyDown(keyEvent); + sinon.assert.calledOnce(this.redoSpy); + sinon.assert.calledWith(this.redoSpy, true); + sinon.assert.calledOnce(this.hideChaffSpy); + }); + }); + }); + // Do not undo if a gesture is in progress. + suite('Gesture in progress', function() { + testCases.forEach(function(testCase) { + var testCaseName = testCase[0]; + var keyEvent = testCase[1]; + test(testCaseName, function() { + sinon.stub(Blockly.Gesture, 'inProgress').returns(true); + Blockly.onKeyDown(keyEvent); + sinon.assert.notCalled(this.redoSpy); + sinon.assert.notCalled(this.hideChaffSpy); + }); + }); + }); + // Do not undo if the workspace is in readOnly mode. + suite('Not called when readOnly is true', function() { + testCases.forEach(function(testCase) { + var testCaseName = testCase[0]; + var keyEvent = testCase[1]; + runReadOnlyTest(keyEvent, testCaseName); + }); + }); + }); + + suite('UndoWindows', function() { + setup(function() { + this.ctrlYEvent = createKeyDownEvent(Blockly.utils.KeyCodes.Y, 'NotAField', [Blockly.utils.KeyCodes.CTRL]); + this.undoSpy = sinon.spy(this.workspace, 'undo'); + this.hideChaffSpy = sinon.spy(Blockly, 'hideChaff'); + }); + test('Simple', function() { + Blockly.onKeyDown(this.ctrlYEvent); + sinon.assert.calledOnce(this.undoSpy); + sinon.assert.calledWith(this.undoSpy, true); + sinon.assert.calledOnce(this.hideChaffSpy); + }); + test('Not called when a gesture is in progress', function() { + sinon.stub(Blockly.Gesture, 'inProgress').returns(true); + Blockly.onKeyDown(this.ctrlYEvent); + sinon.assert.notCalled(this.undoSpy); + sinon.assert.notCalled(this.hideChaffSpy); + }); + runReadOnlyTest(createKeyDownEvent(Blockly.utils.KeyCodes.Y, 'NotAField', [Blockly.utils.KeyCodes.CTRL])); + }); +}); diff --git a/tests/mocha/test_helpers.js b/tests/mocha/test_helpers.js index b14bb7ad9..14ffd18c2 100644 --- a/tests/mocha/test_helpers.js +++ b/tests/mocha/test_helpers.js @@ -564,6 +564,36 @@ function dispatchPointerEvent(target, type, properties) { target.dispatchEvent(event); } +/** + * Creates a key down event used for testing. + * @param {number} keyCode The keycode for the event. Use Blockly.utils.KeyCodes enum. + * @param {string} type The type of the target. This only matters for the + * Blockly.utils.isTargetInput method. + * @param {Array} modifiers A list of modifiers. Use Blockly.utils.KeyCodes enum. + * @return {{keyCode: *, getModifierState: (function(): boolean), + * preventDefault: preventDefault, target: {type: *}}} The mocked keydown event. + */ +function createKeyDownEvent(keyCode, type, modifiers) { + var event = { + keyCode: keyCode, + target: { + type: type + }, + getModifierState: function() { + return false; + }, + preventDefault: function() {} + }; + if (modifiers && modifiers.length > 0) { + event.altKey = modifiers.indexOf(Blockly.utils.KeyCodes.ALT) > -1; + event.ctrlKey = modifiers.indexOf(Blockly.utils.KeyCodes.CTRL) > -1; + event.metaKey = modifiers.indexOf(Blockly.utils.KeyCodes.META) > -1; + event.shiftKey = modifiers.indexOf(Blockly.utils.KeyCodes.SHIFT) > -1; + } + return event; +} + + /** * Simulates mouse click by triggering relevant mouse events. * @param {!EventTarget} target The object receiving the event.