From c32c835088e59e19a1c58eaea12ee76d0b4cef0d Mon Sep 17 00:00:00 2001 From: Beka Westberg Date: Tue, 29 Oct 2019 15:31:40 -0700 Subject: [PATCH] Fixed programmatically added connections not being added to the connection db (#3318) * Fixed programmatically added connections not getting added to the connection db. --- core/block_svg.js | 58 +- core/keyboard_nav/navigation.js | 8 +- core/rendered_connection.js | 45 +- core/xml.js | 4 +- tests/mocha/block_test.js | 1831 ++++++++++++++++--------------- 5 files changed, 1024 insertions(+), 922 deletions(-) diff --git a/core/block_svg.js b/core/block_svg.js index 18f09d5d0..a1fb14f09 100644 --- a/core/block_svg.js +++ b/core/block_svg.js @@ -140,14 +140,6 @@ Blockly.BlockSvg = function(workspace, prototypeName, opt_id) { * @private */ this.markerSvg_ = null; - - /** - * Should the block tell its connections to start tracking inside the render - * method? - * @type {boolean} - * @private - */ - this.callTrackConnections_ = true; }; Blockly.utils.object.inherits(Blockly.BlockSvg, Blockly.Block); @@ -1525,58 +1517,45 @@ Blockly.BlockSvg.prototype.appendInput_ = function(type, name) { }; /** - * Tell the block to wait for an outside source to call - * startTrackingConnections, rather than starting connection - * tracking automatically. + * Sets whether this block's connections are tracked in the database or not. * - * Also tells children of this block to wait. + * Used by the deserializer to be more efficient. Setting a connection's + * tracked_ value to false keeps it from adding itself to the db when it + * gets its first moveTo call, saving expensive ops for later. + * @param {boolean} track If true, start tracking. If false, stop tracking. * @package */ -Blockly.BlockSvg.prototype.waitToTrackConnections = function() { - this.callTrackConnections_ = false; - var children = this.getChildren(false); - for (var i = 0, child; child = children[i]; i++) { - child.waitToTrackConnections(); - } -}; - -/** - * Tell this block's connections to add themselves to the connection - * database (i.e. start tracking). - * - * All following/next blocks will be told to start tracking. Inner blocks - * (i.e. blocks attached to value/statement inputs) will be told to start - * tracking if this block is not collapsed. - * @package - */ -Blockly.BlockSvg.prototype.startTrackingConnections = function() { +Blockly.BlockSvg.prototype.setConnectionTracking = function(track) { if (this.previousConnection) { - this.previousConnection.setTracking(true); + this.previousConnection.setTracking(track); } if (this.outputConnection) { - this.outputConnection.setTracking(true); + this.outputConnection.setTracking(track); } if (this.nextConnection) { - this.nextConnection.setTracking(true); + this.nextConnection.setTracking(track); var child = this.nextConnection.targetBlock(); if (child) { - child.startTrackingConnections(); + child.setConnectionTracking(track); } } if (this.collapsed_) { + // When track is true, we don't want to start tracking collapsed + // connections. When track is false, we're already not tracking + // collapsed connections, so no need to update. return; } for (var i = 0; i < this.inputList.length; i++) { var conn = this.inputList[i].connection; if (conn) { - conn.setTracking(true); + conn.setTracking(track); // Pass tracking on down the chain. var block = conn.targetBlock(); if (block) { - block.startTrackingConnections(); + block.setConnectionTracking(track); } } } @@ -1776,13 +1755,6 @@ Blockly.BlockSvg.prototype.render = function(opt_bubble) { (/** @type {!Blockly.WorkspaceSvg} */ (this.workspace)).getRenderer().render(this); // No matter how we rendered, connection locations should now be correct. this.updateConnectionLocations_(); - // TODO: This should be handled inside a robust init method, because it would - // make it a lot cleaner, but for now it's handled here for backwards - // compatibility. - if (this.callTrackConnections_) { - this.startTrackingConnections(); - this.callTrackConnections_ = false; - } if (opt_bubble !== false) { // Render all blocks above this one (propagate a reflow). var parentBlock = this.getParent(); diff --git a/core/keyboard_nav/navigation.js b/core/keyboard_nav/navigation.js index 7f6523554..b5d4aaa08 100644 --- a/core/keyboard_nav/navigation.js +++ b/core/keyboard_nav/navigation.js @@ -202,10 +202,10 @@ Blockly.navigation.insertFromFlyout = function() { var newBlock = flyout.createBlock(curBlock); // Render to get the sizing right. newBlock.render(); - // Connections are hidden when the block is first created. Normally there's - // enough time for them to become unhidden in the user's mouse movements, - // but not here. - newBlock.startTrackingConnections(); + // Connections are not tracked when the block is first created. Normally + // there's enough time for them to become tracked in the user's mouse + // movements, but not here. + newBlock.setConnectionTracking(true); workspace.getCursor().setCurNode( Blockly.ASTNode.createBlockNode(newBlock)); if (!Blockly.navigation.modify_()) { diff --git a/core/rendered_connection.js b/core/rendered_connection.js index acce573ad..67129772e 100644 --- a/core/rendered_connection.js +++ b/core/rendered_connection.js @@ -65,14 +65,32 @@ Blockly.RenderedConnection = function(source, type) { this.offsetInBlock_ = new Blockly.utils.Coordinate(0, 0); /** - * Whether this connections is tracked in the database or not. - * @type {boolean} + * Describes the state of this connection's tracked-ness. + * @type {Blockly.RenderedConnection.TrackedState} * @private */ - this.tracked_ = false; + this.trackedState_ = Blockly.RenderedConnection.TrackedState.WILL_TRACK; }; Blockly.utils.object.inherits(Blockly.RenderedConnection, Blockly.Connection); +/** + * Enum for different kinds of tracked states. + * + * WILL_TRACK means that this connection will add itself to + * the db on the next moveTo call it receives. + * + * UNTRACKED means that this connection will not add + * itself to the database until setTracking(true) is explicitly called. + * + * TRACKED means that this connection is currently being tracked. + * @enum {number} + */ +Blockly.RenderedConnection.TrackedState = { + WILL_TRACK: -1, + UNTRACKED: 0, + TRACKED: 1 +}; + /** * Dispose of this connection. Remove it from the database (if it is * tracked) and call the super-function to deal with connected blocks. @@ -81,7 +99,7 @@ Blockly.utils.object.inherits(Blockly.RenderedConnection, Blockly.Connection); */ Blockly.RenderedConnection.prototype.dispose = function() { Blockly.RenderedConnection.superClass_.dispose.call(this); - if (this.tracked_) { + if (this.trackedState_ == Blockly.RenderedConnection.TrackedState.TRACKED) { this.db_.removeConnection(this, this.y); } }; @@ -174,7 +192,11 @@ Blockly.RenderedConnection.prototype.bumpAwayFrom = function(staticConnection) { * @param {number} y New absolute y coordinate, in workspace coordinates. */ Blockly.RenderedConnection.prototype.moveTo = function(x, y) { - if (this.tracked_) { + if (this.trackedState_ == Blockly.RenderedConnection.TrackedState.WILL_TRACK) { + this.db_.addConnection(this, y); + this.trackedState_ = Blockly.RenderedConnection.TrackedState.TRACKED; + } else if (this.trackedState_ == Blockly.RenderedConnection + .TrackedState.TRACKED) { this.db_.removeConnection(this, this.y); this.db_.addConnection(this, y); } @@ -307,7 +329,10 @@ Blockly.RenderedConnection.prototype.unhighlight = function() { * @package */ Blockly.RenderedConnection.prototype.setTracking = function(doTracking) { - if (doTracking == this.tracked_) { + if ((doTracking && this.trackedState_ == + Blockly.RenderedConnection.TrackedState.TRACKED) || + (!doTracking && this.trackedState_ == + Blockly.RenderedConnection.TrackedState.UNTRACKED)) { return; } if (this.sourceBlock_.isInFlyout) { @@ -316,10 +341,14 @@ Blockly.RenderedConnection.prototype.setTracking = function(doTracking) { } if (doTracking) { this.db_.addConnection(this, this.y); - } else { + this.trackedState_ = Blockly.RenderedConnection.TrackedState.TRACKED; + return; + } + if (this.trackedState_ == Blockly.RenderedConnection + .TrackedState.TRACKED) { this.db_.removeConnection(this, this.y); } - this.tracked_ = doTracking; + this.trackedState_ = Blockly.RenderedConnection.TrackedState.UNTRACKED; }; /** diff --git a/core/xml.js b/core/xml.js index f581da08f..f146b7761 100644 --- a/core/xml.js +++ b/core/xml.js @@ -545,7 +545,7 @@ Blockly.Xml.domToBlock = function(xmlBlock, workspace) { var blocks = topBlock.getDescendants(false); if (workspace.rendered) { // Wait to track connections to speed up assembly. - topBlock.waitToTrackConnections(); + topBlock.setConnectionTracking(false); // Render each block. for (var i = blocks.length - 1; i >= 0; i--) { blocks[i].initSvg(); @@ -557,7 +557,7 @@ Blockly.Xml.domToBlock = function(xmlBlock, workspace) { // blocks have rendered. setTimeout(function() { if (!topBlock.disposed) { - topBlock.startTrackingConnections(); + topBlock.setConnectionTracking(true); } }, 1); topBlock.updateDisabled(); diff --git a/tests/mocha/block_test.js b/tests/mocha/block_test.js index d0cc4f1a8..27b496fcb 100644 --- a/tests/mocha/block_test.js +++ b/tests/mocha/block_test.js @@ -19,6 +19,10 @@ suite('Blocks', function() { setup(function() { this.workspace = new Blockly.Workspace(); Blockly.defineBlocksWithJsonArray([ + { + "type": "empty_block", + "message0": "" + }, { "type": "stack_block", "message0": "", @@ -56,914 +60,1011 @@ suite('Blocks', function() { delete Blockly.Blocks['statement_block']; }); - suite('Connection Management', function() { - suite('Unplug', function() { - function assertUnpluggedNoheal(blocks) { - // A has nothing connected to it. - assertEquals(0, blocks.A.getChildren().length); - // B and C are still connected. - assertEquals(blocks.B, blocks.C.getParent()); - // B is the top of its stack. - assertNull(blocks.B.getParent()); - } - function assertUnpluggedHealed(blocks) { - // A and C are connected. - assertEquals(1, blocks.A.getChildren().length); - assertEquals(blocks.A, blocks.C.getParent()); - // B has nothing connected to it. - assertEquals(0, blocks.B.getChildren().length); - // B is the top of its stack. - assertNull(blocks.B.getParent()); - } - function assertUnpluggedHealFailed(blocks) { - // A has nothing connected to it. - assertEquals(0, blocks.A.getChildren().length); - // B has nothing connected to it. - assertEquals(0, blocks.B.getChildren().length); - // B is the top of its stack. - assertNull(blocks.B.getParent()); - // C is the top of its stack. - assertNull(blocks.C.getParent()); - } + suite('Unplug', function() { + function assertUnpluggedNoheal(blocks) { + // A has nothing connected to it. + assertEquals(0, blocks.A.getChildren().length); + // B and C are still connected. + assertEquals(blocks.B, blocks.C.getParent()); + // B is the top of its stack. + assertNull(blocks.B.getParent()); + } + function assertUnpluggedHealed(blocks) { + // A and C are connected. + assertEquals(1, blocks.A.getChildren().length); + assertEquals(blocks.A, blocks.C.getParent()); + // B has nothing connected to it. + assertEquals(0, blocks.B.getChildren().length); + // B is the top of its stack. + assertNull(blocks.B.getParent()); + } + function assertUnpluggedHealFailed(blocks) { + // A has nothing connected to it. + assertEquals(0, blocks.A.getChildren().length); + // B has nothing connected to it. + assertEquals(0, blocks.B.getChildren().length); + // B is the top of its stack. + assertNull(blocks.B.getParent()); + // C is the top of its stack. + assertNull(blocks.C.getParent()); + } - suite('Row', function() { - setup(function() { - var blockA = this.workspace.newBlock('row_block'); - var blockB = this.workspace.newBlock('row_block'); - var blockC = this.workspace.newBlock('row_block'); - - blockA.inputList[0].connection.connect(blockB.outputConnection); - blockB.inputList[0].connection.connect(blockC.outputConnection); - - assertEquals(blockB, blockC.getParent()); - - this.blocks = { - A: blockA, - B: blockB, - C: blockC - }; - }); - - test('Don\'t heal', function() { - this.blocks.B.unplug(false); - assertUnpluggedNoheal(this.blocks); - }); - test('Heal', function() { - this.blocks.B.unplug(true); - // Each block has only one input, and the types work. - assertUnpluggedHealed(this.blocks); - }); - test('Heal with bad checks', function() { - var blocks = this.blocks; - - // A and C can't connect, but both can connect to B. - blocks.A.inputList[0].connection.setCheck('type1'); - blocks.C.outputConnection.setCheck('type2'); - - // Each block has only one input, but the types don't work. - blocks.B.unplug(true); - assertUnpluggedHealFailed(blocks); - }); - test('A has multiple inputs', function() { - var blocks = this.blocks; - // Add extra input to parent - blocks.A.appendValueInput("INPUT").setCheck(null); - blocks.B.unplug(true); - assertUnpluggedHealed(blocks); - }); - test('B has multiple inputs', function() { - var blocks = this.blocks; - // Add extra input to middle block - blocks.B.appendValueInput("INPUT").setCheck(null); - blocks.B.unplug(true); - assertUnpluggedHealed(blocks); - }); - test('C has multiple inputs', function() { - var blocks = this.blocks; - // Add extra input to child block - blocks.C.appendValueInput("INPUT").setCheck(null); - // Child block input count doesn't matter. - blocks.B.unplug(true); - assertUnpluggedHealed(blocks); - }); - test('C is Shadow', function() { - var blocks = this.blocks; - blocks.C.setShadow(true); - blocks.B.unplug(true); - // Even though we're asking to heal, it will appear as if it has not - // healed because shadows always stay with the parent. - assertUnpluggedNoheal(blocks); - }); - }); - suite('Stack', function() { - setup(function() { - var blockA = this.workspace.newBlock('stack_block'); - var blockB = this.workspace.newBlock('stack_block'); - var blockC = this.workspace.newBlock('stack_block'); - - blockA.nextConnection.connect(blockB.previousConnection); - blockB.nextConnection.connect(blockC.previousConnection); - - assertEquals(blockB, blockC.getParent()); - - this.blocks = { - A: blockA, - B: blockB, - C: blockC - }; - }); - - test('Don\'t heal', function() { - this.blocks.B.unplug(); - assertUnpluggedNoheal(this.blocks); - }); - test('Heal', function() { - this.blocks.B.unplug(true); - assertUnpluggedHealed(this.blocks); - }); - test('Heal with bad checks', function() { - var blocks = this.blocks; - // A and C can't connect, but both can connect to B. - blocks.A.nextConnection.setCheck('type1'); - blocks.C.previousConnection.setCheck('type2'); - - // The types don't work. - blocks.B.unplug(true); - - assertUnpluggedHealFailed(blocks); - }); - test('C is Shadow', function() { - var blocks = this.blocks; - blocks.C.setShadow(true); - blocks.B.unplug(true); - // Even though we're asking to heal, it will appear as if it has not - // healed because shadows always stay with the parent. - assertUnpluggedNoheal(blocks); - }); - }); - }); - suite('Dispose', function() { - function assertDisposedNoheal(blocks) { - chai.assert.isFalse(blocks.A.disposed); - // A has nothing connected to it. - chai.assert.equal(0, blocks.A.getChildren().length); - // B is disposed. - chai.assert.isTrue(blocks.B.disposed); - // And C is disposed. - chai.assert.isTrue(blocks.C.disposed); - } - function assertDisposedHealed(blocks) { - chai.assert.isFalse(blocks.A.disposed); - chai.assert.isFalse(blocks.C.disposed); - // A and C are connected. - assertEquals(1, blocks.A.getChildren().length); - assertEquals(blocks.A, blocks.C.getParent()); - // B is disposed. - chai.assert.isTrue(blocks.B.disposed); - } - function assertDisposedHealFailed(blocks) { - chai.assert.isFalse(blocks.A.disposed); - chai.assert.isFalse(blocks.C.disposed); - // A has nothing connected to it. - chai.assert.equal(0, blocks.A.getChildren().length); - // B is disposed. - chai.assert.isTrue(blocks.B.disposed); - // C is the top of its stack. - assertNull(blocks.C.getParent()); - } - - suite('Row', function() { - setup(function() { - var blockA = this.workspace.newBlock('row_block'); - var blockB = this.workspace.newBlock('row_block'); - var blockC = this.workspace.newBlock('row_block'); - - blockA.inputList[0].connection.connect(blockB.outputConnection); - blockB.inputList[0].connection.connect(blockC.outputConnection); - - assertEquals(blockB, blockC.getParent()); - - this.blocks = { - A: blockA, - B: blockB, - C: blockC - }; - }); - - test('Don\'t heal', function() { - this.blocks.B.dispose(false); - assertDisposedNoheal(this.blocks); - }); - test('Heal', function() { - this.blocks.B.dispose(true); - // Each block has only one input, and the types work. - assertDisposedHealed(this.blocks); - }); - test('Heal with bad checks', function() { - var blocks = this.blocks; - - // A and C can't connect, but both can connect to B. - blocks.A.inputList[0].connection.setCheck('type1'); - blocks.C.outputConnection.setCheck('type2'); - - // Each block has only one input, but the types don't work. - blocks.B.dispose(true); - assertDisposedHealFailed(blocks); - }); - test('A has multiple inputs', function() { - var blocks = this.blocks; - // Add extra input to parent - blocks.A.appendValueInput("INPUT").setCheck(null); - blocks.B.dispose(true); - assertDisposedHealed(blocks); - }); - test('B has multiple inputs', function() { - var blocks = this.blocks; - // Add extra input to middle block - blocks.B.appendValueInput("INPUT").setCheck(null); - blocks.B.dispose(true); - assertDisposedHealed(blocks); - }); - test('C has multiple inputs', function() { - var blocks = this.blocks; - // Add extra input to child block - blocks.C.appendValueInput("INPUT").setCheck(null); - // Child block input count doesn't matter. - blocks.B.dispose(true); - assertDisposedHealed(blocks); - }); - test('C is Shadow', function() { - var blocks = this.blocks; - blocks.C.setShadow(true); - blocks.B.dispose(true); - // Even though we're asking to heal, it will appear as if it has not - // healed because shadows always get destroyed. - assertDisposedNoheal(blocks); - }); - }); - suite('Stack', function() { - setup(function() { - var blockA = this.workspace.newBlock('stack_block'); - var blockB = this.workspace.newBlock('stack_block'); - var blockC = this.workspace.newBlock('stack_block'); - - blockA.nextConnection.connect(blockB.previousConnection); - blockB.nextConnection.connect(blockC.previousConnection); - - assertEquals(blockB, blockC.getParent()); - - this.blocks = { - A: blockA, - B: blockB, - C: blockC - }; - }); - - test('Don\'t heal', function() { - this.blocks.B.dispose(); - assertDisposedNoheal(this.blocks); - }); - test('Heal', function() { - this.blocks.B.dispose(true); - assertDisposedHealed(this.blocks); - }); - test('Heal with bad checks', function() { - var blocks = this.blocks; - // A and C can't connect, but both can connect to B. - blocks.A.nextConnection.setCheck('type1'); - blocks.C.previousConnection.setCheck('type2'); - - // The types don't work. - blocks.B.dispose(true); - - assertDisposedHealFailed(blocks); - }); - test('C is Shadow', function() { - var blocks = this.blocks; - blocks.C.setShadow(true); - blocks.B.dispose(true); - // Even though we're asking to heal, it will appear as if it has not - // healed because shadows always get destroyed. - assertDisposedNoheal(blocks); - }); - }); - }); - suite('Remove Input', function() { + suite('Row', function() { setup(function() { - Blockly.defineBlocksWithJsonArray([ - { - "type": "value_block", - "message0": "%1", - "args0": [ - { - "type": "input_value", - "name": "VALUE" - } - ] - }, - { - "type": "statement_block", - "message0": "%1", - "args0": [ - { - "type": "input_statement", - "name": "STATEMENT" - } - ] - }, - ]); - }); - teardown(function() { - delete Blockly.Blocks['value_block']; - delete Blockly.Blocks['statement_block']; + var blockA = this.workspace.newBlock('row_block'); + var blockB = this.workspace.newBlock('row_block'); + var blockC = this.workspace.newBlock('row_block'); + + blockA.inputList[0].connection.connect(blockB.outputConnection); + blockB.inputList[0].connection.connect(blockC.outputConnection); + + assertEquals(blockB, blockC.getParent()); + + this.blocks = { + A: blockA, + B: blockB, + C: blockC + }; }); - suite('Value', function() { - setup(function() { - this.blockA = this.workspace.newBlock('value_block'); - }); - - test('No Connected', function() { - this.blockA.removeInput('VALUE'); - chai.assert.isNull(this.blockA.getInput('VALUE')); - }); - test('Block Connected', function() { - var blockB = this.workspace.newBlock('row_block'); - this.blockA.getInput('VALUE').connection - .connect(blockB.outputConnection); - - this.blockA.removeInput('VALUE'); - chai.assert.isFalse(blockB.disposed); - chai.assert.equal(this.blockA.getChildren().length, 0); - }); - test('Shadow Connected', function() { - var blockB = this.workspace.newBlock('row_block'); - blockB.setShadow(true); - this.blockA.getInput('VALUE').connection - .connect(blockB.outputConnection); - - this.blockA.removeInput('VALUE'); - chai.assert.isTrue(blockB.disposed); - chai.assert.equal(this.blockA.getChildren().length, 0); - }); + test('Don\'t heal', function() { + this.blocks.B.unplug(false); + assertUnpluggedNoheal(this.blocks); }); - suite('Statement', function() { - setup(function() { - this.blockA = this.workspace.newBlock('statement_block'); - }); + test('Heal', function() { + this.blocks.B.unplug(true); + // Each block has only one input, and the types work. + assertUnpluggedHealed(this.blocks); + }); + test('Heal with bad checks', function() { + var blocks = this.blocks; - test('No Connected', function() { - this.blockA.removeInput('STATEMENT'); - chai.assert.isNull(this.blockA.getInput('STATEMENT')); - }); - test('Block Connected', function() { - var blockB = this.workspace.newBlock('stack_block'); - this.blockA.getInput('STATEMENT').connection - .connect(blockB.previousConnection); + // A and C can't connect, but both can connect to B. + blocks.A.inputList[0].connection.setCheck('type1'); + blocks.C.outputConnection.setCheck('type2'); - this.blockA.removeInput('STATEMENT'); - chai.assert.isFalse(blockB.disposed); - chai.assert.equal(this.blockA.getChildren().length, 0); - }); - test('Shadow Connected', function() { - var blockB = this.workspace.newBlock('stack_block'); - blockB.setShadow(true); - this.blockA.getInput('STATEMENT').connection - .connect(blockB.previousConnection); - - this.blockA.removeInput('STATEMENT'); - chai.assert.isTrue(blockB.disposed); - chai.assert.equal(this.blockA.getChildren().length, 0); - }); + // Each block has only one input, but the types don't work. + blocks.B.unplug(true); + assertUnpluggedHealFailed(blocks); + }); + test('A has multiple inputs', function() { + var blocks = this.blocks; + // Add extra input to parent + blocks.A.appendValueInput("INPUT").setCheck(null); + blocks.B.unplug(true); + assertUnpluggedHealed(blocks); + }); + test('B has multiple inputs', function() { + var blocks = this.blocks; + // Add extra input to middle block + blocks.B.appendValueInput("INPUT").setCheck(null); + blocks.B.unplug(true); + assertUnpluggedHealed(blocks); + }); + test('C has multiple inputs', function() { + var blocks = this.blocks; + // Add extra input to child block + blocks.C.appendValueInput("INPUT").setCheck(null); + // Child block input count doesn't matter. + blocks.B.unplug(true); + assertUnpluggedHealed(blocks); + }); + test('C is Shadow', function() { + var blocks = this.blocks; + blocks.C.setShadow(true); + blocks.B.unplug(true); + // Even though we're asking to heal, it will appear as if it has not + // healed because shadows always stay with the parent. + assertUnpluggedNoheal(blocks); }); }); - suite('Connection Tracking', function() { + suite('Stack', function() { setup(function() { - this.workspace.dispose(); - // The new rendered workspace will get disposed by the parent teardown. - this.workspace = Blockly.inject('blocklyDiv'); + var blockA = this.workspace.newBlock('stack_block'); + var blockB = this.workspace.newBlock('stack_block'); + var blockC = this.workspace.newBlock('stack_block'); - this.getInputs = function() { - return this.workspace - .connectionDBList[Blockly.INPUT_VALUE].connections_; - }; - this.getOutputs = function() { - return this.workspace - .connectionDBList[Blockly.OUTPUT_VALUE].connections_; - }; - this.getNext = function() { - return this.workspace - .connectionDBList[Blockly.NEXT_STATEMENT].connections_; - }; - this.getPrevious = function() { - return this.workspace - .connectionDBList[Blockly.PREVIOUS_STATEMENT].connections_; - }; + blockA.nextConnection.connect(blockB.previousConnection); + blockB.nextConnection.connect(blockC.previousConnection); - this.assertConnectionsEmpty = function() { - chai.assert.isEmpty(this.getInputs()); - chai.assert.isEmpty(this.getOutputs()); - chai.assert.isEmpty(this.getNext()); - chai.assert.isEmpty(this.getPrevious()); - }; + assertEquals(blockB, blockC.getParent()); - this.clock = sinon.useFakeTimers(); + this.blocks = { + A: blockA, + B: blockB, + C: blockC + }; }); - teardown(function() { - this.clock.restore(); + + test('Don\'t heal', function() { + this.blocks.B.unplug(); + assertUnpluggedNoheal(this.blocks); }); - suite('Deserialization', function() { - test('Stack', function() { - Blockly.Xml.appendDomToWorkspace(Blockly.Xml.textToDom( - '' + - ' ' + - '' - ), this.workspace); - this.assertConnectionsEmpty(); - this.clock.tick(1); - chai.assert.equal(this.getPrevious().length, 1); - chai.assert.equal(this.getNext().length, 1); - }); - test('Multi-Stack', function() { - Blockly.Xml.appendDomToWorkspace(Blockly.Xml.textToDom( - '' + - ' ' + - ' ' + - ' ' + - ' ' + - ' ' + - ' ' + - ' ' + - ' ' + - ' ' + - '' - ), this.workspace); - this.assertConnectionsEmpty(); - this.clock.tick(1); - chai.assert.equal(this.getPrevious().length, 3); - chai.assert.equal(this.getNext().length, 3); - }); - test('Collapsed Stack', function() { - Blockly.Xml.appendDomToWorkspace(Blockly.Xml.textToDom( - '' + - ' ' + - '' - ), this.workspace); - this.assertConnectionsEmpty(); - this.clock.tick(1); - chai.assert.equal(this.getPrevious().length, 1); - chai.assert.equal(this.getNext().length, 1); - }); - test('Collapsed Multi-Stack', function() { - Blockly.Xml.appendDomToWorkspace(Blockly.Xml.textToDom( - '' + - ' ' + - ' ' + - ' ' + - ' ' + - ' ' + - ' ' + - ' ' + - ' ' + - ' ' + - '' - ), this.workspace); - this.assertConnectionsEmpty(); - this.clock.tick(1); - chai.assert.equal(this.getPrevious().length, 3); - chai.assert.equal(this.getNext().length, 3); - }); - test('Row', function() { - Blockly.Xml.appendDomToWorkspace(Blockly.Xml.textToDom( - '' + - ' ' + - '' - ), this.workspace); - this.assertConnectionsEmpty(); - this.clock.tick(1); - chai.assert.equal(this.getOutputs().length, 1); - chai.assert.equal(this.getInputs().length, 1); - }); - test('Multi-Row', function() { - Blockly.Xml.appendDomToWorkspace(Blockly.Xml.textToDom( - '' + - ' ' + - ' ' + - ' ' + - ' ' + - ' ' + - ' ' + - ' ' + - ' ' + - ' ' + - '' - ), this.workspace); - this.assertConnectionsEmpty(); - this.clock.tick(1); - chai.assert.equal(this.getOutputs().length, 3); - chai.assert.equal(this.getInputs().length, 3); - }); - test('Collapsed Row', function() { - Blockly.Xml.appendDomToWorkspace(Blockly.Xml.textToDom( - '' + - ' ' + - '' - ), this.workspace); - this.assertConnectionsEmpty(); - this.clock.tick(1); - chai.assert.equal(this.getOutputs().length, 1); - chai.assert.equal(this.getInputs().length, 0); - }); - test('Collapsed Multi-Row', function() { - Blockly.Xml.appendDomToWorkspace(Blockly.Xml.textToDom( - '' + - ' ' + - ' ' + - ' ' + - ' ' + - ' ' + - ' ' + - ' ' + - ' ' + - ' ' + - '' - ), this.workspace); - this.assertConnectionsEmpty(); - this.clock.tick(1); - chai.assert.equal(this.getOutputs().length, 1); - chai.assert.equal(this.getInputs().length, 0); - }); - test('Collapsed Multi-Row Middle', function() { - Blockly.Xml.appendDomToWorkspace(Blockly.Xml.textToDom( - '' + - ' ' + - ' ' + - ' ' + - ' ' + - ' ' + - ' ' + - ' ' + - ' ' + - ' ' + - '' - ), this.workspace); - this.assertConnectionsEmpty(); - this.clock.tick(1); - chai.assert.equal(this.getOutputs().length, 2); - chai.assert.equal(this.getInputs().length, 1); - }); - test('Statement', function() { - Blockly.Xml.appendDomToWorkspace(Blockly.Xml.textToDom( - '' + - ' ' + - '' - ), this.workspace); - this.assertConnectionsEmpty(); - this.clock.tick(1); - chai.assert.equal(this.getPrevious().length, 1); - chai.assert.equal(this.getNext().length, 2); - }); - test('Multi-Statement', function() { - Blockly.Xml.appendDomToWorkspace(Blockly.Xml.textToDom( - '' + - ' ' + - ' ' + - ' ' + - ' ' + - ' ' + - ' ' + - ' ' + - ' ' + - ' ' + - '' - ), this.workspace); - this.assertConnectionsEmpty(); - this.clock.tick(1); - chai.assert.equal(this.getPrevious().length, 3); - chai.assert.equal(this.getNext().length, 6); - }); - test('Collapsed Statement', function() { - Blockly.Xml.appendDomToWorkspace(Blockly.Xml.textToDom( - '' + - ' ' + - '' - ), this.workspace); - this.assertConnectionsEmpty(); - this.clock.tick(1); - chai.assert.equal(this.getPrevious().length, 1); - chai.assert.equal(this.getNext().length, 1); - }); - test('Collapsed Multi-Statement', function() { - Blockly.Xml.appendDomToWorkspace(Blockly.Xml.textToDom( - '' + - ' ' + - ' ' + - ' ' + - ' ' + - ' ' + - ' ' + - ' ' + - ' ' + - ' ' + - '' - ), this.workspace); - this.assertConnectionsEmpty(); - this.clock.tick(1); - chai.assert.equal(this.getPrevious().length, 1); - chai.assert.equal(this.getNext().length, 1); - }); - test('Collapsed Multi-Statement Middle', function() { - Blockly.Xml.appendDomToWorkspace(Blockly.Xml.textToDom( - '' + - ' ' + - ' ' + - ' ' + - ' ' + - ' ' + - ' ' + - ' ' + - ' ' + - ' ' + - '' - ), this.workspace); - this.assertConnectionsEmpty(); - this.clock.tick(1); - chai.assert.equal(this.getPrevious().length, 2); - chai.assert.equal(this.getNext().length, 3); - }); + test('Heal', function() { + this.blocks.B.unplug(true); + assertUnpluggedHealed(this.blocks); }); - suite('Programmatic Block Creation', function() { - test('Stack', function() { - var block = this.workspace.newBlock('stack_block'); - this.assertConnectionsEmpty(); - block.initSvg(); - block.render(); + test('Heal with bad checks', function() { + var blocks = this.blocks; + // A and C can't connect, but both can connect to B. + blocks.A.nextConnection.setCheck('type1'); + blocks.C.previousConnection.setCheck('type2'); - chai.assert.equal(this.getPrevious().length, 1); - chai.assert.equal(this.getNext().length, 1); - }); - test('Row', function() { - var block = this.workspace.newBlock('row_block'); - this.assertConnectionsEmpty(); - block.initSvg(); - block.render(); + // The types don't work. + blocks.B.unplug(true); - chai.assert.equal(this.getOutputs().length, 1); - chai.assert.equal(this.getInputs().length, 1); - }); - test('Statement', function() { - var block = this.workspace.newBlock('statement_block'); - this.assertConnectionsEmpty(); - block.initSvg(); - block.render(); - - chai.assert.equal(this.getPrevious().length, 1); - chai.assert.equal(this.getNext().length, 2); - }); + assertUnpluggedHealFailed(blocks); }); - suite('setCollapsed', function() { - test('Stack', function() { - var block = Blockly.Xml.domToBlock(Blockly.Xml.textToDom( - '' - ), this.workspace); - this.clock.tick(1); - chai.assert.equal(this.getPrevious().length, 1); - chai.assert.equal(this.getNext().length, 1); + test('C is Shadow', function() { + var blocks = this.blocks; + blocks.C.setShadow(true); + blocks.B.unplug(true); + // Even though we're asking to heal, it will appear as if it has not + // healed because shadows always stay with the parent. + assertUnpluggedNoheal(blocks); + }); + }); + }); + suite('Dispose', function() { + function assertDisposedNoheal(blocks) { + chai.assert.isFalse(blocks.A.disposed); + // A has nothing connected to it. + chai.assert.equal(0, blocks.A.getChildren().length); + // B is disposed. + chai.assert.isTrue(blocks.B.disposed); + // And C is disposed. + chai.assert.isTrue(blocks.C.disposed); + } + function assertDisposedHealed(blocks) { + chai.assert.isFalse(blocks.A.disposed); + chai.assert.isFalse(blocks.C.disposed); + // A and C are connected. + assertEquals(1, blocks.A.getChildren().length); + assertEquals(blocks.A, blocks.C.getParent()); + // B is disposed. + chai.assert.isTrue(blocks.B.disposed); + } + function assertDisposedHealFailed(blocks) { + chai.assert.isFalse(blocks.A.disposed); + chai.assert.isFalse(blocks.C.disposed); + // A has nothing connected to it. + chai.assert.equal(0, blocks.A.getChildren().length); + // B is disposed. + chai.assert.isTrue(blocks.B.disposed); + // C is the top of its stack. + assertNull(blocks.C.getParent()); + } - block.setCollapsed(true); - chai.assert.equal(this.getPrevious().length, 1); - chai.assert.equal(this.getNext().length, 1); + suite('Row', function() { + setup(function() { + var blockA = this.workspace.newBlock('row_block'); + var blockB = this.workspace.newBlock('row_block'); + var blockC = this.workspace.newBlock('row_block'); - block.setCollapsed(false); - chai.assert.equal(this.getPrevious().length, 1); - chai.assert.equal(this.getNext().length, 1); - }); - test('Multi-Stack', function() { - var block = Blockly.Xml.domToBlock(Blockly.Xml.textToDom( - '' + - ' ' + - ' ' + - ' ' + - ' ' + - ' ' + - ' ' + - ' ' + - '' - ), this.workspace); - this.assertConnectionsEmpty(); - this.clock.tick(1); - chai.assert.equal(this.getPrevious().length, 3); - chai.assert.equal(this.getNext().length, 3); + blockA.inputList[0].connection.connect(blockB.outputConnection); + blockB.inputList[0].connection.connect(blockC.outputConnection); - block.setCollapsed(true); - chai.assert.equal(this.getPrevious().length, 3); - chai.assert.equal(this.getNext().length, 3); + assertEquals(blockB, blockC.getParent()); - block.setCollapsed(false); - chai.assert.equal(this.getPrevious().length, 3); - chai.assert.equal(this.getNext().length, 3); - }); - test('Row', function() { - var block = Blockly.Xml.domToBlock(Blockly.Xml.textToDom( - '' - ), this.workspace); - this.clock.tick(1); - chai.assert.equal(this.getOutputs().length, 1); - chai.assert.equal(this.getInputs().length, 1); + this.blocks = { + A: blockA, + B: blockB, + C: blockC + }; + }); - block.setCollapsed(true); - chai.assert.equal(this.getOutputs().length, 1); - chai.assert.equal(this.getInputs().length, 0); + test('Don\'t heal', function() { + this.blocks.B.dispose(false); + assertDisposedNoheal(this.blocks); + }); + test('Heal', function() { + this.blocks.B.dispose(true); + // Each block has only one input, and the types work. + assertDisposedHealed(this.blocks); + }); + test('Heal with bad checks', function() { + var blocks = this.blocks; - block.setCollapsed(false); - chai.assert.equal(this.getOutputs().length, 1); - chai.assert.equal(this.getInputs().length, 1); - }); - test('Multi-Row', function() { - var block = Blockly.Xml.domToBlock(Blockly.Xml.textToDom( - '' + - ' ' + - ' ' + - ' ' + - ' ' + - ' ' + - ' ' + - ' ' + - '' - ), this.workspace); - this.clock.tick(1); - chai.assert.equal(this.getOutputs().length, 3); - chai.assert.equal(this.getInputs().length, 3); + // A and C can't connect, but both can connect to B. + blocks.A.inputList[0].connection.setCheck('type1'); + blocks.C.outputConnection.setCheck('type2'); - block.setCollapsed(true); - chai.assert.equal(this.getOutputs().length, 1); - chai.assert.equal(this.getInputs().length, 0); + // Each block has only one input, but the types don't work. + blocks.B.dispose(true); + assertDisposedHealFailed(blocks); + }); + test('A has multiple inputs', function() { + var blocks = this.blocks; + // Add extra input to parent + blocks.A.appendValueInput("INPUT").setCheck(null); + blocks.B.dispose(true); + assertDisposedHealed(blocks); + }); + test('B has multiple inputs', function() { + var blocks = this.blocks; + // Add extra input to middle block + blocks.B.appendValueInput("INPUT").setCheck(null); + blocks.B.dispose(true); + assertDisposedHealed(blocks); + }); + test('C has multiple inputs', function() { + var blocks = this.blocks; + // Add extra input to child block + blocks.C.appendValueInput("INPUT").setCheck(null); + // Child block input count doesn't matter. + blocks.B.dispose(true); + assertDisposedHealed(blocks); + }); + test('C is Shadow', function() { + var blocks = this.blocks; + blocks.C.setShadow(true); + blocks.B.dispose(true); + // Even though we're asking to heal, it will appear as if it has not + // healed because shadows always get destroyed. + assertDisposedNoheal(blocks); + }); + }); + suite('Stack', function() { + setup(function() { + var blockA = this.workspace.newBlock('stack_block'); + var blockB = this.workspace.newBlock('stack_block'); + var blockC = this.workspace.newBlock('stack_block'); - block.setCollapsed(false); - chai.assert.equal(this.getOutputs().length, 3); - chai.assert.equal(this.getInputs().length, 3); - }); - test('Multi-Row Middle', function() { - var block = Blockly.Xml.domToBlock(Blockly.Xml.textToDom( - '' + - ' ' + - ' ' + - ' ' + - ' ' + - ' ' + - ' ' + - ' ' + - '' - ), this.workspace); - this.clock.tick(1); - chai.assert.equal(this.getOutputs().length, 3); - chai.assert.equal(this.getInputs().length, 3); + blockA.nextConnection.connect(blockB.previousConnection); + blockB.nextConnection.connect(blockC.previousConnection); - block = block.getInputTargetBlock('INPUT'); - block.setCollapsed(true); - chai.assert.equal(this.getOutputs().length, 2); - chai.assert.equal(this.getInputs().length, 1); + assertEquals(blockB, blockC.getParent()); - block.setCollapsed(false); - chai.assert.equal(this.getOutputs().length, 3); - chai.assert.equal(this.getInputs().length, 3); - }); - test('Multi-Row Double Collapse', function() { - // Collapse middle -> Collapse top -> - // Uncollapse top -> Uncollapse middle - var block = Blockly.Xml.domToBlock(Blockly.Xml.textToDom( - '' + - ' ' + - ' ' + - ' ' + - ' ' + - ' ' + - ' ' + - ' ' + - '' - ), this.workspace); - this.clock.tick(1); - chai.assert.equal(this.getOutputs().length, 3); - chai.assert.equal(this.getInputs().length, 3); + this.blocks = { + A: blockA, + B: blockB, + C: blockC + }; + }); - var middleBlock = block.getInputTargetBlock('INPUT'); - middleBlock.setCollapsed(true); - chai.assert.equal(this.getOutputs().length, 2); - chai.assert.equal(this.getInputs().length, 1); + test('Don\'t heal', function() { + this.blocks.B.dispose(); + assertDisposedNoheal(this.blocks); + }); + test('Heal', function() { + this.blocks.B.dispose(true); + assertDisposedHealed(this.blocks); + }); + test('Heal with bad checks', function() { + var blocks = this.blocks; + // A and C can't connect, but both can connect to B. + blocks.A.nextConnection.setCheck('type1'); + blocks.C.previousConnection.setCheck('type2'); - block.setCollapsed(true); - chai.assert.equal(this.getOutputs().length, 1); - chai.assert.equal(this.getInputs().length, 0); + // The types don't work. + blocks.B.dispose(true); - block.setCollapsed(false); - chai.assert.equal(this.getOutputs().length, 2); - chai.assert.equal(this.getInputs().length, 1); + assertDisposedHealFailed(blocks); + }); + test('C is Shadow', function() { + var blocks = this.blocks; + blocks.C.setShadow(true); + blocks.B.dispose(true); + // Even though we're asking to heal, it will appear as if it has not + // healed because shadows always get destroyed. + assertDisposedNoheal(blocks); + }); + }); + }); + suite('Remove Input', function() { + setup(function() { + Blockly.defineBlocksWithJsonArray([ + { + "type": "value_block", + "message0": "%1", + "args0": [ + { + "type": "input_value", + "name": "VALUE" + } + ] + }, + { + "type": "statement_block", + "message0": "%1", + "args0": [ + { + "type": "input_statement", + "name": "STATEMENT" + } + ] + }, + ]); + }); + teardown(function() { + delete Blockly.Blocks['value_block']; + delete Blockly.Blocks['statement_block']; + }); - middleBlock.setCollapsed(false); - chai.assert.equal(this.getOutputs().length, 3); - chai.assert.equal(this.getInputs().length, 3); - }); - test('Statement', function() { - var block = Blockly.Xml.domToBlock(Blockly.Xml.textToDom( - '' - ), this.workspace); - this.clock.tick(1); - chai.assert.equal(this.getPrevious().length, 1); - chai.assert.equal(this.getNext().length, 2); + suite('Value', function() { + setup(function() { + this.blockA = this.workspace.newBlock('value_block'); + }); - block.setCollapsed(true); - chai.assert.equal(this.getPrevious().length, 1); - chai.assert.equal(this.getNext().length, 1); + test('No Connected', function() { + this.blockA.removeInput('VALUE'); + chai.assert.isNull(this.blockA.getInput('VALUE')); + }); + test('Block Connected', function() { + var blockB = this.workspace.newBlock('row_block'); + this.blockA.getInput('VALUE').connection + .connect(blockB.outputConnection); - block.setCollapsed(false); - chai.assert.equal(this.getPrevious().length, 1); - chai.assert.equal(this.getNext().length, 2); - }); - test('Multi-Statement', function() { - var block = Blockly.Xml.domToBlock(Blockly.Xml.textToDom( - '' + - ' ' + - ' ' + - ' ' + - ' ' + - ' ' + - ' ' + - ' ' + - '' - ), this.workspace); - this.assertConnectionsEmpty(); - this.clock.tick(1); - chai.assert.equal(this.getPrevious().length, 3); - chai.assert.equal(this.getNext().length, 6); + this.blockA.removeInput('VALUE'); + chai.assert.isFalse(blockB.disposed); + chai.assert.equal(this.blockA.getChildren().length, 0); + }); + test('Shadow Connected', function() { + var blockB = this.workspace.newBlock('row_block'); + blockB.setShadow(true); + this.blockA.getInput('VALUE').connection + .connect(blockB.outputConnection); - block.setCollapsed(true); - chai.assert.equal(this.getPrevious().length, 1); - chai.assert.equal(this.getNext().length, 1); + this.blockA.removeInput('VALUE'); + chai.assert.isTrue(blockB.disposed); + chai.assert.equal(this.blockA.getChildren().length, 0); + }); + }); + suite('Statement', function() { + setup(function() { + this.blockA = this.workspace.newBlock('statement_block'); + }); - block.setCollapsed(false); - chai.assert.equal(this.getPrevious().length, 3); - chai.assert.equal(this.getNext().length, 6); - }); - test('Multi-Statement Middle', function() { - var block = Blockly.Xml.domToBlock(Blockly.Xml.textToDom( - '' + - ' ' + - ' ' + - ' ' + - ' ' + - ' ' + - ' ' + - ' ' + - '' - ), this.workspace); - this.assertConnectionsEmpty(); - this.clock.tick(1); - chai.assert.equal(this.getPrevious().length, 3); - chai.assert.equal(this.getNext().length, 6); + test('No Connected', function() { + this.blockA.removeInput('STATEMENT'); + chai.assert.isNull(this.blockA.getInput('STATEMENT')); + }); + test('Block Connected', function() { + var blockB = this.workspace.newBlock('stack_block'); + this.blockA.getInput('STATEMENT').connection + .connect(blockB.previousConnection); - block = block.getInputTargetBlock('STATEMENT'); - block.setCollapsed(true); - chai.assert.equal(this.getPrevious().length, 2); - chai.assert.equal(this.getNext().length, 3); + this.blockA.removeInput('STATEMENT'); + chai.assert.isFalse(blockB.disposed); + chai.assert.equal(this.blockA.getChildren().length, 0); + }); + test('Shadow Connected', function() { + var blockB = this.workspace.newBlock('stack_block'); + blockB.setShadow(true); + this.blockA.getInput('STATEMENT').connection + .connect(blockB.previousConnection); - block.setCollapsed(false); - chai.assert.equal(this.getPrevious().length, 3); - chai.assert.equal(this.getNext().length, 6); - }); - test('Multi-Statement Double Collapse', function() { - var block = Blockly.Xml.domToBlock(Blockly.Xml.textToDom( - '' + - ' ' + - ' ' + - ' ' + - ' ' + - ' ' + - ' ' + - ' ' + - '' - ), this.workspace); - this.assertConnectionsEmpty(); - this.clock.tick(1); - chai.assert.equal(this.getPrevious().length, 3); - chai.assert.equal(this.getNext().length, 6); + this.blockA.removeInput('STATEMENT'); + chai.assert.isTrue(blockB.disposed); + chai.assert.equal(this.blockA.getChildren().length, 0); + }); + }); + }); + suite('Connection Tracking', function() { + setup(function() { + this.workspace.dispose(); + // The new rendered workspace will get disposed by the parent teardown. + this.workspace = Blockly.inject('blocklyDiv'); - var middleBlock = block.getInputTargetBlock('STATEMENT'); - middleBlock.setCollapsed(true); - chai.assert.equal(this.getPrevious().length, 2); - chai.assert.equal(this.getNext().length, 3); + this.getInputs = function() { + return this.workspace + .connectionDBList[Blockly.INPUT_VALUE].connections_; + }; + this.getOutputs = function() { + return this.workspace + .connectionDBList[Blockly.OUTPUT_VALUE].connections_; + }; + this.getNext = function() { + return this.workspace + .connectionDBList[Blockly.NEXT_STATEMENT].connections_; + }; + this.getPrevious = function() { + return this.workspace + .connectionDBList[Blockly.PREVIOUS_STATEMENT].connections_; + }; - block.setCollapsed(true); - chai.assert.equal(this.getPrevious().length, 1); - chai.assert.equal(this.getNext().length, 1); + this.assertConnectionsEmpty = function() { + chai.assert.isEmpty(this.getInputs()); + chai.assert.isEmpty(this.getOutputs()); + chai.assert.isEmpty(this.getNext()); + chai.assert.isEmpty(this.getPrevious()); + }; - block.setCollapsed(false); - chai.assert.equal(this.getPrevious().length, 2); - chai.assert.equal(this.getNext().length, 3); + this.clock = sinon.useFakeTimers(); + }); + teardown(function() { + this.clock.restore(); + }); + suite('Deserialization', function() { + test('Stack', function() { + Blockly.Xml.appendDomToWorkspace(Blockly.Xml.textToDom( + '' + + ' ' + + '' + ), this.workspace); + this.assertConnectionsEmpty(); + this.clock.tick(1); + chai.assert.equal(this.getPrevious().length, 1); + chai.assert.equal(this.getNext().length, 1); + }); + test('Multi-Stack', function() { + Blockly.Xml.appendDomToWorkspace(Blockly.Xml.textToDom( + '' + + ' ' + + ' ' + + ' ' + + ' ' + + ' ' + + ' ' + + ' ' + + ' ' + + ' ' + + '' + ), this.workspace); + this.assertConnectionsEmpty(); + this.clock.tick(1); + chai.assert.equal(this.getPrevious().length, 3); + chai.assert.equal(this.getNext().length, 3); + }); + test('Collapsed Stack', function() { + Blockly.Xml.appendDomToWorkspace(Blockly.Xml.textToDom( + '' + + ' ' + + '' + ), this.workspace); + this.assertConnectionsEmpty(); + this.clock.tick(1); + chai.assert.equal(this.getPrevious().length, 1); + chai.assert.equal(this.getNext().length, 1); + }); + test('Collapsed Multi-Stack', function() { + Blockly.Xml.appendDomToWorkspace(Blockly.Xml.textToDom( + '' + + ' ' + + ' ' + + ' ' + + ' ' + + ' ' + + ' ' + + ' ' + + ' ' + + ' ' + + '' + ), this.workspace); + this.assertConnectionsEmpty(); + this.clock.tick(1); + chai.assert.equal(this.getPrevious().length, 3); + chai.assert.equal(this.getNext().length, 3); + }); + test('Row', function() { + Blockly.Xml.appendDomToWorkspace(Blockly.Xml.textToDom( + '' + + ' ' + + '' + ), this.workspace); + this.assertConnectionsEmpty(); + this.clock.tick(1); + chai.assert.equal(this.getOutputs().length, 1); + chai.assert.equal(this.getInputs().length, 1); + }); + test('Multi-Row', function() { + Blockly.Xml.appendDomToWorkspace(Blockly.Xml.textToDom( + '' + + ' ' + + ' ' + + ' ' + + ' ' + + ' ' + + ' ' + + ' ' + + ' ' + + ' ' + + '' + ), this.workspace); + this.assertConnectionsEmpty(); + this.clock.tick(1); + chai.assert.equal(this.getOutputs().length, 3); + chai.assert.equal(this.getInputs().length, 3); + }); + test('Collapsed Row', function() { + Blockly.Xml.appendDomToWorkspace(Blockly.Xml.textToDom( + '' + + ' ' + + '' + ), this.workspace); + this.assertConnectionsEmpty(); + this.clock.tick(1); + chai.assert.equal(this.getOutputs().length, 1); + chai.assert.equal(this.getInputs().length, 0); + }); + test('Collapsed Multi-Row', function() { + Blockly.Xml.appendDomToWorkspace(Blockly.Xml.textToDom( + '' + + ' ' + + ' ' + + ' ' + + ' ' + + ' ' + + ' ' + + ' ' + + ' ' + + ' ' + + '' + ), this.workspace); + this.assertConnectionsEmpty(); + this.clock.tick(1); + chai.assert.equal(this.getOutputs().length, 1); + chai.assert.equal(this.getInputs().length, 0); + }); + test('Collapsed Multi-Row Middle', function() { + Blockly.Xml.appendDomToWorkspace(Blockly.Xml.textToDom( + '' + + ' ' + + ' ' + + ' ' + + ' ' + + ' ' + + ' ' + + ' ' + + ' ' + + ' ' + + '' + ), this.workspace); + this.assertConnectionsEmpty(); + this.clock.tick(1); + chai.assert.equal(this.getOutputs().length, 2); + chai.assert.equal(this.getInputs().length, 1); + }); + test('Statement', function() { + Blockly.Xml.appendDomToWorkspace(Blockly.Xml.textToDom( + '' + + ' ' + + '' + ), this.workspace); + this.assertConnectionsEmpty(); + this.clock.tick(1); + chai.assert.equal(this.getPrevious().length, 1); + chai.assert.equal(this.getNext().length, 2); + }); + test('Multi-Statement', function() { + Blockly.Xml.appendDomToWorkspace(Blockly.Xml.textToDom( + '' + + ' ' + + ' ' + + ' ' + + ' ' + + ' ' + + ' ' + + ' ' + + ' ' + + ' ' + + '' + ), this.workspace); + this.assertConnectionsEmpty(); + this.clock.tick(1); + chai.assert.equal(this.getPrevious().length, 3); + chai.assert.equal(this.getNext().length, 6); + }); + test('Collapsed Statement', function() { + Blockly.Xml.appendDomToWorkspace(Blockly.Xml.textToDom( + '' + + ' ' + + '' + ), this.workspace); + this.assertConnectionsEmpty(); + this.clock.tick(1); + chai.assert.equal(this.getPrevious().length, 1); + chai.assert.equal(this.getNext().length, 1); + }); + test('Collapsed Multi-Statement', function() { + Blockly.Xml.appendDomToWorkspace(Blockly.Xml.textToDom( + '' + + ' ' + + ' ' + + ' ' + + ' ' + + ' ' + + ' ' + + ' ' + + ' ' + + ' ' + + '' + ), this.workspace); + this.assertConnectionsEmpty(); + this.clock.tick(1); + chai.assert.equal(this.getPrevious().length, 1); + chai.assert.equal(this.getNext().length, 1); + }); + test('Collapsed Multi-Statement Middle', function() { + Blockly.Xml.appendDomToWorkspace(Blockly.Xml.textToDom( + '' + + ' ' + + ' ' + + ' ' + + ' ' + + ' ' + + ' ' + + ' ' + + ' ' + + ' ' + + '' + ), this.workspace); + this.assertConnectionsEmpty(); + this.clock.tick(1); + chai.assert.equal(this.getPrevious().length, 2); + chai.assert.equal(this.getNext().length, 3); + }); + }); + suite('Programmatic Block Creation', function() { + test('Stack', function() { + var block = this.workspace.newBlock('stack_block'); + this.assertConnectionsEmpty(); + block.initSvg(); + block.render(); - middleBlock.setCollapsed(false); - chai.assert.equal(this.getPrevious().length, 3); - chai.assert.equal(this.getNext().length, 6); - }); + chai.assert.equal(this.getPrevious().length, 1); + chai.assert.equal(this.getNext().length, 1); + }); + test('Row', function() { + var block = this.workspace.newBlock('row_block'); + this.assertConnectionsEmpty(); + block.initSvg(); + block.render(); + + chai.assert.equal(this.getOutputs().length, 1); + chai.assert.equal(this.getInputs().length, 1); + }); + test('Statement', function() { + var block = this.workspace.newBlock('statement_block'); + this.assertConnectionsEmpty(); + block.initSvg(); + block.render(); + + chai.assert.equal(this.getPrevious().length, 1); + chai.assert.equal(this.getNext().length, 2); + }); + }); + suite('setCollapsed', function() { + test('Stack', function() { + var block = Blockly.Xml.domToBlock(Blockly.Xml.textToDom( + '' + ), this.workspace); + this.clock.tick(1); + chai.assert.equal(this.getPrevious().length, 1); + chai.assert.equal(this.getNext().length, 1); + + block.setCollapsed(true); + chai.assert.equal(this.getPrevious().length, 1); + chai.assert.equal(this.getNext().length, 1); + + block.setCollapsed(false); + chai.assert.equal(this.getPrevious().length, 1); + chai.assert.equal(this.getNext().length, 1); + }); + test('Multi-Stack', function() { + var block = Blockly.Xml.domToBlock(Blockly.Xml.textToDom( + '' + + ' ' + + ' ' + + ' ' + + ' ' + + ' ' + + ' ' + + ' ' + + '' + ), this.workspace); + this.assertConnectionsEmpty(); + this.clock.tick(1); + chai.assert.equal(this.getPrevious().length, 3); + chai.assert.equal(this.getNext().length, 3); + + block.setCollapsed(true); + chai.assert.equal(this.getPrevious().length, 3); + chai.assert.equal(this.getNext().length, 3); + + block.setCollapsed(false); + chai.assert.equal(this.getPrevious().length, 3); + chai.assert.equal(this.getNext().length, 3); + }); + test('Row', function() { + var block = Blockly.Xml.domToBlock(Blockly.Xml.textToDom( + '' + ), this.workspace); + this.clock.tick(1); + chai.assert.equal(this.getOutputs().length, 1); + chai.assert.equal(this.getInputs().length, 1); + + block.setCollapsed(true); + chai.assert.equal(this.getOutputs().length, 1); + chai.assert.equal(this.getInputs().length, 0); + + block.setCollapsed(false); + chai.assert.equal(this.getOutputs().length, 1); + chai.assert.equal(this.getInputs().length, 1); + }); + test('Multi-Row', function() { + var block = Blockly.Xml.domToBlock(Blockly.Xml.textToDom( + '' + + ' ' + + ' ' + + ' ' + + ' ' + + ' ' + + ' ' + + ' ' + + '' + ), this.workspace); + this.clock.tick(1); + chai.assert.equal(this.getOutputs().length, 3); + chai.assert.equal(this.getInputs().length, 3); + + block.setCollapsed(true); + chai.assert.equal(this.getOutputs().length, 1); + chai.assert.equal(this.getInputs().length, 0); + + block.setCollapsed(false); + chai.assert.equal(this.getOutputs().length, 3); + chai.assert.equal(this.getInputs().length, 3); + }); + test('Multi-Row Middle', function() { + var block = Blockly.Xml.domToBlock(Blockly.Xml.textToDom( + '' + + ' ' + + ' ' + + ' ' + + ' ' + + ' ' + + ' ' + + ' ' + + '' + ), this.workspace); + this.clock.tick(1); + chai.assert.equal(this.getOutputs().length, 3); + chai.assert.equal(this.getInputs().length, 3); + + block = block.getInputTargetBlock('INPUT'); + block.setCollapsed(true); + chai.assert.equal(this.getOutputs().length, 2); + chai.assert.equal(this.getInputs().length, 1); + + block.setCollapsed(false); + chai.assert.equal(this.getOutputs().length, 3); + chai.assert.equal(this.getInputs().length, 3); + }); + test('Multi-Row Double Collapse', function() { + // Collapse middle -> Collapse top -> + // Uncollapse top -> Uncollapse middle + var block = Blockly.Xml.domToBlock(Blockly.Xml.textToDom( + '' + + ' ' + + ' ' + + ' ' + + ' ' + + ' ' + + ' ' + + ' ' + + '' + ), this.workspace); + this.clock.tick(1); + chai.assert.equal(this.getOutputs().length, 3); + chai.assert.equal(this.getInputs().length, 3); + + var middleBlock = block.getInputTargetBlock('INPUT'); + middleBlock.setCollapsed(true); + chai.assert.equal(this.getOutputs().length, 2); + chai.assert.equal(this.getInputs().length, 1); + + block.setCollapsed(true); + chai.assert.equal(this.getOutputs().length, 1); + chai.assert.equal(this.getInputs().length, 0); + + block.setCollapsed(false); + chai.assert.equal(this.getOutputs().length, 2); + chai.assert.equal(this.getInputs().length, 1); + + middleBlock.setCollapsed(false); + chai.assert.equal(this.getOutputs().length, 3); + chai.assert.equal(this.getInputs().length, 3); + }); + test('Statement', function() { + var block = Blockly.Xml.domToBlock(Blockly.Xml.textToDom( + '' + ), this.workspace); + this.clock.tick(1); + chai.assert.equal(this.getPrevious().length, 1); + chai.assert.equal(this.getNext().length, 2); + + block.setCollapsed(true); + chai.assert.equal(this.getPrevious().length, 1); + chai.assert.equal(this.getNext().length, 1); + + block.setCollapsed(false); + chai.assert.equal(this.getPrevious().length, 1); + chai.assert.equal(this.getNext().length, 2); + }); + test('Multi-Statement', function() { + var block = Blockly.Xml.domToBlock(Blockly.Xml.textToDom( + '' + + ' ' + + ' ' + + ' ' + + ' ' + + ' ' + + ' ' + + ' ' + + '' + ), this.workspace); + this.assertConnectionsEmpty(); + this.clock.tick(1); + chai.assert.equal(this.getPrevious().length, 3); + chai.assert.equal(this.getNext().length, 6); + + block.setCollapsed(true); + chai.assert.equal(this.getPrevious().length, 1); + chai.assert.equal(this.getNext().length, 1); + + block.setCollapsed(false); + chai.assert.equal(this.getPrevious().length, 3); + chai.assert.equal(this.getNext().length, 6); + }); + test('Multi-Statement Middle', function() { + var block = Blockly.Xml.domToBlock(Blockly.Xml.textToDom( + '' + + ' ' + + ' ' + + ' ' + + ' ' + + ' ' + + ' ' + + ' ' + + '' + ), this.workspace); + this.assertConnectionsEmpty(); + this.clock.tick(1); + chai.assert.equal(this.getPrevious().length, 3); + chai.assert.equal(this.getNext().length, 6); + + block = block.getInputTargetBlock('STATEMENT'); + block.setCollapsed(true); + chai.assert.equal(this.getPrevious().length, 2); + chai.assert.equal(this.getNext().length, 3); + + block.setCollapsed(false); + chai.assert.equal(this.getPrevious().length, 3); + chai.assert.equal(this.getNext().length, 6); + }); + test('Multi-Statement Double Collapse', function() { + var block = Blockly.Xml.domToBlock(Blockly.Xml.textToDom( + '' + + ' ' + + ' ' + + ' ' + + ' ' + + ' ' + + ' ' + + ' ' + + '' + ), this.workspace); + this.assertConnectionsEmpty(); + this.clock.tick(1); + chai.assert.equal(this.getPrevious().length, 3); + chai.assert.equal(this.getNext().length, 6); + + var middleBlock = block.getInputTargetBlock('STATEMENT'); + middleBlock.setCollapsed(true); + chai.assert.equal(this.getPrevious().length, 2); + chai.assert.equal(this.getNext().length, 3); + + block.setCollapsed(true); + chai.assert.equal(this.getPrevious().length, 1); + chai.assert.equal(this.getNext().length, 1); + + block.setCollapsed(false); + chai.assert.equal(this.getPrevious().length, 2); + chai.assert.equal(this.getNext().length, 3); + + middleBlock.setCollapsed(false); + chai.assert.equal(this.getPrevious().length, 3); + chai.assert.equal(this.getNext().length, 6); + }); + }); + suite('Remove Connections Programmatically', function() { + test('Output', function() { + var block = this.workspace.newBlock('row_block'); + block.initSvg(); + block.render(); + + block.setOutput(false); + + chai.assert.equal(this.getOutputs().length, 0); + chai.assert.equal(this.getInputs().length, 1); + }); + test('Value', function() { + var block = this.workspace.newBlock('row_block'); + block.initSvg(); + block.render(); + + block.removeInput('INPUT'); + + chai.assert.equal(this.getOutputs().length, 1); + chai.assert.equal(this.getInputs().length, 0); + }); + test('Previous', function() { + var block = this.workspace.newBlock('stack_block'); + block.initSvg(); + block.render(); + + block.setPreviousStatement(false); + + chai.assert.equal(this.getPrevious().length, 0); + chai.assert.equal(this.getNext().length, 1); + }); + test('Next', function() { + var block = this.workspace.newBlock('stack_block'); + block.initSvg(); + block.render(); + + block.setNextStatement(false); + + chai.assert.equal(this.getPrevious().length, 1); + chai.assert.equal(this.getNext().length, 0); + }); + test('Statement', function() { + var block = this.workspace.newBlock('statement_block'); + block.initSvg(); + block.render(); + + block.removeInput('STATEMENT'); + + chai.assert.equal(this.getPrevious().length, 1); + chai.assert.equal(this.getNext().length, 1); + }); + }); + suite('Add Connections Programmatically', function() { + test('Output', function() { + var block = this.workspace.newBlock('empty_block'); + block.initSvg(); + block.render(); + + block.setOutput(true); + + chai.assert.equal(this.getOutputs().length, 1); + }); + test('Value', function() { + var block = this.workspace.newBlock('empty_block'); + block.initSvg(); + block.render(); + + block.appendValueInput('INPUT'); + + chai.assert.equal(this.getInputs().length, 1); + }); + test('Previous', function() { + var block = this.workspace.newBlock('empty_block'); + block.initSvg(); + block.render(); + + block.setPreviousStatement(true); + + chai.assert.equal(this.getPrevious().length, 1); + }); + test('Next', function() { + var block = this.workspace.newBlock('empty_block'); + block.initSvg(); + block.render(); + + block.setNextStatement(true); + + chai.assert.equal(this.getNext().length, 1); + }); + test('Statement', function() { + var block = this.workspace.newBlock('empty_block'); + block.initSvg(); + block.render(); + + block.appendStatementInput('STATEMENT'); + + chai.assert.equal(this.getNext().length, 1); }); }); });