feat: adds hooks for serializing plugins (#5276)

* Reformat registry tests

* Add tests for plugin hooks

* Add plugin hooks for serialization

* Switch PluginSerializer to IPluginSerializer

* fix: types

* fix: PR comments

* fix: tests

* cleanup: formatting

* fix: types

* feat: add respecting case in registry

* feat: add separate registry for serializers

* fix: rename serialiation registry alias

* fix: move serializer interface into interface dir
This commit is contained in:
Beka Westberg
2021-08-20 19:49:35 +00:00
committed by alschmiedt
parent 486123e4ff
commit 07057d087c
13 changed files with 733 additions and 251 deletions

View File

@@ -0,0 +1,71 @@
/**
* @license
* Copyright 2021 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
/**
* @fileoverview The record type for an object containing functions for
* serializing part of the workspace.
*/
'use strict';
goog.module('Blockly.serialization.ISerializer');
goog.module.declareLegacyNamespace();
// eslint-disable-next-line no-unused-vars
const Workspace = goog.requireType('Blockly.Workspace');
/**
* Serializes and deserializes a plugin.
* @interface
*/
class ISerializer {
constructor() {
/**
* A priority value used to determine the order of deserializing plugins.
* More positive priorities are deserialized before less positive
* priorities. Eg if you have priorities (0, -10, 10, 100) the order of
* deserialiation will be (100, 10, 0, -10).
* If two plugins have the same priority, they are deserialized in an
* arbitrary order relative to each other.
* @type {number}
*/
this.priority;
}
/* eslint-disable no-unused-vars, valid-jsdoc */
/**
* Saves the state of the plugin.
* @param {!Workspace} workspace The workspace the plugin to serialize is
* associated with.
* @return {?} A JS object containing the plugin's state, or null if
* there is no state to record.
*/
save(workspace) {}
/* eslint-enable valid-jsdoc */
/**
* Loads the state of the plugin.
* @param {?} state The state of the plugin to deserialize. This will always
* be non-null.
* @param {!Workspace} workspace The workspace the plugin to deserialize is
* associated with.
*/
load(state, workspace) {}
/**
* Clears the state of the plugin.
* @param {!Workspace} workspace The workspace the plugin to clear the state
* of is associated with.
*/
clear(workspace) {}
/* eslint-enable no-unused-vars */
}
exports.ISerializer = ISerializer;

View File

@@ -38,6 +38,8 @@ const Renderer = goog.requireType('Blockly.blockRendering.Renderer');
const Theme = goog.requireType('Blockly.Theme');
/* eslint-disable-next-line no-unused-vars */
const ToolboxItem = goog.requireType('Blockly.ToolboxItem');
/* eslint-disable-next-line no-unused-vars */
const ISerializer = goog.requireType('Blockly.serialization.ISerializer');
/**
@@ -45,12 +47,20 @@ const ToolboxItem = goog.requireType('Blockly.ToolboxItem');
* registering and the value being the constructor function.
* e.g. {'field': {'field_angle': Blockly.FieldAngle}}
*
* @type {Object<string, Object<string, function(new:?)>>}
* @type {!Object<string, !Object<string, (function(new:?)|!Object)>>}
*/
const typeMap = Object.create(null);
/** @private */
exports.typeMap_ = typeMap;
/**
* A map of maps. With the keys being the type and caseless name of the class we
* are registring, and the value being the most recent cased name for that
* registration.
* @type {!Object<string, !Object<string, string>>}
*/
const nameMap = Object.create(null);
/**
* The string used to register the default class for a type of plugin.
* @type {string}
@@ -118,6 +128,12 @@ Type.METRICS_MANAGER = new Type('metricsManager');
/** @type {!Type<IBlockDragger>} */
Type.BLOCK_DRAGGER = new Type('blockDragger');
/**
* @type {!Type<ISerializer>}
* @package
*/
Type.SERIALIZER = new Type('serializer');
/**
* Registers a class based on a type and name.
* @param {string|!Type<T>} type The type of the plugin.
@@ -129,7 +145,7 @@ Type.BLOCK_DRAGGER = new Type('blockDragger');
* an already registered item.
* @throws {Error} if the type or name is empty, a name with the given type has
* already been registered, or if the given class or object is not valid for
* it's type.
* it's type.
* @template T
*/
const register = function(type, name, registryItem, opt_allowOverrides) {
@@ -146,25 +162,29 @@ const register = function(type, name, registryItem, opt_allowOverrides) {
'Invalid name "' + name + '". The name must be a' +
' non-empty string.');
}
name = name.toLowerCase();
const caselessName = name.toLowerCase();
if (!registryItem) {
throw Error('Can not register a null value');
}
let typeRegistry = typeMap[type];
let nameRegistry = nameMap[type];
// If the type registry has not been created, create it.
if (!typeRegistry) {
typeRegistry = typeMap[type] = Object.create(null);
nameRegistry = nameMap[type] = Object.create(null);
}
// Validate that the given class has all the required properties.
validate(type, registryItem);
// Don't throw an error if opt_allowOverrides is true.
if (!opt_allowOverrides && typeRegistry[name]) {
if (!opt_allowOverrides && typeRegistry[caselessName]) {
throw Error(
'Name "' + name + '" with type "' + type + '" already registered.');
'Name "' + caselessName + '" with type "' + type +
'" already registered.');
}
typeRegistry[name] = registryItem;
typeRegistry[caselessName] = registryItem;
nameRegistry[caselessName] = name;
};
exports.register = register;
@@ -203,6 +223,7 @@ const unregister = function(type, name) {
return;
}
delete typeMap[type][name];
delete nameMap[type][name];
};
exports.unregister = unregister;
@@ -288,6 +309,43 @@ const getObject = function(type, name, opt_throwIfMissing) {
};
exports.getObject = getObject;
/**
* Returns a map of items registered with the given type.
* @param {string|!Type<T>} type The type of the plugin. (e.g. Category)
* @param {boolean} opt_cased Whether or not to return a map with cased keys
* (rather than caseless keys). False by default.
* @param {boolean=} opt_throwIfMissing Whether or not to throw an error if we
* are unable to find the object. False by default.
* @return {?Object<string, ?T|?function(new:T, ...?)>} A map of objects with
* the given type, or null if none exists.
* @template T
*/
const getAllItems = function(type, opt_cased, opt_throwIfMissing) {
type = String(type).toLowerCase();
const typeRegistry = typeMap[type];
if (!typeRegistry) {
const msg = `Unable to find [${type}] in the registry.`;
if (opt_throwIfMissing) {
throw new Error(`${msg} You must require or register a ${type} plugin.`);
} else {
console.warn(msg);
}
return null;
}
if (!opt_cased) {
return typeRegistry;
}
const nameRegistry = nameMap[type];
const casedRegistry = Object.create(null);
const keys = Object.keys(typeRegistry);
for (let i = 0; i < keys.length; i++) {
const key = keys[i];
casedRegistry[nameRegistry[key]] = typeRegistry[key];
}
return casedRegistry;
};
exports.getAllItems = getAllItems;
/**
* Gets the class from Blockly options for the given type.
* This is used for plugins that override a built in feature. (e.g. Toolbox)

View File

@@ -82,4 +82,7 @@ goog.require('Blockly.zelos.Renderer');
// Classic is the default theme.
goog.require('Blockly.Themes.Classic');
goog.require('Blockly.serialization.blocks');
goog.require('Blockly.serialization.registry');
goog.require('Blockly.serialization.variables');
goog.require('Blockly.serialization.workspaces');

View File

@@ -19,10 +19,14 @@ 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 {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 inputTypes = goog.require('Blockly.inputTypes');
const priorities = goog.require('Blockly.serialization.priorities');
const serializationRegistry = goog.require('Blockly.serialization.registry');
// TODO: Remove this once lint is fixed.
@@ -577,3 +581,71 @@ const initBlock = function(block, rendered) {
block.initModel();
}
};
// Aliases to disambiguate saving/loading within the serializer.
const saveBlock = save;
const loadBlock = load;
/**
* Serializer for saving and loading block state.
* @implements {ISerializer}
*/
class BlockSerializer {
constructor() {
/**
* The priority for deserializing blocks.
* @type {number}
*/
this.priority = priorities.BLOCKS;
}
/**
* Serializes the blocks of the given workspace.
* @param {!Workspace} workspace The workspace to save the blocks of.
* @return {?{languageVersion: number, blocks:!Array<!State>}} The state of
* the workspace's blocks, or null if there are no blocks.
*/
save(workspace) {
const blockState = [];
for (const block of workspace.getTopBlocks(false)) {
const state = saveBlock(block, {addCoordinates: true});
if (state) {
blockState.push(state);
}
}
if (blockState.length) {
return {
'languageVersion': 0, // Currently unused.
'blocks': blockState
};
}
return null;
}
/**
* Deserializes the blocks defined by the given state into the given
* workspace.
* @param {{languageVersion: number, blocks:!Array<!State>}} state The state
* of the blocks to deserialize.
* @param {!Workspace} workspace The workspace to deserialize into.
*/
load(state, workspace) {
const blockStates = state['blocks'];
for (const state of blockStates) {
loadBlock(state, workspace, {recordUndo: Events.getRecordUndo()});
}
}
/**
* Disposes of any blocks that exist on the workspace.
* @param {!Workspace} workspace The workspace to clear the blocks of.
*/
clear(workspace) {
// Cannot use workspace.clear() because that also removes variables.
for (const block of workspace.getTopBlocks(false)) {
block.dispose(false);
}
}
}
serializationRegistry.register('blocks', new BlockSerializer());

View File

@@ -0,0 +1,34 @@
/**
* @license
* Copyright 2021 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
/**
* @fileoverview Includes constants for the priorities of different plugin
* serializers. Higher priorities are deserialized first.
*/
'use strict';
/**
* The top level namespace for priorities of plugin serializers.
* @namespace Blockly.serialization.priorities
*/
goog.module('Blockly.serialization.priorities');
goog.module.declareLegacyNamespace();
/**
* The priority for deserializing variables.
* @type {number}
* @const
*/
exports.VARIABLES = 100;
/**
* The priority for deserializing blocks.
* @type {number}
* @const
*/
exports.BLOCKS = 50;

View File

@@ -0,0 +1,40 @@
/**
* @license
* Copyright 2021 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
/**
* @fileoverview Contains functions registering serializers (eg blocks,
* variables, plugins, etc).
*/
'use strict';
goog.module('Blockly.serialization.registry');
goog.module.declareLegacyNamespace();
// eslint-disable-next-line no-unused-vars
const {ISerializer} = goog.requireType('Blockly.serialization.ISerializer');
const registry = goog.require('Blockly.registry');
/**
* Registers the given serializer so that it can be used for serialization and
* deserialization.
* @param {string} name The name of the serializer to register.
* @param {ISerializer} serializer The serializer to register.
*/
const register = function(name, serializer) {
registry.register(registry.Type.SERIALIZER, name, serializer);
};
exports.register = register;
/**
* Unregisters the serializer associated with the given name.
* @param {string} name The name of the serializer to unregister.
*/
const unregister = function(name) {
registry.unregister(registry.Type.SERIALIZER, name);
};
exports.unregister = unregister;

View File

@@ -15,9 +15,13 @@ goog.module.declareLegacyNamespace();
const Events = goog.require('Blockly.Events');
// eslint-disable-next-line no-unused-vars
const {ISerializer} = goog.requireType('Blockly.serialization.ISerializer');
// eslint-disable-next-line no-unused-vars
const VariableModel = goog.requireType('Blockly.VariableModel');
// eslint-disable-next-line no-unused-vars
const Workspace = goog.requireType('Blockly.Workspace');
const priorities = goog.require('Blockly.serialization.priorities');
const serializationRegistry = goog.require('Blockly.serialization.registry');
/**
@@ -36,7 +40,7 @@ exports.State = State;
* @param {!VariableModel} variableModel The variable to serialize.
* @return {!State} The serialized state of the variable.
*/
const save = function(variableModel) {
const saveVariable = function(variableModel) {
const state = {
'name': variableModel.name,
'id': variableModel.getId()
@@ -46,12 +50,9 @@ const save = function(variableModel) {
}
return state;
};
/** @package */
exports.save = save;
/**
* Loads the variable represented by the given state into the given workspace.
* Do not call this directly, use workspace.createVariable instead.
* @param {!State} state The state of a variable to deserialize into the
* workspace.
* @param {!Workspace} workspace The workspace to add the variable to.
@@ -59,7 +60,7 @@ exports.save = save;
* recordUndo: If true, events triggered by this function will be undo-able
* by the user. False by default.
*/
const load = function(state, workspace, {recordUndo = false} = {}) {
const loadVariable = function(state, workspace, {recordUndo = false} = {}) {
const prevRecordUndo = Events.getRecordUndo();
Events.setRecordUndo(recordUndo);
const existingGroup = Events.getGroup();
@@ -72,5 +73,54 @@ const load = function(state, workspace, {recordUndo = false} = {}) {
Events.setGroup(existingGroup);
Events.setRecordUndo(prevRecordUndo);
};
/** @package */
exports.load = load;
/**
* Serializer for saving and loading variable state.
* @implements {ISerializer}
*/
class VariableSerializer {
constructor() {
/**
* The priority for deserializing variables.
* @type {number}
*/
this.priority = priorities.VARIABLES;
}
/**
* Serializes the variables of the given workspace.
* @param {!Workspace} workspace The workspace to save the variables of.
* @return {?Array<!State>} The state of the workspace's variables, or null
* if there are no variables.
*/
save(workspace) {
const variableStates = [];
for (const variable of workspace.getAllVariables()) {
variableStates.push(saveVariable(variable));
}
return variableStates.length ? variableStates : null;
}
/**
* Deserializes the variable defined by the given state into the given
* workspace.
* @param {!Array<!State>} state The state of the variables to deserialize.
* @param {!Workspace} workspace The workspace to deserialize into.
*/
load(state, workspace) {
for (const varState of state) {
loadVariable(varState, workspace, {recordUndo: Events.getRecordUndo()});
}
}
/**
* Disposes of any variables or potential variables that exist on the
* workspace.
* @param {!Workspace} workspace The workspace to clear the variables of.
*/
clear(workspace) {
workspace.getVariableMap().clear();
}
}
serializationRegistry.register('variables', new VariableSerializer());

View File

@@ -16,9 +16,8 @@ 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');
const registry = goog.require('Blockly.registry');
/**
@@ -28,31 +27,13 @@ const variables = goog.require('Blockly.serialization.variables');
*/
const save = function(workspace) {
const state = Object.create(null);
// TODO: Switch this to use plugin serialization system (once it is built).
const variableStates = [];
const vars = workspace.getAllVariables();
for (let i = 0; i < vars.length; i++) {
variableStates.push(variables.save(vars[i]));
}
if (variableStates.length) {
state['variables'] = variableStates;
}
const blockStates = [];
for (let block of workspace.getTopBlocks(false)) {
const blockState = blocks.save(block, {addCoordinates: true});
if (blockState) {
blockStates.push(blockState);
const serializerMap = registry.getAllItems(registry.Type.SERIALIZER, true);
for (const key in serializerMap) {
const save = serializerMap[key].save(workspace);
if (save) {
state[key] = save;
}
}
if (blockStates.length) {
// This is an object to support adding language version later.
state['blocks'] = {
'blocks': blockStates
};
}
return state;
};
exports.save = save;
@@ -67,8 +48,14 @@ exports.save = save;
* by the user. False by default.
*/
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 serializerMap = registry.getAllItems(registry.Type.SERIALIZER, true);
if (!serializerMap) {
return;
}
const deserializers = Object.entries(serializerMap)
.sort(([, {priority: priorityA}], [, {priority: priorityB}]) =>
priorityB - priorityA);
const prevRecordUndo = Events.getRecordUndo();
Events.setRecordUndo(recordUndo);
@@ -81,18 +68,19 @@ const load = function(state, workspace, {recordUndo = false} = {}) {
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, {recordUndo});
}
// We want to trigger clearing in reverse priority order so plugins don't end
// up missing dependencies.
for (const [, deserializer] of deserializers.reverse()) {
deserializer.clear(workspace);
}
if (state['blocks']) {
const blockStates = state['blocks']['blocks'];
for (let i = 0; i < blockStates.length; i++) {
blocks.load(blockStates[i], workspace, {recordUndo});
// reverse() is destructive, so we have to re-reverse to correct the order.
for (let [name, deserializer] of deserializers.reverse()) {
name = /** @type {string} */ (name);
const pluginState = state[name];
if (pluginState) {
deserializer.load(state[name], workspace);
}
}

View File

@@ -116,6 +116,7 @@ goog.addDependency('../../core/interfaces/i_registrable.js', ['Blockly.IRegistra
goog.addDependency('../../core/interfaces/i_registrable_field.js', ['Blockly.IRegistrableField'], [], {'lang': 'es6', 'module': 'goog'});
goog.addDependency('../../core/interfaces/i_selectable.js', ['Blockly.ISelectable'], [], {'lang': 'es6', 'module': 'goog'});
goog.addDependency('../../core/interfaces/i_selectable_toolbox_item.js', ['Blockly.ISelectableToolboxItem'], [], {'lang': 'es6', 'module': 'goog'});
goog.addDependency('../../core/interfaces/i_serializer.js', ['Blockly.serialization.ISerializer'], [], {'lang': 'es6', 'module': 'goog'});
goog.addDependency('../../core/interfaces/i_styleable.js', ['Blockly.IStyleable'], [], {'lang': 'es6', 'module': 'goog'});
goog.addDependency('../../core/interfaces/i_toolbox.js', ['Blockly.IToolbox'], [], {'lang': 'es6', 'module': 'goog'});
goog.addDependency('../../core/interfaces/i_toolbox_item.js', ['Blockly.IToolboxItem'], [], {'lang': 'es6', 'module': 'goog'});
@@ -193,13 +194,15 @@ goog.addDependency('../../core/renderers/zelos/measurables/row_elements.js', ['B
goog.addDependency('../../core/renderers/zelos/measurables/top_row.js', ['Blockly.zelos.TopRow'], ['Blockly.blockRendering.TopRow', 'Blockly.utils.object'], {'lang': 'es6', 'module': 'goog'});
goog.addDependency('../../core/renderers/zelos/path_object.js', ['Blockly.zelos.PathObject'], ['Blockly.blockRendering.PathObject', 'Blockly.utils.Svg', 'Blockly.utils.dom', 'Blockly.utils.object'], {'lang': 'es6', 'module': 'goog'});
goog.addDependency('../../core/renderers/zelos/renderer.js', ['Blockly.zelos.Renderer'], ['Blockly.InsertionMarkerManager', 'Blockly.blockRendering', 'Blockly.blockRendering.Renderer', 'Blockly.connectionTypes', 'Blockly.utils.object', 'Blockly.zelos.ConstantProvider', 'Blockly.zelos.Drawer', 'Blockly.zelos.MarkerSvg', 'Blockly.zelos.PathObject', 'Blockly.zelos.RenderInfo'], {'lang': 'es6', 'module': 'goog'});
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/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.utils.Size'], {'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/exceptions.js', ['Blockly.serialization.exceptions'], [], {'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/serialization/priorities.js', ['Blockly.serialization.priorities'], [], {'module': 'goog'});
goog.addDependency('../../core/serialization/registry.js', ['Blockly.serialization.registry'], ['Blockly.registry'], {'lang': 'es6', 'module': 'goog'});
goog.addDependency('../../core/serialization/variables.js', ['Blockly.serialization.variables'], ['Blockly.Events', 'Blockly.serialization.priorities', 'Blockly.serialization.registry'], {'lang': 'es6', 'module': 'goog'});
goog.addDependency('../../core/serialization/workspaces.js', ['Blockly.serialization.workspaces'], ['Blockly.Events', 'Blockly.Workspace', 'Blockly.registry', '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'});

View File

@@ -116,6 +116,7 @@ goog.addDependency('../../core/interfaces/i_registrable.js', ['Blockly.IRegistra
goog.addDependency('../../core/interfaces/i_registrable_field.js', ['Blockly.IRegistrableField'], [], {'lang': 'es6', 'module': 'goog'});
goog.addDependency('../../core/interfaces/i_selectable.js', ['Blockly.ISelectable'], [], {'lang': 'es6', 'module': 'goog'});
goog.addDependency('../../core/interfaces/i_selectable_toolbox_item.js', ['Blockly.ISelectableToolboxItem'], [], {'lang': 'es6', 'module': 'goog'});
goog.addDependency('../../core/interfaces/i_serializer.js', ['Blockly.serialization.ISerializer'], [], {'lang': 'es6', 'module': 'goog'});
goog.addDependency('../../core/interfaces/i_styleable.js', ['Blockly.IStyleable'], [], {'lang': 'es6', 'module': 'goog'});
goog.addDependency('../../core/interfaces/i_toolbox.js', ['Blockly.IToolbox'], [], {'lang': 'es6', 'module': 'goog'});
goog.addDependency('../../core/interfaces/i_toolbox_item.js', ['Blockly.IToolboxItem'], [], {'lang': 'es6', 'module': 'goog'});
@@ -193,13 +194,15 @@ goog.addDependency('../../core/renderers/zelos/measurables/row_elements.js', ['B
goog.addDependency('../../core/renderers/zelos/measurables/top_row.js', ['Blockly.zelos.TopRow'], ['Blockly.blockRendering.TopRow', 'Blockly.utils.object'], {'lang': 'es6', 'module': 'goog'});
goog.addDependency('../../core/renderers/zelos/path_object.js', ['Blockly.zelos.PathObject'], ['Blockly.blockRendering.PathObject', 'Blockly.utils.Svg', 'Blockly.utils.dom', 'Blockly.utils.object'], {'lang': 'es6', 'module': 'goog'});
goog.addDependency('../../core/renderers/zelos/renderer.js', ['Blockly.zelos.Renderer'], ['Blockly.InsertionMarkerManager', 'Blockly.blockRendering', 'Blockly.blockRendering.Renderer', 'Blockly.connectionTypes', 'Blockly.utils.object', 'Blockly.zelos.ConstantProvider', 'Blockly.zelos.Drawer', 'Blockly.zelos.MarkerSvg', 'Blockly.zelos.PathObject', 'Blockly.zelos.RenderInfo'], {'lang': 'es6', 'module': 'goog'});
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/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.utils.Size'], {'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/exceptions.js', ['Blockly.serialization.exceptions'], [], {'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/serialization/priorities.js', ['Blockly.serialization.priorities'], [], {'module': 'goog'});
goog.addDependency('../../core/serialization/registry.js', ['Blockly.serialization.registry'], ['Blockly.registry'], {'lang': 'es6', 'module': 'goog'});
goog.addDependency('../../core/serialization/variables.js', ['Blockly.serialization.variables'], ['Blockly.Events', 'Blockly.serialization.priorities', 'Blockly.serialization.registry'], {'lang': 'es6', 'module': 'goog'});
goog.addDependency('../../core/serialization/workspaces.js', ['Blockly.serialization.workspaces'], ['Blockly.Events', 'Blockly.Workspace', 'Blockly.registry', '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'});

View File

@@ -94,188 +94,128 @@ suite('JSO Deserialization', function() {
});
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'
}
},
]
test('Just var', function() {
const state = {
'variables': [
{
'name': 'test',
'id': '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);
});
]
};
Blockly.serialization.workspaces.load(state, this.workspace);
assertEventFired(
this.eventsFireStub,
Blockly.Events.VarCreate,
{
'varName': 'test',
'varId': 'testId',
'varType': '',
'recordUndo': false
},
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 = {
'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('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 = {
'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('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,
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': [
{
'varName': 'test',
'varId': 'testId',
'varType': '',
'group': 'my group'
'type': 'variables_get',
'id': 'blockId',
'x': 42,
'y': 42,
'fields': {
'VAR': 'testId'
}
},
this.workspace.id);
});
]
}
};
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);
});
});
@@ -681,4 +621,60 @@ suite('JSO Deserialization', function() {
});
});
});
test('Priority', function() {
const blocksSerializer = Blockly.registry.getClass(
Blockly.registry.Type.SERIALIZER, 'blocks');
const variablesSerializer = Blockly.registry.getClass(
Blockly.registry.Type.SERIALIZER, 'variables');
Blockly.serialization.registry.unregister('blocks');
Blockly.serialization.registry.unregister('variables');
const calls = [];
const first = {
priority: 100,
save: () => null,
load: () => calls.push('first-load'),
clear: () => calls.push('first-clear'),
};
const second = {
priority: 0,
save: () => null,
load: () => calls.push('second-load'),
clear: () => calls.push('second-clear'),
};
const third = {
priority: -10,
save: () => null,
load: () => calls.push('third-load'),
clear: () => calls.push('third-clear'),
};
Blockly.serialization.registry.register('third', third);
Blockly.serialization.registry.register('first', first);
Blockly.serialization.registry.register('second', second);
Blockly.serialization.workspaces.load(
{'first': {}, 'third': {}, 'second': {}}, this.workspace);
Blockly.serialization.registry.unregister('first');
Blockly.serialization.registry.unregister('second');
Blockly.serialization.registry.unregister('third');
Blockly.serialization.registry.register('blocks', blocksSerializer);
Blockly.serialization.registry.register('variables', variablesSerializer);
chai.assert.deepEqual(
calls,
[
'third-clear',
'second-clear',
'first-clear',
'first-load',
'second-load',
'third-load'
]);
});
});

View File

@@ -699,20 +699,21 @@ suite('JSO Serialization', function() {
suite('Variables', function() {
test('Without type', function() {
const variable = this.workspace.createVariable('testVar', '', 'testId');
const jso = Blockly.serialization.variables.save(variable);
assertProperty(jso, 'name', 'testVar');
assertProperty(jso, 'id', 'testId');
assertNoProperty(jso, 'type');
this.workspace.createVariable('testVar', '', 'testId');
const jso = Blockly.serialization.workspaces.save(this.workspace);
const variable = jso['variables'][0];
assertProperty(variable, 'name', 'testVar');
assertProperty(variable, 'id', 'testId');
assertNoProperty(variable, 'type');
});
test('With type', function() {
const variable = this.workspace
.createVariable('testVar', 'testType', 'testId');
const jso = Blockly.serialization.variables.save(variable);
assertProperty(jso, 'name', 'testVar');
assertProperty(jso, 'id', 'testId');
assertProperty(jso, 'type', 'testType');
this.workspace.createVariable('testVar', 'testType', 'testId');
const jso = Blockly.serialization.workspaces.save(this.workspace);
const variable = jso['variables'][0];
assertProperty(variable, 'name', 'testVar');
assertProperty(variable, 'id', 'testId');
assertProperty(variable, 'type', 'testType');
});
});
});

View File

@@ -9,6 +9,7 @@ goog.module('Blockly.test.registry');
const {sharedTestSetup, sharedTestTeardown} = goog.require('Blockly.test.helpers');
suite('Registry', function() {
var TestClass = function() {};
TestClass.prototype.testMethod = function() {
@@ -18,39 +19,195 @@ suite('Registry', function() {
setup(function() {
sharedTestSetup.call(this);
});
teardown(function() {
sharedTestTeardown.call(this);
if (Blockly.registry.typeMap_['test'] &&
Blockly.registry.typeMap_['test']['test_name']) {
delete Blockly.registry.typeMap_['test']['test_name'];
if (Blockly.registry.hasItem('test', 'test_name')) {
Blockly.registry.unregister('test', 'test_name');
}
});
suite('Registration', function() {
test('Simple', function() {
Blockly.registry.register('test', 'test_name', TestClass);
});
test('Empty String Key', function() {
chai.assert.throws(function() {
Blockly.registry.register('test', '', TestClass);
}, 'Invalid name');
});
test('Class as Key', function() {
chai.assert.throws(function() {
Blockly.registry.register('test', TestClass, '');
}, 'Invalid name');
});
test('Overwrite a Key', function() {
Blockly.registry.register('test', 'test_name', TestClass);
chai.assert.throws(function() {
Blockly.registry.register('test', 'test_name', TestClass);
}, 'already registered');
});
test('Null Value', function() {
chai.assert.throws(function() {
Blockly.registry.register('test', 'field_custom_test', null);
}, 'Can not register a null value');
});
});
suite('hasItem', function() {
setup(function() {
Blockly.registry.register('test', 'test_name', TestClass);
});
test('Has', function() {
chai.assert.isTrue(Blockly.registry.hasItem('test', 'test_name'));
});
suite('Does not have', function() {
test('Type', function() {
chai.assert.isFalse(Blockly.registry.hasItem('bad_type', 'test_name'));
});
test('Name', function() {
chai.assert.isFalse(Blockly.registry.hasItem('test', 'bad_name'));
});
});
suite('Case', function() {
test('Caseless type', function() {
chai.assert.isTrue(Blockly.registry.hasItem('TEST', 'test_name'));
});
test('Caseless name', function() {
chai.assert.isTrue(Blockly.registry.hasItem('test', 'TEST_NAME'));
});
});
});
suite('getClass', function() {
setup(function() {
Blockly.registry.register('test', 'test_name', TestClass);
});
test('Has', function() {
chai.assert.isNotNull(Blockly.registry.getClass('test', 'test_name'));
});
suite('Does not have', function() {
test('Type', function() {
chai.assert.isNull(Blockly.registry.getClass('bad_type', 'test_name'));
});
test('Name', function() {
chai.assert.isNull(Blockly.registry.getClass('test', 'bad_name'));
});
test('Throw if missing', function() {
chai.assert.throws(function() {
Blockly.registry.getClass('test', 'bad_name', true);
});
});
});
suite('Case', function() {
test('Caseless type', function() {
chai.assert.isNotNull(Blockly.registry.getClass('TEST', 'test_name'));
});
test('Caseless name', function() {
chai.assert.isNotNull(Blockly.registry.getClass('test', 'TEST_NAME'));
});
});
});
suite('getObject', function() {
setup(function() {
Blockly.registry.register('test', 'test_name', {});
});
test('Has', function() {
chai.assert.isNotNull(Blockly.registry.getObject('test', 'test_name'));
});
suite('Does not have', function() {
test('Type', function() {
chai.assert.isNull(Blockly.registry.getObject('bad_type', 'test_name'));
});
test('Name', function() {
chai.assert.isNull(Blockly.registry.getObject('test', 'bad_name'));
});
test('Throw if missing', function() {
chai.assert.throws(function() {
Blockly.registry.getObject('test', 'bad_name', true);
});
});
});
suite('Case', function() {
test('Caseless type', function() {
chai.assert.isNotNull(Blockly.registry.getObject('TEST', 'test_name'));
});
test('Caseless name', function() {
chai.assert.isNotNull(Blockly.registry.getObject('test', 'TEST_NAME'));
});
});
});
suite('getAllItems', function() {
setup(function() {
Blockly.registry.register('test', 'test_name', {});
Blockly.registry.register('test', 'casedNAME', {});
});
teardown(function() {
Blockly.registry.unregister('test', 'casedname');
});
test('Has', function() {
chai.assert.isNotNull(Blockly.registry.getAllItems('test'));
});
test('Does not have', function() {
chai.assert.isNull(Blockly.registry.getAllItems('bad_type'));
});
test('Throw if missing', function() {
chai.assert.throws(function() {
Blockly.registry.getAllItems('bad_type', false, true);
});
});
test('Ignore type case', function() {
chai.assert.isNotNull(Blockly.registry.getAllItems('TEST'));
});
test('Respect name case', function() {
chai.assert.deepEqual(
Blockly.registry.getAllItems('test', true),
{
'test_name': {},
'casedNAME': {}
});
});
test('Respect overwriting name case', function() {
Blockly.registry.register('test', 'CASEDname', {}, true);
chai.assert.deepEqual(
Blockly.registry.getAllItems('test', true),
{
'test_name': {},
'CASEDname': {}
});
});
});
suite('getClassFromOptions', function() {
setup(function() {
this.defaultClass = function() {};
@@ -62,25 +219,31 @@ suite('Registry', function() {
'test' : 'test_name'
}
};
Blockly.registry.typeMap_['test'] = {
'test_name': TestClass,
'default': this.defaultClass
};
Blockly.registry.register('test', 'test_name', TestClass);
Blockly.registry.register('test', 'default', this.defaultClass);
});
teardown(function() {
Blockly.registry.unregister('test', 'default');
});
test('Simple - Plugin name given', function() {
var testClass = Blockly.registry.getClassFromOptions('test', this.options);
chai.assert.instanceOf(new testClass(), TestClass);
});
test('Simple - Plugin class given', function() {
this.options.plugins['test'] = TestClass;
var testClass = Blockly.registry.getClassFromOptions('test', this.options);
chai.assert.instanceOf(new testClass(), TestClass);
});
test('No Plugin Name Given', function() {
delete this.options['plugins']['test'];
var testClass = Blockly.registry.getClassFromOptions('test', this.options);
chai.assert.instanceOf(new testClass(), this.defaultClass);
});
test('Incorrect Plugin Name', function() {
this.options['plugins']['test'] = 'random';
var testClass;