diff --git a/core/block.js b/core/block.js
index b5229edc5..cfeaddebd 100644
--- a/core/block.js
+++ b/core/block.js
@@ -742,25 +742,33 @@ Blockly.Block.prototype.getChildren = function(ordered) {
* @package
*/
Blockly.Block.prototype.setParent = function(newParent) {
- if (newParent == this.parentBlock_) {
+ if (newParent === this.parentBlock_) {
return;
}
+
+ // Check that block is connected to new parent if new parent is not null and
+ // that block is not connected to superior one if new parent is null.
+ var connection = this.previousConnection || this.outputConnection;
+ var isConnected = !!(connection && connection.targetBlock());
+
+ if (isConnected && newParent && connection.targetBlock() !== newParent) {
+ throw Error('Block connected to superior one that is not new parent.');
+ } else if (!isConnected && newParent) {
+ throw Error('Block not connected to new parent.');
+ } else if (isConnected && !newParent) {
+ throw Error('Cannot set parent to null while block is still connected to' +
+ ' superior block.');
+ }
+
if (this.parentBlock_) {
// Remove this block from the old parent's child list.
Blockly.utils.arrayRemove(this.parentBlock_.childBlocks_, this);
- // Disconnect from superior blocks.
- if (this.previousConnection && this.previousConnection.isConnected()) {
- throw Error('Still connected to previous block.');
- }
- if (this.outputConnection && this.outputConnection.isConnected()) {
- throw Error('Still connected to parent block.');
- }
- this.parentBlock_ = null;
// This block hasn't actually moved on-screen, so there's no need to update
- // its connection locations.
+ // its connection locations.
} else {
- // Remove this block from the workspace's list of top-most blocks.
+ // New parent must be non-null so remove this block from the workspace's
+ // list of top-most blocks.
this.workspace.removeTopBlock(this);
}
diff --git a/tests/mocha/block_test.js b/tests/mocha/block_test.js
index cf602a386..7c841a134 100644
--- a/tests/mocha/block_test.js
+++ b/tests/mocha/block_test.js
@@ -890,6 +890,96 @@ suite('Blocks', function() {
chai.assert.equal(this.getNext().length, 6);
});
});
+ suite('Setting Parent Block', function() {
+ setup(function() {
+ this.printBlock = Blockly.Xml.domToBlock(Blockly.Xml.textToDom(
+ '' +
+ ' ' +
+ ' ' +
+ ' ' +
+ ' ' +
+ ' ' +
+ ' ' +
+ ' ' +
+ ' ' +
+ ' ' +
+ ''
+ ), this.workspace);
+ this.textJoinBlock = this.printBlock.getInputTargetBlock('TEXT');
+ this.textBlock = this.textJoinBlock.getInputTargetBlock('ADD0');
+ });
+
+ function assertBlockIsOnlyChild(parent, child, inputName) {
+ chai.assert.equal(parent.getChildren().length, 1);
+ chai.assert.equal(parent.getInputTargetBlock(inputName), child);
+ chai.assert.equal(child.getParent(), parent);
+ }
+ function assertNonParentAndOrphan(nonParent, orphan, inputName) {
+ chai.assert.equal(nonParent.getChildren().length, 0);
+ chai.assert.isNull(nonParent.getInputTargetBlock('TEXT'));
+ chai.assert.isNull(orphan.getParent());
+ }
+ function assertOriginalSetup() {
+ assertBlockIsOnlyChild(this.printBlock, this.textJoinBlock, 'TEXT');
+ assertBlockIsOnlyChild(this.textJoinBlock, this.textBlock, 'ADD0');
+ }
+
+ test('Setting to connected parent', function() {
+ chai.assert.doesNotThrow(this.textJoinBlock.setParent
+ .bind(this.textJoinBlock, this.printBlock));
+ assertOriginalSetup.call(this);
+ });
+ test('Setting to new parent after connecting to it', function() {
+ this.textJoinBlock.outputConnection.disconnect();
+ this.textBlock.outputConnection
+ .connect(this.printBlock.getInput('TEXT').connection);
+ chai.assert.doesNotThrow(this.textBlock.setParent
+ .bind(this.textBlock, this.printBlock));
+ assertBlockIsOnlyChild(this.printBlock, this.textBlock, 'TEXT');
+ });
+ test('Setting to new parent while connected to other block', function() {
+ // Setting to grandparent with no available input connection.
+ chai.assert.throws(this.textBlock.setParent
+ .bind(this.textBlock, this.printBlock));
+ this.textJoinBlock.outputConnection.disconnect();
+ // Setting to block with available input connection.
+ chai.assert.throws(this.textBlock.setParent
+ .bind(this.textBlock, this.printBlock));
+ assertNonParentAndOrphan(this.printBlock, this.textJoinBlock, 'TEXT');
+ assertBlockIsOnlyChild(this.textJoinBlock, this.textBlock, 'ADD0');
+ });
+ test('Setting to same parent after disconnecting from it', function() {
+ this.textJoinBlock.outputConnection.disconnect();
+ chai.assert.throws(this.textJoinBlock.setParent
+ .bind(this.textJoinBlock, this.printBlock));
+ assertNonParentAndOrphan(this.printBlock, this.textJoinBlock, 'TEXT');
+ });
+ test('Setting to new parent when orphan', function() {
+ this.textBlock.outputConnection.disconnect();
+ // When new parent has no available input connection.
+ chai.assert.throws(this.textBlock.setParent
+ .bind(this.textBlock, this.printBlock));
+ this.textJoinBlock.outputConnection.disconnect();
+ // When new parent has available input connection.
+ chai.assert.throws(this.textBlock.setParent
+ .bind(this.textBlock, this.printBlock));
+
+ assertNonParentAndOrphan(this.printBlock, this.textJoinBlock, 'TEXT');
+ assertNonParentAndOrphan(this.printBlock, this.textBlock, 'TEXT');
+ assertNonParentAndOrphan(this.textJoinBlock, this.textBlock, 'ADD0');
+ });
+ test('Setting parent to null after disconnecting', function() {
+ this.textBlock.outputConnection.disconnect();
+ chai.assert.doesNotThrow(this.textBlock.setParent
+ .bind(this.textBlock, null));
+ assertNonParentAndOrphan(this.textJoinBlock, this.textBlock, 'ADD0');
+ });
+ test('Setting parent to null without disconnecting', function() {
+ chai.assert.throws(this.textBlock.setParent
+ .bind(this.textBlock, null));
+ assertOriginalSetup.call(this);
+ });
+ });
suite('Remove Connections Programmatically', function() {
test('Output', function() {
var block = createRenderedBlock(this.workspace, 'row_block');
@@ -1106,11 +1196,16 @@ suite('Blocks', function() {
});
suite('Getting/Setting Field (Values)', function() {
setup(function() {
+ this.workspace = Blockly.inject('blocklyDiv');
this.block = Blockly.Xml.domToBlock(Blockly.Xml.textToDom(
'test'
), this.workspace);
});
+ teardown(function() {
+ workspaceTeardown.call(this, this.workspace);
+ });
+
test('Getting Field', function() {
chai.assert.instanceOf(this.block.getField('TEXT'), Blockly.Field);
});