Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 20 additions & 11 deletions packages/blockly/core/serialization/blocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
}
}

Expand Down
35 changes: 35 additions & 0 deletions packages/blockly/core/xml.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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 () {
Expand All @@ -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();
Expand Down
49 changes: 45 additions & 4 deletions packages/blockly/tests/mocha/jso_deserialization_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
import {
sharedTestSetup,
sharedTestTeardown,
workspaceTeardown,
} from './test_helpers/setup_teardown.js';

suite('JSO Deserialization', function () {
Expand Down Expand Up @@ -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);
}
});
});

Expand Down
86 changes: 86 additions & 0 deletions packages/blockly/tests/mocha/xml_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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(
'<xml>' +
'<block type="typed_input_block" x="10" y="10">' +
' <value name="INPUT">' +
' <block type="typed_output_block"/>' +
' </value>' +
'</block>' +
'</xml>',
);
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(
'<xml>' +
'<block type="typed_input_block" x="10" y="10">' +
' <value name="INPUT">' +
' <block type="typed_output_block"/>' +
' </value>' +
'</block>' +
'</xml>',
);
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);
}
});
});
});
Loading