From c732484180d658702fe226834257377bb19c5618 Mon Sep 17 00:00:00 2001 From: Beka Westberg Date: Tue, 8 Sep 2020 08:50:01 -0700 Subject: [PATCH] Add programmatically setting shadows Take 2 (#4215) * Add programmatically setting shadows --- core/block_events.js | 12 + core/connection.js | 30 +- core/input.js | 24 + core/rendered_connection.js | 24 +- core/xml.js | 18 +- tests/mocha/connection_test.js | 725 ++++++++++++++++++++ tests/mocha/event_test.js | 142 ++++ tests/mocha/workspace_test.js | 1127 +++++++++++++++++++++++--------- 8 files changed, 1772 insertions(+), 330 deletions(-) diff --git a/core/block_events.js b/core/block_events.js index 06bd0fc5f..03e80b26b 100644 --- a/core/block_events.js +++ b/core/block_events.js @@ -218,6 +218,10 @@ Blockly.Events.Create = function(opt_block) { if (!opt_block) { return; // Blank event to be populated by fromJson. } + if (opt_block.isShadow()) { + // Moving shadow blocks is handled via disconnection. + this.recordUndo = false; + } if (opt_block.workspace.rendered) { this.xml = Blockly.Xml.blockToDomWithXY(opt_block); @@ -302,6 +306,10 @@ Blockly.Events.Delete = function(opt_block) { if (opt_block.getParent()) { throw Error('Connected blocks cannot be deleted.'); } + if (opt_block.isShadow()) { + // Respawning shadow blocks is handled via disconnection. + this.recordUndo = false; + } if (opt_block.workspace.rendered) { this.oldXml = Blockly.Xml.blockToDomWithXY(opt_block); @@ -380,6 +388,10 @@ Blockly.Events.Move = function(opt_block) { if (!opt_block) { return; // Blank event to be populated by fromJson. } + if (opt_block.isShadow()) { + // Moving shadow blocks is handled via disconnection. + this.recordUndo = false; + } var location = this.currentLocation_(); this.oldParentId = location.parentId; diff --git a/core/connection.js b/core/connection.js index 8974ed292..556fb69f9 100644 --- a/core/connection.js +++ b/core/connection.js @@ -111,7 +111,7 @@ Blockly.Connection.prototype.connect_ = function(childConnection) { var orphanBlock = parentConnection.targetBlock(); var shadowDom = parentConnection.getShadowDom(); // Temporarily set the shadow DOM to null so it does not respawn. - parentConnection.setShadowDom(null); + parentConnection.shadowDom_ = null; // Displaced shadow blocks dissolve rather than reattaching or bumping. if (orphanBlock.isShadow()) { // Save the shadow block so that field values are preserved. @@ -179,7 +179,7 @@ Blockly.Connection.prototype.connect_ = function(childConnection) { } } // Restore the shadow DOM. - parentConnection.setShadowDom(shadowDom); + parentConnection.shadowDom_ = shadowDom; } var event; @@ -204,12 +204,11 @@ Blockly.Connection.prototype.dispose = function() { // isConnected returns true for shadows and non-shadows. if (this.isConnected()) { + // Destroy the attached shadow block & its children (if it exists). this.setShadowDom(null); + var targetBlock = this.targetBlock(); - if (targetBlock.isShadow()) { - // Destroy the attached shadow block & its children. - targetBlock.dispose(false); - } else { + if (targetBlock) { // Disconnect the attached normal block. targetBlock.unplug(); } @@ -444,7 +443,10 @@ Blockly.Connection.prototype.disconnect = function() { Blockly.Events.setGroup(true); } this.disconnectInternal_(parentBlock, childBlock); - parentConnection.respawnShadow_(); + if (!childBlock.isShadow()) { + // If we were disconnecting a shadow, no need to spawn a new one. + parentConnection.respawnShadow_(); + } if (!eventGroup) { Blockly.Events.setGroup(false); } @@ -479,7 +481,7 @@ Blockly.Connection.prototype.disconnectInternal_ = function(parentBlock, Blockly.Connection.prototype.respawnShadow_ = function() { var parentBlock = this.getSourceBlock(); var shadow = this.getShadowDom(); - if (parentBlock.workspace && shadow && Blockly.Events.recordUndo) { + if (parentBlock.workspace && shadow) { var blockShadow = Blockly.Xml.domToBlock(shadow, parentBlock.workspace); if (blockShadow.outputConnection) { @@ -586,15 +588,23 @@ Blockly.Connection.prototype.getCheck = function() { }; /** - * Change a connection's shadow block. + * Changes the connection's shadow block. * @param {Element} shadow DOM representation of a block or null. */ Blockly.Connection.prototype.setShadowDom = function(shadow) { this.shadowDom_ = shadow; + var target = this.targetBlock(); + if (!target) { + this.respawnShadow_(); + } else if (target.isShadow()) { + // The disconnect from dispose will automatically generate the new shadow. + target.dispose(false); + this.respawnShadow_(); + } }; /** - * Return a connection's shadow block. + * Returns the xml representation of the connection's shadow block. * @return {Element} Shadow DOM representation of a block or null. */ Blockly.Connection.prototype.getShadowDom = function() { diff --git a/core/input.js b/core/input.js index 2b31c7315..40a454e31 100644 --- a/core/input.js +++ b/core/input.js @@ -245,6 +245,30 @@ Blockly.Input.prototype.setAlign = function(align) { return this; }; +/** + * Changes the connection's shadow block. + * @param {Element} shadow DOM representation of a block or null. + * @return {Blockly.Input} The input being modified (to allow chaining). + */ +Blockly.Input.prototype.setShadowDom = function(shadow) { + if (!this.connection) { + throw Error('This input does not have a connection.'); + } + this.connection.setShadowDom(shadow); + return this; +}; + +/** + * Returns the xml representation of the connection's shadow block. + * @return {Element} Shadow DOM representation of a block or null. + */ +Blockly.Input.prototype.getShadowDom = function() { + if (!this.connection) { + throw Error('This input does not have a connection.'); + } + return this.connection.getShadowDom(); +}; + /** * Initialize the fields on this input. */ diff --git a/core/rendered_connection.js b/core/rendered_connection.js index cbf9994d4..cfb06580f 100644 --- a/core/rendered_connection.js +++ b/core/rendered_connection.js @@ -478,20 +478,18 @@ Blockly.RenderedConnection.prototype.disconnectInternal_ = function(parentBlock, * @private */ Blockly.RenderedConnection.prototype.respawnShadow_ = function() { + Blockly.RenderedConnection.superClass_.respawnShadow_.call(this); + var blockShadow = this.targetBlock(); + if (!blockShadow) { + // This connection must not have a shadowDom_. + return; + } + blockShadow.initSvg(); + blockShadow.render(false); + var parentBlock = this.getSourceBlock(); - // Respawn the shadow block if there is one. - var shadow = this.getShadowDom(); - if (parentBlock.workspace && shadow && Blockly.Events.recordUndo) { - Blockly.RenderedConnection.superClass_.respawnShadow_.call(this); - var blockShadow = this.targetBlock(); - if (!blockShadow) { - throw Error('Couldn\'t respawn the shadow block that should exist here.'); - } - blockShadow.initSvg(); - blockShadow.render(false); - if (parentBlock.rendered) { - parentBlock.render(); - } + if (parentBlock.rendered) { + parentBlock.render(); } }; diff --git a/core/xml.js b/core/xml.js index 899b3c64a..ae245b864 100644 --- a/core/xml.js +++ b/core/xml.js @@ -650,10 +650,6 @@ Blockly.Xml.domToBlockHeadless_ = function(xmlBlock, workspace) { } } } - // Use the shadow block if there is no child block. - if (!childBlockElement && childShadowElement) { - childBlockElement = childShadowElement; - } var name = xmlChild.getAttribute('name'); var xmlChildElement = /** @type {!Element} */ (xmlChild); @@ -708,9 +704,6 @@ Blockly.Xml.domToBlockHeadless_ = function(xmlBlock, workspace) { prototypeName); break; } - if (childShadowElement) { - input.connection.setShadowDom(childShadowElement); - } if (childBlockElement) { blockChild = Blockly.Xml.domToBlockHeadless_(childBlockElement, workspace); @@ -723,11 +716,12 @@ Blockly.Xml.domToBlockHeadless_ = function(xmlBlock, workspace) { 'Child block does not have output or previous statement.'); } } + // Set shadow after so we don't create a shadow we delete immediately. + if (childShadowElement) { + input.connection.setShadowDom(childShadowElement); + } break; case 'next': - if (childShadowElement && block.nextConnection) { - block.nextConnection.setShadowDom(childShadowElement); - } if (childBlockElement) { if (!block.nextConnection) { throw TypeError('Next statement does not exist.'); @@ -743,6 +737,10 @@ Blockly.Xml.domToBlockHeadless_ = function(xmlBlock, workspace) { } block.nextConnection.connect(blockChild.previousConnection); } + // Set shadow after so we don't create a shadow we delete immediately. + if (childShadowElement && block.nextConnection) { + block.nextConnection.setShadowDom(childShadowElement); + } break; default: // Unknown tag; ignore. Same principle as HTML parsers. diff --git a/tests/mocha/connection_test.js b/tests/mocha/connection_test.js index 48bee2513..73882b90b 100644 --- a/tests/mocha/connection_test.js +++ b/tests/mocha/connection_test.js @@ -18,9 +18,11 @@ suite('Connection', function() { return connection; }; }); + teardown(function() { sharedTestTeardown.call(this); }); + test('Deprecated - canConnectWithReason passes', function() { var deprecateWarnSpy = createDeprecationWarningStub(); var conn1 = this.createConnection(Blockly.PREVIOUS_STATEMENT); @@ -30,6 +32,7 @@ suite('Connection', function() { assertSingleDeprecationWarningCall(deprecateWarnSpy, 'Connection.prototype.canConnectWithReason'); }); + test('Deprecated - canConnectWithReason fails', function() { var deprecateWarnSpy = createDeprecationWarningStub(); var conn1 = this.createConnection(Blockly.PREVIOUS_STATEMENT); @@ -39,6 +42,7 @@ suite('Connection', function() { assertSingleDeprecationWarningCall(deprecateWarnSpy, 'Connection.prototype.canConnectWithReason'); }); + test('Deprecated - checkConnection passes', function() { var deprecateWarnSpy = createDeprecationWarningStub(); var conn1 = this.createConnection(Blockly.PREVIOUS_STATEMENT); @@ -49,6 +53,7 @@ suite('Connection', function() { assertSingleDeprecationWarningCall(deprecateWarnSpy, 'Connection.prototype.checkConnection'); }); + test('Deprecated - checkConnection fails', function() { var deprecateWarnSpy = createDeprecationWarningStub(); var conn1 = this.createConnection(Blockly.PREVIOUS_STATEMENT); @@ -59,4 +64,724 @@ suite('Connection', function() { assertSingleDeprecationWarningCall(deprecateWarnSpy, 'Connection.prototype.checkConnection'); }); + + suite('Set Shadow Dom', function() { + + function assertBlockMatches(block, isShadow, opt_id) { + chai.assert.equal(block.isShadow(), isShadow, + `expected block ${block.id} to ${isShadow ? '' : 'not'} be a shadow`); + if (opt_id) { + chai.assert.equal(block.id, opt_id); + } + } + + function assertInputHasBlock(parent, inputName, isShadow, opt_name) { + var block = parent.getInputTargetBlock(inputName); + chai.assert.exists(block, + `expected block ${opt_name || ''} to be attached to ${inputName}`); + assertBlockMatches(block, isShadow, opt_name); + } + + function assertNextHasBlock(parent, isShadow, opt_name) { + var block = parent.getNextBlock(); + chai.assert.exists(block, + `expected block ${opt_name || ''} to be attached to next connection`); + assertBlockMatches(block, isShadow, opt_name); + } + + function assertInputNotHasBlock(parent, inputName) { + var block = parent.getInputTargetBlock(inputName); + chai.assert.notExists(block, + `expected block ${block && block.id} to not be attached to ${inputName}`); + } + + function assertNextNotHasBlock(parent) { + var block = parent.getNextBlock(); + chai.assert.notExists(block, + `expected block ${block && block.id} to not be attached to next connection`); + } + + var testSuites = [ + { + title: 'Rendered', + createWorkspace: () => { + return Blockly.inject('blocklyDiv'); + }, + }, + { + title: 'Headless', + createWorkspace: () => { + return new Blockly.Workspace(); + }, + } + ]; + + testSuites.forEach((testSuite) => { + // Create a suite for each suite. + suite(testSuite.title, function() { + setup(function() { + this.workspace = testSuite.createWorkspace(); + + Blockly.defineBlocksWithJsonArray([ + { + "type": "stack_block", + "message0": "", + "previousStatement": null, + "nextStatement": null + }, + { + "type": "row_block", + "message0": "%1", + "args0": [ + { + "type": "input_value", + "name": "INPUT" + } + ], + "output": null + }, + { + "type": "statement_block", + "message0": "%1", + "args0": [ + { + "type": "input_statement", + "name": "STATEMENT" + } + ], + "previousStatement": null, + "nextStatement": null + }]); + }); + + teardown(function() { + workspaceTeardown.call(this, this.workspace); + delete Blockly.Blocks['stack_block']; + delete Blockly.Blocks['row_block']; + delete Blockly.Blocks['statement_block']; + }); + + suite('Add - No Block Connected', function() { + // These are defined separately in each suite. + function createRowBlock(workspace) { + var block = Blockly.Xml.domToBlock(Blockly.Xml.textToDom( + '' + ), workspace); + return block; + } + + function createStatementBlock(workspace) { + var block = Blockly.Xml.domToBlock(Blockly.Xml.textToDom( + '' + ), workspace); + return block; + } + + function createStackBlock(workspace) { + var block = Blockly.Xml.domToBlock(Blockly.Xml.textToDom( + '' + ), workspace); + return block; + } + + test('Value', function() { + var parent = createRowBlock(this.workspace); + var xml = Blockly.Xml.textToDom( + '' + ); + parent.getInput('INPUT').connection.setShadowDom(xml); + assertInputHasBlock(parent, 'INPUT', true); + }); + + test('Multiple Value', function() { + var parent = createRowBlock(this.workspace); + var xml = Blockly.Xml.textToDom( + '' + + ' ' + + ' ' + + ' ' + + '' + ); + parent.getInput('INPUT').connection.setShadowDom(xml); + assertInputHasBlock(parent, 'INPUT', true); + assertInputHasBlock( + parent.getInputTargetBlock('INPUT'), 'INPUT', true); + }); + + test('Statement', function() { + var parent = createStatementBlock(this.workspace); + var xml = Blockly.Xml.textToDom( + '' + ); + parent.getInput('STATEMENT').connection.setShadowDom(xml); + assertInputHasBlock(parent, 'STATEMENT', true); + }); + + test('Multiple Statement', function() { + var parent = createStatementBlock(this.workspace); + var xml = Blockly.Xml.textToDom( + '' + + ' ' + + ' ' + + ' ' + + '' + ); + parent.getInput('STATEMENT').connection.setShadowDom(xml); + assertInputHasBlock(parent, 'STATEMENT', true); + assertInputHasBlock( + parent.getInputTargetBlock('STATEMENT'), 'STATEMENT', true); + }); + + test('Next', function() { + var parent = createStackBlock(this.workspace); + var xml = Blockly.Xml.textToDom( + '' + ); + parent.nextConnection.setShadowDom(xml); + assertNextHasBlock(parent, true); + }); + + test('Multiple Next', function() { + var parent = createStackBlock(this.workspace); + var xml = Blockly.Xml.textToDom( + '' + + ' ' + + ' ' + + ' ' + + '' + ); + parent.nextConnection.setShadowDom(xml); + assertNextHasBlock(parent, true); + assertNextHasBlock(parent.getNextBlock(), true); + }); + }); + + suite('Add - With Block Connected', function() { + // These are defined separately in each suite. + function createRowBlocks(workspace) { + var block = Blockly.Xml.domToBlock(Blockly.Xml.textToDom( + '' + + ' ' + + ' ' + + ' ' + + '' + ), workspace); + return block; + } + + function createStatementBlocks(workspace) { + var block = Blockly.Xml.domToBlock(Blockly.Xml.textToDom( + '' + + ' ' + + ' ' + + ' ' + + '' + ), workspace); + return block; + } + + function createStackBlocks(workspace) { + var block = Blockly.Xml.domToBlock(Blockly.Xml.textToDom( + '' + + ' ' + + ' ' + + ' ' + + '' + ), workspace); + return block; + } + + test('Value', function() { + var parent = createRowBlocks(this.workspace); + var xml = Blockly.Xml.textToDom( + '' + ); + parent.getInput('INPUT').connection.setShadowDom(xml); + assertInputHasBlock(parent, 'INPUT', false); + parent.getInput('INPUT').connection.disconnect(); + assertInputHasBlock(parent, 'INPUT', true); + }); + + test('Multiple Value', function() { + var parent = createRowBlocks(this.workspace); + var xml = Blockly.Xml.textToDom( + '' + + ' ' + + ' ' + + ' ' + + '' + ); + parent.getInput('INPUT').connection.setShadowDom(xml); + assertInputHasBlock(parent, 'INPUT', false); + assertInputNotHasBlock(parent.getInputTargetBlock('INPUT'), 'INPUT'); + parent.getInput('INPUT').connection.disconnect(); + assertInputHasBlock(parent, 'INPUT', true); + assertInputHasBlock( + parent.getInputTargetBlock('INPUT'), 'INPUT', true); + }); + + test('Statement', function() { + var parent = createStatementBlocks(this.workspace); + var xml = Blockly.Xml.textToDom( + '' + ); + parent.getInput('STATEMENT').connection.setShadowDom(xml); + assertInputHasBlock(parent, 'STATEMENT', false); + parent.getInput('STATEMENT').connection.disconnect(); + assertInputHasBlock(parent, 'STATEMENT', true); + }); + + test('Multiple Statement', function() { + var parent = createStatementBlocks(this.workspace); + var xml = Blockly.Xml.textToDom( + '' + + ' ' + + ' ' + + ' ' + + '' + ); + parent.getInput('STATEMENT').connection.setShadowDom(xml); + assertInputHasBlock(parent, 'STATEMENT', false); + assertInputNotHasBlock( + parent.getInputTargetBlock('STATEMENT'), 'STATEMENT'); + parent.getInput('STATEMENT').connection.disconnect(); + assertInputHasBlock(parent, 'STATEMENT', true); + assertInputHasBlock( + parent.getInputTargetBlock('STATEMENT'), 'STATEMENT', true); + }); + + test('Next', function() { + var parent = createStackBlocks(this.workspace); + var xml = Blockly.Xml.textToDom( + '' + ); + parent.nextConnection.setShadowDom(xml); + assertNextHasBlock(parent, false); + parent.nextConnection.disconnect(); + assertNextHasBlock(parent, true); + }); + + test('Multiple Next', function() { + var parent = createStackBlocks(this.workspace); + var xml = Blockly.Xml.textToDom( + '' + + ' ' + + ' ' + + ' ' + + '' + ); + parent.nextConnection.setShadowDom(xml); + assertNextHasBlock(parent, false); + assertNextNotHasBlock(parent.getNextBlock()); + parent.nextConnection.disconnect(); + assertNextHasBlock(parent, true); + assertNextHasBlock(parent.getNextBlock(), true); + }); + }); + + suite('Add - With Shadow Connected', function() { + // These are defined separately in each suite. + function createRowBlock(workspace) { + var block = Blockly.Xml.domToBlock(Blockly.Xml.textToDom( + '' + ), workspace); + return block; + } + + function createStatementBlock(workspace) { + var block = Blockly.Xml.domToBlock(Blockly.Xml.textToDom( + '' + ), workspace); + return block; + } + + function createStackBlock(workspace) { + var block = Blockly.Xml.domToBlock(Blockly.Xml.textToDom( + '' + ), workspace); + return block; + } + + test('Value', function() { + var parent = createRowBlock(this.workspace); + var xml = Blockly.Xml.textToDom( + '' + ); + parent.getInput('INPUT').connection.setShadowDom(xml); + assertInputHasBlock(parent, 'INPUT', true, '1'); + var xml = Blockly.Xml.textToDom( + '' + ); + parent.getInput('INPUT').connection.setShadowDom(xml); + assertInputHasBlock(parent, 'INPUT', true, '2'); + }); + + test('Multiple Value', function() { + var parent = createRowBlock(this.workspace); + var xml = Blockly.Xml.textToDom( + '' + + ' ' + + ' ' + + ' ' + + '' + ); + parent.getInput('INPUT').connection.setShadowDom(xml); + assertInputHasBlock(parent, 'INPUT', true, '1'); + assertInputHasBlock( + parent.getInputTargetBlock('INPUT'), 'INPUT', true, 'a'); + var xml = Blockly.Xml.textToDom( + '' + + ' ' + + ' ' + + ' ' + + '' + ); + parent.getInput('INPUT').connection.setShadowDom(xml); + assertInputHasBlock(parent, 'INPUT', true, '2'); + assertInputHasBlock( + parent.getInputTargetBlock('INPUT'), 'INPUT', true, 'b'); + }); + + test('Statement', function() { + var parent = createStatementBlock(this.workspace); + var xml = Blockly.Xml.textToDom( + '' + ); + parent.getInput('STATEMENT').connection.setShadowDom(xml); + assertInputHasBlock(parent, 'STATEMENT', true, '1'); + var xml = Blockly.Xml.textToDom( + '' + ); + parent.getInput('STATEMENT').connection.setShadowDom(xml); + assertInputHasBlock(parent, 'STATEMENT', true, '2'); + }); + + test('Multiple Statement', function() { + var parent = createStatementBlock(this.workspace); + var xml = Blockly.Xml.textToDom( + '' + + ' ' + + ' ' + + ' ' + + '' + ); + parent.getInput('STATEMENT').connection.setShadowDom(xml); + assertInputHasBlock(parent, 'STATEMENT', true, '1'); + assertInputHasBlock( + parent.getInputTargetBlock('STATEMENT'), 'STATEMENT', true, 'a'); + var xml = Blockly.Xml.textToDom( + '' + + ' ' + + ' ' + + ' ' + + '' + ); + parent.getInput('STATEMENT').connection.setShadowDom(xml); + assertInputHasBlock(parent, 'STATEMENT', true, '2'); + assertInputHasBlock( + parent.getInputTargetBlock('STATEMENT'), 'STATEMENT', true, 'b'); + }); + + test('Next', function() { + var parent = createStackBlock(this.workspace); + var xml = Blockly.Xml.textToDom( + '' + ); + parent.nextConnection.setShadowDom(xml); + assertNextHasBlock(parent, true, '1'); + var xml = Blockly.Xml.textToDom( + '' + ); + parent.nextConnection.setShadowDom(xml); + assertNextHasBlock(parent, true, '2'); + }); + + test('Multiple Next', function() { + var parent = createStackBlock(this.workspace); + var xml = Blockly.Xml.textToDom( + '' + + ' ' + + ' ' + + ' ' + + '' + ); + parent.nextConnection.setShadowDom(xml); + assertNextHasBlock(parent, true, '1'); + assertNextHasBlock(parent.getNextBlock(), true, 'a'); + var xml = Blockly.Xml.textToDom( + '' + + ' ' + + ' ' + + ' ' + + '' + ); + parent.nextConnection.setShadowDom(xml); + assertNextHasBlock(parent, true, '2'); + assertNextHasBlock(parent.getNextBlock(), true, 'b'); + }); + }); + + suite('Remove - No Block Connected', function() { + // These are defined separately in each suite. + function createRowBlock(workspace) { + var block = Blockly.Xml.domToBlock(Blockly.Xml.textToDom( + '' + + ' ' + + ' ' + + ' ' + + '' + ), workspace); + return block; + } + + function createStatementBlock(workspace) { + var block = Blockly.Xml.domToBlock(Blockly.Xml.textToDom( + '' + + ' ' + + ' ' + + ' ' + + '' + ), workspace); + return block; + } + + function createStackBlock(workspace) { + var block = Blockly.Xml.domToBlock(Blockly.Xml.textToDom( + '' + + ' ' + + ' ' + + ' ' + + '' + ), workspace); + return block; + } + + test('Value', function() { + var parent = createRowBlock(this.workspace); + parent.getInput('INPUT').connection.setShadowDom(null); + assertInputNotHasBlock(parent, 'INPUT'); + }); + + test('Statement', function() { + var parent = createStatementBlock(this.workspace); + parent.getInput('STATEMENT').connection.setShadowDom(null); + assertInputNotHasBlock(parent, 'STATMENT'); + }); + + test('Next', function() { + var parent = createStackBlock(this.workspace); + parent.nextConnection.setShadowDom(null); + assertNextNotHasBlock(parent); + }); + }); + + suite('Remove - Block Connected', function() { + // These are defined separately in each suite. + function createRowBlock(workspace) { + var block = Blockly.Xml.domToBlock(Blockly.Xml.textToDom( + '' + + ' ' + + ' ' + + ' ' + + ' ' + + '' + ), workspace); + return block; + } + + function createStatementBlock(workspace) { + var block = Blockly.Xml.domToBlock(Blockly.Xml.textToDom( + '' + + ' ' + + ' ' + + ' ' + + ' ' + + '' + ), workspace); + return block; + } + + function createStackBlock(workspace) { + var block = Blockly.Xml.domToBlock(Blockly.Xml.textToDom( + '' + + ' ' + + ' ' + + ' ' + + ' ' + + '' + ), workspace); + return block; + } + + test('Value', function() { + var parent = createRowBlock(this.workspace); + parent.getInput('INPUT').connection.setShadowDom(null); + assertInputHasBlock(parent, 'INPUT', false); + parent.getInput('INPUT').connection.disconnect(); + assertInputNotHasBlock(parent, 'INPUT'); + }); + + test('Statement', function() { + var parent = createStatementBlock(this.workspace); + parent.getInput('STATEMENT').connection.setShadowDom(null); + assertInputHasBlock(parent, 'STATEMENT', false); + parent.getInput('STATEMENT').connection.disconnect(); + assertInputNotHasBlock(parent, 'STATEMENT'); + }); + + test('Next', function() { + var parent = createStackBlock(this.workspace); + parent.nextConnection.setShadowDom(null); + assertNextHasBlock(parent, false); + parent.nextConnection.disconnect(); + assertNextNotHasBlock(parent); + }); + }); + + suite('Add - Connect & Disconnect - Remove', function() { + // These are defined separately in each suite. + function createRowBlock(workspace) { + var block = Blockly.Xml.domToBlock(Blockly.Xml.textToDom( + '' + ), workspace); + return block; + } + + function createStatementBlock(workspace) { + var block = Blockly.Xml.domToBlock(Blockly.Xml.textToDom( + '' + ), workspace); + return block; + } + + function createStackBlock(workspace) { + var block = Blockly.Xml.domToBlock(Blockly.Xml.textToDom( + '' + ), workspace); + return block; + } + + test('Value', function() { + var parent = createRowBlock(this.workspace); + var xml = Blockly.Xml.textToDom( + '' + ); + parent.getInput('INPUT').connection.setShadowDom(xml); + assertInputHasBlock(parent, 'INPUT', true); + var child = createRowBlock(this.workspace); + parent.getInput('INPUT').connection.connect(child.outputConnection); + assertInputHasBlock(parent, 'INPUT', false); + parent.getInput('INPUT').connection.disconnect(); + assertInputHasBlock(parent, 'INPUT', true); + parent.getInput('INPUT').connection.setShadowDom(null); + assertInputNotHasBlock(parent, 'INPUT'); + }); + + test('Multiple Value', function() { + var parent = createRowBlock(this.workspace); + var xml = Blockly.Xml.textToDom( + '' + + ' ' + + ' ' + + ' ' + + '' + ); + parent.getInput('INPUT').connection.setShadowDom(xml); + assertInputHasBlock(parent, 'INPUT', true); + assertInputHasBlock( + parent.getInputTargetBlock('INPUT'), 'INPUT', true); + var child = createRowBlock(this.workspace); + parent.getInput('INPUT').connection.connect(child.outputConnection); + assertInputHasBlock(parent, 'INPUT', false); + parent.getInput('INPUT').connection.disconnect(); + assertInputHasBlock(parent, 'INPUT', true); + assertInputHasBlock( + parent.getInputTargetBlock('INPUT'), 'INPUT', true); + parent.getInput('INPUT').connection.setShadowDom(null); + assertInputNotHasBlock(parent, 'INPUT'); + }); + + test('Statement', function() { + var parent = createStatementBlock(this.workspace); + var xml = Blockly.Xml.textToDom( + '' + ); + parent.getInput('STATEMENT').connection.setShadowDom(xml); + assertInputHasBlock(parent, 'STATEMENT', true); + var child = createStatementBlock(this.workspace); + parent.getInput('STATEMENT').connection + .connect(child.previousConnection); + assertInputHasBlock(parent, 'STATEMENT', false); + parent.getInput('STATEMENT').connection.disconnect(); + assertInputHasBlock(parent, 'STATEMENT', true); + parent.getInput('STATEMENT').connection.setShadowDom(null); + assertInputNotHasBlock(parent, 'STATEMENT'); + }); + + test('Multiple Statement', function() { + var parent = createStatementBlock(this.workspace); + var xml = Blockly.Xml.textToDom( + '' + + ' ' + + ' ' + + ' ' + + '' + ); + parent.getInput('STATEMENT').connection.setShadowDom(xml); + assertInputHasBlock(parent, 'STATEMENT', true); + assertInputHasBlock( + parent.getInputTargetBlock('STATEMENT'), 'STATEMENT', true); + var child = createStatementBlock(this.workspace); + parent.getInput('STATEMENT').connection + .connect(child.previousConnection); + assertInputHasBlock(parent, 'STATEMENT', false); + parent.getInput('STATEMENT').connection.disconnect(); + assertInputHasBlock(parent, 'STATEMENT', true); + assertInputHasBlock( + parent.getInputTargetBlock('STATEMENT'), 'STATEMENT', true); + parent.getInput('STATEMENT').connection.setShadowDom(null); + assertInputNotHasBlock(parent, 'STATEMENT'); + }); + + test('Next', function() { + var parent = createStackBlock(this.workspace); + var xml = Blockly.Xml.textToDom( + '' + ); + parent.nextConnection.setShadowDom(xml); + assertNextHasBlock(parent, true); + var child = createStatementBlock(this.workspace); + parent.nextConnection.connect(child.previousConnection); + assertNextHasBlock(parent, false); + parent.nextConnection.disconnect(); + assertNextHasBlock(parent, true); + parent.nextConnection.setShadowDom(null); + assertNextNotHasBlock(parent); + }); + + test('Multiple Next', function() { + var parent = createStackBlock(this.workspace); + var xml = Blockly.Xml.textToDom( + '' + + ' ' + + ' ' + + ' ' + + '' + ); + parent.nextConnection.setShadowDom(xml); + assertNextHasBlock(parent, true); + assertNextHasBlock(parent.getNextBlock(), true); + var child = createStatementBlock(this.workspace); + parent.nextConnection.connect(child.previousConnection); + assertNextHasBlock(parent, false); + parent.nextConnection.disconnect(); + assertNextHasBlock(parent, true); + assertNextHasBlock(parent.getNextBlock(), true); + parent.nextConnection.setShadowDom(null); + assertNextNotHasBlock(parent); + }); + }); + }); + }); + }); }); diff --git a/tests/mocha/event_test.js b/tests/mocha/event_test.js index f265ce93c..67828f5c9 100644 --- a/tests/mocha/event_test.js +++ b/tests/mocha/event_test.js @@ -221,6 +221,148 @@ suite('Events', function() { }); }); + suite('With shadow blocks', function() { + setup(function() { + this.TEST_BLOCK_ID = 'test_block_id'; + this.TEST_PARENT_ID = 'parent'; + // genUid is expected to be called either once or twice in this suite. + this.genUidStub = createGenUidStubWithReturns( + [this.TEST_BLOCK_ID, this.TEST_PARENT_ID]); + this.block = createSimpleTestBlock(this.workspace); + this.block.setShadow(true); + }); + + test('Block base', function() { + var event = new Blockly.Events.BlockBase(this.block); + sinon.assert.calledOnce(this.genUidStub); + assertEventEquals(event, undefined, + this.workspace.id, this.TEST_BLOCK_ID, + { + 'varId': undefined, + 'recordUndo': true, + 'group': '', + }); + }); + + test('Change', function() { + var event = new Blockly.Events.Change( + this.block, 'field', 'FIELD_NAME', 'old', 'new'); + sinon.assert.calledOnce(this.genUidStub); + assertEventEquals(event, Blockly.Events.CHANGE, + this.workspace.id, this.TEST_BLOCK_ID, + { + 'varId': undefined, + 'element': 'field', + 'name': 'FIELD_NAME', + 'oldValue': 'old', + 'newValue': 'new', + 'recordUndo': true, + 'group': '', + }); + }); + + test('Block change', function() { + var event = new Blockly.Events.BlockChange( + this.block, 'field', 'FIELD_NAME', 'old', 'new'); + sinon.assert.calledOnce(this.genUidStub); + assertEventEquals(event, Blockly.Events.CHANGE, + this.workspace.id, this.TEST_BLOCK_ID, + { + 'varId': undefined, + 'element': 'field', + 'name': 'FIELD_NAME', + 'oldValue': 'old', + 'newValue': 'new', + 'recordUndo': true, + 'group': '', + }); + }); + + test('Create', function() { + var event = new Blockly.Events.Create(this.block); + sinon.assert.calledOnce(this.genUidStub); + assertEventEquals(event, Blockly.Events.CREATE, + this.workspace.id, this.TEST_BLOCK_ID, + { + 'recordUndo': false, + 'group': '', + }); + }); + + test('Block create', function() { + var event = new Blockly.Events.BlockCreate(this.block); + sinon.assert.calledOnce(this.genUidStub); + assertEventEquals(event, Blockly.Events.CREATE, + this.workspace.id, this.TEST_BLOCK_ID, + { + 'recordUndo': false, + 'group': '', + }); + }); + + test('Delete', function() { + var event = new Blockly.Events.Delete(this.block); + sinon.assert.calledOnce(this.genUidStub); + assertEventEquals(event, Blockly.Events.DELETE, + this.workspace.id, this.TEST_BLOCK_ID, + { + 'recordUndo': false, + 'group': '', + }); + }); + + test('Block delete', function() { + var event = new Blockly.Events.BlockDelete(this.block); + sinon.assert.calledOnce(this.genUidStub); + assertEventEquals(event, Blockly.Events.DELETE, + this.workspace.id, this.TEST_BLOCK_ID, + { + 'recordUndo': false, + 'group': '', + }); + }); + + suite('Move', function() { + setup(function() { + this.parentBlock = createSimpleTestBlock(this.workspace); + this.block.parentBlock_ = this.parentBlock; + this.block.xy_ = new Blockly.utils.Coordinate(3, 4); + }); + + teardown(function() { + // This needs to be cleared, otherwise workspace.dispose will fail. + this.block.parentBlock_ = null; + }); + + test('Move', function() { + var event = new Blockly.Events.Move(this.block); + sinon.assert.calledTwice(this.genUidStub); + assertEventEquals(event, Blockly.Events.MOVE, this.workspace.id, + this.TEST_BLOCK_ID, { + 'oldParentId': this.TEST_PARENT_ID, + 'oldInputName': undefined, + 'oldCoordinate': undefined, + 'recordUndo': false, + 'group': '' + }); + }); + + test('Block move', function() { + var event = new Blockly.Events.BlockMove(this.block); + sinon.assert.calledTwice(this.genUidStub); + assertEventEquals(event, Blockly.Events.MOVE, this.workspace.id, + this.TEST_BLOCK_ID, + { + 'oldParentId': this.TEST_PARENT_ID, + 'oldInputName': undefined, + 'oldCoordinate': undefined, + 'recordUndo': false, + 'group': '' + }); + }); + }); + }); + suite('With variable getter blocks', function() { setup(function() { this.genUidStub = createGenUidStubWithReturns( diff --git a/tests/mocha/workspace_test.js b/tests/mocha/workspace_test.js index c215dd8a4..401850868 100644 --- a/tests/mocha/workspace_test.js +++ b/tests/mocha/workspace_test.js @@ -640,369 +640,902 @@ function testAWorkspace() { }); suite('Undo/Redo', function() { - function createTwoVarsDifferentTypes(workspace) { - workspace.createVariable('name1', 'type1', 'id1'); - workspace.createVariable('name2', 'type2', 'id2'); + + /** + * Assert that two nodes are equal. + * @param {!Element} actual the actual node. + * @param {!Element} expected the expected node. + */ + function assertNodesEqual(actual, expected) { + var actualString = '\n' + Blockly.Xml.domToPrettyText(actual) + '\n'; + var expectedString = '\n' + Blockly.Xml.domToPrettyText(expected) + '\n'; + + chai.assert.equal(actual.tagName, expected.tagName); + for (var i = 0, attr; (attr = expected.attributes[i]); i++) { + chai.assert.equal(actual.getAttribute(attr.name), attr.value, + `expected attribute ${attr.name} on ${actualString} to match ` + + `${expectedString}`); + } + chai.assert.equal(actual.childElementCount, expected.childElementCount, + `expected node ${actualString} to have the same children as node ` + + `${expectedString}`); + for (var i = 0; i < expected.childElementCount; i++) { + assertNodesEqual(actual.children[i], expected.children[i]); + } } - suite('createVariable', function() { - test('Undo only', function() { - createTwoVarsDifferentTypes(this.workspace); - - this.workspace.undo(); - assertVariableValues(this.workspace, 'name1', 'type1', 'id1'); - chai.assert.isNull(this.workspace.getVariableById('id2')); - - this.workspace.undo(); - chai.assert.isNull(this.workspace.getVariableById('id1')); - chai.assert.isNull(this.workspace.getVariableById('id2')); - }); - - test('Undo and redo', function() { - createTwoVarsDifferentTypes(this.workspace); - - this.workspace.undo(); - assertVariableValues(this.workspace, 'name1', 'type1', 'id1'); - chai.assert.isNull(this.workspace.getVariableById('id2')); - - this.workspace.undo(true); - - // Expect that variable 'id2' is recreated - assertVariableValues(this.workspace, 'name1', 'type1', 'id1'); - assertVariableValues(this.workspace, 'name2', 'type2', 'id2'); - - this.workspace.undo(); - this.workspace.undo(); - chai.assert.isNull(this.workspace.getVariableById('id1')); - chai.assert.isNull(this.workspace.getVariableById('id2')); - this.workspace.undo(true); - - // Expect that variable 'id1' is recreated - assertVariableValues(this.workspace, 'name1', 'type1', 'id1'); - chai.assert.isNull(this.workspace.getVariableById('id2')); - }); - }); - - suite('deleteVariableById', function() { - test('Undo only no usages', function() { - createTwoVarsDifferentTypes(this.workspace); - this.workspace.deleteVariableById('id1'); - this.workspace.deleteVariableById('id2'); - - this.workspace.undo(); - chai.assert.isNull(this.workspace.getVariableById('id1')); - assertVariableValues(this.workspace, 'name2', 'type2', 'id2'); - - this.workspace.undo(); - assertVariableValues(this.workspace, 'name1', 'type1', 'id1'); - assertVariableValues(this.workspace, 'name2', 'type2', 'id2'); - }); - - test('Undo only with usages', function() { - createTwoVarsDifferentTypes(this.workspace); - // Create blocks to refer to both of them. - createVarBlocksNoEvents(this.workspace, ['id1', 'id2']); - this.workspace.deleteVariableById('id1'); - this.workspace.deleteVariableById('id2'); - - this.workspace.undo(); - assertBlockVarModelName(this.workspace, 0, 'name2'); - chai.assert.isNull(this.workspace.getVariableById('id1')); - assertVariableValues(this.workspace, 'name2', 'type2', 'id2'); - - this.workspace.undo(); - assertBlockVarModelName(this.workspace, 0, 'name2'); - assertBlockVarModelName(this.workspace, 1, 'name1'); - assertVariableValues(this.workspace, 'name1', 'type1', 'id1'); - assertVariableValues(this.workspace, 'name2', 'type2', 'id2'); - }); - - test('Reference exists no usages', function() { - createTwoVarsDifferentTypes(this.workspace); - this.workspace.deleteVariableById('id1'); - this.workspace.deleteVariableById('id2'); - - this.workspace.undo(); - chai.assert.isNull(this.workspace.getVariableById('id1')); - assertVariableValues(this.workspace, 'name2', 'type2', 'id2'); - - this.workspace.undo(true); - // Expect that both variables are deleted - chai.assert.isNull(this.workspace.getVariableById('id1')); - chai.assert.isNull(this.workspace.getVariableById('id2')); - - this.workspace.undo(); - this.workspace.undo(); - assertVariableValues(this.workspace, 'name1', 'type1', 'id1'); - assertVariableValues(this.workspace, 'name2', 'type2', 'id2'); - - this.workspace.undo(true); - // Expect that variable 'id2' is recreated - chai.assert.isNull(this.workspace.getVariableById('id1')); - assertVariableValues(this.workspace, 'name2', 'type2', 'id2'); - }); - - test('Reference exists with usages', function() { - createTwoVarsDifferentTypes(this.workspace); - // Create blocks to refer to both of them. - createVarBlocksNoEvents(this.workspace, ['id1', 'id2']); - this.workspace.deleteVariableById('id1'); - this.workspace.deleteVariableById('id2'); - - this.workspace.undo(); - assertBlockVarModelName(this.workspace, 0, 'name2'); - chai.assert.isNull(this.workspace.getVariableById('id1')); - assertVariableValues(this.workspace, 'name2', 'type2', 'id2'); - - this.workspace.undo(true); - // Expect that both variables are deleted - chai.assert.equal(this.workspace.topBlocks_.length, 0); - chai.assert.isNull(this.workspace.getVariableById('id1')); - chai.assert.isNull(this.workspace.getVariableById('id2')); - - this.workspace.undo(); - this.workspace.undo(); - assertBlockVarModelName(this.workspace, 0, 'name2'); - assertBlockVarModelName(this.workspace, 1, 'name1'); - assertVariableValues(this.workspace, 'name1', 'type1', 'id1'); - assertVariableValues(this.workspace, 'name2', 'type2', 'id2'); - - this.workspace.undo(true); - // Expect that variable 'id2' is recreated - assertBlockVarModelName(this.workspace,0, 'name2'); - chai.assert.isNull(this.workspace.getVariableById('id1')); - assertVariableValues(this.workspace, 'name2', 'type2', 'id2'); - }); - - test('Delete same variable twice no usages', function() { - this.workspace.createVariable('name1', 'type1', 'id1'); - this.workspace.deleteVariableById('id1'); - var workspace = this.workspace; - assertWarnings(() => { - workspace.deleteVariableById('id1'); - }, [/Can't delete non-existent variable/]); - // Check the undoStack only recorded one delete event. - var undoStack = this.workspace.undoStack_; - chai.assert.equal(undoStack[undoStack.length - 1].type, 'var_delete'); - chai.assert.notEqual(undoStack[undoStack.length - 2].type, 'var_delete'); - - // Undo delete - this.workspace.undo(); - assertVariableValues(this.workspace, 'name1', 'type1', 'id1'); - - // Redo delete - this.workspace.undo(true); - chai.assert.isNull(this.workspace.getVariableById('id1')); - - // Redo delete, nothing should happen - this.workspace.undo(true); - chai.assert.isNull(this.workspace.getVariableById('id1')); - }); - - test('Delete same variable twice with usages', function() { - this.workspace.createVariable('name1', 'type1', 'id1'); - createVarBlocksNoEvents(this.workspace, ['id1']); - this.workspace.deleteVariableById('id1'); - var workspace = this.workspace; - assertWarnings(() => { - workspace.deleteVariableById('id1'); - }, [/Can't delete non-existent variable/]); - // Check the undoStack only recorded one delete event. - var undoStack = this.workspace.undoStack_; - chai.assert.equal(undoStack[undoStack.length - 1].type, 'var_delete'); - chai.assert.equal(undoStack[undoStack.length - 2].type, 'delete'); - chai.assert.notEqual(undoStack[undoStack.length - 3].type, 'var_delete'); - - // Undo delete - this.workspace.undo(); - assertBlockVarModelName(this.workspace, 0, 'name1'); - assertVariableValues(this.workspace, 'name1', 'type1', 'id1'); - - // Redo delete - this.workspace.undo(true); - chai.assert.equal(this.workspace.topBlocks_.length, 0); - chai.assert.isNull(this.workspace.getVariableById('id1')); - - // Redo delete, nothing should happen - this.workspace.undo(true); - chai.assert.equal(this.workspace.topBlocks_.length, 0); - chai.assert.isNull(this.workspace.getVariableById('id1')); - }); - }); - - suite('renameVariableById', function() { + suite('Undo Delete', function() { setup(function() { - this.workspace.createVariable('name1', 'type1', 'id1'); + Blockly.defineBlocksWithJsonArray([ + { + "type": "stack_block", + "message0": "", + "previousStatement": null, + "nextStatement": null + }, + { + "type": "row_block", + "message0": "%1", + "args0": [ + { + "type": "input_value", + "name": "INPUT" + } + ], + "output": null + }, + { + "type": "statement_block", + "message0": "%1", + "args0": [ + { + "type": "input_statement", + "name": "STATEMENT" + } + ], + "previousStatement": null, + "nextStatement": null + }]); }); - test('Reference exists no usages rename to name2', function() { - this.workspace.renameVariableById('id1', 'name2'); + teardown(function() { + delete Blockly.Blocks['stack_block']; + delete Blockly.Blocks['row_block']; + delete Blockly.Blocks['statement_block']; + }); + function testUndoDelete(xmlText) { + var xml = Blockly.Xml.textToDom(xmlText); + Blockly.Xml.domToBlock(xml, this.workspace); + this.workspace.getTopBlocks()[0].dispose(false); this.workspace.undo(); - assertVariableValues(this.workspace, 'name1', 'type1', 'id1'); - - this.workspace.undo(true); - assertVariableValues(this.workspace, 'name2', 'type1', 'id1'); + var newXml = Blockly.Xml.workspaceToDom(this.workspace); + assertNodesEqual(newXml.firstChild, xml); + } + test('Stack', function() { + testUndoDelete.call(this, ''); }); - test('Reference exists with usages rename to name2', function() { - createVarBlocksNoEvents(this.workspace, ['id1']); - this.workspace.renameVariableById('id1', 'name2'); + test('Row', function() { + testUndoDelete.call(this, ''); + }); + test('Statement', function() { + testUndoDelete.call(this, ''); + }); + + test('Stack w/ child', function() { + testUndoDelete.call(this, + '' + + ' ' + + ' ' + + ' ' + + '' + ); + }); + + test('Row w/ child', function() { + testUndoDelete.call(this, + '' + + ' ' + + ' ' + + ' ' + + '' + ); + }); + + test('Statement w/ child', function() { + testUndoDelete.call(this, + '' + + ' ' + + ' ' + + ' ' + + '' + ); + }); + + test('Stack w/ shadow', function() { + testUndoDelete.call(this, + '' + + ' ' + + ' ' + + ' ' + + '' + ); + }); + + test('Row w/ shadow', function() { + testUndoDelete.call(this, + '' + + ' ' + + ' ' + + ' ' + + '' + ); + }); + + test('Statement w/ shadow', function() { + testUndoDelete.call(this, + '' + + ' ' + + ' ' + + ' ' + + '' + ); + }); + }); + + suite('Undo Connect', function() { + + setup(function() { + Blockly.defineBlocksWithJsonArray([ + { + "type": "stack_block", + "message0": "", + "previousStatement": null, + "nextStatement": null + }, + { + "type": "row_block", + "message0": "%1", + "args0": [ + { + "type": "input_value", + "name": "INPUT" + } + ], + "output": null + }, + { + "type": "statement_block", + "message0": "%1", + "args0": [ + { + "type": "input_statement", + "name": "STATEMENT" + } + ], + "previousStatement": null, + "nextStatement": null + }]); + }); + + teardown(function() { + delete Blockly.Blocks['stack_block']; + delete Blockly.Blocks['row_block']; + delete Blockly.Blocks['statement_block']; + }); + + function testUndoConnect(xmlText, parentId, childId, func) { + var xml = Blockly.Xml.textToDom(xmlText); + Blockly.Xml.domToWorkspace(xml, this.workspace); + + var parent = this.workspace.getBlockById(parentId); + var child = this.workspace.getBlockById(childId); + func.call(this, parent, child); this.workspace.undo(); - assertBlockVarModelName(this.workspace, 0, 'name1'); - assertVariableValues(this.workspace, 'name1', 'type1', 'id1'); - this.workspace.undo(true); - assertBlockVarModelName(this.workspace, 0, 'name2'); - assertVariableValues(this.workspace, 'name2', 'type1', 'id1'); + var newXml = Blockly.Xml.workspaceToDom(this.workspace); + assertNodesEqual(newXml, xml); + } + + test('Stack', function() { + var xml = + '' + + ' ' + + ' ' + + ''; + + testUndoConnect.call(this, xml, 1, 2, (parent, child) => { + parent.nextConnection.connect(child.previousConnection); + }); }); - test('Reference exists different capitalization no usages rename to Name1', function() { - this.workspace.renameVariableById('id1', 'Name1'); + test('Row', function() { + var xml = + '' + + ' ' + + ' ' + + ''; + testUndoConnect.call(this, xml, 1, 2, (parent, child) => { + parent.getInput('INPUT').connection.connect(child.outputConnection); + }); + }); + + test('Statement', function() { + var xml = + '' + + ' ' + + ' ' + + ''; + + testUndoConnect.call(this, xml, 1, 2, (parent, child) => { + parent.getInput('STATEMENT').connection + .connect(child.previousConnection); + }); + }); + + test('Stack w/ child', function() { + var xml = + '' + + ' ' + + ' ' + + ' ' + + ' ' + + ' ' + + ' ' + + ''; + + testUndoConnect.call(this, xml, 1, 2, (parent, child) => { + parent.nextConnection.connect(child.previousConnection); + }); + }); + + test('Row w/ child', function() { + var xml = + '' + + ' ' + + ' ' + + ' ' + + ' ' + + ' ' + + ' ' + + ''; + + testUndoConnect.call(this, xml, 1, 2, (parent, child) => { + parent.getInput('INPUT').connection.connect(child.outputConnection); + }); + }); + + test('Statement w/ child', function() { + var xml = + '' + + ' ' + + ' ' + + ' ' + + ' ' + + ' ' + + ' ' + + ''; + + testUndoConnect.call(this, xml, 1, 2, (parent, child) => { + parent.getInput('STATEMENT').connection + .connect(child.previousConnection); + }); + }); + + test('Stack w/ shadow', function() { + var xml = + '' + + ' ' + + ' ' + + ' ' + + ' ' + + ' ' + + ' ' + + ''; + + testUndoConnect.call(this, xml, 1, 2, (parent, child) => { + parent.nextConnection.connect(child.previousConnection); + }); + }); + + test('Row w/ shadow', function() { + var xml = + '' + + ' ' + + ' ' + + ' ' + + ' ' + + ' ' + + ' ' + + ''; + + testUndoConnect.call(this, xml, 1, 2, (parent, child) => { + parent.getInput('INPUT').connection.connect(child.outputConnection); + }); + }); + + test('Statement w/ shadow', function() { + var xml = + '' + + ' ' + + ' ' + + ' ' + + ' ' + + ' ' + + ' ' + + ''; + + testUndoConnect.call(this, xml, 1, 2, (parent, child) => { + parent.getInput('STATEMENT').connection + .connect(child.previousConnection); + }); + }); + }); + + suite('Undo Disconnect', function() { + + setup(function() { + Blockly.defineBlocksWithJsonArray([ + { + "type": "stack_block", + "message0": "", + "previousStatement": null, + "nextStatement": null + }, + { + "type": "row_block", + "message0": "%1", + "args0": [ + { + "type": "input_value", + "name": "INPUT" + } + ], + "output": null + }, + { + "type": "statement_block", + "message0": "%1", + "args0": [ + { + "type": "input_statement", + "name": "STATEMENT" + } + ], + "previousStatement": null, + "nextStatement": null + }]); + }); + + teardown(function() { + delete Blockly.Blocks['stack_block']; + delete Blockly.Blocks['row_block']; + delete Blockly.Blocks['statement_block']; + }); + + function testUndoDisconnect(xmlText, childId) { + var xml = Blockly.Xml.textToDom(xmlText); + Blockly.Xml.domToWorkspace(xml, this.workspace); + + var child = this.workspace.getBlockById(childId); + if (child.outputConnection) { + child.outputConnection.disconnect(); + } else { + child.previousConnection.disconnect(); + } this.workspace.undo(); - assertVariableValues(this.workspace, 'name1', 'type1', 'id1'); - this.workspace.undo(true); - assertVariableValues(this.workspace, 'Name1', 'type1', 'id1'); + var newXml = Blockly.Xml.workspaceToDom(this.workspace); + assertNodesEqual(newXml, xml); + } + + test('Stack', function() { + var xml = + '' + + ' ' + + ' ' + + ' ' + + ' ' + + ' ' + + ''; + testUndoDisconnect.call(this, xml, 2); }); - test('Reference exists different capitalization with usages rename to Name1', function() { - createVarBlocksNoEvents(this.workspace, ['id1']); - this.workspace.renameVariableById('id1', 'Name1'); - - this.workspace.undo(); - assertBlockVarModelName(this.workspace, 0, 'name1'); - assertVariableValues(this.workspace, 'name1', 'type1', 'id1'); - - this.workspace.undo(true); - assertBlockVarModelName(this.workspace, 0, 'Name1'); - assertVariableValues(this.workspace, 'Name1', 'type1', 'id1'); + test('Row', function() { + var xml = + '' + + ' ' + + ' ' + + ' ' + + ' ' + + ' ' + + ''; + testUndoDisconnect.call(this, xml, 2); }); - suite('Two variables rename overlap', function() { - test('Same type no usages rename variable with id1 to name2', function() { - this.workspace.createVariable('name2', 'type1', 'id2'); - this.workspace.renameVariableById('id1', 'name2'); + test('Statement', function() { + var xml = + '' + + ' ' + + ' ' + + ' ' + + ' ' + + ' ' + + ''; + testUndoDisconnect.call(this, xml, 2); + }); + + test('Stack w/ child', function() { + var xml = + '' + + ' ' + + ' ' + + ' ' + + ' ' + + ' ' + + ' ' + + ' ' + + ' ' + + ' ' + + ''; + testUndoDisconnect.call(this, xml, 2); + }); + + test('Row w/ child', function() { + var xml = + '' + + ' ' + + ' ' + + ' ' + + ' ' + + ' ' + + ' ' + + ' ' + + ' ' + + ' ' + + ''; + testUndoDisconnect.call(this, xml, 2); + }); + + test('Statement w/ child', function() { + var xml = + '' + + ' ' + + ' ' + + ' ' + + ' ' + + ' ' + + ' ' + + ' ' + + ' ' + + ' ' + + ''; + testUndoDisconnect.call(this, xml, 2); + }); + + test('Stack w/ shadow', function() { + // TODO: For some reason on next connections shadows are + // serialized second. + var xml = + '' + + ' ' + + ' ' + + ' ' + + ' ' + + ' ' + + ' ' + + ''; + testUndoDisconnect.call(this, xml, 2); + chai.assert.equal(this.workspace.getAllBlocks().length, 2, + 'expected there to only be 2 blocks on the workspace ' + + '(check for shadows)'); + }); + + test('Row w/ shadow', function() { + var xml = + '' + + ' ' + + ' ' + + ' ' + + ' ' + + ' ' + + ' ' + + ''; + testUndoDisconnect.call(this, xml, 2); + chai.assert.equal(this.workspace.getAllBlocks().length, 2, + 'expected there to only be 2 blocks on the workspace ' + + '(check for shadows)'); + }); + + test('Statement w/ shadow', function() { + var xml = + '' + + ' ' + + ' ' + + ' ' + + ' ' + + ' ' + + ' ' + + ''; + testUndoDisconnect.call(this, xml, 2); + }); + }); + + suite('Variables', function() { + function createTwoVarsDifferentTypes(workspace) { + workspace.createVariable('name1', 'type1', 'id1'); + workspace.createVariable('name2', 'type2', 'id2'); + } + + suite('createVariable', function() { + test('Undo only', function() { + createTwoVarsDifferentTypes(this.workspace); this.workspace.undo(); assertVariableValues(this.workspace, 'name1', 'type1', 'id1'); - assertVariableValues(this.workspace, 'name2', 'type1', 'id2'); + chai.assert.isNull(this.workspace.getVariableById('id2')); - this.workspace.undo(true); - assertVariableValues(this.workspace, 'name2', 'type1', 'id2'); + this.workspace.undo(); chai.assert.isNull(this.workspace.getVariableById('id1')); + chai.assert.isNull(this.workspace.getVariableById('id2')); }); - test('Same type with usages rename variable with id1 to name2', function() { - this.workspace.createVariable('name2', 'type1', 'id2'); - createVarBlocksNoEvents(this.workspace, ['id1', 'id2']); - this.workspace.renameVariableById('id1', 'name2'); + test('Undo and redo', function() { + createTwoVarsDifferentTypes(this.workspace); this.workspace.undo(); - assertBlockVarModelName(this.workspace, 0, 'name1'); - assertBlockVarModelName(this.workspace, 1, 'name2'); assertVariableValues(this.workspace, 'name1', 'type1', 'id1'); - assertVariableValues(this.workspace, 'name2', 'type1', 'id2'); + chai.assert.isNull(this.workspace.getVariableById('id2')); this.workspace.undo(true); - assertVariableValues(this.workspace, 'name2', 'type1', 'id2'); + + // Expect that variable 'id2' is recreated + assertVariableValues(this.workspace, 'name1', 'type1', 'id1'); + assertVariableValues(this.workspace, 'name2', 'type2', 'id2'); + + this.workspace.undo(); + this.workspace.undo(); chai.assert.isNull(this.workspace.getVariableById('id1')); - }); + chai.assert.isNull(this.workspace.getVariableById('id2')); + this.workspace.undo(true); - test('Same type different capitalization no usages rename variable with id1 to Name2', function() { - this.workspace.createVariable('name2', 'type1', 'id2'); - this.workspace.renameVariableById('id1', 'Name2'); + // Expect that variable 'id1' is recreated + assertVariableValues(this.workspace, 'name1', 'type1', 'id1'); + chai.assert.isNull(this.workspace.getVariableById('id2')); + }); + }); + + suite('deleteVariableById', function() { + test('Undo only no usages', function() { + createTwoVarsDifferentTypes(this.workspace); + this.workspace.deleteVariableById('id1'); + this.workspace.deleteVariableById('id2'); this.workspace.undo(); - assertVariableValues(this.workspace, 'name1', 'type1', 'id1'); - assertVariableValues(this.workspace, 'name2', 'type1', 'id2'); - - this.workspace.undo(true); - assertVariableValues(this.workspace, 'Name2', 'type1', 'id2'); - chai.assert.isNull(this.workspace.getVariable('name1')); - }); - - test('Same type different capitalization with usages rename variable with id1 to Name2', function() { - this.workspace.createVariable('name2', 'type1', 'id2'); - createVarBlocksNoEvents(this.workspace, ['id1', 'id2']); - this.workspace.renameVariableById('id1', 'Name2'); - - this.workspace.undo(); - assertBlockVarModelName(this.workspace, 0, 'name1'); - assertBlockVarModelName(this.workspace, 1, 'name2'); - assertVariableValues(this.workspace, 'name1', 'type1', 'id1'); - assertVariableValues(this.workspace, 'name2', 'type1', 'id2'); - - this.workspace.undo(true); - assertVariableValues(this.workspace, 'Name2', 'type1', 'id2'); chai.assert.isNull(this.workspace.getVariableById('id1')); - assertBlockVarModelName(this.workspace, 0, 'Name2'); - assertBlockVarModelName(this.workspace, 1, 'Name2'); - }); - - test('Different type no usages rename variable with id1 to name2', function() { - this.workspace.createVariable('name2', 'type2', 'id2'); - this.workspace.renameVariableById('id1', 'name2'); + assertVariableValues(this.workspace, 'name2', 'type2', 'id2'); this.workspace.undo(); assertVariableValues(this.workspace, 'name1', 'type1', 'id1'); assertVariableValues(this.workspace, 'name2', 'type2', 'id2'); - - this.workspace.undo(true); - assertVariableValues(this.workspace, 'name2', 'type1', 'id1'); - assertVariableValues(this.workspace, 'name2', 'type2', 'id2'); }); - test('Different type with usages rename variable with id1 to name2', function() { - this.workspace.createVariable('name2', 'type2', 'id2'); + test('Undo only with usages', function() { + createTwoVarsDifferentTypes(this.workspace); + // Create blocks to refer to both of them. createVarBlocksNoEvents(this.workspace, ['id1', 'id2']); - this.workspace.renameVariableById('id1', 'name2'); + this.workspace.deleteVariableById('id1'); + this.workspace.deleteVariableById('id2'); this.workspace.undo(); - assertVariableValues(this.workspace, 'name1', 'type1', 'id1'); - assertVariableValues(this.workspace, 'name2', 'type2', 'id2'); - assertBlockVarModelName(this.workspace, 0, 'name1'); - assertBlockVarModelName(this.workspace, 1, 'name2'); - - this.workspace.undo(true); - assertVariableValues(this.workspace, 'name2', 'type1', 'id1'); - assertVariableValues(this.workspace, 'name2', 'type2', 'id2'); assertBlockVarModelName(this.workspace, 0, 'name2'); - assertBlockVarModelName(this.workspace, 1, 'name2'); + chai.assert.isNull(this.workspace.getVariableById('id1')); + assertVariableValues(this.workspace, 'name2', 'type2', 'id2'); + + this.workspace.undo(); + assertBlockVarModelName(this.workspace, 0, 'name2'); + assertBlockVarModelName(this.workspace, 1, 'name1'); + assertVariableValues(this.workspace, 'name1', 'type1', 'id1'); + assertVariableValues(this.workspace, 'name2', 'type2', 'id2'); }); - test('Different type different capitalization no usages rename variable with id1 to Name2', function() { - this.workspace.createVariable('name2', 'type2', 'id2'); - this.workspace.renameVariableById('id1', 'Name2'); + test('Reference exists no usages', function() { + createTwoVarsDifferentTypes(this.workspace); + this.workspace.deleteVariableById('id1'); + this.workspace.deleteVariableById('id2'); + this.workspace.undo(); + chai.assert.isNull(this.workspace.getVariableById('id1')); + assertVariableValues(this.workspace, 'name2', 'type2', 'id2'); + + this.workspace.undo(true); + // Expect that both variables are deleted + chai.assert.isNull(this.workspace.getVariableById('id1')); + chai.assert.isNull(this.workspace.getVariableById('id2')); + + this.workspace.undo(); this.workspace.undo(); assertVariableValues(this.workspace, 'name1', 'type1', 'id1'); assertVariableValues(this.workspace, 'name2', 'type2', 'id2'); this.workspace.undo(true); - assertVariableValues(this.workspace, 'Name2', 'type1', 'id1'); + // Expect that variable 'id2' is recreated + chai.assert.isNull(this.workspace.getVariableById('id1')); assertVariableValues(this.workspace, 'name2', 'type2', 'id2'); }); - test('Different type different capitalization with usages rename variable with id1 to Name2', function() { - this.workspace.createVariable('name2', 'type2', 'id2'); + test('Reference exists with usages', function() { + createTwoVarsDifferentTypes(this.workspace); + // Create blocks to refer to both of them. createVarBlocksNoEvents(this.workspace, ['id1', 'id2']); - this.workspace.renameVariableById('id1', 'Name2'); + this.workspace.deleteVariableById('id1'); + this.workspace.deleteVariableById('id2'); + + this.workspace.undo(); + assertBlockVarModelName(this.workspace, 0, 'name2'); + chai.assert.isNull(this.workspace.getVariableById('id1')); + assertVariableValues(this.workspace, 'name2', 'type2', 'id2'); + + this.workspace.undo(true); + // Expect that both variables are deleted + chai.assert.equal(this.workspace.topBlocks_.length, 0); + chai.assert.isNull(this.workspace.getVariableById('id1')); + chai.assert.isNull(this.workspace.getVariableById('id2')); + + this.workspace.undo(); + this.workspace.undo(); + assertBlockVarModelName(this.workspace, 0, 'name2'); + assertBlockVarModelName(this.workspace, 1, 'name1'); + assertVariableValues(this.workspace, 'name1', 'type1', 'id1'); + assertVariableValues(this.workspace, 'name2', 'type2', 'id2'); + + this.workspace.undo(true); + // Expect that variable 'id2' is recreated + assertBlockVarModelName(this.workspace,0, 'name2'); + chai.assert.isNull(this.workspace.getVariableById('id1')); + assertVariableValues(this.workspace, 'name2', 'type2', 'id2'); + }); + + test('Delete same variable twice no usages', function() { + this.workspace.createVariable('name1', 'type1', 'id1'); + this.workspace.deleteVariableById('id1'); + var workspace = this.workspace; + var warnings = captureWarnings(function() { + workspace.deleteVariableById('id1'); + }); + chai.assert.equal(warnings.length, 1, + 'Expected 1 warning for second deleteVariableById call.'); + + // Check the undoStack only recorded one delete event. + var undoStack = this.workspace.undoStack_; + chai.assert.equal(undoStack[undoStack.length - 1].type, 'var_delete'); + chai.assert.notEqual(undoStack[undoStack.length - 2].type, 'var_delete'); + + // Undo delete + this.workspace.undo(); + assertVariableValues(this.workspace, 'name1', 'type1', 'id1'); + + // Redo delete + this.workspace.undo(true); + chai.assert.isNull(this.workspace.getVariableById('id1')); + + // Redo delete, nothing should happen + this.workspace.undo(true); + chai.assert.isNull(this.workspace.getVariableById('id1')); + }); + + test('Delete same variable twice with usages', function() { + this.workspace.createVariable('name1', 'type1', 'id1'); + createVarBlocksNoEvents(this.workspace, ['id1']); + this.workspace.deleteVariableById('id1'); + var workspace = this.workspace; + var warnings = captureWarnings(function() { + workspace.deleteVariableById('id1'); + }); + chai.assert.equal(warnings.length, 1, + 'Expected 1 warning for second deleteVariableById call.'); + + // Check the undoStack only recorded one delete event. + var undoStack = this.workspace.undoStack_; + chai.assert.equal(undoStack[undoStack.length - 1].type, 'var_delete'); + chai.assert.equal(undoStack[undoStack.length - 2].type, 'delete'); + chai.assert.notEqual(undoStack[undoStack.length - 3].type, 'var_delete'); + + // Undo delete + this.workspace.undo(); + assertBlockVarModelName(this.workspace, 0, 'name1'); + assertVariableValues(this.workspace, 'name1', 'type1', 'id1'); + + // Redo delete + this.workspace.undo(true); + chai.assert.equal(this.workspace.topBlocks_.length, 0); + chai.assert.isNull(this.workspace.getVariableById('id1')); + + // Redo delete, nothing should happen + this.workspace.undo(true); + chai.assert.equal(this.workspace.topBlocks_.length, 0); + chai.assert.isNull(this.workspace.getVariableById('id1')); + }); + }); + + suite('renameVariableById', function() { + setup(function() { + this.workspace.createVariable('name1', 'type1', 'id1'); + }); + + test('Reference exists no usages rename to name2', function() { + this.workspace.renameVariableById('id1', 'name2'); this.workspace.undo(); assertVariableValues(this.workspace, 'name1', 'type1', 'id1'); - assertVariableValues(this.workspace, 'name2', 'type2', 'id2'); - assertBlockVarModelName(this.workspace, 0, 'name1'); - assertBlockVarModelName(this.workspace, 1, 'name2'); this.workspace.undo(true); - assertVariableValues(this.workspace, 'Name2', 'type1', 'id1'); - assertVariableValues(this.workspace, 'name2', 'type2', 'id2'); - assertBlockVarModelName(this.workspace, 0, 'Name2'); - assertBlockVarModelName(this.workspace, 1, 'name2'); + assertVariableValues(this.workspace, 'name2', 'type1', 'id1'); + + }); + + test('Reference exists with usages rename to name2', function() { + createVarBlocksNoEvents(this.workspace, ['id1']); + this.workspace.renameVariableById('id1', 'name2'); + + this.workspace.undo(); + assertBlockVarModelName(this.workspace, 0, 'name1'); + assertVariableValues(this.workspace, 'name1', 'type1', 'id1'); + + this.workspace.undo(true); + assertBlockVarModelName(this.workspace, 0, 'name2'); + assertVariableValues(this.workspace, 'name2', 'type1', 'id1'); + }); + + test('Reference exists different capitalization no usages rename to Name1', function() { + this.workspace.renameVariableById('id1', 'Name1'); + + this.workspace.undo(); + assertVariableValues(this.workspace, 'name1', 'type1', 'id1'); + + this.workspace.undo(true); + assertVariableValues(this.workspace, 'Name1', 'type1', 'id1'); + }); + + test('Reference exists different capitalization with usages rename to Name1', function() { + createVarBlocksNoEvents(this.workspace, ['id1']); + this.workspace.renameVariableById('id1', 'Name1'); + + this.workspace.undo(); + assertBlockVarModelName(this.workspace, 0, 'name1'); + assertVariableValues(this.workspace, 'name1', 'type1', 'id1'); + + this.workspace.undo(true); + assertBlockVarModelName(this.workspace, 0, 'Name1'); + assertVariableValues(this.workspace, 'Name1', 'type1', 'id1'); + }); + + suite('Two variables rename overlap', function() { + test('Same type no usages rename variable with id1 to name2', function() { + this.workspace.createVariable('name2', 'type1', 'id2'); + this.workspace.renameVariableById('id1', 'name2'); + + this.workspace.undo(); + assertVariableValues(this.workspace, 'name1', 'type1', 'id1'); + assertVariableValues(this.workspace, 'name2', 'type1', 'id2'); + + this.workspace.undo(true); + assertVariableValues(this.workspace, 'name2', 'type1', 'id2'); + chai.assert.isNull(this.workspace.getVariableById('id1')); + }); + + test('Same type with usages rename variable with id1 to name2', function() { + this.workspace.createVariable('name2', 'type1', 'id2'); + createVarBlocksNoEvents(this.workspace, ['id1', 'id2']); + this.workspace.renameVariableById('id1', 'name2'); + + this.workspace.undo(); + assertBlockVarModelName(this.workspace, 0, 'name1'); + assertBlockVarModelName(this.workspace, 1, 'name2'); + assertVariableValues(this.workspace, 'name1', 'type1', 'id1'); + assertVariableValues(this.workspace, 'name2', 'type1', 'id2'); + + this.workspace.undo(true); + assertVariableValues(this.workspace, 'name2', 'type1', 'id2'); + chai.assert.isNull(this.workspace.getVariableById('id1')); + }); + + test('Same type different capitalization no usages rename variable with id1 to Name2', function() { + this.workspace.createVariable('name2', 'type1', 'id2'); + this.workspace.renameVariableById('id1', 'Name2'); + + this.workspace.undo(); + assertVariableValues(this.workspace, 'name1', 'type1', 'id1'); + assertVariableValues(this.workspace, 'name2', 'type1', 'id2'); + + this.workspace.undo(true); + assertVariableValues(this.workspace, 'Name2', 'type1', 'id2'); + chai.assert.isNull(this.workspace.getVariable('name1')); + }); + + test('Same type different capitalization with usages rename variable with id1 to Name2', function() { + this.workspace.createVariable('name2', 'type1', 'id2'); + createVarBlocksNoEvents(this.workspace, ['id1', 'id2']); + this.workspace.renameVariableById('id1', 'Name2'); + + this.workspace.undo(); + assertBlockVarModelName(this.workspace, 0, 'name1'); + assertBlockVarModelName(this.workspace, 1, 'name2'); + assertVariableValues(this.workspace, 'name1', 'type1', 'id1'); + assertVariableValues(this.workspace, 'name2', 'type1', 'id2'); + + this.workspace.undo(true); + assertVariableValues(this.workspace, 'Name2', 'type1', 'id2'); + chai.assert.isNull(this.workspace.getVariableById('id1')); + assertBlockVarModelName(this.workspace, 0, 'Name2'); + assertBlockVarModelName(this.workspace, 1, 'Name2'); + }); + + test('Different type no usages rename variable with id1 to name2', function() { + this.workspace.createVariable('name2', 'type2', 'id2'); + this.workspace.renameVariableById('id1', 'name2'); + + this.workspace.undo(); + assertVariableValues(this.workspace, 'name1', 'type1', 'id1'); + assertVariableValues(this.workspace, 'name2', 'type2', 'id2'); + + this.workspace.undo(true); + assertVariableValues(this.workspace, 'name2', 'type1', 'id1'); + assertVariableValues(this.workspace, 'name2', 'type2', 'id2'); + }); + + test('Different type with usages rename variable with id1 to name2', function() { + this.workspace.createVariable('name2', 'type2', 'id2'); + createVarBlocksNoEvents(this.workspace, ['id1', 'id2']); + this.workspace.renameVariableById('id1', 'name2'); + + this.workspace.undo(); + assertVariableValues(this.workspace, 'name1', 'type1', 'id1'); + assertVariableValues(this.workspace, 'name2', 'type2', 'id2'); + assertBlockVarModelName(this.workspace, 0, 'name1'); + assertBlockVarModelName(this.workspace, 1, 'name2'); + + this.workspace.undo(true); + assertVariableValues(this.workspace, 'name2', 'type1', 'id1'); + assertVariableValues(this.workspace, 'name2', 'type2', 'id2'); + assertBlockVarModelName(this.workspace, 0, 'name2'); + assertBlockVarModelName(this.workspace, 1, 'name2'); + }); + + test('Different type different capitalization no usages rename variable with id1 to Name2', function() { + this.workspace.createVariable('name2', 'type2', 'id2'); + this.workspace.renameVariableById('id1', 'Name2'); + + this.workspace.undo(); + assertVariableValues(this.workspace, 'name1', 'type1', 'id1'); + assertVariableValues(this.workspace, 'name2', 'type2', 'id2'); + + this.workspace.undo(true); + assertVariableValues(this.workspace, 'Name2', 'type1', 'id1'); + assertVariableValues(this.workspace, 'name2', 'type2', 'id2'); + }); + + test('Different type different capitalization with usages rename variable with id1 to Name2', function() { + this.workspace.createVariable('name2', 'type2', 'id2'); + createVarBlocksNoEvents(this.workspace, ['id1', 'id2']); + this.workspace.renameVariableById('id1', 'Name2'); + + this.workspace.undo(); + assertVariableValues(this.workspace, 'name1', 'type1', 'id1'); + assertVariableValues(this.workspace, 'name2', 'type2', 'id2'); + assertBlockVarModelName(this.workspace, 0, 'name1'); + assertBlockVarModelName(this.workspace, 1, 'name2'); + + this.workspace.undo(true); + assertVariableValues(this.workspace, 'Name2', 'type1', 'id1'); + assertVariableValues(this.workspace, 'name2', 'type2', 'id2'); + assertBlockVarModelName(this.workspace, 0, 'Name2'); + assertBlockVarModelName(this.workspace, 1, 'name2'); + }); }); }); });