diff --git a/core/variable_map.js b/core/variable_map.js index 0a25384cb..109b59731 100644 --- a/core/variable_map.js +++ b/core/variable_map.js @@ -86,11 +86,19 @@ Blockly.VariableMap.prototype.renameVariable = function(variable, newName) { } else if (variableIndex == newVariableIndex || variableIndex != -1 && newVariableIndex == -1) { // Only changing case, or renaming to a completely novel name. - this.variableMap_[type][variableIndex].name = newName; + var variableToRename = this.variableMap_[type][variableIndex]; + Blockly.Events.fire(new Blockly.Events.VarRename(variableToRename, + newName)); + variableToRename.name = newName; } else if (variableIndex != -1 && newVariableIndex != -1) { // Renaming one existing variable to another existing variable. // The case might have changed, so we update the destination ID. - this.variableMap_[type][newVariableIndex].name = newName; + var variableToRename = this.variableMap_[type][newVariableIndex]; + Blockly.Events.fire(new Blockly.Events.VarRename(variableToRename, + newName)); + var variableToDelete = this.variableMap_[type][variableIndex]; + Blockly.Events.fire(new Blockly.Events.VarDelete(variableToDelete)); + variableToRename.name = newName; this.variableMap_[type].splice(variableIndex, 1); } }; @@ -148,6 +156,7 @@ Blockly.VariableMap.prototype.deleteVariable = function(variable) { for (var i = 0, tempVar; tempVar = variableList[i]; i++) { if (tempVar.getId() == variable.getId()) { variableList.splice(i, 1); + Blockly.Events.fire(new Blockly.Events.VarDelete(variable)); return; } } diff --git a/core/variable_model.js b/core/variable_model.js index 7124ed16e..990702a9b 100644 --- a/core/variable_model.js +++ b/core/variable_model.js @@ -75,6 +75,8 @@ Blockly.VariableModel = function(workspace, name, opt_type, opt_id) { * @private */ this.id_ = opt_id || Blockly.utils.genUid(); + + Blockly.Events.fire(new Blockly.Events.VarCreate(this)); }; /** diff --git a/core/workspace.js b/core/workspace.js index b707c968b..ecdccfce2 100644 --- a/core/workspace.js +++ b/core/workspace.js @@ -267,9 +267,8 @@ Blockly.Workspace.prototype.renameVariableInternal_ = function(variable, newName blocks[i].renameVar(oldCase, newName); } } - Blockly.Events.setGroup(false); - this.variableMap_.renameVariable(variable, newName); + Blockly.Events.setGroup(false); }; @@ -400,9 +399,8 @@ Blockly.Workspace.prototype.deleteVariableInternal_ = function(variable) { for (var i = 0; i < uses.length; i++) { uses[i].dispose(true, false); } - Blockly.Events.setGroup(false); - this.variableMap_.deleteVariable(variable); + Blockly.Events.setGroup(false); }; /** diff --git a/tests/jsunit/index.html b/tests/jsunit/index.html index 2b3512751..d2fa6f479 100644 --- a/tests/jsunit/index.html +++ b/tests/jsunit/index.html @@ -21,6 +21,7 @@ + diff --git a/tests/jsunit/workspace_undo_redo_test.js b/tests/jsunit/workspace_undo_redo_test.js new file mode 100644 index 000000000..022b11be6 --- /dev/null +++ b/tests/jsunit/workspace_undo_redo_test.js @@ -0,0 +1,390 @@ +/** + * @license + * Visual Blocks Editor + * + * Copyright 2017 Google Inc. + * https://developers.google.com/blockly/ + * + * 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. + */ + + /** + * @fileoverview Tests for Blockly.Workspace.undo. + * @author marisaleung@google.com (Marisa Leung) + */ +'use strict'; + +goog.require('goog.events.EventHandler'); +goog.require('goog.testing'); +goog.require('goog.testing.events'); +goog.require('goog.testing.MockControl'); + +var workspace; +var mockControl_; +var savedMsg = Blockly.Msg.DELETE_VARIABLE; +var savedFireFunc = Blockly.Events.fire; +Blockly.defineBlocksWithJsonArray([{ + "type": "get_var_block", + "message0": "%1", + "args0": [ + { + "type": "field_variable", + "name": "VAR", + } + ] +}]); + +function temporary_fireEvent(event) { + if (!Blockly.Events.isEnabled()) { + return; + } + Blockly.Events.FIRE_QUEUE_.push(event); + Blockly.Events.fireNow_(); +} + +function undoRedoTest_setUp() { + workspace = new Blockly.Workspace(); + mockControl_ = new goog.testing.MockControl(); + Blockly.Events.fire = temporary_fireEvent; +} + +function undoRedoTest_setUpWithMockBlocks() { + undoRedoTest_setUp(); + // Need to define this because field_variable's dropdownCreate() calls replace + // on undefined value, Blockly.Msg.DELETE_VARIABLE. To fix this, define + // Blockly.Msg.DELETE_VARIABLE as %1 so the replace function finds the %1 it + // expects. + Blockly.Msg.DELETE_VARIABLE = '%1'; +} + +function undoRedoTest_tearDown() { + mockControl_.$tearDown(); + workspace.dispose(); + Blockly.Events.fire = savedFireFunc; +} + +function undoRedoTest_tearDownWithMockBlocks() { + undoRedoTest_tearDown(); + Blockly.Msg.DELETE_VARIABLE = savedMsg; +} + +/** + * Create a test get_var_block. + * @param {string} variableName The string to put into the variable field. + * @return {!Blockly.Block} The created block. + */ +function createMockBlock(variableName) { + var block = new Blockly.Block(workspace, 'get_var_block'); + block.inputList[0].fieldRow[0].setValue(variableName); + return block; +} + +/** + * Check if a variable with the given values exists. + * @param {string} name The expected name of the variable. + * @param {string} type The expected type of the variable. + * @param {string} id The expected id of the variable. + */ +function undoRedoTest_checkVariableValues(name, type, id) { + var variable = workspace.getVariable(name); + assertNotUndefined(variable); + assertEquals(name, variable.name); + assertEquals(type, variable.type); + assertEquals(id, variable.getId()); +} + +/** + * Check that the top block with the given index contains a variable with + * the given name. + * @param {number} blockIndex The index of the top block. + * @param {string} name The expected name of the variable in the block. + */ +function undoRedoTest_checkBlockVariableName(blockIndex, name) { + var blockVarName = workspace.topBlocks_[blockIndex].getVars()[0]; + assertEquals(name, blockVarName); +} + +function createTwoVarsEmptyType() { + workspace.createVariable('name1', '', 'id1'); + workspace.createVariable('name2', '', 'id2'); +} + +function test_undoCreateVariable_Trivial() { + undoRedoTest_setUp(); + workspace.createVariable('name1', 'type1', 'id1'); + workspace.createVariable('name2', 'type2', 'id2'); + + workspace.undo(); + undoRedoTest_checkVariableValues('name1', 'type1', 'id1'); + assertNull(workspace.getVariableById('id2')); + workspace.undo(); + assertNull(workspace.getVariableById('id1')); + assertNull(workspace.getVariableById('id2')); + undoRedoTest_tearDown(); +} + +function test_redoAndUndoCreateVariable_Trivial() { + undoRedoTest_setUp(); + workspace.createVariable('name1', 'type1', 'id1'); + workspace.createVariable('name2', 'type2', 'id2'); + + workspace.undo(); + workspace.undo(true); + + // Expect that variable 'id2' is recreated + undoRedoTest_checkVariableValues('name1', 'type1', 'id1'); + undoRedoTest_checkVariableValues('name2', 'type2', 'id2'); + + workspace.undo(); + workspace.undo(); + workspace.undo(true); + + // Expect that variable 'id1' is recreated + undoRedoTest_checkVariableValues('name1', 'type1', 'id1'); + assertNull(workspace.getVariableById('id2')); + undoRedoTest_tearDown(); +} + +function test_undoDeleteVariable_NoBlocks() { + undoRedoTest_setUp(); + workspace.createVariable('name1', 'type1', 'id1'); + workspace.createVariable('name2', 'type2', 'id2'); + workspace.deleteVariableById('id1'); + workspace.deleteVariableById('id2'); + + workspace.undo(); + assertNull(workspace.getVariableById('id1')); + undoRedoTest_checkVariableValues('name2', 'type2', 'id2'); + + workspace.undo(); + undoRedoTest_checkVariableValues('name1', 'type1', 'id1'); + undoRedoTest_checkVariableValues('name2', 'type2', 'id2'); + undoRedoTest_tearDown(); +} + +function test_undoDeleteVariable_WithBlocks() { + undoRedoTest_setUpWithMockBlocks(); + workspace.createVariable('name1', 'type1', 'id1'); + workspace.createVariable('name2', 'type2', 'id2'); + createMockBlock('name1'); + createMockBlock('name2'); + workspace.deleteVariableById('id1'); + workspace.deleteVariableById('id2'); + + workspace.undo(); + undoRedoTest_checkBlockVariableName(0, 'name2'); + assertNull(workspace.getVariableById('id1')); + undoRedoTest_checkVariableValues('name2', 'type2', 'id2'); + + workspace.undo(); + undoRedoTest_checkBlockVariableName(0, 'name2'); + undoRedoTest_checkBlockVariableName(1, 'name1'); + undoRedoTest_checkVariableValues('name1', 'type1', 'id1'); + undoRedoTest_checkVariableValues('name2', 'type2', 'id2'); + undoRedoTest_tearDownWithMockBlocks(); +} + +function test_redoAndUndoDeleteVariable() { + undoRedoTest_setUp(); + workspace.createVariable('name1', 'type1', 'id1'); + workspace.createVariable('name2', 'type2', 'id2'); + workspace.deleteVariableById('id1'); + workspace.deleteVariableById('id2'); + + workspace.undo(); + workspace.undo(true); + // Expect that both variables are deleted + assertNull(workspace.getVariableById('id1')); + assertNull(workspace.getVariableById('id2')); + + workspace.undo(); + workspace.undo(); + workspace.undo(true); + // Expect that variable 'id2' is recreated + assertNull(workspace.getVariableById('id1')); + undoRedoTest_checkVariableValues('name2', 'type2', 'id2'); + undoRedoTest_tearDown(); +} + +function test_redoAndUndoDeleteVariableWithBlocks() { + undoRedoTest_setUpWithMockBlocks(); + workspace.createVariable('name1', 'type1', 'id1'); + workspace.createVariable('name2', 'type2', 'id2'); + createMockBlock('name1'); + createMockBlock('name2'); + workspace.deleteVariableById('id1'); + workspace.deleteVariableById('id2'); + + workspace.undo(); + workspace.undo(true); + // Expect that both variables are deleted + assertEquals(0, workspace.topBlocks_.length); + assertNull(workspace.getVariableById('id1')); + assertNull(workspace.getVariableById('id2')); + + workspace.undo(); + workspace.undo(); + workspace.undo(true); + // Expect that variable 'id2' is recreated + undoRedoTest_checkBlockVariableName(0, 'name2'); + assertNull(workspace.getVariableById('id1')); + undoRedoTest_checkVariableValues('name2', 'type2', 'id2'); + undoRedoTest_tearDownWithMockBlocks(); +} + +function test_undoRedoRenameVariable_NeitherVariableExists() { + // Expect that a variable with the name, 'name2', and the generated UUID, + // 'id2', to be created when rename is called. Undo removes this variable + // and redo recreates it. + undoRedoTest_setUp(); + setUpMockMethod(mockControl_, Blockly.utils, 'genUid', null, + ['rename_group', 'id2', 'delete_group']); + workspace.renameVariable('name1', 'name2'); + + workspace.undo(); + assertNull(workspace.getVariableById('id2')); + + workspace.undo(true); + undoRedoTest_checkVariableValues('name2', '', 'id2'); + undoRedoTest_tearDown(); +} + +function test_undoRedoRenameVariable_OneExists_NoBlocks() { + undoRedoTest_setUp(); + workspace.createVariable('name1', '', 'id1'); + workspace.renameVariable('name1', 'name2'); + + workspace.undo(); + undoRedoTest_checkVariableValues('name1', '', 'id1'); + assertNull(workspace.getVariable('name2')); + + workspace.undo(true); + undoRedoTest_checkVariableValues('name2', '', 'id1'); + undoRedoTest_tearDown(); +} + +function test_undoRedoRenameVariable_OneExists_WithBlocks() { + undoRedoTest_setUpWithMockBlocks(); + workspace.createVariable('name1', '', 'id1'); + createMockBlock('name1'); + workspace.renameVariable('name1', 'name2'); + + workspace.undo(); + undoRedoTest_checkBlockVariableName(0, 'name1'); + undoRedoTest_checkVariableValues('name1', '', 'id1'); + assertNull(workspace.getVariable('name2')); + + workspace.undo(true); + undoRedoTest_checkVariableValues('name2', '', 'id1'); + undoRedoTest_checkBlockVariableName(0, 'name2'); + undoRedoTest_tearDownWithMockBlocks(); +} + +function test_undoRedoRenameVariable_BothExist_NoBlocks() { + undoRedoTest_setUp(); + createTwoVarsEmptyType(); + workspace.renameVariable('name1', 'name2'); + + workspace.undo(); + undoRedoTest_checkVariableValues('name1', '', 'id1'); + undoRedoTest_checkVariableValues('name2', '', 'id2'); + + workspace.undo(true); + undoRedoTest_checkVariableValues('name2', '', 'id2'); + assertNull(workspace.getVariable('name1')); + undoRedoTest_tearDown(); +} + +function test_undoRedoRenameVariable_BothExist_WithBlocks() { + undoRedoTest_setUpWithMockBlocks(); + createTwoVarsEmptyType(); + createMockBlock('name1'); + createMockBlock('name2'); + workspace.renameVariable('name1', 'name2'); + + workspace.undo(); + undoRedoTest_checkBlockVariableName(0, 'name1'); + undoRedoTest_checkBlockVariableName(1, 'name2'); + undoRedoTest_checkVariableValues('name1', '', 'id1'); + undoRedoTest_checkVariableValues('name2', '', 'id2'); + + workspace.undo(true); + undoRedoTest_checkBlockVariableName(0, 'name2'); + undoRedoTest_checkBlockVariableName(1, 'name2'); + undoRedoTest_tearDownWithMockBlocks(); +} + +function test_undoRedoRenameVariable_BothExistCaseChange_NoBlocks() { + undoRedoTest_setUp(); + createTwoVarsEmptyType(); + workspace.renameVariable('name1', 'Name2'); + + workspace.undo(); + undoRedoTest_checkVariableValues('name1', '', 'id1'); + undoRedoTest_checkVariableValues('name2', '', 'id2'); + + workspace.undo(true); + undoRedoTest_checkVariableValues('Name2', '', 'id2'); + assertNull(workspace.getVariable('name1')); + undoRedoTest_tearDown(); +} + +function test_undoRedoRenameVariable_BothExistCaseChange_WithBlocks() { + undoRedoTest_setUpWithMockBlocks(); + createTwoVarsEmptyType(); + createMockBlock('name1'); + createMockBlock('name2'); + workspace.renameVariable('name1', 'Name2'); + + workspace.undo(); + undoRedoTest_checkBlockVariableName(0, 'name1'); + undoRedoTest_checkBlockVariableName(1, 'name2'); + undoRedoTest_checkVariableValues('name1', '', 'id1'); + undoRedoTest_checkVariableValues('name2', '', 'id2'); + + workspace.undo(true); + undoRedoTest_checkVariableValues('Name2', '', 'id2'); + assertNull(workspace.getVariable('name1')); + undoRedoTest_checkBlockVariableName(0, 'Name2'); + undoRedoTest_checkBlockVariableName(1, 'Name2'); + undoRedoTest_tearDownWithMockBlocks(); +} + +function test_undoRedoRenameVariable_OnlyCaseChange_NoBlocks() { + undoRedoTest_setUp(); + workspace.createVariable('name1', '', 'id1'); + workspace.renameVariable('name1', 'Name1'); + + workspace.undo(); + undoRedoTest_checkVariableValues('name1', '', 'id1'); + + workspace.undo(true); + undoRedoTest_checkVariableValues('Name1', '', 'id1'); + undoRedoTest_tearDown(); +} + +function test_undoRedoRenameVariable_OnlyCaseChange_WithBlocks() { + undoRedoTest_setUpWithMockBlocks(); + workspace.createVariable('name1', '', 'id1'); + createMockBlock('name1'); + workspace.renameVariable('name1', 'Name1'); + + workspace.undo(); + undoRedoTest_checkBlockVariableName(0, 'name1'); + undoRedoTest_checkVariableValues('name1', '', 'id1'); + + workspace.undo(true); + undoRedoTest_checkVariableValues('Name1', '', 'id1'); + undoRedoTest_checkBlockVariableName(0, 'Name1'); + undoRedoTest_tearDownWithMockBlocks(); +}