diff --git a/core/field.js b/core/field.js index fbc4ff34198..647a366c8c7 100644 --- a/core/field.js +++ b/core/field.js @@ -45,11 +45,13 @@ const Tooltip = goog.require('Blockly.Tooltip'); const WidgetDiv = goog.require('Blockly.WidgetDiv'); /* eslint-disable-next-line no-unused-vars */ const WorkspaceSvg = goog.requireType('Blockly.WorkspaceSvg'); +const Xml = goog.require('Blockly.Xml'); const dom = goog.require('Blockly.utils.dom'); const browserEvents = goog.require('Blockly.browserEvents'); const style = goog.require('Blockly.utils.style'); const userAgent = goog.require('Blockly.utils.userAgent'); const utils = goog.require('Blockly.utils'); +const utilsXml = goog.require('Blockly.utils.xml'); /** @suppress {extraRequire} */ goog.require('Blockly.Events.BlockChange'); /** @suppress {extraRequire} */ @@ -439,6 +441,10 @@ Field.prototype.toXml = function(fieldElement) { * @package */ Field.prototype.saveState = function() { + const legacyState = this.saveLegacyState(Field); + if (legacyState !== null) { + return legacyState; + } return this.getValue(); }; @@ -449,9 +455,56 @@ Field.prototype.saveState = function() { * @package */ Field.prototype.loadState = function(state) { + if (this.loadLegacyState(Field, state)) { + return; + } this.setValue(state); }; +// eslint-disable-next-line valid-jsdoc +/** + * Returns a stringified version of the XML state, if it should be used. + * Otherwise this returns null, to signal the field should use its own + * serialization. + * @param {?} callingClass The class calling this method. + * Used to see if `this` has overridden any relevant hooks. + * @return {?string} The stringified version of the XML state, or null. + * @protected + */ +Field.prototype.saveLegacyState = function(callingClass) { + if (callingClass.prototype.saveState === this.saveState && + callingClass.prototype.toXml !== this.toXml) { + const elem = utilsXml.createElement("field"); + elem.setAttribute("name", this.name || ''); + const text = Xml.domToText(this.toXml(elem)); + return text.replace( + ' xmlns="https://developers.google.com/blockly/xml"', ''); + } + // Either they called this on purpose from their saveState, or they have + // no implementations of either hook. Just do our thing. + return null; +}; + +// eslint-disable-next-line valid-jsdoc +/** + * Loads the given state using either the old XML hoooks, if they should be + * used. Returns true to indicate loading has been handled, false otherwise. + * @param {?} callingClass The class calling this method. + * Used to see if `this` has overridden any relevant hooks. + * @param {*} state The state to apply to the field. + * @return {boolean} Whether the state was applied or not. + */ +Field.prototype.loadLegacyState = function(callingClass, state) { + if (callingClass.prototype.loadState === this.loadState && + callingClass.prototype.fromXml !== this.fromXml) { + this.fromXml(Xml.textToDom(/** @type {string} */ (state))); + return true; + } + // Either they called this on purpose from their loadState, or they have + // no implementations of either hook. Just do our thing. + return false; +}; + /** * Dispose of all DOM objects and events belonging to this editable field. * @package diff --git a/core/field_angle.js b/core/field_angle.js index c4c29279986..ab877589a22 100644 --- a/core/field_angle.js +++ b/core/field_angle.js @@ -254,26 +254,6 @@ Blockly.FieldAngle.prototype.initView = function() { this.textElement_.appendChild(this.symbol_); }; -/** - * Saves this field's value. - * @return {number} The angle value held by this field. - * @override - * @package - */ -Blockly.FieldAngle.prototype.saveState = function() { - return /** @type {number} */ (this.getValue()); -}; - -/** - * Sets the field's value based on the given state. - * @param {*} state The state to apply to the angle field. - * @override - * @package - */ -Blockly.FieldAngle.prototype.loadState = function(state) { - this.setValue(state); -}; - /** * Updates the graph when the field rerenders. * @protected diff --git a/core/field_checkbox.js b/core/field_checkbox.js index 274123995f6..388e18a26dc 100644 --- a/core/field_checkbox.js +++ b/core/field_checkbox.js @@ -104,22 +104,16 @@ FieldCheckbox.prototype.configure_ = function(config) { /** * Saves this field's value. - * @return {boolean} The boolean value held by this field. + * @return {*} The boolean value held by this field. * @override * @package */ FieldCheckbox.prototype.saveState = function() { - return /** @type {boolean} */ (this.getValueBoolean()); -}; - -/** - * Sets the field's value based on the given state. - * @param {*} state The state to apply to the checkbox field. - * @override - * @package - */ -FieldCheckbox.prototype.loadState = function(state) { - this.setValue(state); + const legacyState = this.saveLegacyState(FieldCheckbox); + if (legacyState !== null) { + return legacyState; + } + return this.getValueBoolean(); }; /** diff --git a/core/field_colour.js b/core/field_colour.js index a8ac41e8f43..b0529046047 100644 --- a/core/field_colour.js +++ b/core/field_colour.js @@ -188,26 +188,6 @@ FieldColour.prototype.initView = function() { } }; -/** - * Saves this field's value. - * @return {string} The colour value held by this field. - * @override - * @package - */ -FieldColour.prototype.saveState = function() { - return /** @type {string} */ (this.getValue()); -}; - -/** - * Sets the field's value based on the given state. - * @param {*} state The state to apply to the colour field. - * @override - * @package - */ -FieldColour.prototype.loadState = function(state) { - this.setValue(state); -}; - /** * @override */ diff --git a/core/field_dropdown.js b/core/field_dropdown.js index c9671149ea1..cec7bab9711 100644 --- a/core/field_dropdown.js +++ b/core/field_dropdown.js @@ -170,16 +170,6 @@ FieldDropdown.prototype.fromXml = function(fieldElement) { this.setValue(fieldElement.textContent); }; -/** - * Saves this field's value. - * @return {*} The dropdown value held by this field. - * @override - * @package - */ -FieldDropdown.prototype.saveState = function() { - return this.getValue(); -}; - /** * Sets the field's value based on the given state. * @param {*} state The state to apply to the dropdown field. @@ -187,6 +177,9 @@ FieldDropdown.prototype.saveState = function() { * @package */ FieldDropdown.prototype.loadState = function(state) { + if (this.loadLegacyState(FieldDropdown, state)) { + return; + } if (this.isOptionListDynamic()) { this.getOptions(false); } diff --git a/core/field_label_serializable.js b/core/field_label_serializable.js index df2119c1523..dee9da1ef60 100644 --- a/core/field_label_serializable.js +++ b/core/field_label_serializable.js @@ -68,26 +68,6 @@ FieldLabelSerializable.prototype.EDITABLE = false; */ FieldLabelSerializable.prototype.SERIALIZABLE = true; -/** - * Saves this field's value. - * @return {string} The text value held by this field. - * @override - * @package - */ -FieldLabelSerializable.prototype.saveState = function() { - return /** @type {string} */ (this.getValue()); -}; - -/** - * Sets the field's value based on the given state. - * @param {*} state The state to apply to the label field. - * @override - * @package - */ -FieldLabelSerializable.prototype.loadState = function(state) { - this.setValue(state); -}; - fieldRegistry.register('field_label_serializable', FieldLabelSerializable); exports = FieldLabelSerializable; diff --git a/core/field_multilineinput.js b/core/field_multilineinput.js index c72f9983d6e..7d0d4bcea2d 100644 --- a/core/field_multilineinput.js +++ b/core/field_multilineinput.js @@ -124,21 +124,27 @@ FieldMultilineInput.prototype.fromXml = function(fieldElement) { /** * Saves this field's value. - * @return {string} The text value held by this field. - * @override + * @return {*} The state of this field. * @package */ FieldMultilineInput.prototype.saveState = function() { - return /** @type {string} */ (this.getValue()); + const legacyState = this.saveLegacyState(FieldMultilineInput); + if (legacyState !== null) { + return legacyState; + } + return this.getValue(); }; /** * Sets the field's value based on the given state. - * @param {*} state The state to apply to the multiline input field. + * @param {*} state The state of the variable to assign to this variable field. * @override * @package */ FieldMultilineInput.prototype.loadState = function(state) { + if (this.loadLegacyState(Field, state)) { + return; + } this.setValue(state); }; diff --git a/core/field_number.js b/core/field_number.js index 50cbc7fe775..4ed5ac5aa5a 100644 --- a/core/field_number.js +++ b/core/field_number.js @@ -118,26 +118,6 @@ FieldNumber.prototype.configure_ = function(config) { this.setPrecisionInternal_(config['precision']); }; -/** - * Saves this field's value. - * @return {number} The number value held by this field. - * @override - * @package - */ -FieldNumber.prototype.saveState = function() { - return /** @type {number} */ (this.getValue()); -}; - -/** - * Sets the field's value based on the given state. - * @param {*} state The state to apply to the nuber field. - * @override - * @package - */ -FieldNumber.prototype.loadState = function(state) { - this.setValue(state); -}; - /** * Set the maximum, minimum and precision constraints on this field. * Any of these properties may be undefined or NaN to be disabled. diff --git a/core/field_textinput.js b/core/field_textinput.js index e07ac22e4dc..789bc0a96bf 100644 --- a/core/field_textinput.js +++ b/core/field_textinput.js @@ -182,26 +182,6 @@ FieldTextInput.prototype.initView = function() { this.createTextElement_(); }; -/** - * Saves this field's value. - * @return {*} The text value held by this field. - * @override - * @package - */ -FieldTextInput.prototype.saveState = function() { - return this.getValue(); -}; - -/** - * Sets the field's value based on the given state. - * @param {*} state The state to apply to the text input field. - * @override - * @package - */ -FieldTextInput.prototype.loadState = function(state) { - this.setValue(state); -}; - /** * Ensure that the input value casts to a valid string. * @param {*=} opt_newValue The input value. diff --git a/core/field_variable.js b/core/field_variable.js index 9cb95eff52f..7e76f7e3539 100644 --- a/core/field_variable.js +++ b/core/field_variable.js @@ -199,11 +199,15 @@ FieldVariable.prototype.toXml = function(fieldElement) { /** * Saves this field's value. - * @return {{id: string}} The ID of the variable referenced by this field. + * @return {*} The ID of the variable referenced by this field. * @override * @package */ FieldVariable.prototype.saveState = function() { + const legacyState = this.saveLegacyState(FieldVariable); + if (legacyState !== null) { + return legacyState; + } // Make sure the variable is initialized. this.initModel(); return { @@ -218,6 +222,9 @@ FieldVariable.prototype.saveState = function() { * @package */ FieldVariable.prototype.loadState = function(state) { + if (this.loadLegacyState(FieldVariable, state)) { + return; + } // This is necessary so that blocks in the flyout can have custom var names. const variable = Variables.getOrCreateVariablePackage( this.sourceBlock_.workspace, diff --git a/core/serialization/blocks.js b/core/serialization/blocks.js index c795edfa929..f2dd78d4e0d 100644 --- a/core/serialization/blocks.js +++ b/core/serialization/blocks.js @@ -24,6 +24,7 @@ const {ISerializer} = goog.requireType('Blockly.serialization.ISerializer'); const Size = goog.require('Blockly.utils.Size'); // eslint-disable-next-line no-unused-vars const Workspace = goog.requireType('Blockly.Workspace'); +const Xml = goog.require('Blockly.Xml'); const inputTypes = goog.require('Blockly.inputTypes'); const priorities = goog.require('Blockly.serialization.priorities'); const serializationRegistry = goog.require('Blockly.serialization.registry'); @@ -162,6 +163,12 @@ const saveExtraState = function(block, state) { if (extraState !== null) { state['extraState'] = extraState; } + } else if (block.mutationToDom) { + const extraState = block.mutationToDom(); + if (extraState !== null) { + state['extraState'] = Xml.domToText(extraState).replace( + ' xmlns="https://developers.google.com/blockly/xml"', ''); + } } }; @@ -427,7 +434,11 @@ const loadExtraState = function(block, state) { if (!state['extraState']) { return; } - block.loadExtraState(state['extraState']); + if (block.loadExtraState) { + block.loadExtraState(state['extraState']); + } else { + block.domToMutation(Xml.textToDom(state['extraState'])); + } }; /** diff --git a/tests/deps.js b/tests/deps.js index b7ff00900cc..b7a164effec 100644 --- a/tests/deps.js +++ b/tests/deps.js @@ -65,7 +65,7 @@ goog.addDependency('../../core/events/events_var_rename.js', ['Blockly.Events.Va goog.addDependency('../../core/events/events_viewport.js', ['Blockly.Events.ViewportChange'], ['Blockly.Events', 'Blockly.Events.UiBase', 'Blockly.registry', 'Blockly.utils.object'], {'lang': 'es6', 'module': 'goog'}); goog.addDependency('../../core/events/workspace_events.js', ['Blockly.Events.FinishedLoading'], ['Blockly.Events', 'Blockly.Events.Abstract', 'Blockly.registry', 'Blockly.utils.object'], {'lang': 'es6', 'module': 'goog'}); goog.addDependency('../../core/extensions.js', ['Blockly.Extensions'], ['Blockly.utils'], {'lang': 'es6', 'module': 'goog'}); -goog.addDependency('../../core/field.js', ['Blockly.Field'], ['Blockly.DropDownDiv', 'Blockly.Events', 'Blockly.Events.BlockChange', 'Blockly.Gesture', 'Blockly.MarkerManager', 'Blockly.Tooltip', 'Blockly.WidgetDiv', 'Blockly.browserEvents', 'Blockly.utils', 'Blockly.utils.Rect', 'Blockly.utils.Size', 'Blockly.utils.Svg', 'Blockly.utils.dom', 'Blockly.utils.style', 'Blockly.utils.userAgent'], {'lang': 'es6', 'module': 'goog'}); +goog.addDependency('../../core/field.js', ['Blockly.Field'], ['Blockly.DropDownDiv', 'Blockly.Events', 'Blockly.Events.BlockChange', 'Blockly.Gesture', 'Blockly.MarkerManager', 'Blockly.Tooltip', 'Blockly.WidgetDiv', 'Blockly.Xml', 'Blockly.browserEvents', 'Blockly.utils', 'Blockly.utils.Rect', 'Blockly.utils.Size', 'Blockly.utils.Svg', 'Blockly.utils.dom', 'Blockly.utils.style', 'Blockly.utils.userAgent', 'Blockly.utils.xml'], {'lang': 'es6', 'module': 'goog'}); goog.addDependency('../../core/field_angle.js', ['Blockly.FieldAngle'], ['Blockly.Css', 'Blockly.DropDownDiv', 'Blockly.FieldTextInput', 'Blockly.WidgetDiv', 'Blockly.browserEvents', 'Blockly.fieldRegistry', 'Blockly.utils.KeyCodes', 'Blockly.utils.Svg', 'Blockly.utils.dom', 'Blockly.utils.math', 'Blockly.utils.object', 'Blockly.utils.userAgent']); goog.addDependency('../../core/field_checkbox.js', ['Blockly.FieldCheckbox'], ['Blockly.Events.BlockChange', 'Blockly.Field', 'Blockly.fieldRegistry', 'Blockly.utils.dom', 'Blockly.utils.object'], {'lang': 'es6', 'module': 'goog'}); goog.addDependency('../../core/field_colour.js', ['Blockly.FieldColour'], ['Blockly.Css', 'Blockly.DropDownDiv', 'Blockly.Events.BlockChange', 'Blockly.Field', 'Blockly.browserEvents', 'Blockly.fieldRegistry', 'Blockly.utils.KeyCodes', 'Blockly.utils.Size', 'Blockly.utils.aria', 'Blockly.utils.colour', 'Blockly.utils.dom', 'Blockly.utils.idGenerator', 'Blockly.utils.object'], {'lang': 'es6', 'module': 'goog'}); @@ -197,7 +197,7 @@ 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.blocks', 'Blockly.serialization.registry', 'Blockly.serialization.variables', '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.Events', 'Blockly.inputTypes', 'Blockly.serialization.exceptions', 'Blockly.serialization.priorities', 'Blockly.serialization.registry', 'Blockly.utils.Size'], {'lang': 'es6', 'module': 'goog'}); +goog.addDependency('../../core/serialization/blocks.js', ['Blockly.serialization.blocks'], ['Blockly.Events', 'Blockly.Xml', 'Blockly.inputTypes', 'Blockly.serialization.exceptions', 'Blockly.serialization.priorities', 'Blockly.serialization.registry', 'Blockly.utils.Size'], {'lang': 'es6', 'module': 'goog'}); goog.addDependency('../../core/serialization/exceptions.js', ['Blockly.serialization.exceptions'], [], {'lang': 'es6', 'module': 'goog'}); goog.addDependency('../../core/serialization/priorities.js', ['Blockly.serialization.priorities'], [], {'module': 'goog'}); goog.addDependency('../../core/serialization/registry.js', ['Blockly.serialization.registry'], ['Blockly.registry'], {'lang': 'es6', 'module': 'goog'}); diff --git a/tests/mocha/field_test.js b/tests/mocha/field_test.js index 46fd65d699c..ed446df717e 100644 --- a/tests/mocha/field_test.js +++ b/tests/mocha/field_test.js @@ -17,9 +17,11 @@ suite('Abstract Fields', function() { // console logs. createDeprecationWarningStub(); }); + teardown(function() { sharedTestTeardown.call(this); }); + suite('Is Serializable', function() { // Both EDITABLE and SERIALIZABLE are default. function FieldDefault() { @@ -73,6 +75,269 @@ suite('Abstract Fields', function() { chai.assert.isTrue(field.isSerializable()); }); }); + + suite('Serialization', function() { + class DefaultSerializationField extends Blockly.Field { + constructor(value, validator = undefined, config = undefined) { + super(value, validator, config); + this.SERIALIZABLE = true; + } + } + + class CustomXmlField extends Blockly.Field { + constructor(value, validator = undefined, config = undefined) { + super(value, validator, config); + this.SERIALIZABLE = true; + } + + toXml(fieldElement) { + fieldElement.textContent = 'custom value'; + return fieldElement; + } + + fromXml(fieldElement) { + this.someProperty = fieldElement.textContent; + } + } + + class CustomXmlCallSuperField extends Blockly.Field { + constructor(value, validator = undefined, config = undefined) { + super(value, validator, config); + this.SERIALIZABLE = true; + } + + toXml(fieldElement) { + super.toXml(fieldElement); + fieldElement.setAttribute('attribute', 'custom value'); + return fieldElement; + } + + fromXml(fieldElement) { + super.fromXml(fieldElement); + this.someProperty = fieldElement.getAttribute('attribute'); + } + } + + class CustomJsoField extends Blockly.Field { + constructor(value, validator = undefined, config = undefined) { + super(value, validator, config); + this.SERIALIZABLE = true; + } + + saveState() { + return 'custom value'; + } + + loadState(state) { + this.someProperty = state; + } + } + + class CustomJsoCallSuperField extends Blockly.Field { + constructor(value, validator = undefined, config = undefined) { + super(value, validator, config); + this.SERIALIZABLE = true; + } + + saveState() { + return { + default: super.saveState(), + val: 'custom value' + }; + } + + loadState(state) { + super.loadState(state.default); + this.someProperty = state.val; + } + } + + class CustomXmlAndJsoField extends Blockly.Field { + constructor(value, validator = undefined, config = undefined) { + super(value, validator, config); + this.SERIALIZABLE = true; + } + + toXml(fieldElement) { + fieldElement.textContent = 'custom value'; + return fieldElement; + } + + fromXml(fieldElement) { + this.someProperty = fieldElement.textContent; + } + + saveState() { + return 'custom value'; + } + + loadState(state) { + this.someProperty = state; + } + } + + suite('Save', function() { + suite('JSO', function() { + test('No implementations', function() { + const field = new DefaultSerializationField('test value'); + const value = field.saveState(); + chai.assert.equal(value, 'test value'); + }); + + test('Xml implementations', function() { + const field = new CustomXmlField('test value'); + const value = field.saveState(); + chai.assert.equal(value, 'custom value'); + }); + + test('Xml super implementation', function() { + const field = new CustomXmlCallSuperField('test value'); + const value = field.saveState(); + chai.assert.equal( + value, + 'test value'); + }); + + test('JSO implementations', function() { + const field = new CustomJsoField('test value'); + const value = field.saveState(); + chai.assert.equal(value, 'custom value'); + }); + + test('JSO super implementations', function() { + const field = new CustomJsoCallSuperField('test value'); + const value = field.saveState(); + chai.assert.deepEqual( + value, {default: 'test value', val: 'custom value'}); + }); + + test('Xml and JSO implementations', function() { + const field = new CustomXmlAndJsoField('test value'); + const value = field.saveState(); + chai.assert.equal(value, 'custom value'); + }); + }); + + suite('Xml', function() { + test('No implementations', function() { + const field = new DefaultSerializationField('test value'); + const element = document.createElement('field'); + const value = Blockly.Xml.domToText(field.toXml(element)); + chai.assert.equal( + value, + 'test value'); + }); + + test('Xml implementations', function() { + const field = new CustomXmlField('test value'); + const element = document.createElement('field'); + const value = Blockly.Xml.domToText(field.toXml(element)); + chai.assert.equal( + value, + 'custom value' + ); + }); + + test('Xml super implementation', function() { + const field = new CustomXmlCallSuperField('test value'); + const element = document.createElement('field'); + const value = Blockly.Xml.domToText(field.toXml(element)); + chai.assert.equal( + value, + 'test value'); + }); + + test('Xml and JSO implementations', function() { + const field = new CustomXmlAndJsoField('test value'); + const element = document.createElement('field'); + const value = Blockly.Xml.domToText(field.toXml(element)); + chai.assert.equal( + value, + 'custom value' + ); + }); + }); + }); + + suite('Load', function() { + suite('JSO', function() { + test('No implementations', function() { + const field = new DefaultSerializationField(''); + field.loadState('test value'); + chai.assert.equal(field.getValue(), 'test value'); + }); + + test('Xml implementations', function() { + const field = new CustomXmlField(''); + field.loadState('custom value'); + chai.assert.equal(field.someProperty, 'custom value'); + }); + + test('Xml super implementation', function() { + const field = new CustomXmlCallSuperField(''); + field.loadState( + 'test value'); + chai.assert.equal(field.getValue(), 'test value'); + chai.assert.equal(field.someProperty, 'custom value'); + }); + + test('JSO implementations', function() { + const field = new CustomJsoField(''); + field.loadState('custom value'); + chai.assert.equal(field.someProperty, 'custom value'); + }); + + test('JSO super implementations', function() { + const field = new CustomJsoCallSuperField(''); + field.loadState({default: 'test value', val: 'custom value'}); + chai.assert.equal(field.getValue(), 'test value'); + chai.assert.equal(field.someProperty, 'custom value'); + }); + + test('Xml and JSO implementations', function() { + const field = new CustomXmlAndJsoField(''); + field.loadState('custom value'); + chai.assert.equal(field.someProperty, 'custom value'); + }); + }); + + suite('Xml', function() { + test('No implementations', function() { + const field = new DefaultSerializationField(''); + field.fromXml( + Blockly.Xml.textToDom('test value')); + chai.assert.equal(field.getValue(), 'test value'); + }); + + test('Xml implementations', function() { + const field = new CustomXmlField(''); + field.fromXml( + Blockly.Xml.textToDom('custom value')); + chai.assert.equal(field.someProperty, 'custom value'); + }); + + test('Xml super implementation', function() { + const field = new CustomXmlCallSuperField(''); + field.fromXml( + Blockly.Xml.textToDom( + 'test value' + ) + ); + chai.assert.equal(field.getValue(), 'test value'); + chai.assert.equal(field.someProperty, 'custom value'); + }); + + test('XML andd JSO implementations', function() { + const field = new CustomXmlAndJsoField(''); + field.fromXml( + Blockly.Xml.textToDom('custom value')); + chai.assert.equal(field.someProperty, 'custom value'); + }); + }); + }); + }); + suite('setValue', function() { function addSpies(field, excludeSpies = []) { if (!excludeSpies.includes('doValueInvalid_')) { @@ -321,6 +586,7 @@ suite('Abstract Fields', function() { sinon.assert.calledOnce(this.field.doValueUpdate_); }); }); + suite('Customization', function() { // All this field does is wrap the abstract field. function CustomField(opt_config) { diff --git a/tests/mocha/jso_deserialization_test.js b/tests/mocha/jso_deserialization_test.js index 003499e5b09..0ec532062db 100644 --- a/tests/mocha/jso_deserialization_test.js +++ b/tests/mocha/jso_deserialization_test.js @@ -681,4 +681,31 @@ suite('JSO Deserialization', function() { 'third-load' ]); }); + + suite('Extra state', function() { + // Most of this is covered by our round-trip tests. But we need one test + // for old xml hooks. + test('Xml hooks', function() { + Blockly.Blocks['test_block'] = { + init: function() { }, + + mutationToDom: function() { }, + + domToMutation: function(element) { + this.someProperty = element.getAttribute('value'); + } + }; + + const block = Blockly.serialization.blocks.load( + { + 'type': 'test_block', + 'extraState': '', + }, + this.workspace); + + delete Blockly.Blocks['test_block']; + + chai.assert.equal(block.someProperty, 'some value'); + }); + }); }); diff --git a/tests/mocha/jso_serialization_test.js b/tests/mocha/jso_serialization_test.js index e7cfeef856d..cfd931d3eb6 100644 --- a/tests/mocha/jso_serialization_test.js +++ b/tests/mocha/jso_serialization_test.js @@ -208,6 +208,18 @@ suite('JSO Serialization', function() { const jso = Blockly.serialization.blocks.save(block); assertProperty(jso, 'extraState', ['state1', 42, true]); }); + + test('Xml hooks', function() { + const block = this.workspace.newBlock('row_block'); + block.mutationToDom = function() { + var container = Blockly.utils.xml.createElement('mutation'); + container.setAttribute('value', 'some value'); + return container; + }; + const jso = Blockly.serialization.blocks.save(block); + assertProperty( + jso, 'extraState', ''); + }); }); suite('Icons', function() {