From bd77b4ad3d97f74e977d0c49dbf63c61bc36e150 Mon Sep 17 00:00:00 2001 From: Beka Westberg Date: Wed, 11 Aug 2021 15:01:24 +0000 Subject: [PATCH] fix: touch up some deserialization behavior (#5181) * Add parameter for recording undo. This sets up the most common default behavior, but also makes it clear to people that it is happening, because it might not be expected. * Add grouping of events * Add text width caching * Add disabling workspace resizing * Add performance optimizations * Respect nulls from blocks.save * Cleanup from rebase * PR Comments * Cleanup from rebase --- core/serialization/blocks.js | 38 +- core/serialization/variables.js | 16 +- core/serialization/workspaces.js | 54 ++- tests/deps.js | 6 +- tests/deps.mocha.js | 6 +- tests/mocha/jso_deserialization_test.js | 491 +++++++++++++++++++----- tests/playground.html | 69 ++++ 7 files changed, 555 insertions(+), 125 deletions(-) diff --git a/core/serialization/blocks.js b/core/serialization/blocks.js index 40d2a1309..9f55c4091 100644 --- a/core/serialization/blocks.js +++ b/core/serialization/blocks.js @@ -17,6 +17,7 @@ goog.module.declareLegacyNamespace(); const Block = goog.requireType('Blockly.Block'); // eslint-disable-next-line no-unused-vars const Connection = goog.requireType('Blockly.Connection'); +const Events = goog.require('Blockly.Events'); // eslint-disable-next-line no-unused-vars const Workspace = goog.requireType('Blockly.Workspace'); const Xml = goog.require('Blockly.Xml'); @@ -71,9 +72,8 @@ exports.State = State; * addNextBlocks: If true, children of the block which are connected to the * block's next connection (if it exists) will be serialized. * True by default. - * @return {?State} The serialized state of the - * block, or null if the block could not be serialied (eg it was an - * insertion marker). + * @return {?State} The serialized state of the block, or null if the block + * could not be serialied (eg it was an insertion marker). */ const save = function( block, @@ -262,21 +262,33 @@ const saveConnection = function(connection) { * Loads the block represented by the given state into the given workspace. * @param {!State} state The state of a block to deserialize into the workspace. * @param {!Workspace} workspace The workspace to add the block to. + * @param {{recordUndo: (boolean|undefined)}=} param1 + * recordUndo: If true, events triggered by this function will be undo-able + * by the user. False by default. * @return {!Block} The block that was just loaded. */ -const load = function(state, workspace) { +const load = function(state, workspace, {recordUndo = false} = {}) { + const prevRecordUndo = Events.getRecordUndo(); + Events.setRecordUndo(recordUndo); + const existingGroup = Events.getGroup(); + if (!existingGroup) { + Events.setGroup(true); + } + // We only want to fire an event for the top block. - Blockly.Events.disable(); + Events.disable(); const block = loadInternal(state, workspace); - Blockly.Events.enable(); - Blockly.Events.fire( - new (Blockly.Events.get(Blockly.Events.BLOCK_CREATE))(block)); + Events.enable(); + Events.fire(new (Events.get(Events.BLOCK_CREATE))(block)); + + Events.setGroup(existingGroup); + Events.setRecordUndo(prevRecordUndo); // Adding connections to the connection db is expensive. This defers that // operation to decrease load time. - if (block instanceof Blockly.BlockSvg) { + if (workspace.rendered) { setTimeout(() => { if (!block.disposed) { block.setConnectionTracking(true); @@ -313,7 +325,7 @@ const loadInternal = function(state, workspace, parentConnection = undefined) { loadFields(block, state); loadInputBlocks(block, state); loadNextBlocks(block, state); - initBlock(block); + initBlock(block, workspace.rendered); return block; }; @@ -453,16 +465,16 @@ const loadConnection = function(connection, connectionState) { /** * Initializes the give block, eg init the model, inits the svg, renders, etc. * @param {!Block} block The block to initialize. + * @param {boolean} rendered Whether the block is a rendered or headless block. */ -const initBlock = function(block) { - if (block instanceof Blockly.BlockSvg) { +const initBlock = function(block, rendered) { + if (rendered) { // Adding connections to the connection db is expensive. This defers that // operation to decrease load time. block.setConnectionTracking(false); block.initSvg(); block.render(false); - block.updateDisabled(); } else { block.initModel(); } diff --git a/core/serialization/variables.js b/core/serialization/variables.js index 8bcb7b408..e071b9f3e 100644 --- a/core/serialization/variables.js +++ b/core/serialization/variables.js @@ -13,6 +13,7 @@ goog.module('Blockly.serialization.variables'); goog.module.declareLegacyNamespace(); +const Events = goog.require('Blockly.Events'); // eslint-disable-next-line no-unused-vars const VariableModel = goog.requireType('Blockly.VariableModel'); // eslint-disable-next-line no-unused-vars @@ -54,9 +55,22 @@ exports.save = save; * @param {!State} state The state of a variable to deserialize into the * workspace. * @param {!Workspace} workspace The workspace to add the variable to. + * @param {{recordUndo: (boolean|undefined)}=} param1 + * recordUndo: If true, events triggered by this function will be undo-able + * by the user. False by default. */ -const load = function(state, workspace) { +const load = function(state, workspace, {recordUndo = false} = {}) { + const prevRecordUndo = Events.getRecordUndo(); + Events.setRecordUndo(recordUndo); + const existingGroup = Events.getGroup(); + if (!existingGroup) { + Events.setGroup(true); + } + workspace.createVariable(state['name'], state['type'], state['id']); + + Events.setGroup(existingGroup); + Events.setRecordUndo(prevRecordUndo); }; /** @package */ exports.load = load; diff --git a/core/serialization/workspaces.js b/core/serialization/workspaces.js index b4600f987..4b82ef7ca 100644 --- a/core/serialization/workspaces.js +++ b/core/serialization/workspaces.js @@ -13,9 +13,11 @@ goog.module('Blockly.serialization.workspaces'); goog.module.declareLegacyNamespace(); +const Events = goog.require('Blockly.Events'); // eslint-disable-next-line no-unused-vars const Workspace = goog.require('Blockly.Workspace'); const blocks = goog.require('Blockly.serialization.blocks'); +const dom = goog.require('Blockly.utils.dom'); const variables = goog.require('Blockly.serialization.variables'); @@ -28,24 +30,27 @@ const save = function(workspace) { const state = Object.create(null); // TODO: Switch this to use plugin serialization system (once it is built). - const variableState = []; + const variableStates = []; const vars = workspace.getAllVariables(); for (let i = 0; i < vars.length; i++) { - variableState.push(variables.save(vars[i])); + variableStates.push(variables.save(vars[i])); } - if (variableState.length) { - state['variables'] = variableState; + if (variableStates.length) { + state['variables'] = variableStates; } - const blockState = []; + const blockStates = []; for (let block of workspace.getTopBlocks(false)) { - blockState.push( - blocks.save(block, {addCoordinates: true})); + const blockState = + blocks.save(block, {addCoordinates: true}); + if (blockState) { + blockStates.push(blockState); + } } - if (blockState.length) { + if (blockStates.length) { // This is an object to support adding language version later. state['blocks'] = { - 'blocks': blockState + 'blocks': blockStates }; } @@ -58,23 +63,48 @@ exports.save = save; * @param {!Object} state The state of the workspace to deserialize * into the workspace. * @param {!Workspace} workspace The workspace to add the new state to. + * @param {{recordUndo: (boolean|undefined)}=} param1 + * recordUndo: If true, events triggered by this function will be undo-able + * by the user. False by default. */ -const load = function(state, workspace) { +const load = function(state, workspace, {recordUndo = false} = {}) { // TODO: Switch this to use plugin serialization system (once it is built). // TODO: Add something for clearing the state before deserializing. + const prevRecordUndo = Events.getRecordUndo(); + Events.setRecordUndo(recordUndo); + const existingGroup = Events.getGroup(); + if (!existingGroup) { + Events.setGroup(true); + } + + dom.startTextWidthCache(); + if (workspace.setResizesEnabled) { + workspace.setResizesEnabled(false); + } + if (state['variables']) { const variableStates = state['variables']; for (let i = 0; i < variableStates.length; i++) { - variables.load(variableStates[i], workspace); + variables.load(variableStates[i], workspace, {recordUndo}); } } if (state['blocks']) { const blockStates = state['blocks']['blocks']; for (let i = 0; i < blockStates.length; i++) { - blocks.load(blockStates[i], workspace); + blocks.load(blockStates[i], workspace, {recordUndo}); } } + + if (workspace.setResizesEnabled) { + workspace.setResizesEnabled(true); + } + dom.stopTextWidthCache(); + + Events.fire(new (Events.get(Events.FINISHED_LOADING))(workspace)); + + Events.setGroup(existingGroup); + Events.setRecordUndo(prevRecordUndo); }; exports.load = load; diff --git a/tests/deps.js b/tests/deps.js index e3f562820..adcd3e2a9 100644 --- a/tests/deps.js +++ b/tests/deps.js @@ -196,9 +196,9 @@ goog.addDependency('../../core/renderers/zelos/renderer.js', ['Blockly.zelos.Ren goog.addDependency('../../core/requires.js', ['Blockly.requires'], ['Blockly', 'Blockly.Comment', 'Blockly.ContextMenuItems', 'Blockly.FieldAngle', 'Blockly.FieldCheckbox', 'Blockly.FieldColour', 'Blockly.FieldDropdown', 'Blockly.FieldImage', 'Blockly.FieldLabelSerializable', 'Blockly.FieldMultilineInput', 'Blockly.FieldNumber', 'Blockly.FieldTextInput', 'Blockly.FieldVariable', 'Blockly.FlyoutButton', 'Blockly.Generator', 'Blockly.HorizontalFlyout', 'Blockly.Mutator', 'Blockly.ShortcutItems', 'Blockly.Themes.Classic', 'Blockly.Toolbox', 'Blockly.Trashcan', 'Blockly.VariablesDynamic', 'Blockly.VerticalFlyout', 'Blockly.Warning', 'Blockly.ZoomControls', 'Blockly.geras.Renderer', 'Blockly.serialization.workspaces', 'Blockly.thrasos.Renderer', 'Blockly.zelos.Renderer']); goog.addDependency('../../core/scrollbar.js', ['Blockly.Scrollbar'], ['Blockly.Touch', 'Blockly.browserEvents', 'Blockly.utils', 'Blockly.utils.Coordinate', 'Blockly.utils.Svg', 'Blockly.utils.dom'], {'lang': 'es6', 'module': 'goog'}); goog.addDependency('../../core/scrollbar_pair.js', ['Blockly.ScrollbarPair'], ['Blockly.Events', 'Blockly.Scrollbar', 'Blockly.utils.Svg', 'Blockly.utils.dom'], {'lang': 'es6', 'module': 'goog'}); -goog.addDependency('../../core/serialization/blocks.js', ['Blockly.serialization.blocks'], ['Blockly.Xml', 'Blockly.inputTypes'], {'lang': 'es6', 'module': 'goog'}); -goog.addDependency('../../core/serialization/variables.js', ['Blockly.serialization.variables'], [], {'lang': 'es6', 'module': 'goog'}); -goog.addDependency('../../core/serialization/workspaces.js', ['Blockly.serialization.workspaces'], ['Blockly.Workspace', 'Blockly.serialization.blocks', 'Blockly.serialization.variables'], {'lang': 'es6', 'module': 'goog'}); +goog.addDependency('../../core/serialization/blocks.js', ['Blockly.serialization.blocks'], ['Blockly.Events', 'Blockly.Xml', 'Blockly.inputTypes'], {'lang': 'es6', 'module': 'goog'}); +goog.addDependency('../../core/serialization/variables.js', ['Blockly.serialization.variables'], ['Blockly.Events'], {'lang': 'es6', 'module': 'goog'}); +goog.addDependency('../../core/serialization/workspaces.js', ['Blockly.serialization.workspaces'], ['Blockly.Events', 'Blockly.Workspace', 'Blockly.serialization.blocks', 'Blockly.serialization.variables', 'Blockly.utils.dom'], {'lang': 'es6', 'module': 'goog'}); goog.addDependency('../../core/shortcut_items.js', ['Blockly.ShortcutItems'], ['Blockly.Gesture', 'Blockly.ShortcutRegistry', 'Blockly.clipboard', 'Blockly.common', 'Blockly.utils.KeyCodes'], {'lang': 'es6', 'module': 'goog'}); goog.addDependency('../../core/shortcut_registry.js', ['Blockly.ShortcutRegistry'], ['Blockly.utils.KeyCodes', 'Blockly.utils.object'], {'lang': 'es6', 'module': 'goog'}); goog.addDependency('../../core/theme.js', ['Blockly.Theme'], ['Blockly.registry', 'Blockly.utils.object'], {'lang': 'es6', 'module': 'goog'}); diff --git a/tests/deps.mocha.js b/tests/deps.mocha.js index cd7ff0cda..f9300ac27 100644 --- a/tests/deps.mocha.js +++ b/tests/deps.mocha.js @@ -196,9 +196,9 @@ goog.addDependency('../../core/renderers/zelos/renderer.js', ['Blockly.zelos.Ren goog.addDependency('../../core/requires.js', ['Blockly.requires'], ['Blockly', 'Blockly.Comment', 'Blockly.ContextMenuItems', 'Blockly.FieldAngle', 'Blockly.FieldCheckbox', 'Blockly.FieldColour', 'Blockly.FieldDropdown', 'Blockly.FieldImage', 'Blockly.FieldLabelSerializable', 'Blockly.FieldMultilineInput', 'Blockly.FieldNumber', 'Blockly.FieldTextInput', 'Blockly.FieldVariable', 'Blockly.FlyoutButton', 'Blockly.Generator', 'Blockly.HorizontalFlyout', 'Blockly.Mutator', 'Blockly.ShortcutItems', 'Blockly.Themes.Classic', 'Blockly.Toolbox', 'Blockly.Trashcan', 'Blockly.VariablesDynamic', 'Blockly.VerticalFlyout', 'Blockly.Warning', 'Blockly.ZoomControls', 'Blockly.geras.Renderer', 'Blockly.serialization.workspaces', 'Blockly.thrasos.Renderer', 'Blockly.zelos.Renderer']); goog.addDependency('../../core/scrollbar.js', ['Blockly.Scrollbar'], ['Blockly.Touch', 'Blockly.browserEvents', 'Blockly.utils', 'Blockly.utils.Coordinate', 'Blockly.utils.Svg', 'Blockly.utils.dom'], {'lang': 'es6', 'module': 'goog'}); goog.addDependency('../../core/scrollbar_pair.js', ['Blockly.ScrollbarPair'], ['Blockly.Events', 'Blockly.Scrollbar', 'Blockly.utils.Svg', 'Blockly.utils.dom'], {'lang': 'es6', 'module': 'goog'}); -goog.addDependency('../../core/serialization/blocks.js', ['Blockly.serialization.blocks'], ['Blockly.Xml', 'Blockly.inputTypes'], {'lang': 'es6', 'module': 'goog'}); -goog.addDependency('../../core/serialization/variables.js', ['Blockly.serialization.variables'], [], {'lang': 'es6', 'module': 'goog'}); -goog.addDependency('../../core/serialization/workspaces.js', ['Blockly.serialization.workspaces'], ['Blockly.Workspace', 'Blockly.serialization.blocks', 'Blockly.serialization.variables'], {'lang': 'es6', 'module': 'goog'}); +goog.addDependency('../../core/serialization/blocks.js', ['Blockly.serialization.blocks'], ['Blockly.Events', 'Blockly.Xml', 'Blockly.inputTypes'], {'lang': 'es6', 'module': 'goog'}); +goog.addDependency('../../core/serialization/variables.js', ['Blockly.serialization.variables'], ['Blockly.Events'], {'lang': 'es6', 'module': 'goog'}); +goog.addDependency('../../core/serialization/workspaces.js', ['Blockly.serialization.workspaces'], ['Blockly.Events', 'Blockly.Workspace', 'Blockly.serialization.blocks', 'Blockly.serialization.variables', 'Blockly.utils.dom'], {'lang': 'es6', 'module': 'goog'}); goog.addDependency('../../core/shortcut_items.js', ['Blockly.ShortcutItems'], ['Blockly.Gesture', 'Blockly.ShortcutRegistry', 'Blockly.clipboard', 'Blockly.common', 'Blockly.utils.KeyCodes'], {'lang': 'es6', 'module': 'goog'}); goog.addDependency('../../core/shortcut_registry.js', ['Blockly.ShortcutRegistry'], ['Blockly.utils.KeyCodes', 'Blockly.utils.object'], {'lang': 'es6', 'module': 'goog'}); goog.addDependency('../../core/theme.js', ['Blockly.Theme'], ['Blockly.registry', 'Blockly.utils.object'], {'lang': 'es6', 'module': 'goog'}); diff --git a/tests/mocha/jso_deserialization_test.js b/tests/mocha/jso_deserialization_test.js index 9d97059c3..56670f849 100644 --- a/tests/mocha/jso_deserialization_test.js +++ b/tests/mocha/jso_deserialization_test.js @@ -21,46 +21,51 @@ suite('JSO Deserialization', function() { }); suite('Events', function() { - test.skip('Finished loading', function() { - const state = { - 'blocks': { - 'blocks': [ - { - 'type': 'controls_if', - 'id': 'testId', - 'x': 42, - 'y': 42 - }, - ] - } - }; - Blockly.serialization.workspaces.load(state, this.workspace); - assertEventFired( - this.eventsFireStub, - Blockly.Events.FinishedLoading, - {}, - this.workspace.id); - }); - - suite('Var create', function() { + suite('Finished loading', function() { test('Just var', function() { const state = { - 'variables': [ - { - 'name': 'test', - 'id': 'testId', - } - ] + 'blocks': { + 'blocks': [ + { + 'type': 'controls_if', + 'id': 'testId', + 'x': 42, + 'y': 42 + }, + ] + } }; Blockly.serialization.workspaces.load(state, this.workspace); assertEventFired( this.eventsFireStub, - Blockly.Events.VarCreate, - {'varName': 'test', 'varId': 'testId', 'varType': ''}, + Blockly.Events.FinishedLoading, + {}, this.workspace.id); }); - test('Only fire one event with var and var on block', function() { + test('Explicit group', function() { + const state = { + 'blocks': { + 'blocks': [ + { + 'type': 'controls_if', + 'id': 'testId', + 'x': 42, + 'y': 42 + }, + ] + } + }; + Blockly.Events.setGroup('my group'); + Blockly.serialization.workspaces.load(state, this.workspace); + assertEventFired( + this.eventsFireStub, + Blockly.Events.FinishedLoading, + {'group': 'my group'}, + this.workspace.id); + }); + + test('Automatic group', function() { const state = { 'variables': [ { @@ -84,78 +89,378 @@ suite('JSO Deserialization', function() { }; Blockly.serialization.workspaces.load(state, this.workspace); const calls = this.eventsFireStub.getCalls(); - const count = calls.reduce((acc, call) => { - if (call.args[0] instanceof Blockly.Events.VarCreate) { - return acc + 1; - } - return acc; - }, 0); - chai.assert.equal(count, 1); - assertEventFired( - this.eventsFireStub, - Blockly.Events.VarCreate, - {'varName': 'test', 'varId': 'testId', 'varType': ''}, - this.workspace.id); + const group = calls[0].args[0].group; + chai.assert.isTrue(calls.every(call => call.args[0].group == group)); + }); + }); + + suite('Var create', function() { + suite('Top-level call', function() { + test('Just var', function() { + const state = { + 'variables': [ + { + 'name': 'test', + 'id': 'testId', + } + ] + }; + Blockly.serialization.workspaces.load(state, this.workspace); + assertEventFired( + this.eventsFireStub, + Blockly.Events.VarCreate, + { + 'varName': 'test', + 'varId': 'testId', + 'varType': '', + 'recordUndo': false + }, + this.workspace.id); + }); + + test('Record undo', function() { + const state = { + 'variables': [ + { + 'name': 'test', + 'id': 'testId', + } + ] + }; + Blockly.serialization.workspaces.load(state, this.workspace, {recordUndo: true}); + assertEventFired( + this.eventsFireStub, + Blockly.Events.VarCreate, + { + 'varName': 'test', + 'varId': 'testId', + 'varType': '', + 'recordUndo': true + }, + this.workspace.id); + }); + + test('Grouping', function() { + const state = { + 'variables': [ + { + 'name': 'test', + 'id': 'testId', + } + ] + }; + Blockly.Events.setGroup('my group'); + Blockly.serialization.workspaces.load(state, this.workspace); + assertEventFired( + this.eventsFireStub, + Blockly.Events.VarCreate, + { + 'varName': 'test', + 'varId': 'testId', + 'varType': '', + 'group': 'my group' + }, + this.workspace.id); + }); + + test('Multiple vars grouped', function() { + const state = { + 'variables': [ + { + 'name': 'test', + 'id': 'testId', + }, + { + 'name': 'test2', + 'id': 'testId2', + } + ] + }; + Blockly.serialization.workspaces.load(state, this.workspace); + const calls = this.eventsFireStub.getCalls(); + const group = calls[0].args[0].group; + chai.assert.isTrue(calls.every(call => call.args[0].group == group)); + }); + + test('Var with block', function() { + const state = { + 'variables': [ + { + 'name': 'test', + 'id': 'testId', + } + ], + 'blocks': { + 'blocks': [ + { + 'type': 'variables_get', + 'id': 'blockId', + 'x': 42, + 'y': 42, + 'fields': { + 'VAR': 'testId' + } + }, + ] + } + }; + Blockly.serialization.workspaces.load(state, this.workspace); + const calls = this.eventsFireStub.getCalls(); + const count = calls.reduce((acc, call) => { + if (call.args[0] instanceof Blockly.Events.VarCreate) { + return acc + 1; + } + return acc; + }, 0); + chai.assert.equal(count, 1); + assertEventFired( + this.eventsFireStub, + Blockly.Events.VarCreate, + {'varName': 'test', 'varId': 'testId', 'varType': ''}, + this.workspace.id); + }); + }); + + suite('Direct call', function() { + test('Just var', function() { + const state = { + 'name': 'test', + 'id': 'testId', + }; + Blockly.serialization.variables.load(state, this.workspace); + assertEventFired( + this.eventsFireStub, + Blockly.Events.VarCreate, + { + 'varName': 'test', + 'varId': 'testId', + 'varType': '', + 'recordUndo': false + }, + this.workspace.id); + }); + + test('Record undo', function() { + const state = { + 'name': 'test', + 'id': 'testId', + }; + Blockly.serialization.variables + .load(state, this.workspace, {recordUndo: true}); + assertEventFired( + this.eventsFireStub, + Blockly.Events.VarCreate, + { + 'varName': 'test', + 'varId': 'testId', + 'varType': '', + 'recordUndo': true + }, + this.workspace.id); + }); + + test('Grouping', function() { + const state = { + 'name': 'test', + 'id': 'testId', + }; + Blockly.Events.setGroup('my group'); + Blockly.serialization.variables.load(state, this.workspace); + assertEventFired( + this.eventsFireStub, + Blockly.Events.VarCreate, + { + 'varName': 'test', + 'varId': 'testId', + 'varType': '', + 'group': 'my group' + }, + this.workspace.id); + }); }); }); suite('Block create', function() { - test('Simple', function() { - const state = { - 'blocks': { - 'blocks': [ - { - 'type': 'controls_if', - 'id': 'testId', - 'x': 42, - 'y': 42 - }, - ] - } - }; - Blockly.serialization.workspaces.load(state, this.workspace); - assertEventFired( - this.eventsFireStub, - Blockly.Events.BlockCreate, - {}, - this.workspace.id, - 'testId'); - }); + suite('Top-level call', function() { + test('No children', function() { + const state = { + 'blocks': { + 'blocks': [ + { + 'type': 'controls_if', + 'id': 'testId', + 'x': 42, + 'y': 42 + }, + ] + } + }; + Blockly.serialization.workspaces.load(state, this.workspace); + assertEventFired( + this.eventsFireStub, + Blockly.Events.BlockCreate, + {'recordUndo': false}, + this.workspace.id, + 'testId'); + }); - test('Only fire event for top block', function() { - const state = { - 'blocks': { - 'blocks': [ - { - 'type': 'controls_if', - 'id': 'id1', - 'x': 42, - 'y': 42, - 'inputs': { - 'DO0': { + test('Record undo', function() { + const state = { + 'blocks': { + 'blocks': [ + { + 'type': 'controls_if', + 'id': 'testId', + 'x': 42, + 'y': 42 + }, + ] + } + }; + Blockly.serialization.workspaces.load(state, this.workspace, {'recordUndo': true}); + assertEventFired( + this.eventsFireStub, + Blockly.Events.BlockCreate, + {'recordUndo': true}, + this.workspace.id, + 'testId'); + }); + + test('Grouping', function() { + const state = { + 'blocks': { + 'blocks': [ + { + 'type': 'controls_if', + 'id': 'testId', + 'x': 42, + 'y': 42 + }, + ] + } + }; + Blockly.Events.setGroup('my group'); + Blockly.serialization.workspaces.load(state, this.workspace); + assertEventFired( + this.eventsFireStub, + Blockly.Events.BlockCreate, + {'group': 'my group'}, + this.workspace.id, + 'testId'); + }); + + test('Multiple blocks grouped', function() { + const state = { + 'blocks': { + 'blocks': [ + { + 'type': 'controls_if', + 'id': 'testId', + 'x': 42, + 'y': 42 + }, + { + 'type': 'controls_if', + 'id': 'testId', + 'x': 84, + 'y': 84 + }, + ] + } + }; + Blockly.serialization.workspaces.load(state, this.workspace); + const calls = this.eventsFireStub.getCalls(); + const group = calls[0].args[0].group; + chai.assert.isTrue(calls.every(call => call.args[0].group == group)); + }); + + test('With children', function() { + const state = { + 'blocks': { + 'blocks': [ + { + 'type': 'controls_if', + 'id': 'id1', + 'x': 42, + 'y': 42, + 'inputs': { + 'DO0': { + 'block': { + 'type': 'controls_if', + 'id': 'id2' + } + } + }, + 'next': { 'block': { 'type': 'controls_if', - 'id': 'id2' + 'id': 'id3' } } }, - 'next': { - 'block': { - 'type': 'controls_if', - 'id': 'id3' - } - } - }, - ] - } - }; - Blockly.serialization.workspaces.load(state, this.workspace); - assertEventFired( - this.eventsFireStub, - Blockly.Events.BlockCreate, - {}, - this.workspace.id, - 'id1'); + ] + } + }; + Blockly.serialization.workspaces.load(state, this.workspace); + assertEventFired( + this.eventsFireStub, + Blockly.Events.BlockCreate, + {}, + this.workspace.id, + 'id1'); + }); + }); + + suite('Direct call', function() { + test('No children', function() { + const state = { + 'type': 'controls_if', + 'id': 'testId', + 'x': 42, + 'y': 42 + }; + Blockly.serialization.blocks.load(state, this.workspace); + assertEventFired( + this.eventsFireStub, + Blockly.Events.BlockCreate, + {'recordUndo': false}, + this.workspace.id, + 'testId'); + }); + + test('Record undo', function() { + const state = { + 'type': 'controls_if', + 'id': 'testId', + 'x': 42, + 'y': 42 + }; + Blockly.serialization.blocks + .load(state, this.workspace, {'recordUndo': true}); + assertEventFired( + this.eventsFireStub, + Blockly.Events.BlockCreate, + {'recordUndo': true}, + this.workspace.id, + 'testId'); + }); + + test('Grouping', function() { + const state = { + 'type': 'controls_if', + 'id': 'testId', + 'x': 42, + 'y': 42 + }; + Blockly.Events.setGroup('my group'); + Blockly.serialization.blocks.load(state, this.workspace); + assertEventFired( + this.eventsFireStub, + Blockly.Events.BlockCreate, + {'group': 'my group'}, + this.workspace.id, + 'testId'); + }); }); }); }); diff --git a/tests/playground.html b/tests/playground.html index 58d768431..c94c05c2f 100644 --- a/tests/playground.html +++ b/tests/playground.html @@ -379,6 +379,74 @@ var spaghettiXml = [ ' ', ' '].join('\n'); + function jsoSpaghetti(n) { + var str = spaghettiJs; + for (var i = 0; i < n; i++) { + str = str.replace(/{}/g, `{"block":${spaghettiJs}}`); + } + var obj = { + 'blocks': { + 'blocks': [ + JSON.parse(str) + ] + } + }; + console.time('Spaghetti serialization'); + Blockly.serialization.load(obj, workspace); + console.timeEnd('Spaghetti serialization'); + } + var spaghettiJs = JSON.stringify({ + 'type': 'controls_if', + 'inputs': { + 'IF0': { + 'block': { + 'type': 'logic_compare', + 'fields': { + 'OP': 'EQ', + }, + 'inputs': { + 'A': { + 'block': { + 'type': 'math_arithmetic', + 'fields': { + 'OP': 'MULTIPLY', + }, + 'inputs': { + 'A': { + 'block': { + 'type': 'math_number', + 'fields': { + 'NUM': 6 + } + } + }, + 'B': { + 'block': { + 'type': 'math_number', + 'fields': { + 'NUM': 7 + } + } + } + } + } + }, + 'B': { + 'block': { + 'type': 'math_number', + 'fields': { + 'NUM': 42 + } + } + } + } + } + }, + 'DO0': { } + }, + 'next': { } + }); +