diff --git a/src/engine/blocks.js b/src/engine/blocks.js index 5a0bbd965..26943af17 100644 --- a/src/engine/blocks.js +++ b/src/engine/blocks.js @@ -862,7 +862,12 @@ class Blocks { // Is this block a top-level block? if (typeof e.newParent === 'undefined') { - this._addScript(e.id); + // When you plug a block into a shadowed input, Blockly will move the shadow block into the top level before + // deleting it. Because we don't delete shadows here, instead keeping them on our blocks, the corresponding + // "delete" event will be ignored and the shadow would otherwise be added into the top level. + // TODO: Blockly inputs' connections keep their own copies of shadow blocks stashed away too. + // We should probably use those instead of duplicating the shadow-saving behavior. + if (!this._blocks[e.id].shadow) this._addScript(e.id); } else { // Remove script, if one exists. this._deleteScript(e.id); diff --git a/test/unit/engine_blocks.js b/test/unit/engine_blocks.js index f4451aa39..95add389d 100644 --- a/test/unit/engine_blocks.js +++ b/test/unit/engine_blocks.js @@ -600,6 +600,98 @@ test('delete inputs', t => { t.end(); }); +test('drop block into shadowed input', t => { + const b = new Blocks(new Runtime()); + b.createBlock({ + id: 'foo', + opcode: 'motion_movesteps', + inputs: { + STEPS: { + name: 'STEPS', + block: 'shadow', + shadow: 'shadow' + } + }, + topLevel: true + }); + b.createBlock({ + id: 'shadow', + opcode: 'math_number', + inputs: {}, + fields: { + NUM: { + name: 'NUM', + value: '25' + } + }, + shadow: true, + topLevel: false + }); + + b.createBlock({ + id: 'reporter', + opcode: 'looks_size', + inputs: {}, + fields: {}, + topLevel: true + }); + + // Mimic the series of events emitted by Blockly when you move a block into a shadowed input + + // 1. Move the shadow block to the top level + b.moveBlock({ + id: 'shadow', + oldParent: 'foo', + newParent: undefined + }); + + // 2: Delete the shadow block + b.deleteBlock('shadow'); + + // 3: Move the new block into the input + b.moveBlock({ + id: 'reporter', + newParent: 'foo' + }); + + t.equals(b._scripts.indexOf('shadow'), -1, 'replaced shadow blocks should not be moved to the top level'); + t.equals(b._scripts.length, 1); + + // Now mimic the series of events when you move the block out of the shadowed input + + // 1. Move the block back out of the shadowed input + b.moveBlock({ + id: 'reporter', + oldParent: 'foo', + newParent: undefined + }); + + // 2. Re-create the shadow block + b.createBlock({ + id: 'shadow', + opcode: 'math_number', + inputs: {}, + fields: { + NUM: { + name: 'NUM', + value: '25' + } + }, + shadow: true, + topLevel: false + }); + + // 3. Move the shadow block into the input + b.moveBlock({ + id: 'shadow', + newParent: 'foo' + }); + + t.equals(b._scripts.indexOf('shadow'), -1, 're-created shadow blocks should not be moved to the top level'); + t.equals(b._scripts.length, 2); + t.end(); +}); + test('updateAssetName function updates name in sound field', t => { const b = new Blocks(new Runtime()); b.createBlock({