From cc559cc55f8c3a9c6f975201e056a8345da95fbb Mon Sep 17 00:00:00 2001 From: Beka Westberg Date: Fri, 30 Jul 2021 08:38:00 -0700 Subject: [PATCH] Add serialization of field values (#5072) * Add tests for field serialization * Add field serialization * Fixup types and tests --- core/serialization/blocks.js | 38 ++- tests/mocha/jso_serialization_test.js | 460 +++++++++++++++----------- 2 files changed, 297 insertions(+), 201 deletions(-) diff --git a/core/serialization/blocks.js b/core/serialization/blocks.js index feb17cfef77..1ad9b57b439 100644 --- a/core/serialization/blocks.js +++ b/core/serialization/blocks.js @@ -13,6 +13,9 @@ goog.module('Blockly.serialization.blocks'); goog.module.declareLegacyNamespace(); +// eslint-disable-next-line no-unused-vars +const Block = goog.requireType('Blockly.Block'); + // TODO: Remove this once lint is fixed. /* eslint-disable no-use-before-define */ @@ -31,7 +34,8 @@ goog.module.declareLegacyNamespace(); * movable: (boolean|undefined), * inline: (boolean|undefined), * data: (string|undefined), - * extra-state: * + * extra-state: *, + * fields: (Object|undefined), * }} */ var State; @@ -39,7 +43,7 @@ exports.State = State; /** * Returns the state of the given block as a plain JavaScript object. - * @param {!Blockly.Block} block The block to serialize. + * @param {!Block} block The block to serialize. * @param {{addCoordinates: (boolean|undefined)}=} param1 * addCoordinates: If true the coordinates of the block are added to the * serialized state. False by default. @@ -62,6 +66,7 @@ const save = function(block, {addCoordinates = false} = {}) { } addAttributes(block, state); addExtraState(block, state); + addFields(block, state); return state; }; @@ -70,7 +75,7 @@ exports.save = save; /** * Adds attributes to the given state object based on the state of the block. * Eg collapsed, disabled, editable, etc. - * @param {!Blockly.Block} block The block to base the attributes on. + * @param {!Block} block The block to base the attributes on. * @param {!State} state The state object to append to. */ const addAttributes = function(block, state) { @@ -103,7 +108,7 @@ const addAttributes = function(block, state) { /** * Adds the coordinates of the given block to the given state object. - * @param {!Blockly.Block} block The block to base the coordinates on + * @param {!Block} block The block to base the coordinates on * @param {!State} state The state object to append to */ const addCoords = function(block, state) { @@ -115,7 +120,7 @@ const addCoords = function(block, state) { /** * Adds any extra state the block may provide to the given state object. - * @param {!Blockly.Block} block The block to serialize the extra state of. + * @param {!Block} block The block to serialize the extra state of. * @param {!State} state The state object to append to. */ const addExtraState = function(block, state) { @@ -126,3 +131,26 @@ const addExtraState = function(block, state) { } } }; + +/** + * Adds the state of all of the fields on the block to the given state object. + * @param {!Block} block The block to serialize the field state of. + * @param {!State} state The state object to append to. + */ +const addFields = function(block, state) { + let hasFieldState = false; + let fields = Object.create(null); + for (let i = 0; i < block.inputList.length; i++) { + const input = block.inputList[i]; + for (let j = 0; j < input.fieldRow.length; j++) { + const field = input.fieldRow[j]; + if (field.isSerializable()) { + hasFieldState = true; + fields[field.name] = field.saveState(); + } + } + } + if (hasFieldState) { + state['fields'] = fields; + } +}; diff --git a/tests/mocha/jso_serialization_test.js b/tests/mocha/jso_serialization_test.js index cb91c24f688..3b46355472b 100644 --- a/tests/mocha/jso_serialization_test.js +++ b/tests/mocha/jso_serialization_test.js @@ -27,6 +27,14 @@ suite('JSO', function() { }); suite('Blocks', function() { + function assertProperty(obj, property, value) { + chai.assert.deepEqual(obj[property], value); + } + + function assertNoProperty(obj, property) { + assertProperty(obj, property, undefined); + } + test('Null on insertionMarkers', function() { const block = this.workspace.newBlock('row_block'); block.setInsertionMarker(true); @@ -34,245 +42,305 @@ suite('JSO', function() { chai.assert.isNull(jso); }); - suite('Save Single Block', function() { - - function assertProperty(obj, property, value) { - chai.assert.deepEqual(obj[property], value); - } + test('Basic', function() { + const block = this.workspace.newBlock('row_block'); + const jso = Blockly.serialization.blocks.save(block); + assertProperty(jso, 'type', 'row_block'); + assertProperty(jso, 'id', 'id0'); + }); - function assertNoProperty(obj, property) { - assertProperty(obj, property, undefined); - } + suite('Attributes', function() { + suite('Collapsed', function() { + test('True', function() { + const block = this.workspace.newBlock('row_block'); + block.setCollapsed(true); + const jso = Blockly.serialization.blocks.save(block); + assertProperty(jso, 'collapsed', true); + }); - test('Basic', function() { - const block = this.workspace.newBlock('row_block'); - const jso = Blockly.serialization.blocks.save(block); - assertProperty(jso, 'type', 'row_block'); - assertProperty(jso, 'id', 'id0'); + test('False', function() { + const block = this.workspace.newBlock('row_block'); + block.setCollapsed(false); + const jso = Blockly.serialization.blocks.save(block); + assertNoProperty(jso, 'collapsed'); + }); }); - suite('Attributes', function() { - suite('Collapsed', function() { - test('True', function() { - const block = this.workspace.newBlock('row_block'); - block.setCollapsed(true); - const jso = Blockly.serialization.blocks.save(block); - assertProperty(jso, 'collapsed', true); - }); - - test('False', function() { - const block = this.workspace.newBlock('row_block'); - block.setCollapsed(false); - const jso = Blockly.serialization.blocks.save(block); - assertNoProperty(jso, 'collapsed'); - }); + suite('Enabled', function() { + test('False', function() { + const block = this.workspace.newBlock('row_block'); + block.setEnabled(false); + const jso = Blockly.serialization.blocks.save(block); + assertProperty(jso, 'enabled', false); }); - suite('Enabled', function() { - test('False', function() { - const block = this.workspace.newBlock('row_block'); - block.setEnabled(false); - const jso = Blockly.serialization.blocks.save(block); - assertProperty(jso, 'enabled', false); - }); - - test('True', function() { - const block = this.workspace.newBlock('row_block'); - block.setEnabled(true); - const jso = Blockly.serialization.blocks.save(block); - assertNoProperty(jso, 'enabled'); - }); + test('True', function() { + const block = this.workspace.newBlock('row_block'); + block.setEnabled(true); + const jso = Blockly.serialization.blocks.save(block); + assertNoProperty(jso, 'enabled'); }); + }); - suite('Deletable', function() { - test('False', function() { - const block = this.workspace.newBlock('row_block'); - block.setDeletable(false); - const jso = Blockly.serialization.blocks.save(block); - assertProperty(jso, 'deletable', false); - }); - - test('True', function() { - const block = this.workspace.newBlock('row_block'); - block.setDeletable(true); - const jso = Blockly.serialization.blocks.save(block); - assertNoProperty(jso, 'deletable'); - }); - - test('False and Shadow', function() { - const block = this.workspace.newBlock('row_block'); - block.setDeletable(false); - block.setShadow(true); - const jso = Blockly.serialization.blocks.save(block); - assertNoProperty(jso, 'deletable'); - }); + suite('Deletable', function() { + test('False', function() { + const block = this.workspace.newBlock('row_block'); + block.setDeletable(false); + const jso = Blockly.serialization.blocks.save(block); + assertProperty(jso, 'deletable', false); + }); + + test('True', function() { + const block = this.workspace.newBlock('row_block'); + block.setDeletable(true); + const jso = Blockly.serialization.blocks.save(block); + assertNoProperty(jso, 'deletable'); }); - suite('Movable', function() { - test('False', function() { - const block = this.workspace.newBlock('row_block'); - block.setMovable(false); - const jso = Blockly.serialization.blocks.save(block); - assertProperty(jso, 'movable', false); - }); - - test('True', function() { - const block = this.workspace.newBlock('row_block'); - block.setMovable(true); - const jso = Blockly.serialization.blocks.save(block); - assertNoProperty(jso, 'movable'); - }); - - test('False and Shadow', function() { - const block = this.workspace.newBlock('row_block'); - block.setMovable(false); - block.setShadow(true); - const jso = Blockly.serialization.blocks.save(block); - assertNoProperty(jso, 'movable'); - }); + test('False and Shadow', function() { + const block = this.workspace.newBlock('row_block'); + block.setDeletable(false); + block.setShadow(true); + const jso = Blockly.serialization.blocks.save(block); + assertNoProperty(jso, 'deletable'); }); + }); - suite('Editable', function() { - test('False', function() { - const block = this.workspace.newBlock('row_block'); - block.setEditable(false); - const jso = Blockly.serialization.blocks.save(block); - assertProperty(jso, 'editable', false); - }); - - test('True', function() { - const block = this.workspace.newBlock('row_block'); - block.setEditable(true); - const jso = Blockly.serialization.blocks.save(block); - assertNoProperty(jso, 'editable'); - }); + suite('Movable', function() { + test('False', function() { + const block = this.workspace.newBlock('row_block'); + block.setMovable(false); + const jso = Blockly.serialization.blocks.save(block); + assertProperty(jso, 'movable', false); }); - suite('Inline', function() { - test('True', function() { - const block = this.workspace.newBlock('statement_block'); - block.setInputsInline(true); - const jso = Blockly.serialization.blocks.save(block); - assertProperty(jso, 'inline', true); - }); - - test('False', function() { - const block = this.workspace.newBlock('statement_block'); - block.setInputsInline(false); - const jso = Blockly.serialization.blocks.save(block); - assertProperty(jso, 'inline', false); - }); - - test('undefined', function() { - const block = this.workspace.newBlock('statement_block'); - const jso = Blockly.serialization.blocks.save(block); - assertNoProperty(jso, 'inline'); - }); - - test('True, matching default', function() { - const block = this.workspace.newBlock('statement_block'); - block.setInputsInline(true); - block.inputsInlineDefault = true; - const jso = Blockly.serialization.blocks.save(block); - assertNoProperty(jso, 'inline'); - }); - - test('False, matching default', function() { - const block = this.workspace.newBlock('statement_block'); - block.setInputsInline(false); - block.inputsInlineDefault = false; - const jso = Blockly.serialization.blocks.save(block); - assertNoProperty(jso, 'inline'); - }); + test('True', function() { + const block = this.workspace.newBlock('row_block'); + block.setMovable(true); + const jso = Blockly.serialization.blocks.save(block); + assertNoProperty(jso, 'movable'); }); - suite('Data', function() { - test('No data', function() { - const block = this.workspace.newBlock('row_block'); - const jso = Blockly.serialization.blocks.save(block); - assertNoProperty(jso, 'data'); - }); - - test('With data', function() { - const block = this.workspace.newBlock('row_block'); - block.data = 'some data'; - const jso = Blockly.serialization.blocks.save(block); - assertProperty(jso, 'data', 'some data'); - }); + test('False and Shadow', function() { + const block = this.workspace.newBlock('row_block'); + block.setMovable(false); + block.setShadow(true); + const jso = Blockly.serialization.blocks.save(block); + assertNoProperty(jso, 'movable'); }); }); - suite('Coords', function() { - test('No coordinates', function() { + suite('Editable', function() { + test('False', function() { const block = this.workspace.newBlock('row_block'); + block.setEditable(false); const jso = Blockly.serialization.blocks.save(block); - assertNoProperty(jso, 'x'); - assertNoProperty(jso, 'y'); + assertProperty(jso, 'editable', false); }); - test('Simple', function() { + test('True', function() { const block = this.workspace.newBlock('row_block'); - block.moveBy(42, 42); - const jso = - Blockly.serialization.blocks.save(block, {addCoordinates: true}); - assertProperty(jso, 'x', 42); - assertProperty(jso, 'y', 42); + block.setEditable(true); + const jso = Blockly.serialization.blocks.save(block); + assertNoProperty(jso, 'editable'); }); + }); - test('Negative', function() { - const block = this.workspace.newBlock('row_block'); - block.moveBy(-42, -42); - const jso = - Blockly.serialization.blocks.save(block, {addCoordinates: true}); - assertProperty(jso, 'x', -42); - assertProperty(jso, 'y', -42); + suite('Inline', function() { + test('True', function() { + const block = this.workspace.newBlock('statement_block'); + block.setInputsInline(true); + const jso = Blockly.serialization.blocks.save(block); + assertProperty(jso, 'inline', true); }); - test('Zero', function() { - const block = this.workspace.newBlock('row_block'); - const jso = - Blockly.serialization.blocks.save(block, {addCoordinates: true}); - assertProperty(jso, 'x', 0); - assertProperty(jso, 'y', 0); + test('False', function() { + const block = this.workspace.newBlock('statement_block'); + block.setInputsInline(false); + const jso = Blockly.serialization.blocks.save(block); + assertProperty(jso, 'inline', false); + }); + + test('undefined', function() { + const block = this.workspace.newBlock('statement_block'); + const jso = Blockly.serialization.blocks.save(block); + assertNoProperty(jso, 'inline'); + }); + + test('True, matching default', function() { + const block = this.workspace.newBlock('statement_block'); + block.setInputsInline(true); + block.inputsInlineDefault = true; + const jso = Blockly.serialization.blocks.save(block); + assertNoProperty(jso, 'inline'); + }); + + test('False, matching default', function() { + const block = this.workspace.newBlock('statement_block'); + block.setInputsInline(false); + block.inputsInlineDefault = false; + const jso = Blockly.serialization.blocks.save(block); + assertNoProperty(jso, 'inline'); }); }); - // Mutators. - suite('Extra state', function() { - test('Simple value', function() { + suite('Data', function() { + test('No data', function() { const block = this.workspace.newBlock('row_block'); - block.saveExtraState = function() { - return 'some extra state'; - }; const jso = Blockly.serialization.blocks.save(block); - assertProperty(jso, 'extraState', 'some extra state'); + assertNoProperty(jso, 'data'); }); - test('Object', function() { + test('With data', function() { const block = this.workspace.newBlock('row_block'); - block.saveExtraState = function() { - return { - 'extra1': 'state1', - 'extra2': 42, - 'extra3': true, - }; - }; + block.data = 'some data'; const jso = Blockly.serialization.blocks.save(block); - assertProperty(jso, 'extraState', { + assertProperty(jso, 'data', 'some data'); + }); + }); + }); + + suite('Coords', function() { + test('No coordinates', function() { + const block = this.workspace.newBlock('row_block'); + const jso = Blockly.serialization.blocks.save(block); + assertNoProperty(jso, 'x'); + assertNoProperty(jso, 'y'); + }); + + test('Simple', function() { + const block = this.workspace.newBlock('row_block'); + block.moveBy(42, 42); + const jso = + Blockly.serialization.blocks.save(block, {addCoordinates: true}); + assertProperty(jso, 'x', 42); + assertProperty(jso, 'y', 42); + }); + + test('Negative', function() { + const block = this.workspace.newBlock('row_block'); + block.moveBy(-42, -42); + const jso = + Blockly.serialization.blocks.save(block, {addCoordinates: true}); + assertProperty(jso, 'x', -42); + assertProperty(jso, 'y', -42); + }); + + test('Zero', function() { + const block = this.workspace.newBlock('row_block'); + const jso = + Blockly.serialization.blocks.save(block, {addCoordinates: true}); + assertProperty(jso, 'x', 0); + assertProperty(jso, 'y', 0); + }); + }); + + // Mutators. + suite('Extra state', function() { + test('Simple value', function() { + const block = this.workspace.newBlock('row_block'); + block.saveExtraState = function() { + return 'some extra state'; + }; + const jso = Blockly.serialization.blocks.save(block); + assertProperty(jso, 'extraState', 'some extra state'); + }); + + test('Object', function() { + const block = this.workspace.newBlock('row_block'); + block.saveExtraState = function() { + return { 'extra1': 'state1', 'extra2': 42, 'extra3': true, - }); + }; + }; + const jso = Blockly.serialization.blocks.save(block); + assertProperty(jso, 'extraState', { + 'extra1': 'state1', + 'extra2': 42, + 'extra3': true, }); + }); - test('Array', function() { - const block = this.workspace.newBlock('row_block'); - block.saveExtraState = function() { - return ['state1', 42, true]; + test('Array', function() { + const block = this.workspace.newBlock('row_block'); + block.saveExtraState = function() { + return ['state1', 42, true]; + }; + const jso = Blockly.serialization.blocks.save(block); + assertProperty(jso, 'extraState', ['state1', 42, true]); + }); + }); + + suite('Fields', function() { + class StringStateField extends Blockly.Field { + constructor(value, validator = undefined, config = undefined) { + super(value, validator, config); + this.SERIALIZABLE = true; + } + + saveState() { + return 'some state'; + } + } + + class ObjectStateField extends Blockly.Field { + constructor(value, validator = undefined, config = undefined) { + super(value, validator, config); + this.SERIALIZABLE = true; + } + + saveState() { + return { + 'prop1': 'state1', + 'prop2': 42, + 'prop3': true, }; - const jso = Blockly.serialization.blocks.save(block); - assertProperty(jso, 'extraState', ['state1', 42, true]); - }); + } + } + + class ArrayStateField extends Blockly.Field { + constructor(value, validator = undefined, config = undefined) { + super(value, validator, config); + this.SERIALIZABLE = true; + } + + saveState() { + return ['state1', 42, true]; + } + } + + class XmlStateField extends Blockly.Field { + constructor(value, validator = undefined, config = undefined) { + super(value, validator, config); + this.SERIALIZABLE = true; + } + } + + test('Simple value', function() { + const block = this.workspace.newBlock('row_block'); + block.getInput('INPUT').appendField(new StringStateField(''), 'FIELD'); + const jso = Blockly.serialization.blocks.save(block); + assertProperty(jso, 'fields', {'FIELD': 'some state'}); + }); + + test('Object', function() { + const block = this.workspace.newBlock('row_block'); + block.getInput('INPUT').appendField(new ObjectStateField(''), 'FIELD'); + const jso = Blockly.serialization.blocks.save(block); + assertProperty(jso, 'fields', {'FIELD': { + 'prop1': 'state1', + 'prop2': 42, + 'prop3': true, + }}); + }); + + test('Array', function() { + const block = this.workspace.newBlock('row_block'); + block.getInput('INPUT').appendField(new ArrayStateField(''), 'FIELD'); + const jso = Blockly.serialization.blocks.save(block); + assertProperty(jso, 'fields', {'FIELD': ['state1', 42, true]}); }); }); });