From fce962c0fcafeaad63da06359fe3fdbf225dea99 Mon Sep 17 00:00:00 2001 From: Paul Kaplan Date: Thu, 22 Jun 2017 10:38:20 -0400 Subject: [PATCH] Merge 06-22 --- core/block.js | 14 +- core/block_dragger.js | 2 +- core/block_svg.js | 2 +- core/comment.js | 4 +- core/connection.js | 4 +- core/contextmenu.js | 2 +- core/events.js | 309 ++++++++++++++++- core/field.js | 4 +- core/field_checkbox.js | 2 +- core/field_colour.js | 2 +- core/field_dropdown.js | 2 +- core/field_textinput.js | 2 +- core/field_variable.js | 17 +- core/flyout_base.js | 14 +- core/mutator.js | 2 +- core/procedures.js | 2 +- core/variable_map.js | 27 +- core/variable_model.js | 11 +- core/workspace.js | 25 +- core/workspace_svg.js | 3 +- core/xml.js | 69 ++-- msg/json/en.json | 2 +- tests/jsunit/event_test.js | 394 +++++++++++++++++++++ tests/jsunit/field_variable_test.js | 79 +++++ tests/jsunit/index.html | 4 + tests/jsunit/test_utilities.js | 91 +++++ tests/jsunit/variable_map_test.js | 63 ++-- tests/jsunit/variable_model_test.js | 41 ++- tests/jsunit/workspace_test.js | 217 ++++-------- tests/jsunit/workspace_undo_redo_test.js | 417 +++++++++++++++++++++++ tests/jsunit/xml_test.js | 90 +---- 31 files changed, 1553 insertions(+), 364 deletions(-) create mode 100644 tests/jsunit/event_test.js create mode 100644 tests/jsunit/field_variable_test.js create mode 100644 tests/jsunit/test_utilities.js create mode 100644 tests/jsunit/workspace_undo_redo_test.js diff --git a/core/block.js b/core/block.js index 02c63010..399752ce 100644 --- a/core/block.js +++ b/core/block.js @@ -182,7 +182,7 @@ Blockly.Block = function(workspace, prototypeName, opt_id) { /** @type {boolean|undefined} */ this.inputsInlineDefault = this.inputsInline; if (Blockly.Events.isEnabled()) { - Blockly.Events.fire(new Blockly.Events.Create(this)); + Blockly.Events.fire(new Blockly.Events.BlockCreate(this)); } // Bind an onchange function, if it exists. if (goog.isFunction(this.onchange)) { @@ -235,7 +235,7 @@ Blockly.Block.prototype.dispose = function(healStack) { } this.unplug(healStack); if (Blockly.Events.isEnabled()) { - Blockly.Events.fire(new Blockly.Events.Delete(this)); + Blockly.Events.fire(new Blockly.Events.BlockDelete(this)); } Blockly.Events.disable(); @@ -957,7 +957,7 @@ Blockly.Block.prototype.setOutput = function(newBoolean, opt_check) { */ Blockly.Block.prototype.setInputsInline = function(newBoolean) { if (this.inputsInline != newBoolean) { - Blockly.Events.fire(new Blockly.Events.Change( + Blockly.Events.fire(new Blockly.Events.BlockChange( this, 'inline', null, this.inputsInline, newBoolean)); this.inputsInline = newBoolean; } @@ -996,7 +996,7 @@ Blockly.Block.prototype.getInputsInline = function() { */ Blockly.Block.prototype.setDisabled = function(disabled) { if (this.disabled != disabled) { - Blockly.Events.fire(new Blockly.Events.Change( + Blockly.Events.fire(new Blockly.Events.BlockChange( this, 'disabled', null, this.disabled, disabled)); this.disabled = disabled; } @@ -1033,7 +1033,7 @@ Blockly.Block.prototype.isCollapsed = function() { */ Blockly.Block.prototype.setCollapsed = function(collapsed) { if (this.collapsed_ != collapsed) { - Blockly.Events.fire(new Blockly.Events.Change( + Blockly.Events.fire(new Blockly.Events.BlockChange( this, 'collapsed', null, this.collapsed_, collapsed)); this.collapsed_ = collapsed; } @@ -1629,7 +1629,7 @@ Blockly.Block.prototype.getCommentText = function() { */ Blockly.Block.prototype.setCommentText = function(text) { if (this.comment != text) { - Blockly.Events.fire(new Blockly.Events.Change( + Blockly.Events.fire(new Blockly.Events.BlockChange( this, 'comment', null, this.comment, text || '')); this.comment = text; } @@ -1719,7 +1719,7 @@ Blockly.Block.prototype.getRelativeToSurfaceXY = function() { */ Blockly.Block.prototype.moveBy = function(dx, dy) { goog.asserts.assert(!this.parentBlock_, 'Block has parent.'); - var event = new Blockly.Events.Move(this); + var event = new Blockly.Events.BlockMove(this); this.xy_.translate(dx, dy); event.recordNew(); Blockly.Events.fire(event); diff --git a/core/block_dragger.js b/core/block_dragger.js index 301a0a1e..d9a30e09 100644 --- a/core/block_dragger.js +++ b/core/block_dragger.js @@ -234,7 +234,7 @@ Blockly.BlockDragger.prototype.endBlockDrag = function(e, currentDragDeltaXY) { * @private */ Blockly.BlockDragger.prototype.fireMoveEvent_ = function() { - var event = new Blockly.Events.Move(this.draggingBlock_); + var event = new Blockly.Events.BlockMove(this.draggingBlock_); event.oldCoordinate = this.startXY_; event.recordNew(); Blockly.Events.fire(event); diff --git a/core/block_svg.js b/core/block_svg.js index 1a4040aa..9d225528 100644 --- a/core/block_svg.js +++ b/core/block_svg.js @@ -379,7 +379,7 @@ Blockly.BlockSvg.prototype.moveBy = function(dx, dy) { goog.asserts.assert(!this.parentBlock_, 'Block has parent.'); var eventsEnabled = Blockly.Events.isEnabled(); if (eventsEnabled) { - var event = new Blockly.Events.Move(this); + var event = new Blockly.Events.BlockMove(this); } var xy = this.getRelativeToSurfaceXY(); this.translate(xy.x + dx, xy.y + dy); diff --git a/core/comment.js b/core/comment.js index 91e10276..83d264c6 100644 --- a/core/comment.js +++ b/core/comment.js @@ -121,7 +121,7 @@ Blockly.Comment.prototype.createEditor_ = function() { }); Blockly.bindEventWithChecks_(textarea, 'change', this, function(/* e */) { if (this.text_ != textarea.value) { - Blockly.Events.fire(new Blockly.Events.Change( + Blockly.Events.fire(new Blockly.Events.BlockChange( this.block_, 'comment', null, this.text_, textarea.value)); this.text_ = textarea.value; } @@ -259,7 +259,7 @@ Blockly.Comment.prototype.getText = function() { */ Blockly.Comment.prototype.setText = function(text) { if (this.text_ != text) { - Blockly.Events.fire(new Blockly.Events.Change( + Blockly.Events.fire(new Blockly.Events.BlockChange( this.block_, 'comment', null, this.text_, text)); this.text_ = text; } diff --git a/core/connection.js b/core/connection.js index 8240a332..e91c2449 100644 --- a/core/connection.js +++ b/core/connection.js @@ -218,7 +218,7 @@ Blockly.Connection.prototype.connect_ = function(childConnection) { var event; if (Blockly.Events.isEnabled()) { - event = new Blockly.Events.Move(childBlock); + event = new Blockly.Events.BlockMove(childBlock); } // Establish the connections. Blockly.Connection.connectReciprocally_(parentConnection, childConnection); @@ -561,7 +561,7 @@ Blockly.Connection.prototype.disconnectInternal_ = function(parentBlock, childBlock) { var event; if (Blockly.Events.isEnabled()) { - event = new Blockly.Events.Move(childBlock); + event = new Blockly.Events.BlockMove(childBlock); } var otherConnection = this.targetConnection; otherConnection.targetConnection = null; diff --git a/core/contextmenu.js b/core/contextmenu.js index 95f4591c..ebb8caeb 100644 --- a/core/contextmenu.js +++ b/core/contextmenu.js @@ -184,7 +184,7 @@ Blockly.ContextMenu.callbackFactory = function(block, xml) { Blockly.Events.enable(); } if (Blockly.Events.isEnabled() && !newBlock.isShadow()) { - Blockly.Events.fire(new Blockly.Events.Create(newBlock)); + Blockly.Events.fire(new Blockly.Events.BlockCreate(newBlock)); } newBlock.select(); }; diff --git a/core/events.js b/core/events.js index 5c96d654..80a2620f 100644 --- a/core/events.js +++ b/core/events.js @@ -55,29 +55,71 @@ Blockly.Events.recordUndo = true; Blockly.Events.disabled_ = 0; /** - * Name of event that creates a block. + * Name of event that creates a block. Will be deprecated for BLOCK_CREATE. * @const */ Blockly.Events.CREATE = 'create'; /** - * Name of event that deletes a block. + * Name of event that creates a block. + * @const + */ +Blockly.Events.BLOCK_CREATE = Blockly.Events.CREATE; + +/** + * Name of event that deletes a block. Will be deprecated for BLOCK_DELETE. * @const */ Blockly.Events.DELETE = 'delete'; /** - * Name of event that changes a block. + * Name of event that deletes a block. + * @const + */ +Blockly.Events.BLOCK_DELETE = Blockly.Events.DELETE; + +/** + * Name of event that changes a block. Will be deprecated for BLOCK_CHANGE. * @const */ Blockly.Events.CHANGE = 'change'; /** - * Name of event that moves a block. + * Name of event that changes a block. + * @const + */ +Blockly.Events.BLOCK_CHANGE = Blockly.Events.CHANGE; + +/** + * Name of event that moves a block. Will be deprecated for BLOCK_MOVE. * @const */ Blockly.Events.MOVE = 'move'; +/** + * Name of event that moves a block. + * @const + */ +Blockly.Events.BLOCK_MOVE = Blockly.Events.MOVE; + +/** + * Name of event that creates a variable. + * @const + */ +Blockly.Events.VAR_CREATE = 'var_create'; + +/** + * Name of event that deletes a variable. + * @const + */ +Blockly.Events.VAR_DELETE = 'var_delete'; + +/** + * Name of event that renames a variable. + * @const + */ +Blockly.Events.VAR_RENAME = 'var_rename'; + /** * Name of event that records a UI change. * @const @@ -276,6 +318,15 @@ Blockly.Events.fromJson = function(json, workspace) { case Blockly.Events.MOVE: event = new Blockly.Events.Move(null); break; + case Blockly.Events.VAR_CREATE: + event = new Blockly.Events.VarCreate(null); + break; + case Blockly.Events.VAR_DELETE: + event = new Blockly.Events.VarDelete(null); + break; + case Blockly.Events.VAR_RENAME: + event = new Blockly.Events.VarRename(null); + break; case Blockly.Events.UI: event = new Blockly.Events.Ui(null); break; @@ -289,13 +340,17 @@ Blockly.Events.fromJson = function(json, workspace) { /** * Abstract class for an event. - * @param {Blockly.Block} block The block. + * @param {Blockly.Block|Blockly.VariableModel} elem The block or variable. * @constructor */ -Blockly.Events.Abstract = function(block) { - if (block) { - this.blockId = block.id; - this.workspaceId = block.workspace.id; +Blockly.Events.Abstract = function(elem) { + if (elem instanceof Blockly.Block) { + this.blockId = elem.id; + this.workspaceId = elem.workspace.id; + } + else if (elem instanceof Blockly.VariableModel){ + this.workspaceId = elem.workspace.id; + this.varId = elem.getId(); } this.group = Blockly.Events.group_; this.recordUndo = Blockly.Events.recordUndo; @@ -312,6 +367,9 @@ Blockly.Events.Abstract.prototype.toJson = function() { if (this.blockId) { json['blockId'] = this.blockId; } + if (this.varId) { + json['varId'] = this.varId; + } if (this.group) { json['group'] = this.group; } @@ -324,6 +382,7 @@ Blockly.Events.Abstract.prototype.toJson = function() { */ Blockly.Events.Abstract.prototype.fromJson = function(json) { this.blockId = json['blockId']; + this.varId = json['varId']; this.group = json['group']; }; @@ -344,6 +403,21 @@ Blockly.Events.Abstract.prototype.run = function(/*forward*/) { // Defined by subclasses. }; +/** + * Get workspace the event belongs to. + * @return {Blockly.Workspace} The workspace the event belongs to. + * @throws {Error} if workspace is null. + * @private + */ +Blockly.Events.Abstract.prototype.getEventWorkspace_ = function() { + var workspace = Blockly.Workspace.getById(this.workspaceId); + if (!workspace) { + throw Error('Workspace is null. Event must have been generated from real' + + ' Blockly events.'); + } + return workspace; +}; + /** * Class for a block creation event. * @param {Blockly.Block} block The created block. Null for a blank event. @@ -365,6 +439,14 @@ Blockly.Events.Create = function(block) { }; goog.inherits(Blockly.Events.Create, Blockly.Events.Abstract); +/** + * Class for a block creation event. + * @param {Blockly.Block} block The created block. Null for a blank event. + * @extends {Blockly.Events.Abstract} + * @constructor + */ +Blockly.Events.BlockCreate = Blockly.Events.Create; + /** * Type of this event. * @type {string} @@ -397,7 +479,7 @@ Blockly.Events.Create.prototype.fromJson = function(json) { * @param {boolean} forward True if run forward, false if run backward (undo). */ Blockly.Events.Create.prototype.run = function(forward) { - var workspace = Blockly.Workspace.getById(this.workspaceId); + var workspace = this.getEventWorkspace_(); if (forward) { var xml = goog.dom.createDom('xml'); xml.appendChild(this.xml); @@ -439,6 +521,14 @@ Blockly.Events.Delete = function(block) { }; goog.inherits(Blockly.Events.Delete, Blockly.Events.Abstract); +/** + * Class for a block deletion event. + * @param {Blockly.Block} block The deleted block. Null for a blank event. + * @extends {Blockly.Events.Abstract} + * @constructor + */ +Blockly.Events.BlockDelete = Blockly.Events.Delete; + /** * Type of this event. * @type {string} @@ -469,7 +559,7 @@ Blockly.Events.Delete.prototype.fromJson = function(json) { * @param {boolean} forward True if run forward, false if run backward (undo). */ Blockly.Events.Delete.prototype.run = function(forward) { - var workspace = Blockly.Workspace.getById(this.workspaceId); + var workspace = this.getEventWorkspace_(); if (forward) { for (var i = 0, id; id = this.ids[i]; i++) { var block = workspace.getBlockById(id); @@ -509,6 +599,18 @@ Blockly.Events.Change = function(block, element, name, oldValue, newValue) { }; goog.inherits(Blockly.Events.Change, Blockly.Events.Abstract); +/** + * Class for a block change event. + * @param {Blockly.Block} block The changed block. Null for a blank event. + * @param {string} element One of 'field', 'comment', 'disabled', etc. + * @param {?string} name Name of input or field affected, or null. + * @param {string} oldValue Previous value of element. + * @param {string} newValue New value of element. + * @extends {Blockly.Events.Abstract} + * @constructor + */ +Blockly.Events.BlockChange = Blockly.Events.Change; + /** * Type of this event. * @type {string} @@ -553,7 +655,7 @@ Blockly.Events.Change.prototype.isNull = function() { * @param {boolean} forward True if run forward, false if run backward (undo). */ Blockly.Events.Change.prototype.run = function(forward) { - var workspace = Blockly.Workspace.getById(this.workspaceId); + var workspace = this.getEventWorkspace_(); var block = workspace.getBlockById(this.blockId); if (!block) { console.warn("Can't change non-existant block: " + this.blockId); @@ -625,6 +727,15 @@ Blockly.Events.Move = function(block) { }; goog.inherits(Blockly.Events.Move, Blockly.Events.Abstract); + +/** + * Class for a block move event. Created before the move. + * @param {Blockly.Block} block The moved block. Null for a blank event. + * @extends {Blockly.Events.Abstract} + * @constructor + */ +Blockly.Events.BlockMove = Blockly.Events.Move; + /** * Type of this event. * @type {string} @@ -713,7 +824,7 @@ Blockly.Events.Move.prototype.isNull = function() { * @param {boolean} forward True if run forward, false if run backward (undo). */ Blockly.Events.Move.prototype.run = function(forward) { - var workspace = Blockly.Workspace.getById(this.workspaceId); + var workspace = this.getEventWorkspace_(); var block = workspace.getBlockById(this.blockId); if (!block) { console.warn("Can't move non-existant block: " + this.blockId); @@ -802,6 +913,178 @@ Blockly.Events.Ui.prototype.fromJson = function(json) { this.newValue = json['newValue']; }; +/** + * Class for a variable creation event. + * @param {Blockly.VariableModel} variable The created variable. + * Null for a blank event. + * @extends {Blockly.Events.Abstract} + * @constructor + */ +Blockly.Events.VarCreate = function(variable) { + if (!variable) { + return; // Blank event to be populated by fromJson. + } + Blockly.Events.VarCreate.superClass_.constructor.call(this, variable); + this.varType = variable.type; + this.varName = variable.name; +}; +goog.inherits(Blockly.Events.VarCreate, Blockly.Events.Abstract); + +/** + * Type of this event. + * @type {string} + */ +Blockly.Events.VarCreate.prototype.type = Blockly.Events.VAR_CREATE; + +/** + * Encode the event as JSON. + * @return {!Object} JSON representation. + */ +Blockly.Events.VarCreate.prototype.toJson = function() { + var json = Blockly.Events.VarCreate.superClass_.toJson.call(this); + json['varType'] = this.varType; + json['varName'] = this.varName; + return json; +}; + +/** + * Decode the JSON event. + * @param {!Object} json JSON representation. + */ +Blockly.Events.VarCreate.prototype.fromJson = function(json) { + Blockly.Events.VarCreate.superClass_.fromJson.call(this, json); + this.varType = json['varType']; + this.varName = json['varName']; +}; + +/** + * Run a variable creation event. + * @param {boolean} forward True if run forward, false if run backward (undo). + */ +Blockly.Events.VarCreate.prototype.run = function(forward) { + var workspace = this.getEventWorkspace_(); + if (forward) { + workspace.createVariable(this.varName, this.varType, this.varId); + } else { + workspace.deleteVariableById(this.varId); + } +}; + +/** + * Class for a variable deletion event. + * @param {Blockly.VariableModel} variable The deleted variable. + * Null for a blank event. + * @extends {Blockly.Events.Abstract} + * @constructor + */ +Blockly.Events.VarDelete = function(variable) { + if (!variable) { + return; // Blank event to be populated by fromJson. + } + Blockly.Events.VarDelete.superClass_.constructor.call(this, variable); + this.varType = variable.type; + this.varName = variable.name; +}; +goog.inherits(Blockly.Events.VarDelete, Blockly.Events.Abstract); + +/** + * Type of this event. + * @type {string} + */ +Blockly.Events.VarDelete.prototype.type = Blockly.Events.VAR_DELETE; + +/** + * Encode the event as JSON. + * @return {!Object} JSON representation. + */ +Blockly.Events.VarDelete.prototype.toJson = function() { + var json = Blockly.Events.VarDelete.superClass_.toJson.call(this); + json['varType'] = this.varType; + json['varName'] = this.varName; + return json; +}; + +/** + * Decode the JSON event. + * @param {!Object} json JSON representation. + */ +Blockly.Events.VarDelete.prototype.fromJson = function(json) { + Blockly.Events.VarDelete.superClass_.fromJson.call(this, json); + this.varType = json['varType']; + this.varName = json['varName']; +}; + +/** + * Run a variable deletion event. + * @param {boolean} forward True if run forward, false if run backward (undo). + */ +Blockly.Events.VarDelete.prototype.run = function(forward) { + var workspace = this.getEventWorkspace_(); + if (forward) { + workspace.deleteVariableById(this.varId); + } else { + workspace.createVariable(this.varName, this.varType, this.varId); + } +}; + +/** + * Class for a variable rename event. + * @param {Blockly.VariableModel} variable The renamed variable. + * Null for a blank event. + * @param {string} newName The new name the variable will be changed to. + * @extends {Blockly.Events.Abstract} + * @constructor + */ +Blockly.Events.VarRename = function(variable, newName) { + if (!variable) { + return; // Blank event to be populated by fromJson. + } + Blockly.Events.VarRename.superClass_.constructor.call(this, variable); + this.oldName = variable.name; + this.newName = newName; +}; +goog.inherits(Blockly.Events.VarRename, Blockly.Events.Abstract); + +/** + * Type of this event. + * @type {string} + */ +Blockly.Events.VarRename.prototype.type = Blockly.Events.VAR_RENAME; + +/** + * Encode the event as JSON. + * @return {!Object} JSON representation. + */ +Blockly.Events.VarRename.prototype.toJson = function() { + var json = Blockly.Events.VarRename.superClass_.toJson.call(this); + json['oldName'] = this.oldName; + json['newName'] = this.newName; + return json; +}; + +/** + * Decode the JSON event. + * @param {!Object} json JSON representation. + */ +Blockly.Events.VarRename.prototype.fromJson = function(json) { + Blockly.Events.VarRename.superClass_.fromJson.call(this, json); + this.oldName = json['oldName']; + this.newName = json['newName']; +}; + +/** + * Run a variable rename event. + * @param {boolean} forward True if run forward, false if run backward (undo). + */ +Blockly.Events.VarRename.prototype.run = function(forward) { + var workspace = this.getEventWorkspace_(); + if (forward) { + workspace.renameVariableById(this.varId, this.newName); + } else { + workspace.renameVariableById(this.varId, this.oldName); + } +}; + /** * Enable/disable a block depending on whether it is properly connected. * Use this on applications where all blocks should be connected to a top block. diff --git a/core/field.js b/core/field.js index 8ecf4c6d..fe02971c 100644 --- a/core/field.js +++ b/core/field.js @@ -399,8 +399,6 @@ Blockly.Field.prototype.render_ = function() { **/ Blockly.Field.prototype.updateWidth = function() { var width = Blockly.Field.getCachedWidth(this.textElement_); - // Calculate width of field - width = Blockly.Field.getCachedWidth(this.textElement_); // Add padding to left and right of text. if (this.EDITABLE) { @@ -623,7 +621,7 @@ Blockly.Field.prototype.setValue = function(newValue) { return; } if (this.sourceBlock_ && Blockly.Events.isEnabled()) { - Blockly.Events.fire(new Blockly.Events.Change( + Blockly.Events.fire(new Blockly.Events.BlockChange( this.sourceBlock_, 'field', this.name, oldValue, newValue)); } this.setText(newValue); diff --git a/core/field_checkbox.js b/core/field_checkbox.js index c5f0b5aa..b938bd1b 100644 --- a/core/field_checkbox.js +++ b/core/field_checkbox.js @@ -94,7 +94,7 @@ Blockly.FieldCheckbox.prototype.setValue = function(newBool) { (newBool.toUpperCase() == 'TRUE') : !!newBool; if (this.state_ !== newState) { if (this.sourceBlock_ && Blockly.Events.isEnabled()) { - Blockly.Events.fire(new Blockly.Events.Change( + Blockly.Events.fire(new Blockly.Events.BlockChange( this.sourceBlock_, 'field', this.name, this.state_, newState)); } this.state_ = newState; diff --git a/core/field_colour.js b/core/field_colour.js index 2695b24c..bd325843 100644 --- a/core/field_colour.js +++ b/core/field_colour.js @@ -101,7 +101,7 @@ Blockly.FieldColour.prototype.getValue = function() { Blockly.FieldColour.prototype.setValue = function(colour) { if (this.sourceBlock_ && Blockly.Events.isEnabled() && this.colour_ != colour) { - Blockly.Events.fire(new Blockly.Events.Change( + Blockly.Events.fire(new Blockly.Events.BlockChange( this.sourceBlock_, 'field', this.name, this.colour_, colour)); } this.colour_ = colour; diff --git a/core/field_dropdown.js b/core/field_dropdown.js index 2a3f17a0..acb253ae 100644 --- a/core/field_dropdown.js +++ b/core/field_dropdown.js @@ -388,7 +388,7 @@ Blockly.FieldDropdown.prototype.setValue = function(newValue) { return; // No change if null. } if (this.sourceBlock_ && Blockly.Events.isEnabled()) { - Blockly.Events.fire(new Blockly.Events.Change( + Blockly.Events.fire(new Blockly.Events.BlockChange( this.sourceBlock_, 'field', this.name, this.value_, newValue)); } // Clear menu item for old value. diff --git a/core/field_textinput.js b/core/field_textinput.js index f80e5833..7816ffe2 100644 --- a/core/field_textinput.js +++ b/core/field_textinput.js @@ -144,7 +144,7 @@ Blockly.FieldTextInput.prototype.setText = function(newText) { return; } if (this.sourceBlock_ && Blockly.Events.isEnabled()) { - Blockly.Events.fire(new Blockly.Events.Change( + Blockly.Events.fire(new Blockly.Events.BlockChange( this.sourceBlock_, 'field', this.name, this.text_, newText)); } Blockly.Field.prototype.setText.call(this, newText); diff --git a/core/field_variable.js b/core/field_variable.js index 880e1580..ac57c5b6 100644 --- a/core/field_variable.js +++ b/core/field_variable.js @@ -107,9 +107,24 @@ Blockly.FieldVariable.prototype.getValue = function() { */ Blockly.FieldVariable.prototype.setValue = function(newValue) { if (this.sourceBlock_ && Blockly.Events.isEnabled()) { - Blockly.Events.fire(new Blockly.Events.Change( + Blockly.Events.fire(new Blockly.Events.BlockChange( this.sourceBlock_, 'field', this.name, this.value_, newValue)); } + if (this.sourceBlock_) { + var variable = this.sourceBlock_.workspace.getVariableById(newValue); + if (variable) { + this.setText(variable.name); + this.value_ = newValue; + return; + } + // TODO(marisaleung): Remove name lookup after converting all Field Variable + // instances to use id instead of name. + else if (variable = this.sourceBlock_.workspace.getVariable(newValue)) { + this.setText(newValue); + this.value_ = variable.getId(); + return; + } + } this.value_ = newValue; this.setText(newValue); }; diff --git a/core/flyout_base.js b/core/flyout_base.js index 3c9d0af2..08037d6e 100644 --- a/core/flyout_base.js +++ b/core/flyout_base.js @@ -19,9 +19,7 @@ */ /** - * @fileoverview Base class for a file tray containing blocks that may be - * dragged into the workspace. Defines basic interactions that are shared - * between vertical and horizontal flyouts. + * @fileoverview Flyout tray containing blocks which may be created. * @author fraser@google.com (Neil Fraser) */ 'use strict'; @@ -278,6 +276,13 @@ Blockly.Flyout.prototype.init = function(targetWorkspace) { // A flyout connected to a workspace doesn't have its own current gesture. this.workspace_.getGesture = this.targetWorkspace_.getGesture.bind(this.targetWorkspace_); + + // Get variables from the main workspace rather than the target workspace. + this.workspace_.getVariable = + this.targetWorkspace_.getVariable.bind(this.targetWorkspace_); + + this.workspace_.getVariableById = + this.targetWorkspace_.getVariableById.bind(this.targetWorkspace_); }; /** @@ -529,7 +534,7 @@ Blockly.Flyout.prototype.clearOldBlocks_ = function() { /** * Add listeners to a block that has been added to the flyout. - * @param {Element} root The root node of the SVG group the block is in. + * @param {!Element} root The root node of the SVG group the block is in. * @param {!Blockly.Block} block The block to add listeners for. * @param {!Element} rect The invisible rectangle under the block that acts as * a button for that block. @@ -631,7 +636,6 @@ Blockly.Flyout.prototype.reflow = function() { this.workspace_.removeChangeListener(this.reflowWrapper_); } var blocks = this.workspace_.getTopBlocks(false); - this.reflowInternal_(blocks); if (this.reflowWrapper_) { this.workspace_.addChangeListener(this.reflowWrapper_); diff --git a/core/mutator.js b/core/mutator.js index 7d108006..e4abf5fc 100644 --- a/core/mutator.js +++ b/core/mutator.js @@ -325,7 +325,7 @@ Blockly.Mutator.prototype.workspaceChanged_ = function() { var newMutationDom = block.mutationToDom(); var newMutation = newMutationDom && Blockly.Xml.domToText(newMutationDom); if (oldMutation != newMutation) { - Blockly.Events.fire(new Blockly.Events.Change( + Blockly.Events.fire(new Blockly.Events.BlockChange( block, 'mutation', null, oldMutation, newMutation)); // Ensure that any bump is part of this mutation's event group. var group = Blockly.Events.getGroup(); diff --git a/core/procedures.js b/core/procedures.js index 5951368b..8cc88c81 100644 --- a/core/procedures.js +++ b/core/procedures.js @@ -278,7 +278,7 @@ Blockly.Procedures.mutateCallers = function(defBlock) { // undo action since it is deterministically tied to the procedure's // definition mutation. Blockly.Events.recordUndo = false; - Blockly.Events.fire(new Blockly.Events.Change( + Blockly.Events.fire(new Blockly.Events.BlockChange( caller, 'mutation', null, oldMutation, newMutation)); Blockly.Events.recordUndo = oldRecordUndo; } diff --git a/core/variable_map.js b/core/variable_map.js index 522516db..553928ee 100644 --- a/core/variable_map.js +++ b/core/variable_map.js @@ -32,10 +32,11 @@ goog.require('Blockly.VariableModel'); * Class for a variable map. This contains a dictionary data structure with * variable types as keys and lists of variables as values. The list of * variables are the type indicated by the key. + * @param {!Blockly.Workspace} workspace The workspace this map belongs to. * @constructor */ -Blockly.VariableMap = function() { - /** +Blockly.VariableMap = function(workspace) { + /** * @type {!Object>} * A map from variable type to list of variable names. The lists contain all * of the named variables in the workspace, including variables @@ -43,6 +44,12 @@ Blockly.VariableMap = function() { * @private */ this.variableMap_ = {}; + + /** + * The workspace this map belongs to. + * @type {!Blockly.Workspace} + */ + this.workspace = workspace; }; /** @@ -54,7 +61,6 @@ Blockly.VariableMap.prototype.clear = function() { /** * Rename the given variable by updating its name in the variable map. - * TODO: #468 * @param {?Blockly.VariableModel} variable Variable to rename. * @param {string} newName New variable name. */ @@ -81,11 +87,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); } }; @@ -123,7 +137,7 @@ Blockly.VariableMap.prototype.createVariable = function(name, opt_type, opt_id) opt_id = opt_id || Blockly.utils.genUid(); opt_type = opt_type || ''; - variable = new Blockly.VariableModel(name, opt_type, opt_id); + variable = new Blockly.VariableModel(this.workspace, name, opt_type, opt_id); // If opt_type is not a key, create a new list. if (!this.variableMap_[opt_type]) { this.variableMap_[opt_type] = [variable]; @@ -143,6 +157,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 b5ec897a..990702a9 100644 --- a/core/variable_model.js +++ b/core/variable_model.js @@ -32,6 +32,7 @@ goog.require('goog.string'); /** * Class for a variable model. * Holds information for the variable including name, id, and type. + * @param {!Blockly.Workspace} workspace The variable's workspace. * @param {!string} name The name of the variable. This must be unique across * variables and procedures. * @param {?string} opt_type The type of the variable like 'int' or 'string'. @@ -42,7 +43,13 @@ goog.require('goog.string'); * @see {Blockly.FieldVariable} * @constructor */ -Blockly.VariableModel = function(name, opt_type, opt_id) { +Blockly.VariableModel = function(workspace, name, opt_type, opt_id) { + /** + * The workspace the variable is in. + * @type {!Blockly.Workspace} + */ + this.workspace = workspace; + /** * The name of the variable, typically defined by the user. It must be * unique across all names used for procedures and variables. It may be @@ -68,6 +75,8 @@ Blockly.VariableModel = function(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 f9403581..5067347d 100644 --- a/core/workspace.js +++ b/core/workspace.js @@ -89,7 +89,7 @@ Blockly.Workspace = function(opt_options) { * that are not currently in use. * @private */ - this.variableMap_ = new Blockly.VariableMap(); + this.variableMap_ = new Blockly.VariableMap(this); }; /** @@ -251,7 +251,6 @@ Blockly.Workspace.prototype.updateVariableStore = function(clear) { /** * Rename a variable by updating its name in the variable map. Identify the * variable to rename with the given variable. - * TODO: #468 * @param {?Blockly.VariableModel} variable Variable to rename. * @param {string} newName New variable name. */ @@ -280,16 +279,14 @@ 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); }; /** * Rename a variable by updating its name in the variable map. Identify the * variable to rename with the given name. - * TODO: #468 * @param {string} oldName Variable to rename. * @param {string} newName New variable name. */ @@ -299,6 +296,7 @@ Blockly.Workspace.prototype.renameVariable = function(oldName, newName) { this.renameVariableInternal_(variable, newName); }; + /** * Rename a variable by updating its name in the variable map. Identify the * variable to rename with the given id. @@ -343,7 +341,7 @@ Blockly.Workspace.prototype.getVariableUses = function(name) { for (var j = 0; j < blockVariables.length; j++) { var varName = blockVariables[j]; // Variable name may be null if the block is only half-built. - if (varName && Blockly.Names.equals(varName, name)) { + if (varName && name && Blockly.Names.equals(varName, name)) { uses.push(blocks[i]); } } @@ -399,6 +397,8 @@ Blockly.Workspace.prototype.deleteVariableById = function(id) { var variable = this.getVariableById(id); if (variable) { this.deleteVariableInternal_(variable); + } else { + console.warn("Can't delete non-existant variable: " + id); } }; @@ -414,9 +414,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); }; /** @@ -498,10 +497,14 @@ Blockly.Workspace.prototype.undo = function(redo) { } events = Blockly.Events.filter(events, redo); Blockly.Events.recordUndo = false; - for (var i = 0, event; event = events[i]; i++) { - event.run(redo); + try { + for (var i = 0, event; event = events[i]; i++) { + event.run(redo); + } + } + finally { + Blockly.Events.recordUndo = true; } - Blockly.Events.recordUndo = true; }; /** diff --git a/core/workspace_svg.js b/core/workspace_svg.js index 2d77fb27..44d780b4 100644 --- a/core/workspace_svg.js +++ b/core/workspace_svg.js @@ -972,7 +972,7 @@ Blockly.WorkspaceSvg.prototype.paste = function(xmlBlock) { Blockly.Events.enable(); } if (Blockly.Events.isEnabled() && !block.isShadow()) { - Blockly.Events.fire(new Blockly.Events.Create(block)); + Blockly.Events.fire(new Blockly.Events.BlockCreate(block)); } block.select(); }; @@ -1036,7 +1036,6 @@ Blockly.WorkspaceSvg.prototype.deleteVariableById = function(id) { /** * Create a new variable with the given name. Update the flyout to show the new * variable immediately. - * TODO: #468 * @param {string} name The new variable's name. * @param {string=} opt_type The type of the variable like 'int' or 'string'. * Does not need to be unique. Field_variable can filter variables based on diff --git a/core/xml.js b/core/xml.js index 48225d30..e5f503bb 100644 --- a/core/xml.js +++ b/core/xml.js @@ -333,38 +333,46 @@ Blockly.Xml.domToWorkspace = function(xml, workspace) { if (workspace.setResizesEnabled) { workspace.setResizesEnabled(false); } - for (var i = 0; i < childCount; i++) { - var xmlChild = xml.childNodes[i]; - var name = xmlChild.nodeName.toLowerCase(); - if (name == 'block' || - (name == 'shadow' && !Blockly.Events.recordUndo)) { - // Allow top-level shadow blocks if recordUndo is disabled since - // that means an undo is in progress. Such a block is expected - // to be moved to a nested destination in the next operation. - var block = Blockly.Xml.domToBlock(xmlChild, workspace); - newBlockIds.push(block.id); - var blockX = parseInt(xmlChild.getAttribute('x'), 10); - var blockY = parseInt(xmlChild.getAttribute('y'), 10); - if (!isNaN(blockX) && !isNaN(blockY)) { - block.moveBy(workspace.RTL ? width - blockX : blockX, blockY); - } - } else if (name == 'shadow') { - goog.asserts.fail('Shadow block cannot be a top-level block.'); - } else if (name == 'variables') { - if (i == 1) { - Blockly.Xml.domToVariables(xmlChild, workspace); - } - else { - throw Error('\'variables\' tag must be the first element in the' + - 'workspace XML, but it was found in another location.'); + var variablesFirst = true; + try { + for (var i = 0; i < childCount; i++) { + var xmlChild = xml.childNodes[i]; + var name = xmlChild.nodeName.toLowerCase(); + if (name == 'block' || + (name == 'shadow' && !Blockly.Events.recordUndo)) { + // Allow top-level shadow blocks if recordUndo is disabled since + // that means an undo is in progress. Such a block is expected + // to be moved to a nested destination in the next operation. + var block = Blockly.Xml.domToBlock(xmlChild, workspace); + newBlockIds.push(block.id); + var blockX = parseInt(xmlChild.getAttribute('x'), 10); + var blockY = parseInt(xmlChild.getAttribute('y'), 10); + if (!isNaN(blockX) && !isNaN(blockY)) { + block.moveBy(workspace.RTL ? width - blockX : blockX, blockY); + } + variablesFirst = false; + } else if (name == 'shadow') { + goog.asserts.fail('Shadow block cannot be a top-level block.'); + variablesFirst = false; + } else if (name == 'variables') { + if (variablesFirst) { + Blockly.Xml.domToVariables(xmlChild, workspace); + } + else { + throw Error('\'variables\' tag must exist once before block and ' + + 'shadow tag elements in the workspace XML, but it was found in ' + + 'another location.'); + } + variablesFirst = false; } } } - if (!existingGroup) { - Blockly.Events.setGroup(false); + finally { + if (!existingGroup) { + Blockly.Events.setGroup(false); + } + Blockly.Field.stopCache(); } - Blockly.Field.stopCache(); - workspace.updateVariableStore(false); // Re-enable workspace resizing. if (workspace.setResizesEnabled) { @@ -491,7 +499,7 @@ Blockly.Xml.domToBlock = function(xmlBlock, workspace) { Blockly.Events.enable(); } if (Blockly.Events.isEnabled()) { - Blockly.Events.fire(new Blockly.Events.Create(topBlock)); + Blockly.Events.fire(new Blockly.Events.BlockCreate(topBlock)); } return topBlock; }; @@ -701,6 +709,9 @@ Blockly.Xml.domToBlockHeadless_ = function(xmlBlock, workspace) { goog.asserts.assert(child.isShadow(), 'Shadow block not allowed non-shadow child.'); } + // Ensure this block doesn't have any variable inputs. + goog.asserts.assert(block.getVars().length == 0, + 'Shadow blocks cannot have variable fields.'); block.setShadow(true); } return block; diff --git a/msg/json/en.json b/msg/json/en.json index bcb80266..c9436eb6 100644 --- a/msg/json/en.json +++ b/msg/json/en.json @@ -1,7 +1,7 @@ { "@metadata": { "author": "Ellen Spertus ", - "lastupdated": "2017-05-25 07:58:37.810709", + "lastupdated": "2017-06-22 11:21:38.108160", "locale": "en", "messagedocumentation" : "qqq" }, diff --git a/tests/jsunit/event_test.js b/tests/jsunit/event_test.js new file mode 100644 index 00000000..c019515e --- /dev/null +++ b/tests/jsunit/event_test.js @@ -0,0 +1,394 @@ +/** + * @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.Events + * @author marisaleung@google.com (Marisa Leung) + */ +'use strict'; + +goog.require('goog.testing'); +goog.require('goog.testing.MockControl'); + +var mockControl_; +var workspace; + +function eventTest_setUp() { + workspace = new Blockly.Workspace(); + mockControl_ = new goog.testing.MockControl(); +} + +function eventTest_setUpWithMockBlocks() { + eventTest_setUp(); + Blockly.defineBlocksWithJsonArray([{ + 'type': 'field_variable_test_block', + 'message0': '%1', + 'args0': [ + { + 'type': 'field_variable', + 'name': 'VAR', + 'variable': 'item' + } + ], + }]); +} + +function eventTest_tearDown() { + mockControl_.$tearDown(); + workspace.dispose(); +} + +function eventTest_tearDownWithMockBlocks() { + eventTest_tearDown(); + delete Blockly.Blocks.field_variable_test_block; +} + +function test_abstract_constructor_block() { + eventTest_setUpWithMockBlocks(); + setUpMockMethod(mockControl_, Blockly.utils, 'genUid', null, '1'); + var block = new Blockly.Block(workspace, 'field_variable_test_block'); + var event = new Blockly.Events.Abstract(block); + assertUndefined(event.varId); + checkExactEventValues(event, {'blockId': '1', 'workspaceId': workspace.id, + 'group': '', 'recordUndo': true}); + eventTest_tearDownWithMockBlocks(); +} + +function test_abstract_constructor_variable() { + eventTest_setUpWithMockBlocks(); + setUpMockMethod(mockControl_, Blockly.utils, 'genUid', null, '1'); + var variable = workspace.createVariable('name1', 'type1', 'id1'); + var event = new Blockly.Events.Abstract(variable); + assertUndefined(event.blockId); + checkExactEventValues(event, {'varId': 'id1', + 'workspaceId': workspace.id, 'group': '', 'recordUndo': true}); + eventTest_tearDownWithMockBlocks(); +} + +function test_abstract_constructor_null() { + eventTest_setUpWithMockBlocks(); + var event = new Blockly.Events.Abstract(null); + assertUndefined(event.blockId); + assertUndefined(event.workspaceId); + checkExactEventValues(event, {'group': '', 'recordUndo': true}); + eventTest_tearDownWithMockBlocks(); +} + +function checkCreateEventValues(event, block, ids, type) { + var expected_xml = Blockly.Xml.domToText(Blockly.Xml.blockToDom(block)); + var result_xml = Blockly.Xml.domToText(event.xml); + assertEquals(expected_xml, result_xml); + isEqualArrays(ids, event.ids); + assertEquals(type, event.type); +} + +function checkDeleteEventValues(event, block, ids, type) { + var expected_xml = Blockly.Xml.domToText(Blockly.Xml.blockToDom(block)); + var result_xml = Blockly.Xml.domToText(event.oldXml); + assertEquals(expected_xml, result_xml); + isEqualArrays(ids, event.ids); + assertEquals(type, event.type); +} + +function checkExactEventValues(event, values) { + var keys = Object.keys(values); + for (var i = 0, field; field = keys[i]; i++) { + assertEquals(values[field], event[field]); + } +} + +function test_create_constructor() { + eventTest_setUpWithMockBlocks(); + setUpMockMethod(mockControl_, Blockly.utils, 'genUid', null, ['1']); + var block = new Blockly.Block(workspace, 'field_variable_test_block'); + var event = new Blockly.Events.Create(block); + checkCreateEventValues(event, block, ['1'], 'create'); + eventTest_tearDownWithMockBlocks(); +} + +function test_blockCreate_constructor() { + // expect that blockCreate behaves the same as create. + eventTest_setUpWithMockBlocks(); + setUpMockMethod(mockControl_, Blockly.utils, 'genUid', null, ['1']); + var block = new Blockly.Block(workspace, 'field_variable_test_block'); + var event = new Blockly.Events.BlockCreate(block); + checkCreateEventValues(event, block, ['1'], 'create'); + eventTest_tearDownWithMockBlocks(); +} + +function test_delete_constructor() { + eventTest_setUpWithMockBlocks(); + setUpMockMethod(mockControl_, Blockly.utils, 'genUid', null, ['1']); + var block = new Blockly.Block(workspace, 'field_variable_test_block'); + var event = new Blockly.Events.Delete(block); + checkDeleteEventValues(event, block, ['1'], 'delete'); + eventTest_tearDownWithMockBlocks(); +} + +function test_blockDelete_constructor() { + eventTest_setUpWithMockBlocks(); + setUpMockMethod(mockControl_, Blockly.utils, 'genUid', null, ['1']); + var block = new Blockly.Block(workspace, 'field_variable_test_block'); + var event = new Blockly.Events.BlockDelete(block); + checkDeleteEventValues(event, block, ['1'], 'delete'); + eventTest_tearDownWithMockBlocks(); +} + +function test_change_constructor() { + eventTest_setUpWithMockBlocks(); + setUpMockMethod(mockControl_, Blockly.utils, 'genUid', null, ['1']); + var block = new Blockly.Block(workspace, 'field_variable_test_block'); + var event = new Blockly.Events.Change(block, 'field', 'VAR', 'item', 'item2'); + checkExactEventValues(event, {'element': 'field', 'name': 'VAR', + 'oldValue': 'item', 'newValue': 'item2', 'type': 'change'}); + eventTest_tearDownWithMockBlocks(); +} + +function test_blockChange_constructor() { + eventTest_setUpWithMockBlocks(); + setUpMockMethod(mockControl_, Blockly.utils, 'genUid', null, ['1']); + var block = new Blockly.Block(workspace, 'field_variable_test_block'); + var event = new Blockly.Events.BlockChange(block, 'field', 'VAR', 'item', + 'item2'); + checkExactEventValues(event, {'element': 'field', 'name': 'VAR', + 'oldValue': 'item', 'newValue': 'item2', 'type': 'change'}); + eventTest_tearDownWithMockBlocks(); +} + +function test_move_constructorCoordinate() { + // Expect the oldCoordinate to be set. + eventTest_setUpWithMockBlocks(); + setUpMockMethod(mockControl_, Blockly.utils, 'genUid', null, ['1', '2']); + var block1 = new Blockly.Block(workspace, 'field_variable_test_block'); + var coordinate = new goog.math.Coordinate(3,4); + block1.xy_ = coordinate; + + var event = new Blockly.Events.Move(block1); + checkExactEventValues(event, {'oldCoordinate': coordinate, + 'type': 'move'}); + eventTest_tearDownWithMockBlocks(); +} + +function test_move_constructoroldParentId() { + // Expect the oldParentId to be set but not the oldCoordinate to be set. + eventTest_setUpWithMockBlocks(); + setUpMockMethod(mockControl_, Blockly.utils, 'genUid', null, ['1', '2']); + var block1 = new Blockly.Block(workspace, 'field_variable_test_block'); + var block2 = new Blockly.Block(workspace, 'field_variable_test_block'); + block1.parentBlock_ = block2; + block1.xy_ = new goog.math.Coordinate(3,4); + + var event = new Blockly.Events.Move(block1); + checkExactEventValues(event, {'oldCoordinate': undefined, + 'oldParentId': '2', 'type': 'move'}); + block1.parentBlock_ = null; + eventTest_tearDownWithMockBlocks(); +} + +function test_blockMove_constructorCoordinate() { + // Expect the oldCoordinate to be set. + eventTest_setUpWithMockBlocks(); + setUpMockMethod(mockControl_, Blockly.utils, 'genUid', null, ['1', '2']); + var block1 = new Blockly.Block(workspace, 'field_variable_test_block'); + var coordinate = new goog.math.Coordinate(3,4); + block1.xy_ = coordinate; + + var event = new Blockly.Events.BlockMove(block1); + checkExactEventValues(event, {'oldCoordinate': coordinate, + 'type': 'move'}); + eventTest_tearDownWithMockBlocks(); +} + +function test_blockMove_constructoroldParentId() { + // Expect the oldParentId to be set but not the oldCoordinate to be set. + eventTest_setUpWithMockBlocks(); + setUpMockMethod(mockControl_, Blockly.utils, 'genUid', null, ['1', '2']); + var block1 = new Blockly.Block(workspace, 'field_variable_test_block'); + var block2 = new Blockly.Block(workspace, 'field_variable_test_block'); + block1.parentBlock_ = block2; + block1.xy_ = new goog.math.Coordinate(3,4); + + var event = new Blockly.Events.BlockMove(block1); + checkExactEventValues(event, {'oldCoordinate': undefined, + 'oldParentId': '2', 'type': 'move'}); + block1.parentBlock_ = null; + eventTest_tearDownWithMockBlocks(); +} + +function test_varCreate_constructor() { + eventTest_setUp(); + var variable = workspace.createVariable('name1', 'type1', 'id1'); + var event = new Blockly.Events.VarCreate(variable); + checkExactEventValues(event, {'varName': 'name1', 'varType': 'type1', + 'type': 'var_create'}); + eventTest_tearDown(); +} + +function test_varCreate_toJson() { + eventTest_setUp(); + var variable = workspace.createVariable('name1', 'type1', 'id1'); + var event = new Blockly.Events.VarCreate(variable); + var json = event.toJson(); + var expectedJson = ({type: "var_create", varId: "id1", varType: "type1", + varName: "name1"}); + + assertEquals(JSON.stringify(expectedJson), JSON.stringify(json)); + eventTest_tearDown(); +} + +function test_varCreate_fromJson() { + eventTest_setUp(); + var variable = workspace.createVariable('name1', 'type1', 'id1'); + var event = new Blockly.Events.VarCreate(variable); + var event2 = new Blockly.Events.VarCreate(null); + var json = event.toJson(); + event2.fromJson(json); + + assertEquals(JSON.stringify(json), JSON.stringify(event2.toJson())); + eventTest_tearDown(); +} + +function test_varCreate_runForward() { + eventTest_setUp(); + var json = {type: "var_create", varId: "id1", varType: "type1", + varName: "name1"}; + var event = Blockly.Events.fromJson(json, workspace); + assertNull(workspace.getVariableById('id1')); + event.run(true); + checkVariableValues(workspace, 'name1', 'type1', 'id1'); + eventTest_tearDown(); +} + +function test_varCreate_runBackwards() { + eventTest_setUp(); + var variable = workspace.createVariable('name1', 'type1', 'id1'); + var event = new Blockly.Events.VarCreate(variable); + assertNotNull(workspace.getVariableById('id1')); + event.run(false); + assertNull(workspace.getVariableById('id1')); + eventTest_tearDown(); +} + +function test_varDelete_constructor() { + eventTest_setUp(); + var variable = workspace.createVariable('name1', 'type1', 'id1'); + var event = new Blockly.Events.VarDelete(variable); + checkExactEventValues(event, {'varName': 'name1', 'varType': 'type1', + 'varId':'id1', 'type': 'var_delete'}); + eventTest_tearDown(); +} + +function test_varDelete_toJson() { + eventTest_setUp(); + var variable = workspace.createVariable('name1', 'type1', 'id1'); + var event = new Blockly.Events.VarDelete(variable); + var json = event.toJson(); + var expectedJson = ({type: "var_delete", varId: "id1", varType: "type1", + varName: "name1"}); + + assertEquals(JSON.stringify(expectedJson), JSON.stringify(json)); + eventTest_tearDown(); +} + +function test_varDelete_fromJson() { + eventTest_setUp(); + var variable = workspace.createVariable('name1', 'type1', 'id1'); + var event = new Blockly.Events.VarDelete(variable); + var event2 = new Blockly.Events.VarDelete(null); + var json = event.toJson(); + event2.fromJson(json); + + assertEquals(JSON.stringify(json), JSON.stringify(event2.toJson())); + eventTest_tearDown(); +} + +function test_varDelete_runForwards() { + eventTest_setUp(); + var variable = workspace.createVariable('name1', 'type1', 'id1'); + var event = new Blockly.Events.VarDelete(variable); + assertNotNull(workspace.getVariableById('id1')); + event.run(true); + assertNull(workspace.getVariableById('id1')); + eventTest_tearDown(); +} + +function test_varDelete_runBackwards() { + eventTest_setUp(); + var json = {type: "var_delete", varId: "id1", varType: "type1", + varName: "name1"}; + var event = Blockly.Events.fromJson(json, workspace); + assertNull(workspace.getVariableById('id1')); + event.run(false); + checkVariableValues(workspace, 'name1', 'type1', 'id1'); + eventTest_tearDown(); +} + +function test_varRename_constructor() { + eventTest_setUp(); + var variable = workspace.createVariable('name1', 'type1', 'id1'); + var event = new Blockly.Events.VarRename(variable, 'name2'); + checkExactEventValues(event, {'varId': 'id1', 'oldName': 'name1', + 'newName': 'name2', 'type': 'var_rename'}); + eventTest_tearDown(); +} + +function test_varRename_toJson() { + eventTest_setUp(); + var variable = workspace.createVariable('name1', 'type1', 'id1'); + var event = new Blockly.Events.VarRename(variable, 'name2'); + var json = event.toJson(); + var expectedJson = ({type: "var_rename", varId: "id1", oldName: "name1", + newName: "name2"}); + + assertEquals(JSON.stringify(expectedJson), JSON.stringify(json)); + eventTest_tearDown(); +} + +function test_varRename_fromJson() { + eventTest_setUp(); + var variable = workspace.createVariable('name1', 'type1', 'id1'); + var event = new Blockly.Events.VarRename(variable, ''); + var event2 = new Blockly.Events.VarRename(null); + var json = event.toJson(); + event2.fromJson(json); + + assertEquals(JSON.stringify(json), JSON.stringify(event2.toJson())); + eventTest_tearDown(); +} + +function test_varRename_runForward() { + eventTest_setUp(); + var variable = workspace.createVariable('name1', 'type1', 'id1'); + var event = new Blockly.Events.VarRename(variable, 'name2'); + event.run(true); + assertNull(workspace.getVariable('name1')); + checkVariableValues(workspace, 'name2', 'type1', 'id1'); + eventTest_tearDown(); +} + +function test_varBackard_runForward() { + eventTest_setUp(); + var variable = workspace.createVariable('name1', 'type1', 'id1'); + var event = new Blockly.Events.VarRename(variable, 'name2'); + event.run(false); + assertNull(workspace.getVariable('name2')); + checkVariableValues(workspace, 'name1', 'type1', 'id1'); + eventTest_tearDown(); +} diff --git a/tests/jsunit/field_variable_test.js b/tests/jsunit/field_variable_test.js new file mode 100644 index 00000000..59c3d267 --- /dev/null +++ b/tests/jsunit/field_variable_test.js @@ -0,0 +1,79 @@ +/** + * @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.FieldVariable + * @author marisaleung@google.com (Marisa Leung) + */ +'use strict'; + +goog.require('goog.testing'); + + +function fieldVariable_mockBlock(workspace) { + return {'workspace': workspace, 'isShadow': function(){return false;}}; +} + +function test_fieldVariable_Constructor() { + var workspace = new Blockly.Workspace(); + var fieldVariable = new Blockly.FieldVariable('name1'); + assertEquals('name1', fieldVariable.getText()); + workspace.dispose(); +} + +function test_fieldVariable_setValueMatchId() { + // Expect the fieldVariable value to be set to variable name + var workspace = new Blockly.Workspace(); + workspace.createVariable('name2', null, 'id1'); + var fieldVariable = new Blockly.FieldVariable('name1'); + var mockBlock = fieldVariable_mockBlock(workspace); + fieldVariable.setSourceBlock(mockBlock); + fieldVariable.setValue('id1'); + assertEquals('name2', fieldVariable.getText()); + assertEquals('id1', fieldVariable.value_); + workspace.dispose(); +} + +function test_fieldVariable_setValueMatchName() { + // Expect the fieldVariable value to be set to variable name + var workspace = new Blockly.Workspace(); + workspace.createVariable('name2', null, 'id2'); + var fieldVariable = new Blockly.FieldVariable('name1'); + var mockBlock = fieldVariable_mockBlock(workspace); + fieldVariable.setSourceBlock(mockBlock); + fieldVariable.setValue('name2'); + assertEquals('name2', fieldVariable.getText()); + assertEquals('id2', fieldVariable.value_); + workspace.dispose(); +} + +function test_fieldVariable_setValueNoVariable() { + // Expect the fieldVariable value to be set to the passed in string. No error + // should be thrown. + var workspace = new Blockly.Workspace(); + var fieldVariable = new Blockly.FieldVariable('name1'); + var mockBlock = {'workspace': workspace, + 'isShadow': function(){return false;}}; + fieldVariable.setSourceBlock(mockBlock); + fieldVariable.setValue('id1'); + assertEquals('id1', fieldVariable.getText()); + assertEquals('id1', fieldVariable.value_); + workspace.dispose(); +} diff --git a/tests/jsunit/index.html b/tests/jsunit/index.html index a4f699e5..0703d079 100644 --- a/tests/jsunit/index.html +++ b/tests/jsunit/index.html @@ -4,17 +4,21 @@ + + + + diff --git a/tests/jsunit/test_utilities.js b/tests/jsunit/test_utilities.js new file mode 100644 index 00000000..6a80a354 --- /dev/null +++ b/tests/jsunit/test_utilities.js @@ -0,0 +1,91 @@ +/** + * @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 Test utilities. + * @author marisaleung@google.com (Marisa Leung) + */ +'use strict'; + +goog.require('goog.testing'); + + +/** + * Check that two arrays have the same content. + * @param {!Array.} array1 The first array. + * @param {!Array.} array2 The second array. + */ +function isEqualArrays(array1, array2) { + assertEquals(array1.length, array2.length); + for (var i = 0; i < array1.length; i++) { + assertEquals(array1[i], array2[i]); + } +} + +/** + * Creates a controlled MethodMock. Sets the expected return values and + * the parameters if any exist. Sets the method to replay. + * @param {!goog.testing.MockControl} mockControl Object that holds a set + * of mocks for this test. + * @param {!Object} scope The scope of the method to be mocked out. + * @param {!string} funcName The name of the function we're going to mock. + * @param {Array} parameters The parameters to call the mock with. + * @param {Array} return_values The values to return when called. + * @return {!goog.testing.MockInterface} The mocked method. + */ +function setUpMockMethod(mockControl, scope, funcName, parameters, + return_values) { + var mockMethod = mockControl.createMethodMock(scope, funcName); + if (return_values) { + for (var i = 0, return_value; return_value = return_values[i]; i++) { + if (parameters && i < parameters.length) { + mockMethod(parameters[i]).$returns(return_value); + } + else { + mockMethod().$returns(return_value); + } + } + } + // If there are no return values but there are parameters, we are only + // recording specific method calls. + else if (parameters) { + for (var i = 0; i < parameters.length; i++) { + mockMethod(parameters[i]); + } + } + mockMethod.$replay(); + return mockMethod; +} + +/** + * Check if a variable with the given values exists. + * @param {Blockly.Workspace|Blockly.VariableMap} container The workspace or + * variableMap the checked variable belongs to. + * @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 checkVariableValues(container, name, type, id) { + var variable = container.getVariableById(id); + assertNotUndefined(variable); + assertEquals(name, variable.name); + assertEquals(type, variable.type); + assertEquals(id, variable.getId()); +} diff --git a/tests/jsunit/variable_map_test.js b/tests/jsunit/variable_map_test.js index 7911a33a..51141cff 100644 --- a/tests/jsunit/variable_map_test.js +++ b/tests/jsunit/variable_map_test.js @@ -24,31 +24,20 @@ goog.require('goog.testing.MockControl'); var variable_map; var mockControl_; +var workspace; function variableMapTest_setUp() { - variable_map = new Blockly.VariableMap(); + workspace = new Blockly.Workspace(); + variable_map = new Blockly.VariableMap(workspace); mockControl_ = new goog.testing.MockControl(); } function variableMapTest_tearDown() { + workspace.dispose(); mockControl_.$tearDown(); variable_map = null; } -/** - * 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 variableMapTest_checkVariableValues(name, type, id) { - var variable = variable_map.getVariable(name); - assertNotUndefined(variable); - assertEquals(name, variable.name); - assertEquals(type, variable.type); - assertEquals(id, variable.getId()); -} - function test_getVariable_Trivial() { variableMapTest_setUp(); var var_1 = variable_map.createVariable('name1', 'type1', 'id1'); @@ -96,14 +85,14 @@ function test_getVariableById_NotFound() { function test_createVariableTrivial() { variableMapTest_setUp(); variable_map.createVariable('name1', 'type1', 'id1'); - variableMapTest_checkVariableValues('name1', 'type1', 'id1') + checkVariableValues(variable_map, 'name1', 'type1', 'id1'); variableMapTest_tearDown(); } function test_createVariableAlreadyExists() { // Expect that when the variable already exists, the variableMap_ is unchanged. variableMapTest_setUp(); - var var_1 = variable_map.createVariable('name1', 'type1', 'id1'); + variable_map.createVariable('name1', 'type1', 'id1'); // Assert there is only one variable in the variable_map. var keys = Object.keys(variable_map.variableMap_); @@ -112,7 +101,7 @@ function test_createVariableAlreadyExists() { assertEquals(1, varMapLength); variable_map.createVariable('name1'); - variableMapTest_checkVariableValues('name1', 'type1', 'id1'); + checkVariableValues(variable_map, 'name1', 'type1', 'id1'); // Check that the size of the variableMap_ did not change. keys = Object.keys(variable_map.variableMap_); assertEquals(1, keys.length); @@ -126,18 +115,17 @@ function test_createVariableNullAndUndefinedType() { variable_map.createVariable('name1', null, 'id1'); variable_map.createVariable('name2', undefined, 'id2'); - variableMapTest_checkVariableValues('name1', '', 'id1'); - variableMapTest_checkVariableValues('name2', '', 'id2'); + checkVariableValues(variable_map, 'name1', '', 'id1'); + checkVariableValues(variable_map, 'name2', '', 'id2'); variableMapTest_tearDown(); } function test_createVariableNullId() { variableMapTest_setUp(); - var mockGenUid = setUpMockMethod(Blockly.utils, 'genUid', null, '1'); + setUpMockMethod(mockControl_, Blockly.utils, 'genUid', null, ['1', '2']); try { variable_map.createVariable('name1', 'type1', null); - mockGenUid.$verify(); - variableMapTest_checkVariableValues('name1', 'type1', '1'); + checkVariableValues(variable_map, 'name1', 'type1', '1'); } finally { variableMapTest_tearDown(); @@ -146,11 +134,10 @@ function test_createVariableNullId() { function test_createVariableUndefinedId() { variableMapTest_setUp(); - var mockGenUid = setUpMockMethod(Blockly.utils, 'genUid', null, '1'); + setUpMockMethod(mockControl_, Blockly.utils, 'genUid', null, ['1', '2']); try { variable_map.createVariable('name1', 'type1', undefined); - mockGenUid.$verify(); - variableMapTest_checkVariableValues('name1', 'type1', '1'); + checkVariableValues(variable_map, 'name1', 'type1', '1'); } finally { variableMapTest_tearDown(); @@ -192,8 +179,8 @@ function test_createVariableTwoSameTypes() { variable_map.createVariable('name1', 'type1', 'id1'); variable_map.createVariable('name2', 'type1', 'id2'); - variableMapTest_checkVariableValues('name1', 'type1', 'id1'); - variableMapTest_checkVariableValues('name2', 'type1', 'id2'); + checkVariableValues(variable_map, 'name1', 'type1', 'id1'); + checkVariableValues(variable_map, 'name2', 'type1', 'id2'); variableMapTest_tearDown(); } @@ -205,8 +192,8 @@ function test_getVariablesOfType_Trivial() { variable_map.createVariable('name4', 'type3', 'id4'); var result_array_1 = variable_map.getVariablesOfType('type1'); var result_array_2 = variable_map.getVariablesOfType('type5'); - this.isEqualArrays([var_1, var_2], result_array_1); - this.isEqualArrays([], result_array_2); + isEqualArrays([var_1, var_2], result_array_1); + isEqualArrays([], result_array_2); variableMapTest_tearDown(); } @@ -217,7 +204,7 @@ function test_getVariablesOfType_Null() { var var_3 = variable_map.createVariable('name3', '', 'id3'); variable_map.createVariable('name4', 'type1', 'id4'); var result_array = variable_map.getVariablesOfType(null); - this.isEqualArrays([var_1, var_2, var_3], result_array); + isEqualArrays([var_1, var_2, var_3], result_array); variableMapTest_tearDown(); } @@ -226,7 +213,7 @@ function test_getVariablesOfType_EmptyString() { var var_1 = variable_map.createVariable('name1', null, 'id1'); var var_2 = variable_map.createVariable('name2', null, 'id2'); var result_array = variable_map.getVariablesOfType(''); - this.isEqualArrays([var_1, var_2], result_array); + isEqualArrays([var_1, var_2], result_array); variableMapTest_tearDown(); } @@ -235,14 +222,14 @@ function test_getVariablesOfType_Deleted() { var variable = variable_map.createVariable('name1', null, 'id1'); variable_map.deleteVariable(variable); var result_array = variable_map.getVariablesOfType(''); - this.isEqualArrays([], result_array); + isEqualArrays([], result_array); variableMapTest_tearDown(); } function test_getVariablesOfType_DoesNotExist() { variableMapTest_setUp(); var result_array = variable_map.getVariablesOfType('type1'); - this.isEqualArrays([], result_array); + isEqualArrays([], result_array); variableMapTest_tearDown(); } @@ -253,14 +240,14 @@ function test_getVariableTypes_Trivial() { variable_map.createVariable('name3', 'type2', 'id3'); variable_map.createVariable('name4', 'type3', 'id4'); var result_array = variable_map.getVariableTypes(); - this.isEqualArrays(['type1', 'type2', 'type3'], result_array); + isEqualArrays(['type1', 'type2', 'type3'], result_array); variableMapTest_tearDown(); } function test_getVariableTypes_None() { variableMapTest_setUp(); var result_array = variable_map.getVariableTypes(); - this.isEqualArrays([], result_array); + isEqualArrays([], result_array); variableMapTest_tearDown(); } @@ -270,13 +257,13 @@ function test_getAllVariables_Trivial() { var var_2 = variable_map.createVariable('name2', 'type1', 'id2'); var var_3 = variable_map.createVariable('name3', 'type2', 'id3'); var result_array = variable_map.getAllVariables(); - this.isEqualArrays([var_1, var_2, var_3], result_array); + isEqualArrays([var_1, var_2, var_3], result_array); variableMapTest_tearDown(); } function test_getAllVariables_None() { variableMapTest_setUp(); var result_array = variable_map.getAllVariables(); - this.isEqualArrays([], result_array); + isEqualArrays([], result_array); variableMapTest_tearDown(); } diff --git a/tests/jsunit/variable_model_test.js b/tests/jsunit/variable_model_test.js index 9a2d2ac3..3f22f287 100644 --- a/tests/jsunit/variable_model_test.js +++ b/tests/jsunit/variable_model_test.js @@ -25,8 +25,14 @@ 'use strict'; var variable; +var workspace; -function variableTest_tearDown() { +function variableModelTest_setUp() { + workspace = new Blockly.Workspace(); +} + +function variableModelTest_tearDown() { + workspace.dispose(); variable = null; } @@ -34,45 +40,52 @@ function variableTest_tearDown() { * These tests check the constructor of the variable model. */ function testInit_Trivial() { - variable = new Blockly.VariableModel('test', 'test_type', 'test_id'); + variableModelTest_setUp(); + variable = new Blockly.VariableModel(workspace, 'test', 'test_type', + 'test_id'); assertEquals('test', variable.name); assertEquals('test_type', variable.type); assertEquals('test_id', variable.id_); - variableTest_tearDown(); + variableModelTest_tearDown(); } function testInit_NullType() { - variable = new Blockly.VariableModel('test', null, 'test_id'); + variableModelTest_setUp(); + variable = new Blockly.VariableModel(workspace, 'test', null, 'test_id'); assertEquals('', variable.type); - variableTest_tearDown(); + variableModelTest_tearDown(); } function testInit_UndefinedType() { - variable = new Blockly.VariableModel('test', undefined, 'test_id'); + variableModelTest_setUp(); + variable = new Blockly.VariableModel(workspace, 'test', undefined, 'test_id'); assertEquals('', variable.type); - variableTest_tearDown(); + variableModelTest_tearDown(); } function testInit_NullId() { - variable = new Blockly.VariableModel('test', 'test_type', null); + variableModelTest_setUp(); + variable = new Blockly.VariableModel(workspace, 'test', 'test_type', null); assertEquals('test', variable.name); assertEquals('test_type', variable.type); assertNotNull(variable.id_); - variableTest_tearDown(); + variableModelTest_tearDown(); } function testInit_UndefinedId() { - variable = new Blockly.VariableModel('test', 'test_type', undefined); + variableModelTest_setUp(); + variable = new Blockly.VariableModel(workspace, 'test', 'test_type', undefined); assertEquals('test', variable.name); assertEquals('test_type', variable.type); assertNotNull(variable.id_); - variableTest_tearDown(); + variableModelTest_tearDown(); } function testInit_OnlyNameProvided() { - variable = new Blockly.VariableModel('test'); + variableModelTest_setUp(); + variable = new Blockly.VariableModel(workspace, 'test'); assertEquals('test', variable.name); assertEquals('', variable.type); assertNotNull(variable.id_); - variableTest_tearDown(); -} \ No newline at end of file + variableModelTest_tearDown(); +} diff --git a/tests/jsunit/workspace_test.js b/tests/jsunit/workspace_test.js index bf4618a8..17755f15 100644 --- a/tests/jsunit/workspace_test.js +++ b/tests/jsunit/workspace_test.js @@ -24,16 +24,15 @@ goog.require('goog.testing.MockControl'); var workspace; var mockControl_; -var saved_msg = Blockly.Msg.DELETE_VARIABLE; Blockly.defineBlocksWithJsonArray([{ - "type": "get_var_block", - "message0": "%1", - "args0": [ - { - "type": "field_variable", - "name": "VAR", - } - ] + "type": "get_var_block", + "message0": "%1", + "args0": [ + { + "type": "field_variable", + "name": "VAR", + } + ] }]); function workspaceTest_setUp() { @@ -41,25 +40,11 @@ function workspaceTest_setUp() { mockControl_ = new goog.testing.MockControl(); } -function workspaceTest_setUpWithMockBlocks() { - workspaceTest_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 workspaceTest_tearDown() { mockControl_.$tearDown(); workspace.dispose(); } -function workspaceTest_tearDownWithMockBlocks() { - workspaceTest_tearDown(); - Blockly.Msg.DELETE_VARIABLE = saved_msg; -} - /** * Create a test get_var_block. * @param {?string} variable_name The string to put into the variable field. @@ -71,53 +56,6 @@ function createMockBlock(variable_name) { return block; } -/** - * Check that two arrays have the same content. - * @param {!Array.} array1 The first array. - * @param {!Array.} array2 The second array. - */ -function isEqualArrays(array1, array2) { - assertEquals(array1.length, array2.length); - for (var i = 0; i < array1.length; i++) { - assertEquals(array1[i], array2[i]); - } -} - -/** - * 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 workspaceTest_checkVariableValues(name, type, id) { - var variable = workspace.getVariable(name); - assertNotUndefined(variable); - assertEquals(name, variable.name); - assertEquals(type, variable.type); - assertEquals(id, variable.getId()); -} - -/** - * Creates a controlled MethodMock. Set the expected return values. Set the - * method to replay. - * @param {!Object} scope The scope of the method to be mocked out. - * @param {!string} funcName The name of the function we're going to mock. - * @param {Object} parameters The parameters to call the mock with. - * @param {!Object} return_value The value to return when called. - * @return {!goog.testing.MockInterface} The mocked method. - */ -function setUpMockMethod(scope, funcName, parameters, return_value) { - var mockMethod = mockControl_.createMethodMock(scope, funcName); - if (parameters) { - mockMethod(parameters).$returns(return_value); - } - else { - mockMethod().$returns(return_value); - } - mockMethod.$replay(); - return mockMethod; -} - function test_emptyWorkspace() { workspaceTest_setUp(); try { @@ -220,7 +158,7 @@ function test_getBlockById() { } function test_deleteVariable_InternalTrivial() { - workspaceTest_setUpWithMockBlocks() + workspaceTest_setUp(); var var_1 = workspace.createVariable('name1', 'type1', 'id1'); workspace.createVariable('name2', 'type2', 'id2'); createMockBlock('name1'); @@ -231,9 +169,9 @@ function test_deleteVariable_InternalTrivial() { var variable = workspace.getVariable('name1'); var block_var_name = workspace.topBlocks_[0].getVars()[0]; assertNull(variable); - workspaceTest_checkVariableValues('name2', 'type2', 'id2'); + checkVariableValues(workspace, 'name2', 'type2', 'id2'); assertEquals('name2', block_var_name); - workspaceTest_tearDownWithMockBlocks(); + workspaceTest_tearDown(); } // TODO(marisaleung): Test the alert for deleting a variable that is a procedure. @@ -242,14 +180,13 @@ function test_updateVariableStore_TrivialNoClear() { workspaceTest_setUp(); workspace.createVariable('name1', 'type1', 'id1'); workspace.createVariable('name2', 'type2', 'id2'); - var mockAllUsedVariables = setUpMockMethod(Blockly.Variables, - 'allUsedVariables', workspace, ['name1', 'name2']); + setUpMockMethod(mockControl_, Blockly.Variables, 'allUsedVariables', + [workspace], [['name1', 'name2']]); try { workspace.updateVariableStore(); - mockAllUsedVariables.$verify(); - workspaceTest_checkVariableValues('name1', 'type1', 'id1'); - workspaceTest_checkVariableValues('name2', 'type2', 'id2'); + checkVariableValues(workspace, 'name1', 'type1', 'id1'); + checkVariableValues(workspace, 'name2', 'type2', 'id2'); } finally { workspaceTest_tearDown(); @@ -258,13 +195,13 @@ function test_updateVariableStore_TrivialNoClear() { function test_updateVariableStore_NameNotInvariableMap_NoClear() { workspaceTest_setUp(); - setUpMockMethod(Blockly.Variables, 'allUsedVariables', workspace, ['name1']); - setUpMockMethod(Blockly.utils, 'genUid', null, '1'); + setUpMockMethod(mockControl_, Blockly.Variables, 'allUsedVariables', + [workspace], [['name1']]); + setUpMockMethod(mockControl_, Blockly.utils, 'genUid', null, ['1']); try { workspace.updateVariableStore(); - mockControl_.$verifyAll(); - workspaceTest_checkVariableValues('name1', '', '1'); + checkVariableValues(workspace, 'name1', '', '1'); } finally { workspaceTest_tearDown(); @@ -275,14 +212,13 @@ function test_updateVariableStore_ClearAndAllInUse() { workspaceTest_setUp(); workspace.createVariable('name1', 'type1', 'id1'); workspace.createVariable('name2', 'type2', 'id2'); - var mockAllUsedVariables = setUpMockMethod(Blockly.Variables, - 'allUsedVariables', workspace, ['name1', 'name2']); + setUpMockMethod(mockControl_, Blockly.Variables, 'allUsedVariables', + [workspace], [['name1', 'name2']]); try { workspace.updateVariableStore(true); - mockAllUsedVariables.$verify(); - workspaceTest_checkVariableValues('name1', 'type1', 'id1'); - workspaceTest_checkVariableValues('name2', 'type2', 'id2'); + checkVariableValues(workspace, 'name1', 'type1', 'id1'); + checkVariableValues(workspace, 'name2', 'type2', 'id2'); } finally { workspaceTest_tearDown(); @@ -293,13 +229,12 @@ function test_updateVariableStore_ClearAndOneInUse() { workspaceTest_setUp(); workspace.createVariable('name1', 'type1', 'id1'); workspace.createVariable('name2', 'type2', 'id2'); - var mockAllUsedVariables = setUpMockMethod(Blockly.Variables, - 'allUsedVariables', workspace, ['name1']); + setUpMockMethod(mockControl_, Blockly.Variables, 'allUsedVariables', + [workspace], [['name1']]); try { workspace.updateVariableStore(true); - mockAllUsedVariables.$verify(); - workspaceTest_checkVariableValues('name1', 'type1', 'id1'); + checkVariableValues(workspace, 'name1', 'type1', 'id1'); var variabe = workspace.getVariable('name2'); assertNull(variable); } @@ -309,20 +244,20 @@ function test_updateVariableStore_ClearAndOneInUse() { } function test_addTopBlock_TrivialFlyoutIsTrue() { - workspaceTest_setUpWithMockBlocks() + workspaceTest_setUp(); workspace.isFlyout = true; var block = createMockBlock(); workspace.removeTopBlock(block); - setUpMockMethod(Blockly.Variables, 'allUsedVariables', block, ['name1']); - setUpMockMethod(Blockly.utils, 'genUid', null, '1'); + setUpMockMethod(mockControl_, Blockly.Variables, 'allUsedVariables', [block], + [['name1']]); + setUpMockMethod(mockControl_, Blockly.utils, 'genUid', null, ['1']); try { workspace.addTopBlock(block); - mockControl_.$verifyAll(); - workspaceTest_checkVariableValues('name1', '', '1'); + checkVariableValues(workspace, 'name1', '', '1'); } finally { - workspaceTest_tearDownWithMockBlocks(); + workspaceTest_tearDown(); } } @@ -330,14 +265,11 @@ function test_clear_Trivial() { workspaceTest_setUp(); workspace.createVariable('name1', 'type1', 'id1'); workspace.createVariable('name2', 'type2', 'id2'); - var mockSetGroup = mockControl_.createMethodMock(Blockly.Events, 'setGroup'); - mockSetGroup(true); - mockSetGroup(false); - mockSetGroup.$replay(); + setUpMockMethod(mockControl_, Blockly.Events, 'setGroup', [true, false], + null); try { workspace.clear(); - mockControl_.$verifyAll(); var topBlocks_length = workspace.topBlocks_.length; var varMapLength = Object.keys(workspace.variableMap_.variableMap_).length; assertEquals(0, topBlocks_length); @@ -350,14 +282,11 @@ function test_clear_Trivial() { function test_clear_NoVariables() { workspaceTest_setUp(); - var mockSetGroup = mockControl_.createMethodMock(Blockly.Events, 'setGroup'); - mockSetGroup(true); - mockSetGroup(false); - mockSetGroup.$replay(); + setUpMockMethod(mockControl_, Blockly.Events, 'setGroup', [true, false], + null); try { workspace.clear(); - mockSetGroup.$verify(); var topBlocks_length = workspace.topBlocks_.length; var varMapLength = Object.keys(workspace.variableMap_.variableMap_).length; assertEquals(0, topBlocks_length); @@ -373,18 +302,14 @@ function test_renameVariable_NoBlocks() { workspaceTest_setUp(); var oldName = 'name1'; var newName = 'name2'; - var mockSetGroup = mockControl_.createMethodMock(Blockly.Events, 'setGroup'); - var mockGenUid = mockControl_.createMethodMock(Blockly.utils, 'genUid'); - // Mocked setGroup to ensure only one call to the mocked genUid. - mockSetGroup(true); - mockSetGroup(false); - mockGenUid().$returns('1'); - mockControl_.$replayAll(); + // Mocked setGroup to ensure only one call to the mocked genUid. + setUpMockMethod(mockControl_, Blockly.Events, 'setGroup', [true, false], + null); + setUpMockMethod(mockControl_, Blockly.utils, 'genUid', null, ['1']); try { workspace.renameVariable(oldName, newName); - mockControl_.$verifyAll(); - workspaceTest_checkVariableValues('name2', '', '1'); + checkVariableValues(workspace, 'name2', '', '1'); var variable = workspace.getVariable(oldName); assertNull(variable); } @@ -395,36 +320,36 @@ function test_renameVariable_NoBlocks() { function test_renameVariable_SameNameNoBlocks() { // Expect 'renameVariable' to create new variable with newName. - workspaceTest_setUpWithMockBlocks() + workspaceTest_setUp(); var name = 'name1'; workspace.createVariable(name, 'type1', 'id1'); workspace.renameVariable(name, name); - workspaceTest_checkVariableValues(name, 'type1', 'id1'); - workspaceTest_tearDownWithMockBlocks(); + checkVariableValues(workspace, name, 'type1', 'id1'); + workspaceTest_tearDown(); } function test_renameVariable_OnlyOldNameBlockExists() { // Expect 'renameVariable' to change oldName variable name to newName. - workspaceTest_setUpWithMockBlocks() + workspaceTest_setUp(); var oldName = 'name1'; var newName = 'name2'; workspace.createVariable(oldName, 'type1', 'id1'); createMockBlock(oldName); workspace.renameVariable(oldName, newName); - workspaceTest_checkVariableValues(newName, 'type1', 'id1'); + checkVariableValues(workspace, newName, 'type1', 'id1'); var variable = workspace.getVariable(oldName); var block_var_name = workspace.topBlocks_[0].getVars()[0]; assertNull(variable); assertEquals(newName, block_var_name); - workspaceTest_tearDownWithMockBlocks(); + workspaceTest_tearDown(); } function test_renameVariable_TwoVariablesSameType() { // Expect 'renameVariable' to change oldName variable name to newName. // Expect oldName block name to change to newName - workspaceTest_setUpWithMockBlocks() + workspaceTest_setUp(); var oldName = 'name1'; var newName = 'name2'; workspace.createVariable(oldName, 'type1', 'id1'); @@ -433,19 +358,19 @@ function test_renameVariable_TwoVariablesSameType() { createMockBlock(newName); workspace.renameVariable(oldName, newName); - workspaceTest_checkVariableValues(newName, 'type1', 'id2'); + checkVariableValues(workspace, newName, 'type1', 'id2'); var variable = workspace.getVariable(oldName); var block_var_name_1 = workspace.topBlocks_[0].getVars()[0]; var block_var_name_2 = workspace.topBlocks_[1].getVars()[0]; assertNull(variable); assertEquals(newName, block_var_name_1); assertEquals(newName, block_var_name_2); - workspaceTest_tearDownWithMockBlocks(); + workspaceTest_tearDown(); } function test_renameVariable_TwoVariablesDifferentType() { // Expect triggered error because of different types - workspaceTest_setUpWithMockBlocks() + workspaceTest_setUp(); var oldName = 'name1'; var newName = 'name2'; workspace.createVariable(oldName, 'type1', 'id1'); @@ -459,33 +384,33 @@ function test_renameVariable_TwoVariablesDifferentType() { } catch (e) { // expected } - workspaceTest_checkVariableValues(oldName, 'type1', 'id1'); - workspaceTest_checkVariableValues(newName, 'type2', 'id2'); + checkVariableValues(workspace, oldName, 'type1', 'id1'); + checkVariableValues(workspace, newName, 'type2', 'id2'); var block_var_name_1 = workspace.topBlocks_[0].getVars()[0]; var block_var_name_2 = workspace.topBlocks_[1].getVars()[0]; assertEquals(oldName, block_var_name_1); assertEquals(newName, block_var_name_2); - workspaceTest_tearDownWithMockBlocks(); + workspaceTest_tearDown(); } function test_renameVariable_OldCase() { // Expect triggered error because of different types - workspaceTest_setUpWithMockBlocks(); + workspaceTest_setUp(); var oldCase = 'Name1'; var newName = 'name1'; workspace.createVariable(oldCase, 'type1', 'id1'); createMockBlock(oldCase); workspace.renameVariable(oldCase, newName); - workspaceTest_checkVariableValues(newName, 'type1', 'id1'); - var result_oldCase = workspace.getVariable(oldCase).name + checkVariableValues(workspace, newName, 'type1', 'id1'); + var result_oldCase = workspace.getVariable(oldCase).name; assertNotEquals(oldCase, result_oldCase); - workspaceTest_tearDownWithMockBlocks(); + workspaceTest_tearDown(); } function test_renameVariable_TwoVariablesAndOldCase() { // Expect triggered error because of different types - workspaceTest_setUpWithMockBlocks() + workspaceTest_setUp(); var oldName = 'name1'; var oldCase = 'Name2'; var newName = 'name2'; @@ -496,7 +421,7 @@ function test_renameVariable_TwoVariablesAndOldCase() { workspace.renameVariable(oldName, newName); - workspaceTest_checkVariableValues(newName, 'type1', 'id2'); + checkVariableValues(workspace, newName, 'type1', 'id2'); var variable = workspace.getVariable(oldName); var result_oldCase = workspace.getVariable(oldCase).name; var block_var_name_1 = workspace.topBlocks_[0].getVars()[0]; @@ -505,7 +430,7 @@ function test_renameVariable_TwoVariablesAndOldCase() { assertNotEquals(oldCase, result_oldCase); assertEquals(newName, block_var_name_1); assertEquals(newName, block_var_name_2); - workspaceTest_tearDownWithMockBlocks(); + workspaceTest_tearDown(); } // Extra testing not required for renameVariableById. It calls renameVariable @@ -513,7 +438,7 @@ function test_renameVariable_TwoVariablesAndOldCase() { function test_renameVariableById_TwoVariablesSameType() { // Expect 'renameVariableById' to change oldName variable name to newName. // Expect oldName block name to change to newName - workspaceTest_setUpWithMockBlocks() + workspaceTest_setUp(); var oldName = 'name1'; var newName = 'name2'; workspace.createVariable(oldName, 'type1', 'id1'); @@ -522,44 +447,44 @@ function test_renameVariableById_TwoVariablesSameType() { createMockBlock(newName); workspace.renameVariableById('id1', newName); - workspaceTest_checkVariableValues(newName, 'type1', 'id2'); - var variable = workspace.getVariable(oldName) + checkVariableValues(workspace, newName, 'type1', 'id2'); + var variable = workspace.getVariable(oldName); var block_var_name_1 = workspace.topBlocks_[0].getVars()[0]; var block_var_name_2 = workspace.topBlocks_[1].getVars()[0]; assertNull(variable); assertEquals(newName, block_var_name_1); assertEquals(newName, block_var_name_2); - workspaceTest_tearDownWithMockBlocks(); + workspaceTest_tearDown(); } function test_deleteVariable_Trivial() { - workspaceTest_setUpWithMockBlocks() + workspaceTest_setUp(); workspace.createVariable('name1', 'type1', 'id1'); workspace.createVariable('name2', 'type1', 'id2'); createMockBlock('name1'); createMockBlock('name2'); workspace.deleteVariable('name1'); - workspaceTest_checkVariableValues('name2', 'type1', 'id2'); + checkVariableValues(workspace, 'name2', 'type1', 'id2'); var variable = workspace.getVariable('name1'); var block_var_name = workspace.topBlocks_[0].getVars()[0]; assertNull(variable); assertEquals('name2', block_var_name); - workspaceTest_tearDownWithMockBlocks(); + workspaceTest_tearDown(); } function test_deleteVariableById_Trivial() { - workspaceTest_setUpWithMockBlocks() + workspaceTest_setUp(); workspace.createVariable('name1', 'type1', 'id1'); workspace.createVariable('name2', 'type1', 'id2'); createMockBlock('name1'); createMockBlock('name2'); workspace.deleteVariableById('id1'); - workspaceTest_checkVariableValues('name2', 'type1', 'id2'); + checkVariableValues(workspace, 'name2', 'type1', 'id2'); var variable = workspace.getVariable('name1'); var block_var_name = workspace.topBlocks_[0].getVars()[0]; assertNull(variable); assertEquals('name2', block_var_name); - workspaceTest_tearDownWithMockBlocks(); + workspaceTest_tearDown(); } diff --git a/tests/jsunit/workspace_undo_redo_test.js b/tests/jsunit/workspace_undo_redo_test.js new file mode 100644 index 00000000..33dc855d --- /dev/null +++ b/tests/jsunit/workspace_undo_redo_test.js @@ -0,0 +1,417 @@ +/** + * @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 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_tearDown() { + mockControl_.$tearDown(); + workspace.dispose(); + Blockly.Events.fire = savedFireFunc; +} + +/** + * 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 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(); + checkVariableValues(workspace, '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 + checkVariableValues(workspace, 'name1', 'type1', 'id1'); + checkVariableValues(workspace, 'name2', 'type2', 'id2'); + + workspace.undo(); + workspace.undo(); + workspace.undo(true); + + // Expect that variable 'id1' is recreated + checkVariableValues(workspace, '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')); + checkVariableValues(workspace, 'name2', 'type2', 'id2'); + + workspace.undo(); + checkVariableValues(workspace, 'name1', 'type1', 'id1'); + checkVariableValues(workspace, 'name2', 'type2', 'id2'); + undoRedoTest_tearDown(); +} + +function test_undoDeleteVariable_WithBlocks() { + undoRedoTest_setUp(); + 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')); + checkVariableValues(workspace, 'name2', 'type2', 'id2'); + + workspace.undo(); + undoRedoTest_checkBlockVariableName(0, 'name2'); + undoRedoTest_checkBlockVariableName(1, 'name1'); + checkVariableValues(workspace, 'name1', 'type1', 'id1'); + checkVariableValues(workspace, 'name2', 'type2', 'id2'); + undoRedoTest_tearDown(); +} + +function test_redoAndUndoDeleteVariable_NoBlocks() { + 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')); + checkVariableValues(workspace, 'name2', 'type2', 'id2'); + undoRedoTest_tearDown(); +} + +function test_redoAndUndoDeleteVariable_WithBlocks() { + undoRedoTest_setUp(); + 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')); + checkVariableValues(workspace, 'name2', 'type2', 'id2'); + undoRedoTest_tearDown(); +} + +function test_redoAndUndoDeleteVariableTwice_NoBlocks() { + undoRedoTest_setUp(); + workspace.createVariable('name1', 'type1', 'id1'); + workspace.deleteVariableById('id1'); + workspace.deleteVariableById('id1'); + + // Check the undoStack only recorded one delete event. + var undoStack = workspace.undoStack_; + assertEquals('var_delete', undoStack[undoStack.length-1].type); + assertNotEquals('var_delete', undoStack[undoStack.length-2].type); + + // undo delete + workspace.undo(); + checkVariableValues(workspace, 'name1', 'type1', 'id1'); + + // redo delete + workspace.undo(true); + assertNull(workspace.getVariableById('id1')); + + // redo delete, nothing should happen + workspace.undo(true); + assertNull(workspace.getVariableById('id1')); + undoRedoTest_tearDown(); +} + +function test_redoAndUndoDeleteVariableTwice_WithBlocks() { + undoRedoTest_setUp(); + workspace.createVariable('name1', 'type1', 'id1'); + createMockBlock('name1'); + workspace.deleteVariableById('id1'); + workspace.deleteVariableById('id1'); + + // Check the undoStack only recorded one delete event. + var undoStack = workspace.undoStack_; + assertEquals('var_delete', undoStack[undoStack.length-1].type); + assertEquals('delete', undoStack[undoStack.length-2].type); + assertNotEquals('var_delete', undoStack[undoStack.length-3].type); + + // undo delete + workspace.undo(); + undoRedoTest_checkBlockVariableName(0, 'name1'); + checkVariableValues(workspace, 'name1', 'type1', 'id1'); + + // redo delete + workspace.undo(true); + assertEquals(0, workspace.topBlocks_.length); + assertNull(workspace.getVariableById('id1')); + + // redo delete, nothing should happen + workspace.undo(true); + assertEquals(0, workspace.topBlocks_.length); + assertNull(workspace.getVariableById('id1')); + undoRedoTest_tearDown(); +} + +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); + checkVariableValues(workspace, 'name2', '', 'id2'); + undoRedoTest_tearDown(); +} + +function test_undoRedoRenameVariable_OneExists_NoBlocks() { + undoRedoTest_setUp(); + workspace.createVariable('name1', '', 'id1'); + workspace.renameVariable('name1', 'name2'); + + workspace.undo(); + checkVariableValues(workspace, 'name1', '', 'id1'); + assertNull(workspace.getVariable('name2')); + + workspace.undo(true); + checkVariableValues(workspace, 'name2', '', 'id1'); + undoRedoTest_tearDown(); +} + +function test_undoRedoRenameVariable_OneExists_WithBlocks() { + undoRedoTest_setUp(); + workspace.createVariable('name1', '', 'id1'); + createMockBlock('name1'); + workspace.renameVariable('name1', 'name2'); + + workspace.undo(); + undoRedoTest_checkBlockVariableName(0, 'name1'); + checkVariableValues(workspace, 'name1', '', 'id1'); + assertNull(workspace.getVariable('name2')); + + workspace.undo(true); + checkVariableValues(workspace, 'name2', '', 'id1'); + undoRedoTest_checkBlockVariableName(0, 'name2'); + undoRedoTest_tearDown(); +} + +function test_undoRedoRenameVariable_BothExist_NoBlocks() { + undoRedoTest_setUp(); + createTwoVarsEmptyType(); + workspace.renameVariable('name1', 'name2'); + + workspace.undo(); + checkVariableValues(workspace, 'name1', '', 'id1'); + checkVariableValues(workspace, 'name2', '', 'id2'); + + workspace.undo(true); + checkVariableValues(workspace, 'name2', '', 'id2'); + assertNull(workspace.getVariable('name1')); + undoRedoTest_tearDown(); +} + +function test_undoRedoRenameVariable_BothExist_WithBlocks() { + undoRedoTest_setUp(); + createTwoVarsEmptyType(); + createMockBlock('name1'); + createMockBlock('name2'); + workspace.renameVariable('name1', 'name2'); + + workspace.undo(); + undoRedoTest_checkBlockVariableName(0, 'name1'); + undoRedoTest_checkBlockVariableName(1, 'name2'); + checkVariableValues(workspace, 'name1', '', 'id1'); + checkVariableValues(workspace, 'name2', '', 'id2'); + + workspace.undo(true); + undoRedoTest_checkBlockVariableName(0, 'name2'); + undoRedoTest_checkBlockVariableName(1, 'name2'); + undoRedoTest_tearDown(); +} + +function test_undoRedoRenameVariable_BothExistCaseChange_NoBlocks() { + undoRedoTest_setUp(); + createTwoVarsEmptyType(); + workspace.renameVariable('name1', 'Name2'); + + workspace.undo(); + checkVariableValues(workspace, 'name1', '', 'id1'); + checkVariableValues(workspace, 'name2', '', 'id2'); + + workspace.undo(true); + checkVariableValues(workspace, 'Name2', '', 'id2'); + assertNull(workspace.getVariable('name1')); + undoRedoTest_tearDown(); +} + +function test_undoRedoRenameVariable_BothExistCaseChange_WithBlocks() { + undoRedoTest_setUp(); + createTwoVarsEmptyType(); + createMockBlock('name1'); + createMockBlock('name2'); + workspace.renameVariable('name1', 'Name2'); + + workspace.undo(); + undoRedoTest_checkBlockVariableName(0, 'name1'); + undoRedoTest_checkBlockVariableName(1, 'name2'); + checkVariableValues(workspace, 'name1', '', 'id1'); + checkVariableValues(workspace, 'name2', '', 'id2'); + + workspace.undo(true); + checkVariableValues(workspace, 'Name2', '', 'id2'); + assertNull(workspace.getVariable('name1')); + undoRedoTest_checkBlockVariableName(0, 'Name2'); + undoRedoTest_checkBlockVariableName(1, 'Name2'); + undoRedoTest_tearDown(); +} + +function test_undoRedoRenameVariable_OnlyCaseChange_NoBlocks() { + undoRedoTest_setUp(); + workspace.createVariable('name1', '', 'id1'); + workspace.renameVariable('name1', 'Name1'); + + workspace.undo(); + checkVariableValues(workspace, 'name1', '', 'id1'); + + workspace.undo(true); + checkVariableValues(workspace, 'Name1', '', 'id1'); + undoRedoTest_tearDown(); +} + +function test_undoRedoRenameVariable_OnlyCaseChange_WithBlocks() { + undoRedoTest_setUp(); + workspace.createVariable('name1', '', 'id1'); + createMockBlock('name1'); + workspace.renameVariable('name1', 'Name1'); + + workspace.undo(); + undoRedoTest_checkBlockVariableName(0, 'name1'); + checkVariableValues(workspace, 'name1', '', 'id1'); + + workspace.undo(true); + checkVariableValues(workspace, 'Name1', '', 'id1'); + undoRedoTest_checkBlockVariableName(0, 'Name1'); + undoRedoTest_tearDown(); +} diff --git a/tests/jsunit/xml_test.js b/tests/jsunit/xml_test.js index 835ffbd4..7d0401e9 100644 --- a/tests/jsunit/xml_test.js +++ b/tests/jsunit/xml_test.js @@ -23,7 +23,6 @@ goog.require('goog.testing'); goog.require('goog.testing.MockControl'); var mockControl_; -var saved_msg = Blockly.Msg.DELETE_VARIABLE; var workspace; var XML_TEXT = ['', ' ', @@ -70,11 +69,6 @@ function xmlTest_setUpWithMockBlocks() { } ], }]); - // 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 xmlTest_tearDown() { @@ -85,7 +79,6 @@ function xmlTest_tearDown() { function xmlTest_tearDownWithMockBlocks() { xmlTest_tearDown(); delete Blockly.Blocks.field_variable_test_block; - Blockly.Msg.DELETE_VARIABLE = saved_msg; } /** @@ -129,20 +122,6 @@ function xmlTest_checkVariableDomValues(variableDom, type, id, text) { assertEquals(text, variableDom.textContent); } -/** - * 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 xmlTest_checkVariableValues(name, type, id) { - var variable = workspace.getVariable(name); - assertNotUndefined(variable); - assertEquals(name, variable.name); - assertEquals(type, variable.type); - assertEquals(id, variable.getId()); -} - function test_textToDom() { var dom = Blockly.Xml.textToDom(XML_TEXT); assertEquals('XML tag', 'xml', dom.nodeName); @@ -159,10 +138,7 @@ function test_domToText() { function test_domToWorkspace_BackwardCompatibility() { // Expect that workspace still loads without serialized variables. xmlTest_setUpWithMockBlocks(); - var mockGenUid = mockControl_.createMethodMock(Blockly.utils, 'genUid'); - mockGenUid().$returns('1'); - mockGenUid().$returns('1'); - mockGenUid().$replay(); + setUpMockMethod(mockControl_, Blockly.utils, 'genUid', null, ['1', '1']); try { var dom = Blockly.Xml.textToDom( '' + @@ -172,7 +148,7 @@ function test_domToWorkspace_BackwardCompatibility() { ''); Blockly.Xml.domToWorkspace(dom, workspace); assertEquals('Block count', 1, workspace.getAllBlocks().length); - xmlTest_checkVariableValues('name1', '', '1'); + checkVariableValues(workspace, 'name1', '', '1'); } finally { xmlTest_tearDownWithMockBlocks(); } @@ -195,9 +171,9 @@ function test_domToWorkspace_VariablesAtTop() { ''); Blockly.Xml.domToWorkspace(dom, workspace); assertEquals('Block count', 1, workspace.getAllBlocks().length); - xmlTest_checkVariableValues('name1', 'type1', 'id1'); - xmlTest_checkVariableValues('name2', 'type2', 'id2'); - xmlTest_checkVariableValues('name3', '', 'id3'); + checkVariableValues(workspace, 'name1', 'type1', 'id1'); + checkVariableValues(workspace, 'name2', 'type2', 'id2'); + checkVariableValues(workspace, 'name3', '', 'id3'); } finally { xmlTest_tearDownWithMockBlocks(); } @@ -309,27 +285,26 @@ function test_appendDomToWorkspace() { } function test_blockToDom_fieldToDom_trivial() { - xmlTest_setUpWithMockBlocks() + xmlTest_setUpWithMockBlocks(); workspace.createVariable('name1', 'type1', 'id1'); var block = new Blockly.Block(workspace, 'field_variable_test_block'); block.inputList[0].fieldRow[0].setValue('name1'); var resultFieldDom = Blockly.Xml.blockToDom(block).childNodes[0]; - xmlTest_checkVariableFieldDomValues(resultFieldDom, 'VAR', 'type1', 'id1', 'name1') - xmlTest_tearDownWithMockBlocks() + xmlTest_checkVariableFieldDomValues(resultFieldDom, 'VAR', 'type1', 'id1', + 'name1'); + xmlTest_tearDownWithMockBlocks(); } function test_blockToDom_fieldToDom_defaultCase() { - xmlTest_setUpWithMockBlocks() - var mockGenUid = mockControl_.createMethodMock(Blockly.utils, 'genUid'); - mockGenUid().$returns('1'); - mockGenUid().$replay(); + xmlTest_setUpWithMockBlocks(); + setUpMockMethod(mockControl_, Blockly.utils, 'genUid', null, ['1', '1']); workspace.createVariable('name1'); var block = new Blockly.Block(workspace, 'field_variable_test_block'); block.inputList[0].fieldRow[0].setValue('name1'); var resultFieldDom = Blockly.Xml.blockToDom(block).childNodes[0]; // Expect type is '' and id is '1' since we don't specify type and id. - xmlTest_checkVariableFieldDomValues(resultFieldDom, 'VAR', '', '1', 'name1') - xmlTest_tearDownWithMockBlocks() + xmlTest_checkVariableFieldDomValues(resultFieldDom, 'VAR', '', '1', 'name1'); + xmlTest_tearDownWithMockBlocks(); } function test_blockToDom_fieldToDom_notAFieldVariable() { @@ -344,19 +319,17 @@ function test_blockToDom_fieldToDom_notAFieldVariable() { } ], }]); - xmlTest_setUpWithMockBlocks() + xmlTest_setUpWithMockBlocks(); var block = new Blockly.Block(workspace, 'field_angle_test_block'); var resultFieldDom = Blockly.Xml.blockToDom(block).childNodes[0]; xmlTest_checkNonVariableField(resultFieldDom, 'VAR', '90'); delete Blockly.Blocks.field_angle_block; - xmlTest_tearDownWithMockBlocks() + xmlTest_tearDownWithMockBlocks(); } function test_variablesToDom_oneVariable() { xmlTest_setUp(); - var mockGenUid = mockControl_.createMethodMock(Blockly.utils, 'genUid'); - mockGenUid().$returns('1'); - mockGenUid().$replay(); + setUpMockMethod(mockControl_, Blockly.utils, 'genUid', null, ['1']); workspace.createVariable('name1'); var resultDom = Blockly.Xml.variablesToDom(workspace.getAllVariables()); @@ -392,34 +365,3 @@ function test_variablesToDom_noVariables() { assertEquals(1, resultDom.children.length); xmlTest_tearDown(); } - -/** - * Tests the that appendDomToWorkspace works in a headless mode. - * Also see test_appendDomToWorkspace() in workspace_svg_test.js. - */ -function test_appendDomToWorkspace() { - Blockly.Blocks.test_block = { - init: function() { - this.jsonInit({ - message0: 'test', - }); - } - }; - - try { - var dom = Blockly.Xml.textToDom( - '' + - ' ' + - ' ' + - ''); - var workspace = new Blockly.Workspace(); - Blockly.Xml.appendDomToWorkspace(dom, workspace); - assertEquals('Block count', 1, workspace.getAllBlocks().length); - var newBlockIds = Blockly.Xml.appendDomToWorkspace(dom, workspace); - assertEquals('Block count', 2, workspace.getAllBlocks().length); - assertEquals('Number of new block ids',1,newBlockIds.length); - } finally { - delete Blockly.Blocks.test_block; - workspace.dispose(); - } -}