diff --git a/core/serialization/blocks.js b/core/serialization/blocks.js index d509936bc4c..40d2a130922 100644 --- a/core/serialization/blocks.js +++ b/core/serialization/blocks.js @@ -262,10 +262,31 @@ 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. + * @return {!Block} The block that was just loaded. */ const load = function(state, workspace) { - loadInternal(state, workspace); + // We only want to fire an event for the top block. + Blockly.Events.disable(); + + const block = loadInternal(state, workspace); + + Blockly.Events.enable(); + Blockly.Events.fire( + new (Blockly.Events.get(Blockly.Events.BLOCK_CREATE))(block)); + + // Adding connections to the connection db is expensive. This defers that + // operation to decrease load time. + if (block instanceof Blockly.BlockSvg) { + setTimeout(() => { + if (!block.disposed) { + block.setConnectionTracking(true); + } + }, 1); + } + + return block; }; +exports.load = load; /** * Loads the block represented by the given state into the given workspace. @@ -273,8 +294,9 @@ const load = function(state, workspace) { * clutter our external API. * @param {!State} state The state of a block to deserialize into the workspace. * @param {!Workspace} workspace The workspace to add the block to. - * @param {!Connection} parentConnection The optional parent connection to + * @param {!Connection=} parentConnection The optional parent connection to * attach the block to. + * @return {!Block} The block that was just loaded. */ const loadInternal = function(state, workspace, parentConnection = undefined) { const block = workspace.newBlock(state['type'], state['id']); @@ -291,6 +313,8 @@ const loadInternal = function(state, workspace, parentConnection = undefined) { loadFields(block, state); loadInputBlocks(block, state); loadNextBlocks(block, state); + initBlock(block); + return block; }; /** @@ -424,4 +448,22 @@ const loadConnection = function(connection, connectionState) { connection); } }; -exports.load = load; + +// TODO(#5146): Remove this from the serialization system. +/** + * Initializes the give block, eg init the model, inits the svg, renders, etc. + * @param {!Block} block The block to initialize. + */ +const initBlock = function(block) { + if (block instanceof Blockly.BlockSvg) { + // 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/tests/deps.mocha.js b/tests/deps.mocha.js index 32d309ed9b5..cd7ff0cdab3 100644 --- a/tests/deps.mocha.js +++ b/tests/deps.mocha.js @@ -331,6 +331,7 @@ goog.addDependency('../../tests/mocha/generator_test.js', ['Blockly.test.generat goog.addDependency('../../tests/mocha/gesture_test.js', ['Blockly.test.gesture'], ['Blockly.test.helpers'], {'lang': 'es6', 'module': 'goog'}); goog.addDependency('../../tests/mocha/input_test.js', ['Blockly.test.input'], ['Blockly.test.helpers'], {'lang': 'es6', 'module': 'goog'}); goog.addDependency('../../tests/mocha/insertion_marker_test.js', ['Blockly.test.insertionMarker'], ['Blockly.test.helpers'], {'lang': 'es6', 'module': 'goog'}); +goog.addDependency('../../tests/mocha/jso_deserialization_test.js', ['Blockly.test.jsoDeserialization'], ['Blockly.test.helpers'], {'lang': 'es6', 'module': 'goog'}); goog.addDependency('../../tests/mocha/jso_serialization_test.js', ['Blockly.test.jsoSerialization'], ['Blockly.test.helpers'], {'lang': 'es6', 'module': 'goog'}); goog.addDependency('../../tests/mocha/json_test.js', ['Blockly.test.json'], ['Blockly.test.helpers'], {'lang': 'es6', 'module': 'goog'}); goog.addDependency('../../tests/mocha/keydown_test.js', ['Blockly.test.keydown'], ['Blockly.test.helpers'], {'lang': 'es6', 'module': 'goog'}); diff --git a/tests/mocha/index.html b/tests/mocha/index.html index ab34e2c8edc..9b28c2422c2 100644 --- a/tests/mocha/index.html +++ b/tests/mocha/index.html @@ -82,6 +82,7 @@ goog.require('Blockly.test.gesture'); goog.require('Blockly.test.input'); goog.require('Blockly.test.insertionMarker'); + goog.require('Blockly.test.jsoDeserialization'); goog.require('Blockly.test.jsoSerialization'); goog.require('Blockly.test.json'); goog.require('Blockly.test.keydown'); diff --git a/tests/mocha/jso_deserialization_test.js b/tests/mocha/jso_deserialization_test.js new file mode 100644 index 00000000000..9d97059c378 --- /dev/null +++ b/tests/mocha/jso_deserialization_test.js @@ -0,0 +1,162 @@ +/** + * @license + * Copyright 2021 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +goog.module('Blockly.test.jsoDeserialization'); + +const {assertEventFired, sharedTestSetup, sharedTestTeardown, workspaceTeardown} = goog.require('Blockly.test.helpers'); + + +suite('JSO Deserialization', function() { + setup(function() { + sharedTestSetup.call(this); + this.workspace = new Blockly.Workspace(); + }); + + teardown(function() { + workspaceTeardown.call(this, this.workspace); + sharedTestTeardown.call(this); + }); + + 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() { + 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': ''}, + this.workspace.id); + }); + + test('Only fire one event with var and var on 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('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'); + }); + + test('Only fire event for top block', 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': 'id3' + } + } + }, + ] + } + }; + Blockly.serialization.workspaces.load(state, this.workspace); + assertEventFired( + this.eventsFireStub, + Blockly.Events.BlockCreate, + {}, + this.workspace.id, + 'id1'); + }); + }); + }); +}); diff --git a/tests/mocha/jso_serialization_test.js b/tests/mocha/jso_serialization_test.js index 83531621031..83647f5e7ee 100644 --- a/tests/mocha/jso_serialization_test.js +++ b/tests/mocha/jso_serialization_test.js @@ -9,7 +9,7 @@ goog.module('Blockly.test.jsoSerialization'); const {defineStackBlock, defineRowBlock, defineStatementBlock, createGenUidStubWithReturns, sharedTestSetup, sharedTestTeardown, workspaceTeardown} = goog.require('Blockly.test.helpers'); -suite('JSO', function() { +suite('JSO Serialization', function() { setup(function() { sharedTestSetup.call(this); this.workspace = new Blockly.Workspace(); diff --git a/tests/playground.html b/tests/playground.html index b53e76ea83d..58d768431a0 100644 --- a/tests/playground.html +++ b/tests/playground.html @@ -145,7 +145,7 @@ } taChange(); if (autoimport) { - fromXml(); + load(); } } @@ -203,7 +203,7 @@ } } -function toXml() { +function saveXml() { var output = document.getElementById('importExport'); var xml = Blockly.Xml.workspaceToDom(workspace); output.value = Blockly.Xml.domToPrettyText(xml); @@ -212,13 +212,28 @@ taChange(); } -function fromXml() { +function saveJson() { + var output = document.getElementById('importExport'); + var state = Blockly.serialization.workspaces.save(workspace); + output.value = JSON.stringify(state, null, 2); + output.focus(); + output.select(); + taChange(); +} + +function load() { var input = document.getElementById('importExport'); if (!input.value) { return; } - var xml = Blockly.Xml.textToDom(input.value); - Blockly.Xml.domToWorkspace(xml, workspace); + var valid = saveIsValid(input.value); + if (valid.json) { + var state = JSON.parse(input.value); + Blockly.serialization.workspaces.load(state, workspace); + } else if (valid.xml) { + var xml = Blockly.Xml.textToDom(input.value); + Blockly.Xml.domToWorkspace(xml, workspace); + } taChange(); } @@ -228,20 +243,34 @@ taChange(); } -// Disable the "Import from XML" button if the XML is invalid. +// Disable the "Load" button if the save state is invalid. // Preserve text between page reloads. function taChange() { var textarea = document.getElementById('importExport'); if (sessionStorage) { sessionStorage.setItem('textarea', textarea.value); } - var valid = true; + var valid = saveIsValid(textarea.value); + document.getElementById('import').disabled = !valid.json && !valid.xml; +} + +function saveIsValid(save) { + var validJson = true; + try { + JSON.parse(save); + } catch (e) { + validJson = false; + } + var validXml = true try { - Blockly.Xml.textToDom(textarea.value); + Blockly.Xml.textToDom(save); } catch (e) { - valid = false; + validXml = false; + } + return { + json: validJson, + xml: validXml } - document.getElementById('import').disabled = !valid; } function logEvents(state) { @@ -422,18 +451,14 @@

Blockly Playground

- -   - + + +
-   -   -   -