diff --git a/packages/blockly/core/serialization/blocks.ts b/packages/blockly/core/serialization/blocks.ts
index af8910b3137..7afa363b99b 100644
--- a/packages/blockly/core/serialization/blocks.ts
+++ b/packages/blockly/core/serialization/blocks.ts
@@ -601,18 +601,27 @@ function tryToConnectParent(
if (!connected) {
const checker = child.workspace.connectionChecker;
- throw new BadConnectionCheck(
- checker.getErrorMessage(
- checker.canConnectWithReason(childConnection, parentConnection, false),
- childConnection,
- parentConnection,
- ),
- parentConnection.type === inputTypes.VALUE
- ? 'output connection'
- : 'previous connection',
- child,
- state,
+ const reason = checker.getErrorMessage(
+ checker.canConnectWithReason(childConnection, parentConnection, false),
+ childConnection,
+ parentConnection,
);
+ if (child.isShadow()) {
+ throw new BadConnectionCheck(
+ reason,
+ parentConnection.type === inputTypes.VALUE
+ ? 'output connection'
+ : 'previous connection',
+ child,
+ state,
+ );
+ }
+ console.warn(
+ `Connection check failed during JSON deserialization: ${reason}. ` +
+ `Block "${child.type}" (${child.id}) will be placed as a ` +
+ `top-level block instead.`,
+ );
+ child.setDisabledReason(true, 'orphaned_connection_check');
}
}
diff --git a/packages/blockly/core/xml.ts b/packages/blockly/core/xml.ts
index 3c06a39cca9..05654e6ee04 100644
--- a/packages/blockly/core/xml.ts
+++ b/packages/blockly/core/xml.ts
@@ -636,6 +636,7 @@ export function domToBlockInternal(
// Create top-level block.
eventUtils.disable();
const variablesBeforeCreation = workspace.getVariableMap().getAllVariables();
+ const topBlocksBefore = new Set(workspace.getTopBlocks(false));
let topBlock;
try {
topBlock = domToBlockHeadless(xmlBlock, workspace);
@@ -651,6 +652,30 @@ export function domToBlockInternal(
for (let i = blocks.length - 1; i >= 0; i--) {
(blocks[i] as BlockSvg).queueRender();
}
+
+ // Initialize any orphaned blocks that failed to connect during
+ // deserialization (e.g. due to connection type check failures).
+ // These blocks were created but are not descendants of topBlock.
+ const connectedBlocks = new Set(blocks);
+ for (const block of workspace.getTopBlocks(false)) {
+ if (topBlocksBefore.has(block) || connectedBlocks.has(block)) continue;
+ const orphanSvg = block as BlockSvg;
+ orphanSvg.setConnectionTracking(false);
+ block.setDisabledReason(true, 'orphaned_connection_check');
+ const orphanDescendants = block.getDescendants(false);
+ for (let i = orphanDescendants.length - 1; i >= 0; i--) {
+ (orphanDescendants[i] as BlockSvg).initSvg();
+ }
+ for (let i = orphanDescendants.length - 1; i >= 0; i--) {
+ (orphanDescendants[i] as BlockSvg).queueRender();
+ }
+ setTimeout(function () {
+ if (!orphanSvg.disposed) {
+ orphanSvg.setConnectionTracking(true);
+ }
+ }, 1);
+ }
+
// Populating the connection database may be deferred until after the
// blocks have rendered.
setTimeout(function () {
@@ -666,6 +691,16 @@ export function domToBlockInternal(
for (let i = blocks.length - 1; i >= 0; i--) {
blocks[i].initModel();
}
+ // Initialize orphaned blocks on headless workspaces too.
+ const connectedBlocks = new Set(blocks);
+ for (const block of workspace.getTopBlocks(false)) {
+ if (topBlocksBefore.has(block) || connectedBlocks.has(block)) continue;
+ block.setDisabledReason(true, 'orphaned_connection_check');
+ const orphanDescendants = block.getDescendants(false);
+ for (let i = orphanDescendants.length - 1; i >= 0; i--) {
+ orphanDescendants[i].initModel();
+ }
+ }
}
} finally {
eventUtils.enable();
diff --git a/packages/blockly/tests/mocha/jso_deserialization_test.js b/packages/blockly/tests/mocha/jso_deserialization_test.js
index f6b47d7de6a..c4ad624b90d 100644
--- a/packages/blockly/tests/mocha/jso_deserialization_test.js
+++ b/packages/blockly/tests/mocha/jso_deserialization_test.js
@@ -14,6 +14,7 @@ import {
import {
sharedTestSetup,
sharedTestTeardown,
+ workspaceTeardown,
} from './test_helpers/setup_teardown.js';
suite('JSO Deserialization', function () {
@@ -630,10 +631,50 @@ suite('JSO Deserialization', function () {
],
},
};
- this.assertThrows(
- state,
- Blockly.serialization.exceptions.BadConnectionCheck,
- );
+ Blockly.serialization.workspaces.load(state, this.workspace);
+
+ const allBlocks = this.workspace.getAllBlocks(false);
+ assert.equal(allBlocks.length, 2, 'Both blocks exist');
+
+ const mathBlock = allBlocks.find((b) => b.type === 'math_number');
+ assert.isNotNull(mathBlock, 'math_number block exists');
+ assert.isNull(mathBlock.getParent(), 'Orphan has no parent');
+ });
+
+ test('Bad checks - orphan SVG is initialized on rendered workspace', function () {
+ const workspace = Blockly.inject('blocklyDiv');
+ try {
+ const state = {
+ 'blocks': {
+ 'blocks': [
+ {
+ 'type': 'logic_operation',
+ 'inputs': {
+ 'A': {
+ 'block': {
+ 'type': 'math_number',
+ },
+ },
+ },
+ },
+ ],
+ },
+ };
+ Blockly.serialization.workspaces.load(state, workspace);
+ this.clock.runAll();
+
+ const allBlocks = workspace.getAllBlocks(false);
+ assert.equal(allBlocks.length, 2, 'Both blocks exist');
+
+ const mathBlock = allBlocks.find((b) => b.type === 'math_number');
+ assert.isNotNull(mathBlock, 'math_number block exists');
+ assert.isNotNull(
+ mathBlock.getSvgRoot().parentNode,
+ 'Orphan SVG is in the DOM',
+ );
+ } finally {
+ workspaceTeardown.call(this, workspace);
+ }
});
});
diff --git a/packages/blockly/tests/mocha/xml_test.js b/packages/blockly/tests/mocha/xml_test.js
index 94219f9a067..f1106c572a9 100644
--- a/packages/blockly/tests/mocha/xml_test.js
+++ b/packages/blockly/tests/mocha/xml_test.js
@@ -890,4 +890,90 @@ suite('XML', function () {
});
});
});
+
+ suite('Connection check failures', function () {
+ setup(function () {
+ Blockly.defineBlocksWithJsonArray([
+ {
+ 'type': 'typed_input_block',
+ 'message0': '%1',
+ 'args0': [
+ {
+ 'type': 'input_value',
+ 'name': 'INPUT',
+ 'check': 'Int',
+ },
+ ],
+ },
+ {
+ 'type': 'typed_output_block',
+ 'message0': '',
+ 'output': 'Long',
+ },
+ ]);
+ addBlockTypeToCleanup(this.sharedCleanup, 'typed_input_block');
+ addBlockTypeToCleanup(this.sharedCleanup, 'typed_output_block');
+ });
+
+ test('Orphaned block SVG is in the DOM on rendered workspace', function () {
+ this.workspace = Blockly.inject('blocklyDiv');
+ const xml = Blockly.utils.xml.textToDom(
+ '' +
+ '' +
+ ' ' +
+ ' ' +
+ ' ' +
+ '' +
+ '',
+ );
+ Blockly.Xml.domToWorkspace(xml, this.workspace);
+ this.clock.runAll();
+
+ const allBlocks = this.workspace.getAllBlocks(false);
+ assert.equal(allBlocks.length, 2, 'Both blocks exist');
+
+ const topBlocks = this.workspace.getTopBlocks(false);
+ assert.equal(topBlocks.length, 2, 'Both blocks are top-level');
+
+ const outputBlock = allBlocks.find(
+ (b) => b.type === 'typed_output_block',
+ );
+ assert.isNotNull(
+ outputBlock.getSvgRoot().parentNode,
+ 'Orphaned block SVG is in the DOM',
+ );
+ assert.isNull(outputBlock.getParent(), 'Orphaned block has no parent');
+
+ workspaceTeardown.call(this, this.workspace);
+ });
+
+ test('Orphaned block is initialized on headless workspace', function () {
+ const workspace = new Blockly.Workspace();
+ try {
+ const xml = Blockly.utils.xml.textToDom(
+ '' +
+ '' +
+ ' ' +
+ ' ' +
+ ' ' +
+ '' +
+ '',
+ );
+ Blockly.Xml.domToWorkspace(xml, workspace);
+
+ const allBlocks = workspace.getAllBlocks(false);
+ assert.equal(allBlocks.length, 2, 'Both blocks exist');
+
+ const topBlocks = workspace.getTopBlocks(false);
+ assert.equal(topBlocks.length, 2, 'Both blocks are top-level');
+
+ const outputBlock = allBlocks.find(
+ (b) => b.type === 'typed_output_block',
+ );
+ assert.isNull(outputBlock.getParent(), 'Orphaned block has no parent');
+ } finally {
+ workspaceTeardown.call(this, workspace);
+ }
+ });
+ });
});