diff --git a/core/flyout_base.js b/core/flyout_base.js index 84d6d3870f6..e9974f3b576 100644 --- a/core/flyout_base.js +++ b/core/flyout_base.js @@ -36,7 +36,6 @@ goog.require('Blockly.utils.Coordinate'); goog.require('Blockly.utils.dom'); goog.require('Blockly.utils.Svg'); goog.require('Blockly.utils.toolbox'); -goog.require('Blockly.utils.xml'); goog.require('Blockly.WorkspaceSvg'); goog.require('Blockly.Xml'); @@ -148,6 +147,13 @@ Blockly.Flyout = function(workspaceOptions) { * @package */ this.targetWorkspace = null; + + /** + * A list of blocks that can be reused. + * @type {!Array} + * @private + */ + this.recycledBlocks_ = []; }; Blockly.utils.object.inherits(Blockly.Flyout, Blockly.DeleteArea); @@ -555,6 +561,7 @@ Blockly.Flyout.prototype.show = function(flyoutDef) { this.reflowWrapper_ = this.reflow.bind(this); this.workspace_.addChangeListener(this.reflowWrapper_); + this.emptyRecycledBlocks_(); }; /** @@ -586,13 +593,9 @@ Blockly.Flyout.prototype.createFlyoutInfo_ = function(parsedContent) { switch (contentInfo['kind'].toUpperCase()) { case 'BLOCK': var blockInfo = /** @type {!Blockly.utils.toolbox.BlockInfo} */ (contentInfo); - var blockXml = this.getBlockXml_(blockInfo); - var block = this.createBlock_(blockXml); - // This is a deprecated method for adding gap to a block. - // - var gap = parseInt(blockInfo['gap'] || blockXml.getAttribute('gap'), 10); - gaps.push(isNaN(gap) ? defaultGap : gap); + var block = this.createFlyoutBlock_(blockInfo); contents.push({type: 'block', block: block}); + this.addBlockGap_(blockInfo, gaps, defaultGap); break; case 'SEP': var sepInfo = /** @type {!Blockly.utils.toolbox.SeparatorInfo} */ (contentInfo); @@ -619,23 +622,18 @@ Blockly.Flyout.prototype.createFlyoutInfo_ = function(parsedContent) { /** * Gets the flyout definition for the dynamic category. * @param {string} categoryName The name of the dynamic category. - * @return {!Array} The array of flyout items. + * @return {!Blockly.utils.toolbox.FlyoutDefinition} The definition of the + * flyout in one of its many forms. * @private */ Blockly.Flyout.prototype.getDynamicCategoryContents_ = function(categoryName) { - // Look up the correct category generation function and call that to get a - // valid XML list. - var fnToApply = this.workspace_.targetWorkspace.getToolboxCategoryCallback( - categoryName); + var fnToApply = + this.workspace_.targetWorkspace.getToolboxCategoryCallback(categoryName); if (typeof fnToApply != 'function') { throw TypeError('Couldn\'t find a callback function when opening' + ' a toolbox category.'); } - var flyoutDef = fnToApply(this.workspace_.targetWorkspace); - if (!Array.isArray(flyoutDef)) { - throw new TypeError('Result of toolbox category callback must be an array.'); - } - return flyoutDef; + return fnToApply(this.workspace_.targetWorkspace); }; /** @@ -660,49 +658,79 @@ Blockly.Flyout.prototype.createButton_ = function(btnInfo, isLabel) { /** * Create a block from the xml and permanently disable any blocks that were * defined as disabled. - * @param {!Element} blockXml The xml of the block. + * @param {!Blockly.utils.toolbox.BlockInfo} blockInfo The info of the block. * @return {!Blockly.BlockSvg} The block created from the blockXml. - * @protected + * @private */ -Blockly.Flyout.prototype.createBlock_ = function(blockXml) { - var curBlock = /** @type {!Blockly.BlockSvg} */ ( - Blockly.Xml.domToBlock(blockXml, this.workspace_)); - if (!curBlock.isEnabled()) { +Blockly.Flyout.prototype.createFlyoutBlock_ = function(blockInfo) { + var block; + if (blockInfo['blockxml']) { + var xml = typeof blockInfo['blockxml'] === 'string' ? + Blockly.Xml.textToDom(blockInfo['blockxml']) : + blockInfo['blockxml']; + block = this.getRecycledBlock_(xml.getAttribute('type')); + if (!block) { + block = Blockly.Xml.domToBlock(xml, this.workspace_); + } + } else { + block = this.getRecycledBlock_(blockInfo['type']); + if (!block) { + if (blockInfo['enabled'] === undefined) { + blockInfo['enabled'] = + blockInfo['disabled'] !== 'true' && blockInfo['disabled'] !== true; + } + block = Blockly.serialization.blocks.load( + /** @type {Blockly.serialization.blocks.State} */ (blockInfo), + this.workspace_); + } + } + + if (!block.isEnabled()) { // Record blocks that were initially disabled. // Do not enable these blocks as a result of capacity filtering. - this.permanentlyDisabled_.push(curBlock); + this.permanentlyDisabled_.push(block); } - return curBlock; + return /** @type {!Blockly.BlockSvg} */ (block); }; /** - * Get the xml from the block info object. - * @param {!Blockly.utils.toolbox.BlockInfo} blockInfo The object holding - * information about a block. - * @return {!Element} The xml for the block. - * @throws {Error} if the xml is not a valid block definition. + * Returns a block from the array of recycled blocks with the given type, or + * undefined if one cannot be found. + * @param {string} blockType The type of the block to try to recycle. + * @return {(!Blockly.BlockSvg|undefined)} The recycled block, or undefined if + * one could not be recycled. * @private */ -Blockly.Flyout.prototype.getBlockXml_ = function(blockInfo) { - var blockElement = null; - var blockXml = blockInfo['blockxml']; - - if (blockXml && typeof blockXml != 'string') { - blockElement = blockXml; - } else if (blockXml && typeof blockXml == 'string') { - blockElement = Blockly.Xml.textToDom(blockXml); - blockInfo['blockxml'] = blockElement; - } else if (blockInfo['type']) { - blockElement = Blockly.utils.xml.createElement('xml'); - blockElement.setAttribute('type', blockInfo['type']); - blockElement.setAttribute('disabled', blockInfo['disabled']); - blockInfo['blockxml'] = blockElement; +Blockly.Flyout.prototype.getRecycledBlock_ = function(blockType) { + var index = -1; + for (var i = 0; i < this.recycledBlocks_.length; i++) { + if (this.recycledBlocks_[i].type == blockType) { + index = i; + break; + } } + return index == -1 ? undefined : this.recycledBlocks_.splice(index, 1)[0]; +}; - if (!blockElement) { - throw Error('Error: Invalid block definition. Block definition must have blockxml or type.'); +/** + * Adds a gap in the flyout based on block info. + * @param {!Blockly.utils.toolbox.BlockInfo} blockInfo Information about a + * block. + * @param {!Array} gaps The list of gaps between items in the flyout. + * @param {number} defaultGap The default gap between one element and the next. + * @private + */ +Blockly.Flyout.prototype.addBlockGap_ = function(blockInfo, gaps, defaultGap) { + var gap; + if (blockInfo['gap']) { + gap = parseInt(blockInfo['gap'], 10); + } else if (blockInfo['blockxml']) { + var xml = typeof blockInfo['blockxml'] === 'string' ? + Blockly.Xml.textToDom(blockInfo['blockxml']) : + blockInfo['blockxml']; + gap = parseInt(xml.getAttribute('gap'), 10); } - return blockElement; + gaps.push(isNaN(gap) ? defaultGap : gap); }; /** @@ -729,13 +757,15 @@ Blockly.Flyout.prototype.addSeparatorGap_ = function(sepInfo, gaps, defaultGap) /** * Delete blocks, mats and buttons from a previous showing of the flyout. - * @protected + * @private */ Blockly.Flyout.prototype.clearOldBlocks_ = function() { // Delete any blocks from a previous showing. var oldBlocks = this.workspace_.getTopBlocks(false); for (var i = 0, block; (block = oldBlocks[i]); i++) { - if (block.workspace == this.workspace_) { + if (this.blockIsRecyclable_(block)) { + this.recycleBlock_(block); + } else { block.dispose(false, false); } } @@ -758,6 +788,41 @@ Blockly.Flyout.prototype.clearOldBlocks_ = function() { this.workspace_.getPotentialVariableMap().clear(); }; +/** + * Empties all of the recycled blocks, properly disposing of them. + * @private + */ +Blockly.Flyout.prototype.emptyRecycledBlocks_ = function() { + for (var i = 0; i < this.recycledBlocks_.length; i++) { + this.recycledBlocks_[i].dispose(); + } + this.recycledBlocks_ = []; +}; + +/** + * Returns whether the given block can be recycled or not. + * @param {!Blockly.BlockSvg} _block The block to check for recyclability. + * @return {boolean} True if the block can be recycled. False otherwise. + * @protected + */ +Blockly.Flyout.prototype.blockIsRecyclable_ = function(_block) { + // By default, recycling is disabled. + return false; +}; + +/** + * Puts a previously created block into the recycle bin and moves it to the + * top of the workspace. Used during large workspace swaps to limit the number + * of new DOM elements we need to create. + * @param {!Blockly.BlockSvg} block The block to recycle. + * @private + */ +Blockly.Flyout.prototype.recycleBlock_ = function(block) { + var xy = block.getRelativeToSurfaceXY(); + block.moveBy(-xy.x, -xy.y); + this.recycledBlocks_.push(block); +}; + /** * Add listeners to a block that has been added to the flyout. * @param {!SVGElement} root The root node of the SVG group the block is in. @@ -996,20 +1061,23 @@ Blockly.Flyout.prototype.placeNewBlock_ = function(oldBlock) { throw Error('oldBlock is not rendered.'); } - // Create the new block by cloning the block in the flyout (via XML). - // This cast assumes that the oldBlock can not be an insertion marker. - var xml = /** @type {!Element} */ (Blockly.Xml.blockToDom(oldBlock, true)); - // The target workspace would normally resize during domToBlock, which will - // lead to weird jumps. Save it for terminateDrag. - targetWorkspace.setResizesEnabled(false); - - // Using domToBlock instead of domToWorkspace means that the new block will be - // placed at position (0, 0) in main workspace units. - var block = /** @type {!Blockly.BlockSvg} */ - (Blockly.Xml.domToBlock(xml, targetWorkspace)); - var svgRootNew = block.getSvgRoot(); - if (!svgRootNew) { - throw Error('block is not rendered.'); + if (oldBlock.mutationToDom && !oldBlock.saveExtraState) { + // Create the new block by cloning the block in the flyout (via XML). + // This cast assumes that the oldBlock can not be an insertion marker. + var xml = /** @type {!Element} */ (Blockly.Xml.blockToDom(oldBlock, true)); + // The target workspace would normally resize during domToBlock, which will + // lead to weird jumps. Save it for terminateDrag. + targetWorkspace.setResizesEnabled(false); + // Using domToBlock instead of domToWorkspace means that the new block will be + // placed at position (0, 0) in main workspace units. + var block = /** @type {!Blockly.BlockSvg} */ + (Blockly.Xml.domToBlock(xml, targetWorkspace)); + } else { + var json = /** @type {!Blockly.serialization.blocks.State} */ + (Blockly.serialization.blocks.save(oldBlock)); + targetWorkspace.setResizesEnabled(false); + var block = /** @type {!Blockly.BlockSvg} */ + (Blockly.serialization.blocks.load(json, targetWorkspace)); } // The offset in pixels between the main workspace's origin and the upper left diff --git a/core/serialization/blocks.js b/core/serialization/blocks.js index 650de5ce17f..fd076638692 100644 --- a/core/serialization/blocks.js +++ b/core/serialization/blocks.js @@ -46,14 +46,17 @@ exports.ConnectionState = ConnectionState; * Represents the state of a given block. * @typedef {{ * type: string, - * id: string, + * id: (string|undefined), * x: (number|undefined), * y: (number|undefined), * collapsed: (boolean|undefined), - * disabled: (boolean|undefined), + * enabled: (boolean|undefined), + * editable: (boolean|undefined), + * deletable: (boolean|undefined), + * movable: (boolean|undefined), * inline: (boolean|undefined), * data: (string|undefined), - * extra-state: *, + * extra-state: (*|undefined), * icons: (!Object|undefined), * fields: (!Object|undefined), * inputs: (!Object|undefined), diff --git a/core/utils/toolbox.js b/core/utils/toolbox.js index e9a9d2b2a58..4f1c67d828c 100644 --- a/core/utils/toolbox.js +++ b/core/utils/toolbox.js @@ -26,12 +26,29 @@ goog.requireType('Blockly.ToolboxSeparator'); /** * The information needed to create a block in the toolbox. + * Note that disabled has a different type for backwards compatibility. * @typedef {{ * kind:string, * blockxml:(string|!Node|undefined), * type:(string|undefined), * gap:(string|number|undefined), - * disabled: (string|boolean|undefined) + * disabled: (string|boolean|undefined), + * enabled: (boolean|undefined), + * id: (string|undefined), + * x: (number|undefined), + * y: (number|undefined), + * collapsed: (boolean|undefined), + * editable: (boolean|undefined), + * deletable: (boolean|undefined), + * movable: (boolean|undefined), + * inline: (boolean|undefined), + * data: (string|undefined), + * extra-state: (*|undefined), + * icons: (!Object|undefined), + * fields: (!Object|undefined), + * inputs: (!Object|undefined), + * next: (!Blockly.serialization.blocks.ConnectionState|undefined) * }} */ Blockly.utils.toolbox.BlockInfo; diff --git a/core/workspace_svg.js b/core/workspace_svg.js index 21ee217ae77..6721722bbc6 100644 --- a/core/workspace_svg.js +++ b/core/workspace_svg.js @@ -167,7 +167,8 @@ Blockly.WorkspaceSvg = function( /** * Map from function names to callbacks, for deciding what to do when a custom * toolbox category is opened. - * @type {!Object>} + * @type {!Object} * @private */ this.toolboxCategoryCallbacks_ = Object.create(null); @@ -2498,8 +2499,9 @@ Blockly.WorkspaceSvg.prototype.removeButtonCallback = function(key) { * custom toolbox categories in this workspace. See the variable and procedure * categories as an example. * @param {string} key The name to use to look up this function. - * @param {function(!Blockly.Workspace):!Array} func The function to - * call when the given toolbox category is opened. + * @param {function(!Blockly.Workspace): + * !Blockly.utils.toolbox.FlyoutDefinition} func The function to call when + * the given toolbox category is opened. */ Blockly.WorkspaceSvg.prototype.registerToolboxCategoryCallback = function(key, func) { @@ -2513,9 +2515,9 @@ Blockly.WorkspaceSvg.prototype.registerToolboxCategoryCallback = function(key, * Get the callback function associated with a given key, for populating * custom toolbox categories in this workspace. * @param {string} key The name to use to look up the function. - * @return {?function(!Blockly.Workspace):!Array} The function - * corresponding to the given key for this workspace, or null if no function - * is registered. + * @return {?function(!Blockly.Workspace): + * !Blockly.utils.toolbox.FlyoutDefinition} The function corresponding to + * the given key for this workspace, or null if no function is registered. */ Blockly.WorkspaceSvg.prototype.getToolboxCategoryCallback = function(key) { return this.toolboxCategoryCallbacks_[key] || null; diff --git a/tests/deps.js b/tests/deps.js index 798a29444e7..2c961eb3c94 100644 --- a/tests/deps.js +++ b/tests/deps.js @@ -61,7 +61,7 @@ goog.addDependency('../../core/field_number.js', ['Blockly.FieldNumber'], ['Bloc goog.addDependency('../../core/field_registry.js', ['Blockly.fieldRegistry'], ['Blockly.registry']); goog.addDependency('../../core/field_textinput.js', ['Blockly.FieldTextInput'], ['Blockly.DropDownDiv', 'Blockly.Events', 'Blockly.Events.BlockChange', 'Blockly.Field', 'Blockly.Msg', 'Blockly.WidgetDiv', 'Blockly.browserEvents', 'Blockly.fieldRegistry', 'Blockly.utils', 'Blockly.utils.Coordinate', 'Blockly.utils.KeyCodes', 'Blockly.utils.aria', 'Blockly.utils.dom', 'Blockly.utils.object', 'Blockly.utils.userAgent']); goog.addDependency('../../core/field_variable.js', ['Blockly.FieldVariable'], ['Blockly.Events.BlockChange', 'Blockly.FieldDropdown', 'Blockly.Msg', 'Blockly.VariableModel', 'Blockly.Variables', 'Blockly.Xml', 'Blockly.constants', 'Blockly.fieldRegistry', 'Blockly.utils', 'Blockly.utils.Size', 'Blockly.utils.object']); -goog.addDependency('../../core/flyout_base.js', ['Blockly.Flyout'], ['Blockly.Block', 'Blockly.ComponentManager', 'Blockly.DeleteArea', 'Blockly.Events', 'Blockly.Events.BlockCreate', 'Blockly.Events.VarCreate', 'Blockly.FlyoutMetricsManager', 'Blockly.Gesture', 'Blockly.IFlyout', 'Blockly.ScrollbarPair', 'Blockly.Tooltip', 'Blockly.Touch', 'Blockly.WorkspaceSvg', 'Blockly.Xml', 'Blockly.blockRendering', 'Blockly.browserEvents', 'Blockly.utils', 'Blockly.utils.Coordinate', 'Blockly.utils.Svg', 'Blockly.utils.dom', 'Blockly.utils.toolbox', 'Blockly.utils.xml']); +goog.addDependency('../../core/flyout_base.js', ['Blockly.Flyout'], ['Blockly.Block', 'Blockly.ComponentManager', 'Blockly.DeleteArea', 'Blockly.Events', 'Blockly.Events.BlockCreate', 'Blockly.Events.VarCreate', 'Blockly.FlyoutMetricsManager', 'Blockly.Gesture', 'Blockly.IFlyout', 'Blockly.ScrollbarPair', 'Blockly.Tooltip', 'Blockly.Touch', 'Blockly.WorkspaceSvg', 'Blockly.Xml', 'Blockly.blockRendering', 'Blockly.browserEvents', 'Blockly.utils', 'Blockly.utils.Coordinate', 'Blockly.utils.Svg', 'Blockly.utils.dom', 'Blockly.utils.toolbox']); goog.addDependency('../../core/flyout_button.js', ['Blockly.FlyoutButton'], ['Blockly.Css', 'Blockly.browserEvents', 'Blockly.utils', 'Blockly.utils.Coordinate', 'Blockly.utils.Svg', 'Blockly.utils.dom', 'Blockly.utils.style'], {'lang': 'es5'}); goog.addDependency('../../core/flyout_horizontal.js', ['Blockly.HorizontalFlyout'], ['Blockly.Block', 'Blockly.DropDownDiv', 'Blockly.Flyout', 'Blockly.Scrollbar', 'Blockly.WidgetDiv', 'Blockly.constants', 'Blockly.registry', 'Blockly.utils', 'Blockly.utils.Rect', 'Blockly.utils.object', 'Blockly.utils.toolbox']); goog.addDependency('../../core/flyout_vertical.js', ['Blockly.VerticalFlyout'], ['Blockly.Block', 'Blockly.DropDownDiv', 'Blockly.Flyout', 'Blockly.Scrollbar', 'Blockly.WidgetDiv', 'Blockly.constants', 'Blockly.registry', 'Blockly.utils', 'Blockly.utils.Rect', 'Blockly.utils.object', 'Blockly.utils.toolbox']); @@ -93,6 +93,7 @@ goog.addDependency('../../core/interfaces/i_positionable.js', ['Blockly.IPositio goog.addDependency('../../core/interfaces/i_registrable.js', ['Blockly.IRegistrable'], []); goog.addDependency('../../core/interfaces/i_registrable_field.js', ['Blockly.IRegistrableField'], []); goog.addDependency('../../core/interfaces/i_selectable.js', ['Blockly.ISelectable'], []); +goog.addDependency('../../core/interfaces/i_serializer.js', ['Blockly.serialization.ISerializer'], [], {'lang': 'es6', 'module': 'goog'}); goog.addDependency('../../core/interfaces/i_styleable.js', ['Blockly.IStyleable'], []); goog.addDependency('../../core/interfaces/i_toolbox.js', ['Blockly.IToolbox'], []); goog.addDependency('../../core/interfaces/i_toolbox_item.js', ['Blockly.ICollapsibleToolboxItem', 'Blockly.ISelectableToolboxItem', 'Blockly.IToolboxItem'], []); @@ -106,7 +107,7 @@ goog.addDependency('../../core/menu.js', ['Blockly.Menu'], ['Blockly.browserEven goog.addDependency('../../core/menuitem.js', ['Blockly.MenuItem'], ['Blockly.utils.IdGenerator', 'Blockly.utils.aria', 'Blockly.utils.dom']); goog.addDependency('../../core/metrics_manager.js', ['Blockly.FlyoutMetricsManager', 'Blockly.MetricsManager'], ['Blockly.IMetricsManager', 'Blockly.registry', 'Blockly.utils.Size', 'Blockly.utils.toolbox'], {'lang': 'es5'}); goog.addDependency('../../core/msg.js', ['Blockly.Msg'], ['Blockly.utils.global']); -goog.addDependency('../../core/mutator.js', ['Blockly.Mutator'], ['Blockly.Bubble', 'Blockly.Events', 'Blockly.Events.BlockChange', 'Blockly.Events.BubbleOpen', 'Blockly.Icon', 'Blockly.Options', 'Blockly.WorkspaceSvg', 'Blockly.Xml', 'Blockly.utils', 'Blockly.utils.Svg', 'Blockly.utils.dom', 'Blockly.utils.object', 'Blockly.utils.toolbox', 'Blockly.utils.xml']); +goog.addDependency('../../core/mutator.js', ['Blockly.Mutator'], ['Blockly.Bubble', 'Blockly.Events', 'Blockly.Events.BlockChange', 'Blockly.Events.BubbleOpen', 'Blockly.Icon', 'Blockly.Options', 'Blockly.WorkspaceSvg', 'Blockly.utils', 'Blockly.utils.Svg', 'Blockly.utils.dom', 'Blockly.utils.object', 'Blockly.utils.toolbox', 'Blockly.utils.xml']); goog.addDependency('../../core/names.js', ['Blockly.Names'], ['Blockly.Msg', 'Blockly.constants']); goog.addDependency('../../core/options.js', ['Blockly.Options'], ['Blockly.Theme', 'Blockly.Themes.Classic', 'Blockly.registry', 'Blockly.utils.IdGenerator', 'Blockly.utils.Metrics', 'Blockly.utils.toolbox']); goog.addDependency('../../core/positionable_helpers.js', ['Blockly.uiPosition'], ['Blockly.Scrollbar', 'Blockly.utils.Rect', 'Blockly.utils.toolbox']); @@ -155,7 +156,6 @@ goog.addDependency('../../core/requires.js', ['Blockly.requires'], ['Blockly', ' goog.addDependency('../../core/scrollbar.js', ['Blockly.Scrollbar', 'Blockly.ScrollbarPair'], ['Blockly.Events', 'Blockly.Touch', 'Blockly.browserEvents', 'Blockly.utils', 'Blockly.utils.Coordinate', 'Blockly.utils.Metrics', 'Blockly.utils.Svg', 'Blockly.utils.dom']); 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/i_serializer.js', ['Blockly.serialization.ISerializer'], [], {'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'}); diff --git a/tests/mocha/.eslintrc.json b/tests/mocha/.eslintrc.json index 6ee7cbf44f3..232e3e736a1 100644 --- a/tests/mocha/.eslintrc.json +++ b/tests/mocha/.eslintrc.json @@ -41,8 +41,9 @@ "getDeeplyNestedJSON": true, "getInjectedToolbox": true, "getNonCollapsibleItem": true, + "getProperSimpleJson": true, "getSeparator": true, - "getSimpleJSON": true, + "getSimpleJson": true, "getXmlArray": true, "sharedTestSetup": true, "sharedTestTeardown": true, diff --git a/tests/mocha/flyout_test.js b/tests/mocha/flyout_test.js index 6ccd08d900b..8f9b70cbda5 100644 --- a/tests/mocha/flyout_test.js +++ b/tests/mocha/flyout_test.js @@ -236,74 +236,345 @@ suite('Flyout', function() { suite('createFlyoutInfo_', function() { setup(function() { - this.simpleToolboxJSON = getSimpleJSON(); this.flyout = this.workspace.getFlyout(); this.createFlyoutSpy = sinon.spy(this.flyout, 'createFlyoutInfo_'); - }); - function checkLayoutContents(actual, expected, opt_message) { - chai.assert.equal(actual.length, expected.length, opt_message); - for (var i = 0; i < actual.length; i++) { - chai.assert.equal(actual[i].type, expected[i].type, opt_message); - if (actual[i].type == 'BLOCK') { - chai.assert.typeOf(actual[i]['block'], 'Blockly.Block'); - } else if (actual[i].type == 'BUTTON' || actual[i].type == 'LABEL') { - chai.assert.typeOf(actual[i]['block'], 'Blockly.FlyoutButton'); - } - } - } - function checkFlyoutInfo(flyoutSpy) { - var expectedContents = [ - {type: "block"}, - {type: "button"}, - {type: "button"} - ]; - var expectedGaps = [20, 24, 24]; var flyoutInfo = flyoutSpy.returnValues[0]; var contents = flyoutInfo.contents; var gaps = flyoutInfo.gaps; + + var expectedGaps = [20, 24, 24]; chai.assert.deepEqual(gaps, expectedGaps); - checkLayoutContents(contents, expectedContents, 'Contents'); + + chai.assert.equal(contents.length, 3, 'Contents'); + + chai.assert.equal(contents[0].type, 'block', 'Contents'); + var block = contents[0]['block']; + chai.assert.instanceOf(block, Blockly.BlockSvg); + chai.assert.equal(block.getFieldValue('OP'), 'NEQ'); + var childA = block.getInputTargetBlock('A'); + var childB = block.getInputTargetBlock('B'); + chai.assert.isTrue(childA.isShadow()); + chai.assert.isFalse(childB.isShadow()); + chai.assert.equal(childA.getFieldValue('NUM'), 1); + chai.assert.equal(childB.getFieldValue('NUM'), 2); + + chai.assert.equal(contents[1].type, 'button', 'Contents'); + chai.assert.instanceOf(contents[1]['button'], Blockly.FlyoutButton); + + chai.assert.equal(contents[2].type, 'button', 'Contents'); + chai.assert.instanceOf(contents[2]['button'], Blockly.FlyoutButton); } - test('Node', function() { - this.flyout.show(this.toolboxXml); - checkFlyoutInfo(this.createFlyoutSpy); - }); - test('NodeList', function() { - var nodeList = document.getElementById('toolbox-simple').childNodes; - this.flyout.show(nodeList); - checkFlyoutInfo(this.createFlyoutSpy); + suite('Direct show', function() { + test('Node', function() { + this.flyout.show(this.toolboxXml); + checkFlyoutInfo(this.createFlyoutSpy); + }); + + test('NodeList', function() { + var nodeList = document.getElementById('toolbox-simple').childNodes; + this.flyout.show(nodeList); + checkFlyoutInfo(this.createFlyoutSpy); + }); + + test('Array of JSON', function() { + this.flyout.show(getSimpleJson()); + checkFlyoutInfo(this.createFlyoutSpy); + }); + + test('Array of Proper JSON', function() { + this.flyout.show(getProperSimpleJson()); + checkFlyoutInfo(this.createFlyoutSpy); + }); + + test('Array of XML', function() { + this.flyout.show(getXmlArray()); + checkFlyoutInfo(this.createFlyoutSpy); + }); }); - test('Array of JSON', function() { - this.flyout.show(this.simpleToolboxJSON); - checkFlyoutInfo(this.createFlyoutSpy); + + suite('Dynamic category', function() { + setup(function() { + this.stubAndAssert = function(val) { + sinon.stub( + this.flyout.workspace_.targetWorkspace, + 'getToolboxCategoryCallback') + .returns(function() { return val; }); + this.flyout.show('someString'); + checkFlyoutInfo(this.createFlyoutSpy); + }; + }); + + test('No category available', function() { + chai.assert.throws( + function() { + this.flyout.show('someString'); + }.bind(this), + 'Couldn\'t find a callback function when opening ' + + 'a toolbox category.'); + }); + + test('Node', function() { + this.stubAndAssert(this.toolboxXml); + }); + + test('NodeList', function() { + this.stubAndAssert( + document.getElementById('toolbox-simple').childNodes); + }); + + test('Array of JSON', function() { + this.stubAndAssert(getSimpleJson()); + }); + + test('Array of Proper JSON', function() { + this.stubAndAssert(getProperSimpleJson()); + }); + + test('Array of XML', function() { + this.stubAndAssert(getXmlArray()); + }); }); - test('Array of xml', function() { - this.flyout.show(getXmlArray()); - checkFlyoutInfo(this.createFlyoutSpy); + }); + + suite('Creating blocks', function() { + suite('Enabled/Disabled', function() { + setup(function() { + this.flyout = this.workspace.getFlyout(); + + this.assertDisabled = function(disabled) { + var block = this.flyout.getWorkspace().getTopBlocks(false)[0]; + chai.assert.equal(!block.isEnabled(), disabled); + }; + }); + + suite('XML', function() { + test('True string', function() { + var xml = Blockly.Xml.textToDom( + '' + + '' + + '' + ); + this.flyout.show(xml); + this.assertDisabled(true); + }); + + test('False string', function() { + var xml = Blockly.Xml.textToDom( + '' + + '' + + '' + ); + this.flyout.show(xml); + this.assertDisabled(false); + }); + + test('Disabled string', function() { + // The XML system supports this for some reason!? + var xml = Blockly.Xml.textToDom( + '' + + '' + + '' + ); + this.flyout.show(xml); + this.assertDisabled(true); + }); + + test('Different string', function() { + var xml = Blockly.Xml.textToDom( + '' + + '' + + '' + ); + this.flyout.show(xml); + this.assertDisabled(false); + }); + }); + + suite('JSON', function() { + test('All undefined', function() { + var json = [ + { + 'kind': 'block', + 'type': 'text_print', + } + ]; + this.flyout.show(json); + this.assertDisabled(false); + }); + + test('Enabled true', function() { + var json = [ + { + 'kind': 'block', + 'type': 'text_print', + 'enabled': true, + } + ]; + this.flyout.show(json); + this.assertDisabled(false); + }); + + test('Enabled false', function() { + var json = [ + { + 'kind': 'block', + 'type': 'text_print', + 'enabled': false, + } + ]; + this.flyout.show(json); + this.assertDisabled(true); + }); + + test('Disabled true string', function() { + var json = [ + { + 'kind': 'block', + 'type': 'text_print', + 'disabled': 'true' + } + ]; + this.flyout.show(json); + this.assertDisabled(true); + }); + + test('Disabled false string', function() { + var json = [ + { + 'kind': 'block', + 'type': 'text_print', + 'disabled': 'false' + } + ]; + this.flyout.show(json); + this.assertDisabled(false); + }); + + test('Disabled string', function() { + var json = [ + { + 'kind': 'block', + 'type': 'text_print', + 'disabled': 'disabled' // This is not respected by the JSON! + } + ]; + this.flyout.show(json); + this.assertDisabled(false); + }); + + test('Disabled true value', function() { + var json = [ + { + 'kind': 'block', + 'type': 'text_print', + 'disabled': true + } + ]; + this.flyout.show(json); + this.assertDisabled(true); + }); + + test('Disabled false value', function() { + var json = [ + { + 'kind': 'block', + 'type': 'text_print', + 'disabled': false + } + ]; + this.flyout.show(json); + this.assertDisabled(false); + }); + + test('Disabled different string', function() { + var json = [ + { + 'kind': 'block', + 'type': 'text_print', + 'disabled': 'random' + } + ]; + this.flyout.show(json); + this.assertDisabled(false); + }); + + test('Disabled empty string', function() { + var json = [ + { + 'kind': 'block', + 'type': 'text_print', + 'disabled': '' + } + ]; + this.flyout.show(json); + this.assertDisabled(false); + }); + }); }); - test('Custom Toolbox: No Category Available', function() { - chai.assert.throws(function() { - this.flyout.show('someString'); - }.bind(this), 'Couldn\'t find a callback function when opening' + - ' a toolbox category.'); + }); + + suite('Recycling', function() { + setup(function() { + this.flyout = this.workspace.getFlyout(); }); - test('Custom Toolbox: Function does not return array', function() { - sinon.stub(this.flyout.workspace_.targetWorkspace, - 'getToolboxCategoryCallback').returns(function(){return null;}); - chai.assert.throws(function() { - this.flyout.show('someString'); - }.bind(this), 'Result of toolbox category callback must be an array.'); + + test('Recycling disabled', function() { + this.flyout.show({ + 'contents': [ + { + 'kind': 'BLOCK', + 'type': 'math_number', + 'fields': { + 'NUM': 123 + } + } + ] + }); + this.flyout.show({ + 'contents': [ + { + 'kind': 'BLOCK', + 'type': 'math_number', + 'fields': { + 'NUM': 321 + } + } + ] + }); + const block = this.flyout.workspace_.getAllBlocks()[0]; + chai.assert.equal(block.getFieldValue('NUM'), 321); }); - test('Custom Toolbox: Returns Array', function() { - sinon.stub(this.flyout.workspace_.targetWorkspace, - 'getToolboxCategoryCallback').returns(function(){return getXmlArray();}); - chai.assert.doesNotThrow(function() { - this.flyout.show('someString'); - }.bind(this)); + + test('Recycling enabled', function() { + this.flyout.blockIsRecyclable_ = function() { return true; }; + this.flyout.show({ + 'contents': [ + { + 'kind': 'BLOCK', + 'type': 'math_number', + 'fields': { + 'NUM': 123 + } + } + ] + }); + this.flyout.show({ + 'contents': [ + { + 'kind': 'BLOCK', + 'type': 'math_number', + 'fields': { + 'NUM': 321 + } + } + ] + }); + const block = this.flyout.workspace_.getAllBlocks()[0]; + chai.assert.equal(block.getFieldValue('NUM'), 123); }); }); }); diff --git a/tests/mocha/index.html b/tests/mocha/index.html index d2ede9a2ab6..404ba1041b4 100644 --- a/tests/mocha/index.html +++ b/tests/mocha/index.html @@ -114,12 +114,26 @@
+ + +