diff --git a/blockly_uncompressed.js b/blockly_uncompressed.js index c37ffb66a..f8a07fd95 100644 --- a/blockly_uncompressed.js +++ b/blockly_uncompressed.js @@ -37,6 +37,7 @@ goog.addDependency('../../core/components/tree/basenode.js', ['Blockly.tree.Base goog.addDependency('../../core/components/tree/treecontrol.js', ['Blockly.tree.TreeControl'], ['Blockly.tree.BaseNode', 'Blockly.tree.TreeNode', 'Blockly.utils.aria', 'Blockly.utils.object', 'Blockly.utils.style'], {}); goog.addDependency('../../core/components/tree/treenode.js', ['Blockly.tree.TreeNode'], ['Blockly.tree.BaseNode', 'Blockly.utils.KeyCodes', 'Blockly.utils.object'], {}); goog.addDependency('../../core/connection.js', ['Blockly.Connection'], ['Blockly.Events', 'Blockly.Events.BlockMove', 'Blockly.Xml'], {}); +goog.addDependency('../../core/connection_checker.js', ['Blockly.ConnectionChecker'], [], {}); goog.addDependency('../../core/connection_db.js', ['Blockly.ConnectionDB'], ['Blockly.RenderedConnection'], {}); goog.addDependency('../../core/constants.js', ['Blockly.constants'], [], {}); goog.addDependency('../../core/contextmenu.js', ['Blockly.ContextMenu'], ['Blockly.Events', 'Blockly.Events.BlockCreate', 'Blockly.Menu', 'Blockly.MenuItem', 'Blockly.Msg', 'Blockly.Xml', 'Blockly.utils', 'Blockly.utils.Coordinate', 'Blockly.utils.Rect', 'Blockly.utils.dom', 'Blockly.utils.userAgent'], {}); @@ -74,6 +75,7 @@ goog.addDependency('../../core/input.js', ['Blockly.Input'], ['Blockly.Connectio goog.addDependency('../../core/insertion_marker_manager.js', ['Blockly.InsertionMarkerManager'], ['Blockly.Events', 'Blockly.blockAnimations'], {'lang': 'es5'}); goog.addDependency('../../core/interfaces/i_accessibility.js', ['Blockly.IASTNodeLocation', 'Blockly.IASTNodeLocationSvg', 'Blockly.IASTNodeLocationWithBlock', 'Blockly.IBlocklyActionable'], [], {}); goog.addDependency('../../core/interfaces/i_bounded_element.js', ['Blockly.IBoundedElement'], [], {}); +goog.addDependency('../../core/interfaces/i_connection_checker.js', ['Blockly.IConnectionChecker'], [], {}); goog.addDependency('../../core/interfaces/i_copyable.js', ['Blockly.ICopyable'], [], {}); goog.addDependency('../../core/interfaces/i_deletable.js', ['Blockly.IDeletable'], [], {}); goog.addDependency('../../core/interfaces/i_deletearea.js', ['Blockly.IDeleteArea'], [], {}); @@ -183,7 +185,7 @@ goog.addDependency('../../core/variables.js', ['Blockly.Variables'], ['Blockly.B goog.addDependency('../../core/variables_dynamic.js', ['Blockly.VariablesDynamic'], ['Blockly.Blocks', 'Blockly.Msg', 'Blockly.VariableModel', 'Blockly.Variables', 'Blockly.utils.xml'], {}); goog.addDependency('../../core/warning.js', ['Blockly.Warning'], ['Blockly.Bubble', 'Blockly.Events', 'Blockly.Events.Ui', 'Blockly.Icon', 'Blockly.utils.dom', 'Blockly.utils.object'], {}); goog.addDependency('../../core/widgetdiv.js', ['Blockly.WidgetDiv'], ['Blockly.utils.style'], {}); -goog.addDependency('../../core/workspace.js', ['Blockly.Workspace'], ['Blockly.Events', 'Blockly.Options', 'Blockly.VariableMap', 'Blockly.utils', 'Blockly.utils.math'], {}); +goog.addDependency('../../core/workspace.js', ['Blockly.Workspace'], ['Blockly.ConnectionChecker', 'Blockly.Events', 'Blockly.Options', 'Blockly.VariableMap', 'Blockly.utils', 'Blockly.utils.math'], {}); goog.addDependency('../../core/workspace_audio.js', ['Blockly.WorkspaceAudio'], ['Blockly.utils', 'Blockly.utils.global', 'Blockly.utils.userAgent'], {'lang': 'es5'}); goog.addDependency('../../core/workspace_comment.js', ['Blockly.WorkspaceComment'], ['Blockly.Events', 'Blockly.Events.CommentChange', 'Blockly.Events.CommentCreate', 'Blockly.Events.CommentDelete', 'Blockly.Events.CommentMove', 'Blockly.utils', 'Blockly.utils.Coordinate', 'Blockly.utils.xml'], {}); goog.addDependency('../../core/workspace_comment_render_svg.js', ['Blockly.WorkspaceCommentSvg.render'], ['Blockly.utils', 'Blockly.utils.Coordinate', 'Blockly.utils.dom'], {}); diff --git a/blocks/logic.js b/blocks/logic.js index f473c040b..afec94d3f 100644 --- a/blocks/logic.js +++ b/blocks/logic.js @@ -543,7 +543,8 @@ Blockly.Constants.Logic.LOGIC_COMPARE_ONCHANGE_MIXIN = { var blockB = this.getInputTargetBlock('B'); // Disconnect blocks that existed prior to this change if they don't match. if (blockA && blockB && - !blockA.outputConnection.checkType(blockB.outputConnection)) { + !this.workspace.connectionChecker.doTypeChecks( + blockA.outputConnection, blockB.outputConnection)) { // Mismatch between two inputs. Revert the block connections, // bumping away the newly connected block(s). Blockly.Events.setGroup(e.group); @@ -610,7 +611,9 @@ Blockly.Constants.Logic.LOGIC_TERNARY_ONCHANGE_MIXIN = { if ((blockA || blockB) && parentConnection) { for (var i = 0; i < 2; i++) { var block = (i == 1) ? blockA : blockB; - if (block && !block.outputConnection.checkType(parentConnection)) { + if (block && + !block.workspace.connectionChecker.doTypeChecks( + block.outputConnection, parentConnection)) { // Ensure that any disconnections are grouped with the causing event. Blockly.Events.setGroup(e.group); if (parentConnection === this.prevParentConnection_) { diff --git a/core/block.js b/core/block.js index 4d5612de2..706ac2575 100644 --- a/core/block.js +++ b/core/block.js @@ -456,7 +456,8 @@ Blockly.Block.prototype.unplugFromRow_ = function(opt_healStack) { // Disconnect the child block. childConnection.disconnect(); // Connect child to the parent if possible, otherwise bump away. - if (childConnection.checkType(parentConnection)) { + if (this.workspace.connectionChecker.canConnect( + childConnection, parentConnection, false)) { parentConnection.connect(childConnection); } else { childConnection.onFailedConnect(parentConnection); @@ -508,7 +509,9 @@ Blockly.Block.prototype.unplugFromStack_ = function(opt_healStack) { // Disconnect the next statement. var nextTarget = this.nextConnection.targetConnection; nextTarget.disconnect(); - if (previousTarget && previousTarget.checkType(nextTarget)) { + if (previousTarget && + this.workspace.connectionChecker.canConnect( + previousTarget, nextTarget, false)) { // Attach the next statement to the previous statement. previousTarget.connect(nextTarget); } diff --git a/core/connection.js b/core/connection.js index 1fe595ad7..8974ed292 100644 --- a/core/connection.js +++ b/core/connection.js @@ -14,9 +14,11 @@ goog.provide('Blockly.Connection'); goog.require('Blockly.Events'); goog.require('Blockly.Events.BlockMove'); +goog.require('Blockly.utils.deprecation'); goog.require('Blockly.Xml'); goog.requireType('Blockly.IASTNodeLocationWithBlock'); +goog.requireType('Blockly.IConnectionChecker'); /** @@ -46,6 +48,7 @@ Blockly.Connection.REASON_TARGET_NULL = 3; Blockly.Connection.REASON_CHECKS_FAILED = 4; Blockly.Connection.REASON_DIFFERENT_WORKSPACES = 5; Blockly.Connection.REASON_SHADOW_PARENT = 6; +Blockly.Connection.REASON_DRAG_CHECKS_FAILED = 7; /** * Connection this connection connects to. Null if not connected. @@ -145,8 +148,9 @@ Blockly.Connection.prototype.connect_ = function(childConnection) { if (nextBlock && !nextBlock.isShadow()) { newBlock = nextBlock; } else { - if (orphanBlock.previousConnection.checkType( - newBlock.nextConnection)) { + var checker = orphanBlock.workspace.connectionChecker; + if (checker.canConnect( + orphanBlock.previousConnection, newBlock.nextConnection, false)) { newBlock.nextConnection.connect(orphanBlock.previousConnection); orphanBlock = null; } @@ -245,30 +249,17 @@ Blockly.Connection.prototype.isConnected = function() { * @param {Blockly.Connection} target Connection to check compatibility with. * @return {number} Blockly.Connection.CAN_CONNECT if the connection is legal, * an error code otherwise. + * @deprecated July 2020. Will be deleted July 2021. Use the workspace's + * connectionChecker instead. */ Blockly.Connection.prototype.canConnectWithReason = function(target) { - if (!target) { - return Blockly.Connection.REASON_TARGET_NULL; - } - if (this.isSuperior()) { - var blockA = this.sourceBlock_; - var blockB = target.getSourceBlock(); - } else { - var blockB = this.sourceBlock_; - var blockA = target.getSourceBlock(); - } - if (blockA && blockA == blockB) { - return Blockly.Connection.REASON_SELF_CONNECTION; - } else if (target.type != Blockly.OPPOSITE_TYPE[this.type]) { - return Blockly.Connection.REASON_WRONG_TYPE; - } else if (blockA && blockB && blockA.workspace !== blockB.workspace) { - return Blockly.Connection.REASON_DIFFERENT_WORKSPACES; - } else if (!this.checkType(target)) { - return Blockly.Connection.REASON_CHECKS_FAILED; - } else if (blockA.isShadow() && !blockB.isShadow()) { - return Blockly.Connection.REASON_SHADOW_PARENT; - } - return Blockly.Connection.CAN_CONNECT; + Blockly.utils.deprecation.warn( + 'Connection.prototype.canConnectWithReason', + 'July 2020', + 'July 2021', + 'the workspace\'s connection checker'); + return this.getConnectionChecker().canConnectWithReason( + this, target, false); }; /** @@ -277,130 +268,46 @@ Blockly.Connection.prototype.canConnectWithReason = function(target) { * @param {Blockly.Connection} target The connection to check compatibility * with. * @package + * @deprecated July 2020. Will be deleted July 2021. Use the workspace's + * connectionChecker instead. */ Blockly.Connection.prototype.checkConnection = function(target) { - switch (this.canConnectWithReason(target)) { - case Blockly.Connection.CAN_CONNECT: - break; - case Blockly.Connection.REASON_SELF_CONNECTION: - throw Error('Attempted to connect a block to itself.'); - case Blockly.Connection.REASON_DIFFERENT_WORKSPACES: - // Usually this means one block has been deleted. - throw Error('Blocks not on same workspace.'); - case Blockly.Connection.REASON_WRONG_TYPE: - throw Error('Attempt to connect incompatible types.'); - case Blockly.Connection.REASON_TARGET_NULL: - throw Error('Target connection is null.'); - case Blockly.Connection.REASON_CHECKS_FAILED: - var msg = 'Connection checks failed. '; - msg += this + ' expected ' + this.check_ + ', found ' + target.check_; - throw Error(msg); - case Blockly.Connection.REASON_SHADOW_PARENT: - throw Error('Connecting non-shadow to shadow block.'); - default: - throw Error('Unknown connection failure: this should never happen!'); + Blockly.utils.deprecation.warn( + 'Connection.prototype.checkConnection', + 'July 2020', + 'July 2021', + 'the workspace\'s connection checker'); + var checker = this.getConnectionChecker(); + var reason = checker.canConnectWithReason(this, target, false); + if (reason != Blockly.Connection.CAN_CONNECT) { + throw new Error(checker.getErrorMessage(reason, this, target)); } }; /** - * Check if the two connections can be dragged to connect to each other. - * This is used by the connection database when searching for the closest - * connection. - * @param {!Blockly.Connection} candidate A nearby connection to check, which - * must be a previous connection. - * @return {boolean} True if the connection is allowed, false otherwise. - * @private + * Get the workspace's connection type checker object. + * @return {!Blockly.IConnectionChecker} The connection type checker for the + * source block's workspace. + * @package */ -Blockly.Connection.prototype.canConnectToPrevious_ = function(candidate) { - if (this.targetConnection) { - // This connection is already occupied. - // A next connection will never disconnect itself mid-drag. - return false; - } - - // Don't let blocks try to connect to themselves or ones they nest. - if (Blockly.draggingConnections.indexOf(candidate) != -1) { - return false; - } - - if (!candidate.targetConnection) { - return true; - } - - var targetBlock = candidate.targetBlock(); - // If it is connected to a real block, game over. - if (!targetBlock.isInsertionMarker()) { - return false; - } - // If it's connected to an insertion marker but that insertion marker - // is the first block in a stack, it's still fine. If that insertion - // marker is in the middle of a stack, it won't work. - return !targetBlock.getPreviousBlock(); +Blockly.Connection.prototype.getConnectionChecker = function() { + return this.sourceBlock_.workspace.connectionChecker; }; /** * Check if the two connections can be dragged to connect to each other. * @param {!Blockly.Connection} candidate A nearby connection to check. * @return {boolean} True if the connection is allowed, false otherwise. + * @deprecated July 2020. Will be deleted July 2021. Use the workspace's + * connectionChecker instead. */ Blockly.Connection.prototype.isConnectionAllowed = function(candidate) { - // Don't consider insertion markers. - if (candidate.sourceBlock_.isInsertionMarker()) { - return false; - } - // Type checking. - var canConnect = this.canConnectWithReason(candidate); - if (canConnect != Blockly.Connection.CAN_CONNECT) { - return false; - } - - switch (candidate.type) { - case Blockly.PREVIOUS_STATEMENT: - return this.canConnectToPrevious_(candidate); - case Blockly.OUTPUT_VALUE: { - // Don't offer to connect an already connected left (male) value plug to - // an available right (female) value plug. - if ((candidate.isConnected() && - !candidate.targetBlock().isInsertionMarker()) || - this.isConnected()) { - return false; - } - break; - } - case Blockly.INPUT_VALUE: { - // Offering to connect the left (male) of a value block to an already - // connected value pair is ok, we'll splice it in. - // However, don't offer to splice into an immovable block. - if (candidate.isConnected() && - !candidate.targetBlock().isMovable() && - !candidate.targetBlock().isShadow()) { - return false; - } - break; - } - case Blockly.NEXT_STATEMENT: { - // Don't let a block with no next connection bump other blocks out of the - // stack. But covering up a shadow block or stack of shadow blocks is - // fine. Similarly, replacing a terminal statement with another terminal - // statement is allowed. - if (candidate.isConnected() && - !this.sourceBlock_.nextConnection && - !candidate.targetBlock().isShadow() && - candidate.targetBlock().nextConnection) { - return false; - } - break; - } - default: - throw Error('Unknown connection type in isConnectionAllowed'); - } - - // Don't let blocks try to connect to themselves or ones they nest. - if (Blockly.draggingConnections.indexOf(candidate) != -1) { - return false; - } - - return true; + Blockly.utils.deprecation.warn( + 'Connection.prototype.isConnectionAllowed', + 'July 2020', + 'July 2021', + 'the workspace\'s connection checker'); + return this.getConnectionChecker().canConnect(this, candidate, true); }; /** @@ -422,21 +329,24 @@ Blockly.Connection.prototype.connect = function(otherConnection) { // Already connected together. NOP. return; } - this.checkConnection(otherConnection); - var eventGroup = Blockly.Events.getGroup(); - if (!eventGroup) { - Blockly.Events.setGroup(true); - } - // Determine which block is superior (higher in the source stack). - if (this.isSuperior()) { - // Superior block. - this.connect_(otherConnection); - } else { - // Inferior block. - otherConnection.connect_(this); - } - if (!eventGroup) { - Blockly.Events.setGroup(false); + + var checker = this.getConnectionChecker(); + if (checker.canConnect(this, otherConnection, false)) { + var eventGroup = Blockly.Events.getGroup(); + if (!eventGroup) { + Blockly.Events.setGroup(true); + } + // Determine which block is superior (higher in the source stack). + if (this.isSuperior()) { + // Superior block. + this.connect_(otherConnection); + } else { + // Inferior block. + otherConnection.connect_(this); + } + if (!eventGroup) { + Blockly.Events.setGroup(false); + } } }; @@ -465,10 +375,12 @@ Blockly.Connection.connectReciprocally_ = function(first, second) { */ Blockly.Connection.singleConnection_ = function(block, orphanBlock) { var connection = null; + var output = orphanBlock.outputConnection; for (var i = 0; i < block.inputList.length; i++) { var thisConnection = block.inputList[i].connection; + var typeChecker = output.getConnectionChecker(); if (thisConnection && thisConnection.type == Blockly.INPUT_VALUE && - orphanBlock.outputConnection.checkType(thisConnection)) { + typeChecker.canConnect(output, thisConnection, false)) { if (connection) { return null; // More than one connection. } @@ -596,20 +508,17 @@ Blockly.Connection.prototype.targetBlock = function() { * value type system. E.g. square_root("Hello") is not compatible. * @param {!Blockly.Connection} otherConnection Connection to compare against. * @return {boolean} True if the connections share a type. + * @deprecated July 2020. Will be deleted July 2021. Use the workspace's + * connectionChecker instead. */ Blockly.Connection.prototype.checkType = function(otherConnection) { - if (!this.check_ || !otherConnection.check_) { - // One or both sides are promiscuous enough that anything will fit. - return true; - } - // Find any intersection in the check lists. - for (var i = 0; i < this.check_.length; i++) { - if (otherConnection.check_.indexOf(this.check_[i]) != -1) { - return true; - } - } - // No intersection. - return false; + Blockly.utils.deprecation.warn( + 'Connection.prototype.checkType', + 'October 2019', + 'January 2021', + 'the workspace\'s connection checker'); + return this.getConnectionChecker().canConnect(this, otherConnection, + false); }; /** @@ -618,12 +527,16 @@ Blockly.Connection.prototype.checkType = function(otherConnection) { * @param {!Blockly.Connection} otherConnection Connection to compare against. * @return {boolean} True if the connections share a type. * @private - * @deprecated October 2019, use connection.checkType instead. + * @deprecated October 2019. Will be deleted January 2021. Use the workspace's + * connectionChecker instead. * @suppress {unusedPrivateMembers} */ Blockly.Connection.prototype.checkType_ = function(otherConnection) { - console.warn('Deprecated call to Blockly.Connection.prototype.checkType_, ' + - 'use Blockly.Connection.prototype.checkType instead.'); + Blockly.utils.deprecation.warn( + 'Connection.prototype.checkType_', + 'October 2019', + 'January 2021', + 'the workspace\'s connection checker'); return this.checkType(otherConnection); }; @@ -634,7 +547,8 @@ Blockly.Connection.prototype.checkType_ = function(otherConnection) { Blockly.Connection.prototype.onCheckChanged_ = function() { // The new value type may not be compatible with the existing connection. if (this.isConnected() && (!this.targetConnection || - !this.checkType(this.targetConnection))) { + !this.getConnectionChecker().canConnect( + this, this.targetConnection, false))) { var child = this.isSuperior() ? this.targetBlock() : this.sourceBlock_; child.unplug(); } diff --git a/core/connection_checker.js b/core/connection_checker.js new file mode 100644 index 000000000..d05de79e4 --- /dev/null +++ b/core/connection_checker.js @@ -0,0 +1,280 @@ +/** + * @license + * Copyright 2020 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * @fileoverview An object that encapsulates logic for checking whether a potential + * connection is safe and valid. + * @author fenichel@google.com (Rachel Fenichel) + */ +'use strict'; + +goog.provide('Blockly.ConnectionChecker'); + +goog.requireType('Blockly.Connection'); +goog.requireType('Blockly.IConnectionChecker'); + + +/** + * Class for connection type checking logic. + * @implements {Blockly.IConnectionChecker} + * @constructor + */ +Blockly.ConnectionChecker = function() { +}; + +/** + * Check whether the current connection can connect with the target + * connection. + * @param {Blockly.Connection} a Connection to check compatibility with. + * @param {Blockly.Connection} b Connection to check compatibility with. + * @param {boolean} isDragging True if the connection is being made by dragging + * a block. + * @param {number=} opt_distance The max allowable distance between the + * connections for drag checks. + * @return {boolean} Whether the connection is legal. + * @public + */ +Blockly.ConnectionChecker.prototype.canConnect = function(a, b, + isDragging, opt_distance) { + return this.canConnectWithReason(a, b, isDragging, opt_distance) == + Blockly.Connection.CAN_CONNECT; +}; + +/** + * Checks whether the current connection can connect with the target + * connection, and return an error code if there are problems. + * @param {Blockly.Connection} a Connection to check compatibility with. + * @param {Blockly.Connection} b Connection to check compatibility with. + * @param {boolean} isDragging True if the connection is being made by dragging + * a block. + * @param {number=} opt_distance The max allowable distance between the + * connections for drag checks. + * @return {number} Blockly.Connection.CAN_CONNECT if the connection is legal, + * an error code otherwise. + * @public + */ +Blockly.ConnectionChecker.prototype.canConnectWithReason = function( + a, b, isDragging, opt_distance) { + var safety = this.doSafetyChecks(a, b); + if (safety != Blockly.Connection.CAN_CONNECT) { + return safety; + } + + // If the safety checks passed, both connections are non-null. + var connOne = /** @type {!Blockly.Connection} **/ (a); + var connTwo = /** @type {!Blockly.Connection} **/ (b); + if (!this.doTypeChecks(connOne, connTwo)) { + return Blockly.Connection.REASON_CHECKS_FAILED; + } + + if (isDragging && + !this.doDragChecks( + /** @type {!Blockly.RenderedConnection} **/ (a), + /** @type {!Blockly.RenderedConnection} **/ (b), + opt_distance || 0)) { + return Blockly.Connection.REASON_DRAG_CHECKS_FAILED; + } + + return Blockly.Connection.CAN_CONNECT; +}; + +/** + * Helper method that translates a connection error code into a string. + * @param {number} errorCode The error code. + * @param {Blockly.Connection} a One of the two connections being checked. + * @param {Blockly.Connection} b The second of the two connections being + * checked. + * @return {string} A developer-readable error string. + * @public + */ +Blockly.ConnectionChecker.prototype.getErrorMessage = function(errorCode, + a, b) { + switch (errorCode) { + case Blockly.Connection.REASON_SELF_CONNECTION: + return 'Attempted to connect a block to itself.'; + case Blockly.Connection.REASON_DIFFERENT_WORKSPACES: + // Usually this means one block has been deleted. + return 'Blocks not on same workspace.'; + case Blockly.Connection.REASON_WRONG_TYPE: + return 'Attempt to connect incompatible types.'; + case Blockly.Connection.REASON_TARGET_NULL: + return 'Target connection is null.'; + case Blockly.Connection.REASON_CHECKS_FAILED: + var connOne = /** @type {!Blockly.Connection} **/ (a); + var connTwo = /** @type {!Blockly.Connection} **/ (b); + var msg = 'Connection checks failed. '; + msg += connOne + ' expected ' + connOne.getCheck() + ', found ' + connTwo.getCheck(); + return msg; + case Blockly.Connection.REASON_SHADOW_PARENT: + return 'Connecting non-shadow to shadow block.'; + case Blockly.Connection.REASON_DRAG_CHECKS_FAILED: + return 'Drag checks failed.'; + default: + return 'Unknown connection failure: this should never happen!'; + } +}; + +/** + * Check that connecting the given connections is safe, meaning that it would + * not break any of Blockly's basic assumptions (e.g. no self connections). + * @param {Blockly.Connection} a The first of the connections to check. + * @param {Blockly.Connection} b The second of the connections to check. + * @return {number} An enum with the reason this connection is safe or unsafe. + * @public + */ +Blockly.ConnectionChecker.prototype.doSafetyChecks = function(a, b) { + if (!a || !b) { + return Blockly.Connection.REASON_TARGET_NULL; + } + if (a.isSuperior()) { + var blockA = a.getSourceBlock(); + var blockB = b.getSourceBlock(); + } else { + var blockB = a.getSourceBlock(); + var blockA = b.getSourceBlock(); + } + if (blockA == blockB) { + return Blockly.Connection.REASON_SELF_CONNECTION; + } else if (b.type != Blockly.OPPOSITE_TYPE[a.type]) { + return Blockly.Connection.REASON_WRONG_TYPE; + } else if (blockA.workspace !== blockB.workspace) { + return Blockly.Connection.REASON_DIFFERENT_WORKSPACES; + } else if (blockA.isShadow() && !blockB.isShadow()) { + return Blockly.Connection.REASON_SHADOW_PARENT; + } + return Blockly.Connection.CAN_CONNECT; +}; + +/** + * Check whether this connection is compatible with another connection with + * respect to the value type system. E.g. square_root("Hello") is not + * compatible. + * @param {!Blockly.Connection} a Connection to compare. + * @param {!Blockly.Connection} b Connection to compare against. + * @return {boolean} True if the connections share a type. + * @public + */ +Blockly.ConnectionChecker.prototype.doTypeChecks = function(a, b) { + var checkArrayOne = a.getCheck(); + var checkArrayTwo = b.getCheck(); + + if (!checkArrayOne || !checkArrayTwo) { + // One or both sides are promiscuous enough that anything will fit. + return true; + } + // Find any intersection in the check lists. + for (var i = 0; i < checkArrayOne.length; i++) { + if (checkArrayTwo.indexOf(checkArrayOne[i]) != -1) { + return true; + } + } + // No intersection. + return false; +}; + +/** + * Check whether this connection can be made by dragging. + * @param {!Blockly.RenderedConnection} a Connection to compare. + * @param {!Blockly.RenderedConnection} b Connection to compare against. + * @param {number} distance The maximum allowable distance between connections. + * @return {boolean} True if the connection is allowed during a drag. + * @public + */ +Blockly.ConnectionChecker.prototype.doDragChecks = function(a, b, distance) { + if (a.distanceFrom(b) > distance) { + return false; + } + + // Don't consider insertion markers. + if (b.getSourceBlock().isInsertionMarker()) { + return false; + } + + switch (b.type) { + case Blockly.PREVIOUS_STATEMENT: + return this.canConnectToPrevious_(a, b); + case Blockly.OUTPUT_VALUE: { + // Don't offer to connect an already connected left (male) value plug to + // an available right (female) value plug. + if ((b.isConnected() && + !b.targetBlock().isInsertionMarker()) || + a.isConnected()) { + return false; + } + break; + } + case Blockly.INPUT_VALUE: { + // Offering to connect the left (male) of a value block to an already + // connected value pair is ok, we'll splice it in. + // However, don't offer to splice into an immovable block. + if (b.isConnected() && + !b.targetBlock().isMovable() && + !b.targetBlock().isShadow()) { + return false; + } + break; + } + case Blockly.NEXT_STATEMENT: { + // Don't let a block with no next connection bump other blocks out of the + // stack. But covering up a shadow block or stack of shadow blocks is + // fine. Similarly, replacing a terminal statement with another terminal + // statement is allowed. + if (b.isConnected() && + !a.getSourceBlock().nextConnection && + !b.targetBlock().isShadow() && + b.targetBlock().nextConnection) { + return false; + } + break; + } + default: + // Unexpected connection type. + return false; + } + + // Don't let blocks try to connect to themselves or ones they nest. + if (Blockly.draggingConnections.indexOf(b) != -1) { + return false; + } + + return true; +}; + +/** + * Helper function for drag checking. + * @param {!Blockly.Connection} a The connection to check, which must be a + * statement input or next connection. + * @param {!Blockly.Connection} b A nearby connection to check, which + * must be a previous connection. + * @return {boolean} True if the connection is allowed, false otherwise. + * @protected + */ +Blockly.ConnectionChecker.prototype.canConnectToPrevious_ = function(a, b) { + if (a.targetConnection) { + // This connection is already occupied. + // A next connection will never disconnect itself mid-drag. + return false; + } + + // Don't let blocks try to connect to themselves or ones they nest. + if (Blockly.draggingConnections.indexOf(b) != -1) { + return false; + } + + if (!b.targetConnection) { + return true; + } + + var targetBlock = b.targetBlock(); + // If it is connected to a real block, game over. + if (!targetBlock.isInsertionMarker()) { + return false; + } + // If it's connected to an insertion marker but that insertion marker + // is the first block in a stack, it's still fine. If that insertion + // marker is in the middle of a stack, it won't work. + return !targetBlock.getPreviousBlock(); +}; diff --git a/core/connection_db.js b/core/connection_db.js index 5d71e39c0..d347ddbe0 100644 --- a/core/connection_db.js +++ b/core/connection_db.js @@ -16,20 +16,32 @@ goog.provide('Blockly.ConnectionDB'); goog.require('Blockly.RenderedConnection'); +goog.requireType('Blockly.IConnectionChecker'); + /** * Database of connections. * Connections are stored in order of their vertical component. This way * connections in an area may be looked up quickly using a binary search. + * @param {!Blockly.IConnectionChecker} checker The workspace's + * connection type checker, used to decide if connections are valid during a + * drag. * @constructor */ -Blockly.ConnectionDB = function() { +Blockly.ConnectionDB = function(checker) { /** * Array of connections sorted by y position in workspace units. * @type {!Array.} * @private */ this.connections_ = []; + /** + * The workspace's connection type checker, used to decide if connections are + * valid during a drag. + * @type {!Blockly.IConnectionChecker} + * @private + */ + this.connectionChecker_ = checker; }; /** @@ -240,7 +252,7 @@ Blockly.ConnectionDB.prototype.searchForClosest = function(conn, maxRadius, var pointerMin = closestIndex - 1; while (pointerMin >= 0 && this.isInYRange_(pointerMin, conn.y, maxRadius)) { temp = this.connections_[pointerMin]; - if (conn.isConnectionAllowed(temp, bestRadius)) { + if (this.connectionChecker_.canConnect(conn, temp, true, bestRadius)) { bestConnection = temp; bestRadius = temp.distanceFrom(conn); } @@ -251,7 +263,7 @@ Blockly.ConnectionDB.prototype.searchForClosest = function(conn, maxRadius, while (pointerMax < this.connections_.length && this.isInYRange_(pointerMax, conn.y, maxRadius)) { temp = this.connections_[pointerMax]; - if (conn.isConnectionAllowed(temp, bestRadius)) { + if (this.connectionChecker_.canConnect(conn, temp, true, bestRadius)) { bestConnection = temp; bestRadius = temp.distanceFrom(conn); } @@ -268,14 +280,16 @@ Blockly.ConnectionDB.prototype.searchForClosest = function(conn, maxRadius, /** * Initialize a set of connection DBs for a workspace. + * @param {!Blockly.IConnectionChecker} checker The workspace's + * connection checker, used to decide if connections are valid during a drag. * @return {!Array.} Array of databases. */ -Blockly.ConnectionDB.init = function() { +Blockly.ConnectionDB.init = function(checker) { // Create four databases, one for each connection type. var dbList = []; - dbList[Blockly.INPUT_VALUE] = new Blockly.ConnectionDB(); - dbList[Blockly.OUTPUT_VALUE] = new Blockly.ConnectionDB(); - dbList[Blockly.NEXT_STATEMENT] = new Blockly.ConnectionDB(); - dbList[Blockly.PREVIOUS_STATEMENT] = new Blockly.ConnectionDB(); + dbList[Blockly.INPUT_VALUE] = new Blockly.ConnectionDB(checker); + dbList[Blockly.OUTPUT_VALUE] = new Blockly.ConnectionDB(checker); + dbList[Blockly.NEXT_STATEMENT] = new Blockly.ConnectionDB(checker); + dbList[Blockly.PREVIOUS_STATEMENT] = new Blockly.ConnectionDB(checker); return dbList; }; diff --git a/core/interfaces/i_connection_checker.js b/core/interfaces/i_connection_checker.js new file mode 100644 index 000000000..505401666 --- /dev/null +++ b/core/interfaces/i_connection_checker.js @@ -0,0 +1,94 @@ +/** + * @license + * Copyright 2020 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * @fileoverview The interface for an object that encapsulates logic for + * checking whether a potential connection is safe and valid. + * @author fenichel@google.com (Rachel Fenichel) + */ +'use strict'; + +goog.provide('Blockly.IConnectionChecker'); + +goog.requireType('Blockly.Connection'); + + +/** + * Class for connection type checking logic. + * @interface + */ +Blockly.IConnectionChecker = function() {}; + +/** + * Check whether the current connection can connect with the target + * connection. + * @param {Blockly.Connection} a Connection to check compatibility with. + * @param {Blockly.Connection} b Connection to check compatibility with. + * @param {boolean} isDragging True if the connection is being made by dragging + * a block. + * @param {number=} opt_distance The max allowable distance between the + * connections for drag checks. + * @return {boolean} Whether the connection is legal. + * @public + */ +Blockly.IConnectionChecker.prototype.canConnect; + +/** + * Checks whether the current connection can connect with the target + * connection, and return an error code if there are problems. + * @param {Blockly.Connection} a Connection to check compatibility with. + * @param {Blockly.Connection} b Connection to check compatibility with. + * @param {boolean} isDragging True if the connection is being made by dragging + * a block. + * @param {number=} opt_distance The max allowable distance between the + * connections for drag checks. + * @return {number} Blockly.Connection.CAN_CONNECT if the connection is legal, + * an error code otherwise. + * @public + */ +Blockly.IConnectionChecker.prototype.canConnectWithReason; + +/** + * Helper method that translates a connection error code into a string. + * @param {number} errorCode The error code. + * @param {Blockly.Connection} a One of the two connections being checked. + * @param {Blockly.Connection} b The second of the two connections being + * checked. + * @return {string} A developer-readable error string. + * @public + */ +Blockly.IConnectionChecker.prototype.getErrorMessage; + +/** + * Check that connecting the given connections is safe, meaning that it would + * not break any of Blockly's basic assumptions (e.g. no self connections). + * @param {Blockly.Connection} a The first of the connections to check. + * @param {Blockly.Connection} b The second of the connections to check. + * @return {number} An enum with the reason this connection is safe or unsafe. + * @public + */ +Blockly.IConnectionChecker.prototype.doSafetyChecks; + +/** + * Check whether this connection is compatible with another connection with + * respect to the value type system. E.g. square_root("Hello") is not + * compatible. + * @param {!Blockly.Connection} a Connection to compare. + * @param {!Blockly.Connection} b Connection to compare against. + * @return {boolean} True if the connections share a type. + * @public + */ +Blockly.IConnectionChecker.prototype.doTypeChecks; + +/** + * Check whether this connection can be made by dragging. + * @param {!Blockly.RenderedConnection} a Connection to compare. + * @param {!Blockly.RenderedConnection} b Connection to compare against. + * @param {number} distance The maximum allowable distance between connections. + * @return {boolean} True if the connection is allowed during a drag. + * @public + */ +Blockly.IConnectionChecker.prototype.doDragChecks; diff --git a/core/keyboard_nav/navigation.js b/core/keyboard_nav/navigation.js index 914882592..c162e282e 100644 --- a/core/keyboard_nav/navigation.js +++ b/core/keyboard_nav/navigation.js @@ -18,6 +18,7 @@ goog.require('Blockly.ASTNode'); goog.require('Blockly.utils.Coordinate'); goog.require('Blockly.user.keyMap'); + /** * A function to call to give feedback to the user about logs, warnings, and * errors. You can override this to customize feedback (e.g. warning sounds, @@ -409,9 +410,9 @@ Blockly.navigation.moveAndConnect_ = function(movingConnection, destConnection) } var movingBlock = movingConnection.getSourceBlock(); - if (destConnection.canConnectWithReason(movingConnection) == - Blockly.Connection.CAN_CONNECT) { + var checker = movingConnection.getConnectionChecker(); + if (checker.canConnect(movingConnection, destConnection, false)) { Blockly.navigation.disconnectChild_(movingConnection, destConnection); if (!destConnection.isSuperior()) { @@ -499,13 +500,11 @@ Blockly.navigation.connect_ = function(movingConnection, destConnection) { } else if (Blockly.navigation.moveAndConnect_(movingConnection, destConnection)){ return true; } else { - try { - destConnection.checkConnection(movingConnection); - } - catch (e) { - // If nothing worked report the error from the original connections. - Blockly.navigation.warn_('Connection failed with error: ' + e); - } + var checker = movingConnection.getConnectionChecker(); + var reason = checker.canConnectWithReason( + movingConnection, destConnection, false); + Blockly.navigation.warn_('Connection failed with error: ' + + checker.getErrorMessage(reason, movingConnection, destConnection)); return false; } }; diff --git a/core/rendered_connection.js b/core/rendered_connection.js index b1d19fed8..5ceb347c4 100644 --- a/core/rendered_connection.js +++ b/core/rendered_connection.js @@ -420,6 +420,7 @@ Blockly.RenderedConnection.prototype.startTrackingAll = function() { * @param {number=} maxRadius The maximum radius allowed for connections, in * workspace units. * @return {boolean} True if the connection is allowed, false otherwise. + * @deprecated July 2020 */ Blockly.RenderedConnection.prototype.isConnectionAllowed = function(candidate, maxRadius) { @@ -549,7 +550,8 @@ Blockly.RenderedConnection.prototype.connect_ = function(childConnection) { Blockly.RenderedConnection.prototype.onCheckChanged_ = function() { // The new value type may not be compatible with the existing connection. if (this.isConnected() && (!this.targetConnection || - !this.checkType(this.targetConnection))) { + !this.getConnectionChecker().canConnect( + this, this.targetConnection, false))) { var child = this.isSuperior() ? this.targetBlock() : this.sourceBlock_; child.unplug(); // Bump away. diff --git a/core/renderers/common/renderer.js b/core/renderers/common/renderer.js index 05885d18c..d0d8c1691 100644 --- a/core/renderers/common/renderer.js +++ b/core/renderers/common/renderer.js @@ -254,7 +254,8 @@ Blockly.blockRendering.Renderer.prototype.orphanCanConnectAtEnd = if (!lastConnection) { return false; } - return orphanConnection.checkType(lastConnection); + return orphanConnection.getConnectionChecker().canConnect( + lastConnection, orphanConnection, false); }; /** diff --git a/core/workspace.js b/core/workspace.js index 2c6d2ece3..3f3d0ceff 100644 --- a/core/workspace.js +++ b/core/workspace.js @@ -12,6 +12,7 @@ goog.provide('Blockly.Workspace'); +goog.require('Blockly.ConnectionChecker'); goog.require('Blockly.Events'); goog.require('Blockly.Options'); goog.require('Blockly.utils'); @@ -19,6 +20,7 @@ goog.require('Blockly.utils.math'); goog.require('Blockly.VariableMap'); goog.requireType('Blockly.IASTNodeLocation'); +goog.requireType('Blockly.IConnectionChecker'); /** @@ -42,6 +44,12 @@ Blockly.Workspace = function(opt_options) { /** @type {number} */ this.toolboxPosition = this.options.toolboxPosition; + /** + * An object that encapsulates logic for safety, type, and dragging checks. + * @type {!Blockly.IConnectionChecker} + */ + this.connectionChecker = new Blockly.ConnectionChecker(); + /** * @type {!Array.} * @private diff --git a/core/workspace_svg.js b/core/workspace_svg.js index b1f996e13..97739c44d 100644 --- a/core/workspace_svg.js +++ b/core/workspace_svg.js @@ -68,7 +68,8 @@ Blockly.WorkspaceSvg = function(options, this.setMetrics = options.setMetrics || Blockly.WorkspaceSvg.setTopLevelWorkspaceMetrics_; - this.connectionDBList = Blockly.ConnectionDB.init(); + + this.connectionDBList = Blockly.ConnectionDB.init(this.connectionChecker); if (opt_blockDragSurface) { this.blockDragSurface_ = opt_blockDragSurface; diff --git a/tests/mocha/connection_checker_test.js b/tests/mocha/connection_checker_test.js new file mode 100644 index 000000000..82528107d --- /dev/null +++ b/tests/mocha/connection_checker_test.js @@ -0,0 +1,264 @@ +/** + * @license + * Copyright 2020 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +suite('Connection checker', function() { + suiteSetup(function() { + this.checker = new Blockly.ConnectionChecker(); + }); + suite('Safety checks', function() { + function assertReasonHelper(checker, one, two, reason) { + chai.assert.equal(checker.canConnectWithReason(one, two), reason); + // Order should not matter. + chai.assert.equal(checker.canConnectWithReason(two, one), reason); + } + + test('Target Null', function() { + var connection = new Blockly.Connection({}, Blockly.INPUT_VALUE); + assertReasonHelper( + this.checker, + connection, + null, + Blockly.Connection.REASON_TARGET_NULL); + }); + test('Target Self', function() { + var block = {workspace: 1}; + var connection1 = new Blockly.Connection(block, Blockly.INPUT_VALUE); + var connection2 = new Blockly.Connection(block, Blockly.OUTPUT_VALUE); + + assertReasonHelper( + this.checker, + connection1, + connection2, + Blockly.Connection.REASON_SELF_CONNECTION); + }); + test('Different Workspaces', function() { + var connection1 = new Blockly.Connection( + {workspace: 1}, Blockly.INPUT_VALUE); + var connection2 = new Blockly.Connection( + {workspace: 2}, Blockly.OUTPUT_VALUE); + + assertReasonHelper( + this.checker, + connection1, + connection2, + Blockly.Connection.REASON_DIFFERENT_WORKSPACES); + }); + suite('Types', function() { + setup(function() { + // We have to declare each separately so that the connections belong + // on different blocks. + var prevBlock = { isShadow: function() {}}; + var nextBlock = { isShadow: function() {}}; + var outBlock = { isShadow: function() {}}; + var inBlock = { isShadow: function() {}}; + this.previous = new Blockly.Connection( + prevBlock, Blockly.PREVIOUS_STATEMENT); + this.next = new Blockly.Connection( + nextBlock, Blockly.NEXT_STATEMENT); + this.output = new Blockly.Connection( + outBlock, Blockly.OUTPUT_VALUE); + this.input = new Blockly.Connection( + inBlock, Blockly.INPUT_VALUE); + }); + test('Previous, Next', function() { + assertReasonHelper( + this.checker, + this.previous, + this.next, + Blockly.Connection.CAN_CONNECT); + }); + test('Previous, Output', function() { + assertReasonHelper( + this.checker, + this.previous, + this.output, + Blockly.Connection.REASON_WRONG_TYPE); + }); + test('Previous, Input', function() { + assertReasonHelper( + this.checker, + this.previous, + this.input, + Blockly.Connection.REASON_WRONG_TYPE); + }); + test('Next, Previous', function() { + assertReasonHelper( + this.checker, + this.next, + this.previous, + Blockly.Connection.CAN_CONNECT); + }); + test('Next, Output', function() { + assertReasonHelper( + this.checker, + this.next, + this.output, + Blockly.Connection.REASON_WRONG_TYPE); + }); + test('Next, Input', function() { + assertReasonHelper( + this.checker, + this.next, + this.input, + Blockly.Connection.REASON_WRONG_TYPE); + }); + test('Output, Previous', function() { + assertReasonHelper( + this.checker, + this.previous, + this.output, + Blockly.Connection.REASON_WRONG_TYPE); + }); + test('Output, Next', function() { + assertReasonHelper( + this.checker, + this.output, + this.next, + Blockly.Connection.REASON_WRONG_TYPE); + }); + test('Output, Input', function() { + assertReasonHelper( + this.checker, + this.output, + this.input, + Blockly.Connection.CAN_CONNECT); + }); + test('Input, Previous', function() { + assertReasonHelper( + this.checker, + this.previous, + this.input, + Blockly.Connection.REASON_WRONG_TYPE); + }); + test('Input, Next', function() { + assertReasonHelper( + this.checker, + this.input, + this.next, + Blockly.Connection.REASON_WRONG_TYPE); + }); + test('Input, Output', function() { + assertReasonHelper( + this.checker, + this.input, + this.output, + Blockly.Connection.CAN_CONNECT); + }); + }); + suite('Shadows', function() { + test('Previous Shadow', function() { + var prevBlock = { isShadow: function() { return true; }}; + var nextBlock = { isShadow: function() { return false; }}; + var prev = new Blockly.Connection(prevBlock, Blockly.PREVIOUS_STATEMENT); + var next = new Blockly.Connection(nextBlock, Blockly.NEXT_STATEMENT); + + assertReasonHelper( + this.checker, + prev, + next, + Blockly.Connection.CAN_CONNECT); + }); + test('Next Shadow', function() { + var prevBlock = { isShadow: function() { return false; }}; + var nextBlock = { isShadow: function() { return true; }}; + var prev = new Blockly.Connection(prevBlock, Blockly.PREVIOUS_STATEMENT); + var next = new Blockly.Connection(nextBlock, Blockly.NEXT_STATEMENT); + + assertReasonHelper( + this.checker, + prev, + next, + Blockly.Connection.REASON_SHADOW_PARENT); + }); + test('Prev and Next Shadow', function() { + var prevBlock = { isShadow: function() { return true; }}; + var nextBlock = { isShadow: function() { return true; }}; + var prev = new Blockly.Connection(prevBlock, Blockly.PREVIOUS_STATEMENT); + var next = new Blockly.Connection(nextBlock, Blockly.NEXT_STATEMENT); + + assertReasonHelper( + this.checker, + prev, + next, + Blockly.Connection.CAN_CONNECT); + }); + test('Output Shadow', function() { + var outBlock = { isShadow: function() { return true; }}; + var inBlock = { isShadow: function() { return false; }}; + var outCon = new Blockly.Connection(outBlock, Blockly.OUTPUT_VALUE); + var inCon = new Blockly.Connection(inBlock, Blockly.INPUT_VALUE); + + assertReasonHelper( + this.checker, + outCon, + inCon, + Blockly.Connection.CAN_CONNECT); + }); + test('Input Shadow', function() { + var outBlock = { isShadow: function() { return false; }}; + var inBlock = { isShadow: function() { return true; }}; + var outCon = new Blockly.Connection(outBlock, Blockly.OUTPUT_VALUE); + var inCon = new Blockly.Connection(inBlock, Blockly.INPUT_VALUE); + + assertReasonHelper( + this.checker, + outCon, + inCon, + Blockly.Connection.REASON_SHADOW_PARENT); + }); + test('Output and Input Shadow', function() { + var outBlock = { isShadow: function() { return true; }}; + var inBlock = { isShadow: function() { return true; }}; + var outCon = new Blockly.Connection(outBlock, Blockly.OUTPUT_VALUE); + var inCon = new Blockly.Connection(inBlock, Blockly.INPUT_VALUE); + + assertReasonHelper( + this.checker, + outCon, + inCon, + Blockly.Connection.CAN_CONNECT); + }); + }); + }); + suite('Check Types', function() { + setup(function() { + this.con1 = new Blockly.Connection({}, Blockly.PREVIOUS_STATEMENT); + this.con2 = new Blockly.Connection({}, Blockly.NEXT_STATEMENT); + }); + function assertCheckTypes(checker, one, two) { + chai.assert.isTrue(checker.doTypeChecks(one, two)); + // Order should not matter. + chai.assert.isTrue(checker.doTypeChecks(one, two)); + } + test('No Types', function() { + assertCheckTypes(this.checker, this.con1, this.con2); + }); + test('Same Type', function() { + this.con1.setCheck('type1'); + this.con2.setCheck('type1'); + assertCheckTypes(this.checker, this.con1, this.con2); + }); + test('Same Types', function() { + this.con1.setCheck(['type1', 'type2']); + this.con2.setCheck(['type1', 'type2']); + assertCheckTypes(this.checker, this.con1, this.con2); + }); + test('Single Same Type', function() { + this.con1.setCheck(['type1', 'type2']); + this.con2.setCheck(['type1', 'type3']); + assertCheckTypes(this.checker, this.con1, this.con2); + }); + test('One Typed, One Promiscuous', function() { + this.con1.setCheck('type1'); + assertCheckTypes(this.checker, this.con1, this.con2); + }); + test('No Compatible Types', function() { + this.con1.setCheck('type1'); + this.con2.setCheck('type2'); + chai.assert.isFalse(this.checker.doTypeChecks(this.con1, this.con2)); + }); + }); +}); diff --git a/tests/mocha/connection_db_test.js b/tests/mocha/connection_db_test.js index 99aabcda9..9e43308f0 100644 --- a/tests/mocha/connection_db_test.js +++ b/tests/mocha/connection_db_test.js @@ -6,7 +6,7 @@ suite('Connection Database', function() { setup(function() { - this.database = new Blockly.ConnectionDB(); + this.database = new Blockly.ConnectionDB(new Blockly.ConnectionChecker()); this.assertOrder = function() { var length = this.database.connections_.length; @@ -190,28 +190,35 @@ suite('Connection Database', function() { this.assertOrder(); }); }); - // Does not cover logic for isConnectionAllowed + suite('Search For Closest', function() { setup(function() { - this.allowedStub = null; + this.allowedStubs = []; + // Ignore type checks. + this.allowedStubs.push(sinon.stub(this.database.connectionChecker_, 'doTypeChecks') + .callsFake(function(_a, _b) { + return true; + })); + // Ignore safety checks. + this.allowedStubs.push(sinon.stub(this.database.connectionChecker_, 'doSafetyChecks') + .callsFake(function(_a, _b) { + return Blockly.Connection.CAN_CONNECT; + })); + // Skip everything but the distance checks. + this.allowedStubs.push(sinon.stub(this.database.connectionChecker_, 'doDragChecks') + .callsFake(function(a, b, distance) { + return a.distanceFrom(b) <= distance; + })); this.createCheckConnection = function(x, y) { var checkConnection = this.createConnection(x, y, Blockly.NEXT_STATEMENT, new Blockly.ConnectionDB()); - this.allowedStub = sinon.stub(checkConnection, 'isConnectionAllowed') - .callsFake(function(candidate, maxRadius) { - if (this.distanceFrom(candidate) > maxRadius) { - return false; - } - // Ignore non-distance parameters. - return true; - }); return checkConnection; }; }); teardown(function() { - if (this.allowedStub) { - this.allowedStub.restore(); + for (var i = 0; i < this.allowedStubs.length; i++) { + this.allowedStubs[i].restore(); } }); test('Empty Database', function() { diff --git a/tests/mocha/connection_test.js b/tests/mocha/connection_test.js index f78584cfa..6472cd41b 100644 --- a/tests/mocha/connection_test.js +++ b/tests/mocha/connection_test.js @@ -4,184 +4,44 @@ * SPDX-License-Identifier: Apache-2.0 */ -suite('Connections', function() { - suite('Can Connect With Reason', function() { - test('Target Null', function() { - var connection = new Blockly.Connection({}, Blockly.INPUT_VALUE); - chai.assert.equal(connection.canConnectWithReason(null), - Blockly.Connection.REASON_TARGET_NULL); - }); - test('Target Self', function() { - var block = {workspace: 1}; - var connection1 = new Blockly.Connection(block, Blockly.INPUT_VALUE); - var connection2 = new Blockly.Connection(block, Blockly.OUTPUT_VALUE); - - chai.assert.equal(connection1.canConnectWithReason(connection2), - Blockly.Connection.REASON_SELF_CONNECTION); - }); - test('Different Workspaces', function() { - var connection1 = new Blockly.Connection( - {workspace: 1}, Blockly.INPUT_VALUE); - var connection2 = new Blockly.Connection( - {workspace: 2}, Blockly.OUTPUT_VALUE); - - chai.assert.equal(connection1.canConnectWithReason(connection2), - Blockly.Connection.REASON_DIFFERENT_WORKSPACES); - }); - suite('Types', function() { - setup(function() { - // We have to declare each separately so that the connections belong - // on different blocks. - var prevBlock = { isShadow: function() {}}; - var nextBlock = { isShadow: function() {}}; - var outBlock = { isShadow: function() {}}; - var inBlock = { isShadow: function() {}}; - this.previous = new Blockly.Connection( - prevBlock, Blockly.PREVIOUS_STATEMENT); - this.next = new Blockly.Connection( - nextBlock, Blockly.NEXT_STATEMENT); - this.output = new Blockly.Connection( - outBlock, Blockly.OUTPUT_VALUE); - this.input = new Blockly.Connection( - inBlock, Blockly.INPUT_VALUE); - }); - test('Previous, Next', function() { - chai.assert.equal(this.previous.canConnectWithReason(this.next), - Blockly.Connection.CAN_CONNECT); - }); - test('Previous, Output', function() { - chai.assert.equal(this.previous.canConnectWithReason(this.output), - Blockly.Connection.REASON_WRONG_TYPE); - }); - test('Previous, Input', function() { - chai.assert.equal(this.previous.canConnectWithReason(this.input), - Blockly.Connection.REASON_WRONG_TYPE); - }); - test('Next, Previous', function() { - chai.assert.equal(this.next.canConnectWithReason(this.previous), - Blockly.Connection.CAN_CONNECT); - }); - test('Next, Output', function() { - chai.assert.equal(this.next.canConnectWithReason(this.output), - Blockly.Connection.REASON_WRONG_TYPE); - }); - test('Next, Input', function() { - chai.assert.equal(this.next.canConnectWithReason(this.input), - Blockly.Connection.REASON_WRONG_TYPE); - }); - test('Output, Previous', function() { - chai.assert.equal(this.output.canConnectWithReason(this.previous), - Blockly.Connection.REASON_WRONG_TYPE); - }); - test('Output, Next', function() { - chai.assert.equal(this.output.canConnectWithReason(this.next), - Blockly.Connection.REASON_WRONG_TYPE); - }); - test('Output, Input', function() { - chai.assert.equal(this.output.canConnectWithReason(this.input), - Blockly.Connection.CAN_CONNECT); - }); - test('Input, Previous', function() { - chai.assert.equal(this.input.canConnectWithReason(this.previous), - Blockly.Connection.REASON_WRONG_TYPE); - }); - test('Input, Next', function() { - chai.assert.equal(this.input.canConnectWithReason(this.next), - Blockly.Connection.REASON_WRONG_TYPE); - }); - test('Input, Output', function() { - chai.assert.equal(this.input.canConnectWithReason(this.output), - Blockly.Connection.CAN_CONNECT); - }); - }); - suite('Shadows', function() { - test('Previous Shadow', function() { - var prevBlock = { isShadow: function() { return true; }}; - var nextBlock = { isShadow: function() { return false; }}; - var prev = new Blockly.Connection(prevBlock, Blockly.PREVIOUS_STATEMENT); - var next = new Blockly.Connection(nextBlock, Blockly.NEXT_STATEMENT); - - chai.assert.equal(prev.canConnectWithReason(next), - Blockly.Connection.CAN_CONNECT); - }); - test('Next Shadow', function() { - var prevBlock = { isShadow: function() { return false; }}; - var nextBlock = { isShadow: function() { return true; }}; - var prev = new Blockly.Connection(prevBlock, Blockly.PREVIOUS_STATEMENT); - var next = new Blockly.Connection(nextBlock, Blockly.NEXT_STATEMENT); - - chai.assert.equal(prev.canConnectWithReason(next), - Blockly.Connection.REASON_SHADOW_PARENT); - }); - test('Prev and Next Shadow', function() { - var prevBlock = { isShadow: function() { return true; }}; - var nextBlock = { isShadow: function() { return true; }}; - var prev = new Blockly.Connection(prevBlock, Blockly.PREVIOUS_STATEMENT); - var next = new Blockly.Connection(nextBlock, Blockly.NEXT_STATEMENT); - - chai.assert.equal(prev.canConnectWithReason(next), - Blockly.Connection.CAN_CONNECT); - }); - test('Output Shadow', function() { - var outBlock = { isShadow: function() { return true; }}; - var inBlock = { isShadow: function() { return false; }}; - var outCon = new Blockly.Connection(outBlock, Blockly.OUTPUT_VALUE); - var inCon = new Blockly.Connection(inBlock, Blockly.INPUT_VALUE); - - chai.assert.equal(outCon.canConnectWithReason(inCon), - Blockly.Connection.CAN_CONNECT); - }); - test('Input Shadow', function() { - var outBlock = { isShadow: function() { return false; }}; - var inBlock = { isShadow: function() { return true; }}; - var outCon = new Blockly.Connection(outBlock, Blockly.OUTPUT_VALUE); - var inCon = new Blockly.Connection(inBlock, Blockly.INPUT_VALUE); - - chai.assert.equal(outCon.canConnectWithReason(inCon), - Blockly.Connection.REASON_SHADOW_PARENT); - }); - test('Output and Input Shadow', function() { - var outBlock = { isShadow: function() { return true; }}; - var inBlock = { isShadow: function() { return true; }}; - var outCon = new Blockly.Connection(outBlock, Blockly.OUTPUT_VALUE); - var inCon = new Blockly.Connection(inBlock, Blockly.INPUT_VALUE); - - chai.assert.equal(outCon.canConnectWithReason(inCon), - Blockly.Connection.CAN_CONNECT); - }); +suite('Connection', function() { + suiteSetup(function() { + this.workspace = { + connectionChecker: new Blockly.ConnectionChecker() + }; + this.createConnection = function(type) { + var block = { + workspace: this.workspace, + isShadow: function() { return false; } + }; + var connection = new Blockly.Connection(block, type); + return connection; + }; + }); + test('canConnectWithReason passes', function() { + var conn1 = this.createConnection(Blockly.PREVIOUS_STATEMENT); + var conn2 = this.createConnection(Blockly.NEXT_STATEMENT); + chai.assert.equal(conn1.canConnectWithReason(conn2), + Blockly.Connection.CAN_CONNECT); + }); + test('canConnectWithReason fails', function() { + var conn1 = this.createConnection(Blockly.PREVIOUS_STATEMENT); + var conn2 = this.createConnection(Blockly.OUTPUT_VALUE); + chai.assert.equal(conn1.canConnectWithReason(conn2), + Blockly.Connection.REASON_WRONG_TYPE); + }); + test('checkConnection passes', function() { + var conn1 = this.createConnection(Blockly.PREVIOUS_STATEMENT); + var conn2 = this.createConnection(Blockly.NEXT_STATEMENT); + chai.assert.doesNotThrow(function() { + conn1.checkConnection(conn2); }); }); - suite('Check Types', function() { - setup(function() { - this.con1 = new Blockly.Connection({}, Blockly.PREVIOUS_STATEMENT); - this.con2 = new Blockly.Connection({}, Blockly.NEXT_STATEMENT); - }); - test('No Types', function() { - chai.assert.isTrue(this.con1.checkType((this.con2))); - }); - test('Same Type', function() { - this.con1.setCheck('type1'); - this.con2.setCheck('type1'); - chai.assert.isTrue(this.con1.checkType((this.con2))); - }); - test('Same Types', function() { - this.con1.setCheck(['type1', 'type2']); - this.con2.setCheck(['type1', 'type2']); - chai.assert.isTrue(this.con1.checkType((this.con2))); - }); - test('Single Same Type', function() { - this.con1.setCheck(['type1', 'type2']); - this.con2.setCheck(['type1', 'type3']); - chai.assert.isTrue(this.con1.checkType((this.con2))); - }); - test('One Typed, One Promiscuous', function() { - this.con1.setCheck('type1'); - chai.assert.isTrue(this.con1.checkType((this.con2))); - }); - test('No Compatible Types', function() { - this.con1.setCheck('type1'); - this.con2.setCheck('type2'); - chai.assert.isFalse(this.con1.checkType((this.con2))); + test('checkConnection fails', function() { + var conn1 = this.createConnection(Blockly.PREVIOUS_STATEMENT); + var conn2 = this.createConnection(Blockly.OUTPUT_VALUE); + chai.assert.throws(function() { + conn1.checkConnection(conn2); }); }); }); diff --git a/tests/mocha/index.html b/tests/mocha/index.html index bc236ad93..e4391e0c7 100644 --- a/tests/mocha/index.html +++ b/tests/mocha/index.html @@ -40,8 +40,8 @@ + -