From 13bb9f5bf6e242b5360b4f56653fbc656d081ada Mon Sep 17 00:00:00 2001 From: Beka Westberg Date: Fri, 18 Jun 2021 09:53:43 -0700 Subject: [PATCH] Refactor connect logic (#4880) * Refactor connect logic. * Fixup from rebase * Fix build * PR Comments --- core/block.js | 10 +- core/block_svg.js | 8 +- core/connection.js | 150 +++---- core/insertion_marker_manager.js | 2 +- core/rendered_connection.js | 20 +- core/renderers/common/renderer.js | 22 +- package-lock.json | 64 ++- package.json | 2 +- tests/mocha/connection_test.js | 663 ++++++++++++++---------------- 9 files changed, 441 insertions(+), 500 deletions(-) diff --git a/core/block.js b/core/block.js index 0f4bf1cb0..f4eba58da 100644 --- a/core/block.js +++ b/core/block.js @@ -575,20 +575,21 @@ Blockly.Block.prototype.getConnections_ = function(_all) { /** * Walks down a stack of blocks and finds the last next connection on the stack. + * @param {boolean} ignoreShadows If true,the last connection on a non-shadow + * block will be returned. If false, this will follow shadows to find the + * last connection. * @return {?Blockly.Connection} The last next connection on the stack, or null. * @package */ -Blockly.Block.prototype.lastConnectionInStack = function() { +Blockly.Block.prototype.lastConnectionInStack = function(ignoreShadows) { var nextConnection = this.nextConnection; while (nextConnection) { var nextBlock = nextConnection.targetBlock(); - if (!nextBlock) { - // Found a next connection with nothing on the other side. + if (!nextBlock || (ignoreShadows && nextBlock.isShadow())) { return nextConnection; } nextConnection = nextBlock.nextConnection; } - // Ran out of next connections. return null; }; @@ -607,7 +608,6 @@ Blockly.Block.prototype.bumpNeighbours = function() { * @return {?Blockly.Block} The block (if any) that holds the current block. */ Blockly.Block.prototype.getParent = function() { - // Look at the DOM to see if we are nested in another block. return this.parentBlock_; }; diff --git a/core/block_svg.js b/core/block_svg.js index b5822507b..0917dadf3 100644 --- a/core/block_svg.js +++ b/core/block_svg.js @@ -1475,14 +1475,18 @@ Blockly.BlockSvg.prototype.getConnections_ = function(all) { /** * Walks down a stack of blocks and finds the last next connection on the stack. + * @param {boolean} ignoreShadows If true,the last connection on a non-shadow + * block will be returned. If false, this will follow shadows to find the + * last connection. * @return {?Blockly.RenderedConnection} The last next connection on the stack, * or null. * @package * @override */ -Blockly.BlockSvg.prototype.lastConnectionInStack = function() { +Blockly.BlockSvg.prototype.lastConnectionInStack = function(ignoreShadows) { return /** @type {Blockly.RenderedConnection} */ ( - Blockly.BlockSvg.superClass_.lastConnectionInStack.call(this)); + Blockly.BlockSvg.superClass_ + .lastConnectionInStack.call(this, ignoreShadows)); }; /** diff --git a/core/connection.js b/core/connection.js index c9415dd05..27526c711 100644 --- a/core/connection.js +++ b/core/connection.js @@ -104,106 +104,58 @@ Blockly.Connection.prototype.y = 0; * @protected */ Blockly.Connection.prototype.connect_ = function(childConnection) { + var INPUT = Blockly.connectionTypes.INPUT_VALUE; var parentConnection = this; var parentBlock = parentConnection.getSourceBlock(); var childBlock = childConnection.getSourceBlock(); - // Disconnect any existing parent on the child connection. + + // Make sure the childConnection is available. if (childConnection.isConnected()) { childConnection.disconnect(); } + + // Make sure the parentConnection is available. + var orphan; if (parentConnection.isConnected()) { - // Other connection is already connected to something. - // Disconnect it and reattach it or bump it as needed. - var orphanBlock = parentConnection.targetBlock(); - var shadowDom = parentConnection.getShadowDom(); - // Temporarily set the shadow DOM to null so it does not respawn. - 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. - // This cast assumes that a block can not be both a shadow block and an insertion marker. - shadowDom = /** @type {!Element} */ (Blockly.Xml.blockToDom(orphanBlock)); - orphanBlock.dispose(false); - orphanBlock = null; - } else if (parentConnection.type == Blockly.connectionTypes.INPUT_VALUE) { - // Value connections. - // If female block is already connected, disconnect and bump the male. - if (!orphanBlock.outputConnection) { - throw Error('Orphan block does not have an output connection.'); - } - // Attempt to reattach the orphan at the end of the newly inserted - // block. Since this block may be a row, walk down to the end - // or to the first (and only) shadow block. - var connection = Blockly.Connection.getConnectionForOrphanedOutput( - childBlock, orphanBlock); - if (connection) { - orphanBlock.outputConnection.connect(connection); - orphanBlock = null; - } - } else if ( - parentConnection.type == Blockly.connectionTypes.NEXT_STATEMENT) { - // Statement connections. - // Statement blocks may be inserted into the middle of a stack. - // Split the stack. - if (!orphanBlock.previousConnection) { - throw Error('Orphan block does not have a previous connection.'); - } - // Attempt to reattach the orphan at the bottom of the newly inserted - // block. Since this block may be a stack, walk down to the end. - var newBlock = childBlock; - while (newBlock.nextConnection) { - var nextBlock = newBlock.getNextBlock(); - if (nextBlock && !nextBlock.isShadow()) { - newBlock = nextBlock; - } else { - var checker = orphanBlock.workspace.connectionChecker; - if (checker.canConnect( - orphanBlock.previousConnection, newBlock.nextConnection, false)) { - newBlock.nextConnection.connect(orphanBlock.previousConnection); - orphanBlock = null; - } - break; - } - } - } - if (orphanBlock) { - // Unable to reattach orphan. + var shadowDom = parentConnection.getShadowDom(true); + parentConnection.shadowDom_ = null; // Set to null so it doesn't respawn. + var target = parentConnection.targetBlock(); + if (target.isShadow()) { + target.dispose(false); + } else { parentConnection.disconnect(); - if (Blockly.Events.recordUndo) { - // Bump it off to the side after a moment. - var group = Blockly.Events.getGroup(); - setTimeout(function() { - // Verify orphan hasn't been deleted or reconnected. - if (orphanBlock.workspace && !orphanBlock.getParent()) { - Blockly.Events.setGroup(group); - if (orphanBlock.outputConnection) { - orphanBlock.outputConnection.onFailedConnect(parentConnection); - } else if (orphanBlock.previousConnection) { - orphanBlock.previousConnection.onFailedConnect(parentConnection); - } - Blockly.Events.setGroup(false); - } - }, Blockly.BUMP_DELAY); - } + orphan = target; } - // Restore the shadow DOM. parentConnection.shadowDom_ = shadowDom; } + // Connect the new connection to the parent. var event; if (Blockly.Events.isEnabled()) { event = new (Blockly.Events.get(Blockly.Events.BLOCK_MOVE))(childBlock); } - // Establish the connections. Blockly.Connection.connectReciprocally_(parentConnection, childConnection); - // Demote the inferior block so that one is a child of the superior one. childBlock.setParent(parentBlock); if (event) { event.recordNew(); Blockly.Events.fire(event); } + + // Deal with the orphan if it exists. + if (orphan) { + var orphanConnection = parentConnection.type === INPUT ? + orphan.outputConnection : orphan.previousConnection; + var connection = Blockly.Connection.getConnectionForOrphanedConnection( + childBlock, /** @type {!Blockly.Connection} */ (orphanConnection)); + if (connection) { + orphanConnection.connect(connection); + } else { + orphanConnection.onFailedConnect(parentConnection); + } + } }; + /** * Dispose of this connection and deal with connected blocks. * @package @@ -318,7 +270,8 @@ Blockly.Connection.prototype.isConnectionAllowed = function(candidate) { }; /** - * Behavior after a connection attempt fails. + * Called when an attempted connection fails. NOP by default (i.e. for headless + * workspaces). * @param {!Blockly.Connection} _otherConnection Connection that this connection * failed to connect to. * @package @@ -409,9 +362,9 @@ Blockly.Connection.getSingleConnection_ = function(block, orphanBlock) { * @param {!Blockly.Block} orphanBlock The block that is looking for a home. * @return {?Blockly.Connection} The suitable connection point on the chain * of blocks, or null. - * @package + * @private */ -Blockly.Connection.getConnectionForOrphanedOutput = +Blockly.Connection.getConnectionForOrphanedOutput_ = function(startBlock, orphanBlock) { var newBlock = startBlock; var connection; @@ -425,6 +378,32 @@ Blockly.Connection.getConnectionForOrphanedOutput = return null; }; +/** + * Returns the connection (starting at the startBlock) which will accept + * the given connection. This includes compatible connection types and + * connection checks. + * @param {!Blockly.Block} startBlock The block on which to start the search. + * @param {!Blockly.Connection} orphanConnection The connection that is looking + * for a home. + * @return {?Blockly.Connection} The suitable connection point on the chain of + * blocks, or null. + */ +Blockly.Connection.getConnectionForOrphanedConnection = + function(startBlock, orphanConnection) { + if (orphanConnection.type === Blockly.connectionTypes.OUTPUT_VALUE) { + return Blockly.Connection.getConnectionForOrphanedOutput_( + startBlock, orphanConnection.getSourceBlock()); + } + // Otherwise we're dealing with a stack. + var connection = startBlock.lastConnectionInStack(true); + var checker = orphanConnection.getConnectionChecker(); + if (connection && + checker.canConnect(orphanConnection, connection, false)) { + return connection; + } + return null; + }; + /** * Disconnect this connection. */ @@ -614,11 +593,18 @@ Blockly.Connection.prototype.setShadowDom = function(shadow) { }; /** - * Returns the XML representation of the connection's shadow block. + * Returns the xml representation of the connection's shadow block. + * @param {boolean=} returnCurrent If true, and the shadow block is currently + * attached to this connection, this serializes the state of that block + * and returns it (so that field values are correct). Otherwise the saved + * shadowDom is just returned. * @return {?Element} Shadow DOM representation of a block or null. */ -Blockly.Connection.prototype.getShadowDom = function() { - return this.shadowDom_; +Blockly.Connection.prototype.getShadowDom = function(returnCurrent) { + return (returnCurrent && this.targetBlock().isShadow()) ? + /** @type {!Element} */ (Blockly.Xml.blockToDom( + /** @type {!Blockly.Block} */ (this.targetBlock()))) : + this.shadowDom_; }; /** diff --git a/core/insertion_marker_manager.js b/core/insertion_marker_manager.js index 0e038d666..d0fea4502 100644 --- a/core/insertion_marker_manager.js +++ b/core/insertion_marker_manager.js @@ -331,7 +331,7 @@ Blockly.InsertionMarkerManager.prototype.createMarkerBlock_ = function(sourceBlo Blockly.InsertionMarkerManager.prototype.initAvailableConnections_ = function() { var available = this.topBlock_.getConnections_(false); // Also check the last connection on this stack - var lastOnStack = this.topBlock_.lastConnectionInStack(); + var lastOnStack = this.topBlock_.lastConnectionInStack(true); if (lastOnStack && lastOnStack != this.topBlock_.nextConnection) { available.push(lastOnStack); this.lastOnStack_ = lastOnStack; diff --git a/core/rendered_connection.js b/core/rendered_connection.js index a9b9c8d33..f9d3864e3 100644 --- a/core/rendered_connection.js +++ b/core/rendered_connection.js @@ -449,14 +449,26 @@ Blockly.RenderedConnection.prototype.isConnectionAllowed = function(candidate, /** * Behavior after a connection attempt fails. + * Bumps this connection away from the other connection. Called when an + * attempted connection fails. * @param {!Blockly.Connection} otherConnection Connection that this connection * failed to connect to. * @package */ -Blockly.RenderedConnection.prototype.onFailedConnect = function( - otherConnection) { - this.bumpAwayFrom(otherConnection); -}; +Blockly.RenderedConnection.prototype.onFailedConnect = + function(otherConnection) { + var block = this.getSourceBlock(); + if (Blockly.Events.recordUndo) { + var group = Blockly.Events.getGroup(); + setTimeout(function() { + if (!block.isDisposed() && !block.getParent()) { + Blockly.Events.setGroup(group); + this.bumpAwayFrom(otherConnection); + Blockly.Events.setGroup(false); + } + }.bind(this), Blockly.BUMP_DELAY); + } + }; /** diff --git a/core/renderers/common/renderer.js b/core/renderers/common/renderer.js index 4d7c6ca17..77df5e518 100644 --- a/core/renderers/common/renderer.js +++ b/core/renderers/common/renderer.js @@ -246,23 +246,11 @@ Blockly.blockRendering.Renderer.prototype.shouldHighlightConnection = */ Blockly.blockRendering.Renderer.prototype.orphanCanConnectAtEnd = function(topBlock, orphanBlock, localType) { - var orphanConnection = null; - var lastConnection = null; - if (localType == Blockly.connectionTypes.OUTPUT_VALUE) { - orphanConnection = orphanBlock.outputConnection; - lastConnection = - Blockly.Connection.getConnectionForOrphanedOutput( - /** @type {!Blockly.Block} **/ (topBlock), orphanBlock); - } else { - orphanConnection = orphanBlock.previousConnection; - lastConnection = topBlock.lastConnectionInStack(); - } - - if (!lastConnection) { - return false; - } - return orphanConnection.getConnectionChecker().canConnect( - lastConnection, orphanConnection, false); + var orphanConnection = localType === Blockly.connectionTypes.OUTPUT_VALUE ? + orphanBlock.outputConnection : orphanBlock.previousConnection; + return !!Blockly.Connection.getConnectionForOrphanedConnection( + /** @type {!Blockly.Block} **/ (topBlock), + /** @type {!Blockly.Connection} **/ (orphanConnection)); }; /** diff --git a/package-lock.json b/package-lock.json index d2f40ff1f..a76db89d3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -248,15 +248,15 @@ "dev": true }, "@eslint/eslintrc": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-0.4.1.tgz", - "integrity": "sha512-5v7TDE9plVhvxQeWLXDTvFvJBdH6pEsdnl2g/dAptmuFEPedQ4Erq5rsDsX+mvAM610IhNaO2W5V1dOOnDKxkQ==", + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-0.4.2.tgz", + "integrity": "sha512-8nmGq/4ycLpIwzvhI4tNDmQztZ8sp+hI7cyG8i1nQDhkAbRzHpXPidRAHlNvCZQpJTKw5ItIpMw9RSToGF00mg==", "dev": true, "requires": { "ajv": "^6.12.4", "debug": "^4.1.1", "espree": "^7.3.0", - "globals": "^12.1.0", + "globals": "^13.9.0", "ignore": "^4.0.6", "import-fresh": "^3.2.1", "js-yaml": "^3.13.1", @@ -265,12 +265,12 @@ }, "dependencies": { "globals": { - "version": "12.4.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-12.4.0.tgz", - "integrity": "sha512-BWICuzzDvDoH54NHKCseDanAhE3CeDorgDL5MT6LMXXj2WCnd9UC2szdk4AWLfjdgNBCXLUanXYcpBBKOSWGwg==", + "version": "13.9.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.9.0.tgz", + "integrity": "sha512-74/FduwI/JaIrr1H8e71UbDE+5x7pIPs1C2rrwC52SszOo043CsWOZEMW7o2Y58xwm9b+0RBKDxY5n2sUpEFxA==", "dev": true, "requires": { - "type-fest": "^0.8.1" + "type-fest": "^0.20.2" } } } @@ -2650,13 +2650,13 @@ } }, "eslint": { - "version": "7.27.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.27.0.tgz", - "integrity": "sha512-JZuR6La2ZF0UD384lcbnd0Cgg6QJjiCwhMD6eU4h/VGPcVGwawNNzKU41tgokGXnfjOOyI6QIffthhJTPzzuRA==", + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.28.0.tgz", + "integrity": "sha512-UMfH0VSjP0G4p3EWirscJEQ/cHqnT/iuH6oNZOB94nBjWbMnhGEPxsZm1eyIW0C/9jLI0Fow4W5DXLjEI7mn1g==", "dev": true, "requires": { "@babel/code-frame": "7.12.11", - "@eslint/eslintrc": "^0.4.1", + "@eslint/eslintrc": "^0.4.2", "ajv": "^6.10.0", "chalk": "^4.0.0", "cross-spawn": "^7.0.2", @@ -2673,7 +2673,7 @@ "fast-deep-equal": "^3.1.3", "file-entry-cache": "^6.0.1", "functional-red-black-tree": "^1.0.1", - "glob-parent": "^5.0.0", + "glob-parent": "^5.1.2", "globals": "^13.6.0", "ignore": "^4.0.6", "import-fresh": "^3.0.0", @@ -2706,18 +2706,18 @@ } }, "@babel/helper-validator-identifier": { - "version": "7.14.0", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.14.0.tgz", - "integrity": "sha512-V3ts7zMSu5lfiwWDVWzRDGIN+lnCEUdaXgtVHJgLb1rGaA6jMrtB9EmE7L18foXJIE8Un/A/h6NJfGQp/e1J4A==", + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.14.5.tgz", + "integrity": "sha512-5lsetuxCLilmVGyiLEfoHBRX8UCFD+1m2x3Rj97WrW3V7H3u4RWRXA4evMjImCsin2J2YT0QaVDGf+z8ondbAg==", "dev": true }, "@babel/highlight": { - "version": "7.14.0", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.14.0.tgz", - "integrity": "sha512-YSCOwxvTYEIMSGaBQb5kDDsCopDdiUGsqpatp3fOlI4+2HQSkTmEVWnVuySdAC5EWCqSWWTv0ib63RjR7dTBdg==", + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.14.5.tgz", + "integrity": "sha512-qf9u2WFWVV0MppaL877j2dBtQIDgmidgjGk5VIMw3OadXvYaXn66U1BFlH2t4+t3i+8PhedppRv+i40ABzd+gg==", "dev": true, "requires": { - "@babel/helper-validator-identifier": "^7.14.0", + "@babel/helper-validator-identifier": "^7.14.5", "chalk": "^2.0.0", "js-tokens": "^4.0.0" }, @@ -2899,12 +2899,6 @@ "requires": { "prelude-ls": "^1.2.1" } - }, - "type-fest": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", - "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", - "dev": true } } }, @@ -7219,9 +7213,9 @@ } }, "regexpp": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.1.0.tgz", - "integrity": "sha512-ZOIzd8yVsQQA7j8GCSlPGXwg5PfmA1mrq0JP4nGhh54LaKN3xdai/vHUDu74pKwV8OxseMS65u2NImosQcSD0Q==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz", + "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==", "dev": true }, "registry-auth-token": { @@ -8327,9 +8321,9 @@ }, "dependencies": { "ajv": { - "version": "8.5.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.5.0.tgz", - "integrity": "sha512-Y2l399Tt1AguU3BPRP9Fn4eN+Or+StUGWCUpbnFyXSo8NZ9S4uj+AG2pjs5apK+ZMOwYOz1+a+VKvKH7CudXgQ==", + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.6.0.tgz", + "integrity": "sha512-cnUG4NSBiM4YFBxgZIj/In3/6KX+rQ2l2YPRVcvAMQGWEPKuXoPIhxzwqh31jA3IPbI4qEOp/5ILI4ynioXsGQ==", "dev": true, "requires": { "fast-deep-equal": "^3.1.1", @@ -8661,9 +8655,9 @@ "dev": true }, "type-fest": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", - "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", "dev": true }, "typedarray": { diff --git a/package.json b/package.json index 3df16bb0a..ec9c1b8e1 100644 --- a/package.json +++ b/package.json @@ -65,7 +65,7 @@ "chai": "^4.2.0", "clang-format": "^1.5.0", "concurrently": "^6.0.0", - "eslint": "^7.6.0", + "eslint": "^7.28.0", "google-closure-compiler": "^20210505.0.0", "google-closure-deps": "^20210406.0.0", "gulp": "^4.0.2", diff --git a/tests/mocha/connection_test.js b/tests/mocha/connection_test.js index 118d27412..67f2c75e2 100644 --- a/tests/mocha/connection_test.js +++ b/tests/mocha/connection_test.js @@ -782,329 +782,6 @@ suite('Connection', function() { }); }); - suite('getConnectionForOrphanedOutput', function() { - setup(function() { - this.workspace = new Blockly.Workspace(); - - Blockly.defineBlocksWithJsonArray([ - { - 'type': 'input', - 'message0': '%1', - 'args0': [ - { - 'type': 'input_value', - 'name': 'INPUT', - 'check': 'check' - } - ], - }, - { - 'type': 'output', - 'message0': '', - 'output': 'check', - }, - ]); - }); - - teardown(function() { - workspaceTeardown.call(this, this.workspace); - }); - - suite('No available spots', function() { - setup(function() { - Blockly.defineBlocksWithJsonArray([ - { - 'type': 'output_and_statements', - 'message0': '%1 %2', - 'args0': [ - { - 'type': 'input_statement', - 'name': 'INPUT', - 'check': 'check' - }, - { - 'type': 'input_statement', - 'name': 'INPUT2', - 'check': 'check' - } - ], - 'output': 'check', - }, - { - 'type': 'output_and_inputs', - 'message0': '%1 %2', - 'args0': [ - { - 'type': 'input_value', - 'name': 'INPUT', - 'check': 'check2' - }, - { - 'type': 'input_value', - 'name': 'INPUT2', - 'check': 'check2' - } - ], - 'output': 'check', - }, - { - 'type': 'check_to_check2', - 'message0': '%1', - 'args0': [ - { - 'type': 'input_value', - 'name': 'INPUT', - 'check': 'check2' - }, - ], - 'output': 'check', - }, - { - 'type': 'check2_to_check', - 'message0': '%1', - 'args0': [ - { - 'type': 'input_value', - 'name': 'CHECK2TOCHECKINPUT', - 'check': 'check' - }, - ], - 'output': 'check2', - }, - ]); - }); - - test('No connection', function() { - const parent = this.workspace.newBlock('input'); - const oldChild = this.workspace.newBlock('output'); - const newChild = this.workspace.newBlock('output'); - - parent.getInput('INPUT').connection.connect(oldChild.outputConnection); - chai.assert.notExists( - Blockly.Connection.getConnectionForOrphanedOutput(oldChild, newChild)); - }); - - test('All statements', function() { - const parent = this.workspace.newBlock('input'); - const oldChild = this.workspace.newBlock('output_and_statements'); - const newChild = this.workspace.newBlock('output'); - - parent.getInput('INPUT').connection.connect(oldChild.outputConnection); - chai.assert.notExists( - Blockly.Connection.getConnectionForOrphanedOutput(oldChild, newChild)); - }); - - test('Bad checks', function() { - const parent = this.workspace.newBlock('input'); - const oldChild = this.workspace.newBlock('output_and_inputs'); - const newChild = this.workspace.newBlock('output'); - - parent.getInput('INPUT').connection.connect(oldChild.outputConnection); - chai.assert.notExists( - Blockly.Connection.getConnectionForOrphanedOutput(oldChild, newChild)); - }); - - test('Through different types', function() { - const parent = this.workspace.newBlock('input'); - const oldChild = this.workspace.newBlock('check_to_check2'); - const otherChild = this.workspace.newBlock('check2_to_check'); - const newChild = this.workspace.newBlock('output'); - - parent.getInput('INPUT').connection - .connect(oldChild.outputConnection); - oldChild.getInput('INPUT').connection - .connect(otherChild.outputConnection); - - chai.assert.notExists( - Blockly.Connection.getConnectionForOrphanedOutput(oldChild, newChild)); - }); - }); - - suite('Multiple available spots', function() { - setup(function() { - Blockly.defineBlocksWithJsonArray([ - { - 'type': 'multiple_inputs', - 'message0': '%1 %2', - 'args0': [ - { - 'type': 'input_value', - 'name': 'INPUT', - 'check': 'check' - }, - { - 'type': 'input_value', - 'name': 'INPUT2', - 'check': 'check' - }, - ], - 'output': 'check', - }, - { - 'type': 'single_input', - 'message0': '%1', - 'args0': [ - { - 'type': 'input_value', - 'name': 'INPUT', - 'check': 'check' - }, - ], - 'output': 'check', - }, - ]); - }); - - suite('No shadows', function() { - test('Top block', function() { - const parent = this.workspace.newBlock('input'); - const oldChild = this.workspace.newBlock('multiple_inputs'); - const newChild = this.workspace.newBlock('output'); - - parent.getInput('INPUT').connection.connect(oldChild.outputConnection); - chai.assert.notExists( - Blockly.Connection.getConnectionForOrphanedOutput(oldChild, newChild)); - }); - - test('Child blocks', function() { - const parent = this.workspace.newBlock('input'); - const oldChild = this.workspace.newBlock('multiple_inputs'); - const childX = this.workspace.newBlock('single_input'); - const childY = this.workspace.newBlock('single_input'); - const newChild = this.workspace.newBlock('output'); - - parent.getInput('INPUT').connection.connect(oldChild.outputConnection); - oldChild.getInput('INPUT').connection.connect(childX.outputConnection); - oldChild.getInput('INPUT2').connection.connect(childY.outputConnection); - - chai.assert.notExists( - Blockly.Connection.getConnectionForOrphanedOutput(oldChild, newChild)); - }); - - test('Spots filled', function() { - const parent = this.workspace.newBlock('input'); - const oldChild = this.workspace.newBlock('multiple_inputs'); - const otherChild = this.workspace.newBlock('output'); - const newChild = this.workspace.newBlock('output'); - - parent.getInput('INPUT').connection - .connect(oldChild.outputConnection); - oldChild.getInput('INPUT').connection - .connect(otherChild.outputConnection); - - chai.assert.notExists( - Blockly.Connection.getConnectionForOrphanedOutput(oldChild, newChild)); - }); - }); - - suite('Shadows', function() { - test('Top block', function() { - const parent = this.workspace.newBlock('input'); - const oldChild = this.workspace.newBlock('multiple_inputs'); - const newChild = this.workspace.newBlock('output'); - - parent.getInput('INPUT').connection.connect(oldChild.outputConnection); - oldChild.getInput('INPUT').connection.setShadowDom( - Blockly.Xml.textToDom('') - .firstChild); - oldChild.getInput('INPUT2').connection.setShadowDom( - Blockly.Xml.textToDom('') - .firstChild); - - chai.assert.notExists( - Blockly.Connection.getConnectionForOrphanedOutput(oldChild, newChild)); - }); - - test('Child blocks', function() { - const parent = this.workspace.newBlock('input'); - const oldChild = this.workspace.newBlock('multiple_inputs'); - const childX = this.workspace.newBlock('single_input'); - const childY = this.workspace.newBlock('single_input'); - const newChild = this.workspace.newBlock('output'); - - parent.getInput('INPUT').connection.connect(oldChild.outputConnection); - oldChild.getInput('INPUT').connection.connect(childX.outputConnection); - oldChild.getInput('INPUT2').connection.connect(childY.outputConnection); - childX.getInput('INPUT').connection.setShadowDom( - Blockly.Xml.textToDom('') - .firstChild); - childY.getInput('INPUT').connection.setShadowDom( - Blockly.Xml.textToDom('') - .firstChild); - - chai.assert.notExists( - Blockly.Connection.getConnectionForOrphanedOutput(oldChild, newChild)); - }); - - test('Spots filled', function() { - const parent = this.workspace.newBlock('input'); - const oldChild = this.workspace.newBlock('multiple_inputs'); - const otherChild = this.workspace.newBlock('output'); - const newChild = this.workspace.newBlock('output'); - - parent.getInput('INPUT').connection - .connect(oldChild.outputConnection); - oldChild.getInput('INPUT').connection - .connect(otherChild.outputConnection); - oldChild.getInput('INPUT2').connection.setShadowDom( - Blockly.Xml.textToDom('') - .firstChild); - - chai.assert.notExists( - Blockly.Connection.getConnectionForOrphanedOutput(oldChild, newChild)); - }); - }); - }); - - suite('Single available spot', function() { - setup(function() { - Blockly.defineBlocksWithJsonArray([ - { - 'type': 'single_input', - 'message0': '%1', - 'args0': [ - { - 'type': 'input_value', - 'name': 'INPUT', - 'check': 'check' - }, - ], - 'output': 'check', - }, - ]); - }); - - test('No shadows', function() { - const parent = this.workspace.newBlock('input'); - const oldChild = this.workspace.newBlock('single_input'); - const newChild = this.workspace.newBlock('output'); - - parent.getInput('INPUT').connection.connect(oldChild.outputConnection); - - const result = Blockly.Connection - .getConnectionForOrphanedOutput(oldChild, newChild); - chai.assert.exists(result); - chai.assert.equal(result.getParentInput().name, 'INPUT'); - }); - - test('Shadows', function() { - const parent = this.workspace.newBlock('input'); - const oldChild = this.workspace.newBlock('single_input'); - const newChild = this.workspace.newBlock('output'); - - parent.getInput('INPUT').connection.connect(oldChild.outputConnection); - oldChild.getInput('INPUT').connection.setShadowDom( - Blockly.Xml.textToDom('') - .firstChild); - - const result = Blockly.Connection - .getConnectionForOrphanedOutput(oldChild, newChild); - chai.assert.exists(result); - chai.assert.equal(result.getParentInput().name, 'INPUT'); - }); - }); - }); - suite('Connect', function() { setup(function() { this.workspace = new Blockly.Workspace(); @@ -1185,6 +862,40 @@ suite('Connection', function() { "message0": "", "output": 'check1' }, + { + "type": "row_block_multiple_inputs", + "message0": "%1 %2", + "args0": [ + { + "type": "input_value", + "name": "INPUT", + "check": 'check1' + }, + { + "type": "input_value", + "name": "INPUT2", + "check": 'check1' + } + ], + "output": 'check1' + }, + { + 'type': 'output_to_statements', + 'message0': '%1 %2', + 'args0': [ + { + 'type': 'input_statement', + 'name': 'INPUT', + 'check': 'check1' + }, + { + 'type': 'input_statement', + 'name': 'INPUT2', + 'check': 'check1' + } + ], + 'output': 'check1', + }, { "type": "statement_block", "message0": "%1 %2", @@ -1401,42 +1112,288 @@ suite('Connection', function() { suite('Reattach or bump orphan', function() { suite('Value', function() { - // Only one test for this b/c tested by getConnectionForOrphanedOutput. - test('Simple', function() { - var parent = this.workspace.newBlock('row_block'); - var oldChild = this.workspace.newBlock('row_block'); - var newChild = this.workspace.newBlock('row_block'); - parent.getInput('INPUT').connection.connect(oldChild.outputConnection); - var spy = sinon.spy( - Blockly.Connection, 'getConnectionForOrphanedOutput'); + suite('No available spots', function() { + test('No connection', function() { + var parent = this.workspace.newBlock('row_block'); + var oldChild = this.workspace.newBlock('row_block'); + var newChild = this.workspace.newBlock('row_block_noend'); + parent.getInput('INPUT').connection + .connect(oldChild.outputConnection); - parent.getInput('INPUT').connection.connect(newChild.outputConnection); + parent.getInput('INPUT').connection + .connect(newChild.outputConnection); - chai.assert.isTrue(parent.getInput('INPUT').connection.isConnected()); - chai.assert.equal(parent.getInputTargetBlock('INPUT'), newChild); - chai.assert.isTrue(newChild.getInput('INPUT').connection.isConnected()); - chai.assert.equal(newChild.getInputTargetBlock('INPUT'), oldChild); - // Make sure it is actually getting called, so all functionality has - // been tested. - // Future people: if you ever stop calling this function you need to - // add more tests for reattaching orphans. - chai.assert.isTrue(spy.calledOnce); - this.assertBlockCount(3); + chai.assert.isTrue( + parent.getInput('INPUT').connection.isConnected()); + chai.assert.equal( + parent.getInputTargetBlock('INPUT'), newChild); + chai.assert.isFalse( + oldChild.outputConnection.isConnected()); + }); + + test('All statements', function() { + var parent = this.workspace.newBlock('row_block'); + var oldChild = this.workspace.newBlock('row_block'); + var newChild = this.workspace.newBlock('output_to_statements'); + parent.getInput('INPUT').connection + .connect(oldChild.outputConnection); + + parent.getInput('INPUT').connection + .connect(newChild.outputConnection); + + chai.assert.isTrue( + parent.getInput('INPUT').connection.isConnected()); + chai.assert.equal( + parent.getInputTargetBlock('INPUT'), newChild); + chai.assert.isFalse( + oldChild.outputConnection.isConnected()); + }); + + test('Bad checks', function() { + var parent = this.workspace.newBlock('row_block'); + var oldChild = this.workspace.newBlock('row_block'); + var newChild = this.workspace.newBlock('row_block_2to1'); + parent.getInput('INPUT').connection + .connect(oldChild.outputConnection); + + parent.getInput('INPUT').connection + .connect(newChild.outputConnection); + + chai.assert.isTrue( + parent.getInput('INPUT').connection.isConnected()); + chai.assert.equal( + parent.getInputTargetBlock('INPUT'), newChild); + chai.assert.isFalse( + oldChild.outputConnection.isConnected()); + }); + + test('Through different types', function() { + const parent = this.workspace.newBlock('row_block'); + const oldChild = this.workspace.newBlock('row_block'); + const newChild = this.workspace.newBlock('row_block_2to1'); + const otherChild = this.workspace.newBlock('row_block_1to2'); + + parent.getInput('INPUT').connection + .connect(oldChild.outputConnection); + newChild.getInput('INPUT').connection + .connect(otherChild.outputConnection); + + parent.getInput('INPUT').connection + .connect(newChild.outputConnection); + + chai.assert.isTrue( + parent.getInput('INPUT').connection.isConnected()); + chai.assert.equal( + parent.getInputTargetBlock('INPUT'), newChild); + chai.assert.isFalse( + oldChild.outputConnection.isConnected()); + }); }); - test('Bump', function() { - var parent = this.workspace.newBlock('row_block'); - var oldChild = this.workspace.newBlock('row_block'); - var newChild = this.workspace.newBlock('row_block_noend'); - parent.getInput('INPUT').connection.connect(oldChild.outputConnection); - var spy = sinon.spy(oldChild.outputConnection, 'onFailedConnect'); + suite('Multiple available spots', function() { + suite('No shadows', function() { + test('Top block', function() { + const parent = this.workspace.newBlock('row_block'); + const oldChild = this.workspace.newBlock('row_block'); + const newChild = this.workspace.newBlock( + 'row_block_multiple_inputs'); - parent.getInput('INPUT').connection.connect(newChild.outputConnection); + parent.getInput('INPUT').connection + .connect(oldChild.outputConnection); - chai.assert.isTrue(parent.getInput('INPUT').connection.isConnected()); - chai.assert.equal(parent.getInputTargetBlock('INPUT'), newChild); - chai.assert.isTrue(spy.calledOnce); - this.assertBlockCount(3); + parent.getInput('INPUT').connection + .connect(newChild.outputConnection); + + chai.assert.isTrue( + parent.getInput('INPUT').connection.isConnected()); + chai.assert.equal( + parent.getInputTargetBlock('INPUT'), newChild); + chai.assert.isFalse( + oldChild.outputConnection.isConnected()); + }); + + test('Child blocks', function() { + const parent = this.workspace.newBlock('row_block'); + const oldChild = this.workspace.newBlock('row_block'); + const newChild = this.workspace.newBlock( + 'row_block_multiple_inputs'); + const childX = this.workspace.newBlock('row_block'); + const childY = this.workspace.newBlock('row_block'); + + parent.getInput('INPUT').connection + .connect(oldChild.outputConnection); + newChild.getInput('INPUT').connection + .connect(childX.outputConnection); + newChild.getInput('INPUT2').connection + .connect(childY.outputConnection); + + parent.getInput('INPUT').connection + .connect(newChild.outputConnection); + + chai.assert.isTrue( + parent.getInput('INPUT').connection.isConnected()); + chai.assert.equal( + parent.getInputTargetBlock('INPUT'), newChild); + chai.assert.isFalse( + oldChild.outputConnection.isConnected()); + }); + + test('Spots filled', function() { + const parent = this.workspace.newBlock('row_block'); + const oldChild = this.workspace.newBlock('row_block'); + const newChild = this.workspace.newBlock( + 'row_block_multiple_inputs'); + const otherChild = this.workspace.newBlock('row_block_noend'); + + parent.getInput('INPUT').connection + .connect(oldChild.outputConnection); + newChild.getInput('INPUT').connection + .connect(otherChild.outputConnection); + + parent.getInput('INPUT').connection + .connect(newChild.outputConnection); + + chai.assert.isTrue( + parent.getInput('INPUT').connection.isConnected()); + chai.assert.equal( + parent.getInputTargetBlock('INPUT'), newChild); + chai.assert.isFalse( + oldChild.outputConnection.isConnected()); + }); + }); + + suite('Shadows', function() { + test('Top block', function() { + const parent = this.workspace.newBlock('row_block'); + const oldChild = this.workspace.newBlock('row_block'); + const newChild = this.workspace.newBlock( + 'row_block_multiple_inputs'); + + parent.getInput('INPUT').connection + .connect(oldChild.outputConnection); + newChild.getInput('INPUT').connection.setShadowDom( + Blockly.Xml.textToDom('') + .firstChild); + newChild.getInput('INPUT2').connection.setShadowDom( + Blockly.Xml.textToDom('') + .firstChild); + + parent.getInput('INPUT').connection + .connect(newChild.outputConnection); + + chai.assert.isTrue( + parent.getInput('INPUT').connection.isConnected()); + chai.assert.equal( + parent.getInputTargetBlock('INPUT'), newChild); + chai.assert.isFalse( + oldChild.outputConnection.isConnected()); + }); + + test('Child blocks', function() { + const parent = this.workspace.newBlock('row_block'); + const oldChild = this.workspace.newBlock('row_block'); + const newChild = this.workspace.newBlock( + 'row_block_multiple_inputs'); + const childX = this.workspace.newBlock('row_block'); + const childY = this.workspace.newBlock('row_block'); + + parent.getInput('INPUT').connection + .connect(oldChild.outputConnection); + newChild.getInput('INPUT').connection + .connect(childX.outputConnection); + newChild.getInput('INPUT2').connection + .connect(childY.outputConnection); + childX.getInput('INPUT').connection.setShadowDom( + Blockly.Xml.textToDom('') + .firstChild); + childY.getInput('INPUT').connection.setShadowDom( + Blockly.Xml.textToDom('') + .firstChild); + + parent.getInput('INPUT').connection + .connect(newChild.outputConnection); + + chai.assert.isTrue( + parent.getInput('INPUT').connection.isConnected()); + chai.assert.equal( + parent.getInputTargetBlock('INPUT'), newChild); + chai.assert.isFalse( + oldChild.outputConnection.isConnected()); + }); + + test('Spots filled', function() { + const parent = this.workspace.newBlock('row_block'); + const oldChild = this.workspace.newBlock('row_block'); + const newChild = this.workspace.newBlock( + 'row_block_multiple_inputs'); + const otherChild = this.workspace.newBlock('row_block_noend'); + + parent.getInput('INPUT').connection + .connect(oldChild.outputConnection); + newChild.getInput('INPUT').connection + .connect(otherChild.outputConnection); + newChild.getInput('INPUT2').connection.setShadowDom( + Blockly.Xml.textToDom('') + .firstChild); + + parent.getInput('INPUT').connection + .connect(newChild.outputConnection); + + chai.assert.isTrue( + parent.getInput('INPUT').connection.isConnected()); + chai.assert.equal( + parent.getInputTargetBlock('INPUT'), newChild); + chai.assert.isFalse( + oldChild.outputConnection.isConnected()); + }); + }); + }); + + suite('Single available spot', function() { + test('No shadows', function() { + const parent = this.workspace.newBlock('row_block'); + const oldChild = this.workspace.newBlock('row_block'); + const newChild = this.workspace.newBlock('row_block'); + + parent.getInput('INPUT').connection + .connect(oldChild.outputConnection); + + parent.getInput('INPUT').connection + .connect(newChild.outputConnection); + + chai.assert.isTrue( + parent.getInput('INPUT').connection.isConnected()); + chai.assert.equal( + parent.getInputTargetBlock('INPUT'), newChild); + chai.assert.isTrue( + newChild.getInput('INPUT').connection.isConnected()); + chai.assert.equal( + newChild.getInputTargetBlock('INPUT'), oldChild); + }); + + test('Shadows', function() { + const parent = this.workspace.newBlock('row_block'); + const oldChild = this.workspace.newBlock('row_block'); + const newChild = this.workspace.newBlock('row_block'); + + parent.getInput('INPUT').connection + .connect(oldChild.outputConnection); + newChild.getInput('INPUT').connection.setShadowDom( + Blockly.Xml.textToDom('') + .firstChild); + + parent.getInput('INPUT').connection + .connect(newChild.outputConnection); + + chai.assert.isTrue( + parent.getInput('INPUT').connection.isConnected()); + chai.assert.equal( + parent.getInputTargetBlock('INPUT'), newChild); + chai.assert.isTrue( + newChild.getInput('INPUT').connection.isConnected()); + chai.assert.equal( + newChild.getInputTargetBlock('INPUT'), oldChild); + }); }); });