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 @@
+
-