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();
+}