mirror of
https://github.com/scratchfoundation/scratch-vm.git
synced 2025-08-28 22:30:40 -04:00
Merge pull request #2280 from kchadha/inline-images-in-extensions
Inline Images in Extensions
This commit is contained in:
commit
0a5673d5d3
6 changed files with 250 additions and 74 deletions
|
@ -119,8 +119,50 @@ class SomeBlocks {
|
|||
// ...
|
||||
}
|
||||
```
|
||||
### Block Arguments
|
||||
In addition to displaying text, blocks can have arguments in the form of slots to take other blocks getting plugged in, or dropdown menus to select an argument value from a list of possible values.
|
||||
|
||||
### Defining a Menu
|
||||
The possible types of block arguments are as follows:
|
||||
|
||||
- String - a string input, this is a type-able field which also accepts other reporter blocks to be plugged in
|
||||
- Number - an input similar to the string input, but the type-able values are constrained to numbers.
|
||||
- Angle - an input similar to the number input, but it has an additional UI to be able to pick an angle from a
|
||||
circular dial
|
||||
- Boolean - an input for a boolean (hexagonal shaped) reporter block. This field is not type-able.
|
||||
- Color - an input which displays a color swatch. This field has additional UI to pick a color by choosing values for the color's hue, saturation and brightness. Optionally, the defaultValue for the color picker can also be chosen if the extension developer wishes to display the same color every time the extension is added. If the defaultValue is left out, the default behavior of picking a random color when the extension is loaded will be used.
|
||||
- Matrix - an input which displays a 5 x 5 matrix of cells, where each cell can be filled in or clear.
|
||||
- Note - a numeric input which can select a musical note. This field has additional UI to select a note from a
|
||||
visual keyboard.
|
||||
- Image - an inline image displayed on a block. This is a special argument type in that it does not represent a value and does not accept other blocks to be plugged-in in place of this block field. See the section below about "Adding an Inline Image".
|
||||
|
||||
#### Adding an Inline Image
|
||||
In addition to specifying block arguments (an example of string arguments shown in the code snippet above),
|
||||
you can also specify an inline image for the block. You must include a dataURI for the image. If left unspecified, blank space will be allocated for the image and a warning will be logged in the console.
|
||||
You can optionally also specify `flipRTL`, a property indicating whether the image should be flipped horizontally when the editor has a right to left language selected as its locale. By default, the image is not flipped.
|
||||
|
||||
```js
|
||||
return {
|
||||
// ...
|
||||
blocks: [
|
||||
{
|
||||
//...
|
||||
arguments {
|
||||
MY_IMAGE: {
|
||||
type: ArgumentType.IMAGE,
|
||||
dataURI: 'myImageData',
|
||||
alt: 'This is an image',
|
||||
flipRTL: true
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
#### Defining a Menu
|
||||
|
||||
To display a drop-down menu for a block argument, specify the `menu` property of that argument and a matching item in
|
||||
the `menus` section of your extension's definition:
|
||||
|
@ -201,7 +243,7 @@ menus: {
|
|||
}
|
||||
```
|
||||
|
||||
#### Accepting reporters ("droppable" menus)
|
||||
##### Accepting reporters ("droppable" menus)
|
||||
|
||||
By default it is not possible to specify the value of a dropdown menu by inserting a reporter block. While we
|
||||
encourage extension authors to make their menus accept reporters when possible, doing so requires careful
|
||||
|
|
|
@ -1,4 +1,8 @@
|
|||
const BlockType = require('../extension-support/block-type');
|
||||
const ArgumentType = require('../extension-support/argument-type');
|
||||
|
||||
/* eslint-disable-next-line max-len */
|
||||
const blockIconURI = 'data:image/svg+xml,%3Csvg id="rotate-counter-clockwise" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"%3E%3Cdefs%3E%3Cstyle%3E.cls-1%7Bfill:%233d79cc;%7D.cls-2%7Bfill:%23fff;%7D%3C/style%3E%3C/defs%3E%3Ctitle%3Erotate-counter-clockwise%3C/title%3E%3Cpath class="cls-1" d="M22.68,12.2a1.6,1.6,0,0,1-1.27.63H13.72a1.59,1.59,0,0,1-1.16-2.58l1.12-1.41a4.82,4.82,0,0,0-3.14-.77,4.31,4.31,0,0,0-2,.8,4.25,4.25,0,0,0-1.34,1.73,5.06,5.06,0,0,0,.54,4.62A5.58,5.58,0,0,0,12,17.74h0a2.26,2.26,0,0,1-.16,4.52A10.25,10.25,0,0,1,3.74,18,10.14,10.14,0,0,1,2.25,8.78,9.7,9.7,0,0,1,5.08,4.64,9.92,9.92,0,0,1,9.66,2.5a10.66,10.66,0,0,1,7.72,1.68l1.08-1.35a1.57,1.57,0,0,1,1.24-.6,1.6,1.6,0,0,1,1.54,1.21l1.7,7.37A1.57,1.57,0,0,1,22.68,12.2Z"/%3E%3Cpath class="cls-2" d="M21.38,11.83H13.77a.59.59,0,0,1-.43-1l1.75-2.19a5.9,5.9,0,0,0-4.7-1.58,5.07,5.07,0,0,0-4.11,3.17A6,6,0,0,0,7,15.77a6.51,6.51,0,0,0,5,2.92,1.31,1.31,0,0,1-.08,2.62,9.3,9.3,0,0,1-7.35-3.82A9.16,9.16,0,0,1,3.17,9.12,8.51,8.51,0,0,1,5.71,5.4,8.76,8.76,0,0,1,9.82,3.48a9.71,9.71,0,0,1,7.75,2.07l1.67-2.1a.59.59,0,0,1,1,.21L22,11.08A.59.59,0,0,1,21.38,11.83Z"/%3E%3C/svg%3E';
|
||||
|
||||
/**
|
||||
* An example core block implemented using the extension spec.
|
||||
|
@ -31,6 +35,17 @@ class Scratch3CoreExample {
|
|||
opcode: 'exampleOpcode',
|
||||
blockType: BlockType.REPORTER,
|
||||
text: 'example block'
|
||||
},
|
||||
{
|
||||
opcode: 'exampleWithInlineImage',
|
||||
blockType: BlockType.COMMAND,
|
||||
text: 'block with image [CLOCKWISE] inline',
|
||||
arguments: {
|
||||
CLOCKWISE: {
|
||||
type: ArgumentType.IMAGE,
|
||||
dataURI: blockIconURI
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
@ -45,6 +60,10 @@ class Scratch3CoreExample {
|
|||
return stage ? stage.getName() : 'no stage yet';
|
||||
}
|
||||
|
||||
exampleWithInlineImage () {
|
||||
return;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = Scratch3CoreExample;
|
||||
|
|
|
@ -51,30 +51,55 @@ const defaultExtensionColors = ['#0FBD8C', '#0DA57A', '#0B8E69'];
|
|||
const ArgumentTypeMap = (() => {
|
||||
const map = {};
|
||||
map[ArgumentType.ANGLE] = {
|
||||
shadowType: 'math_angle',
|
||||
fieldType: 'NUM'
|
||||
shadow: {
|
||||
type: 'math_angle',
|
||||
// We specify fieldNames here so that we can pick
|
||||
// create and populate a field with the defaultValue
|
||||
// specified in the extension.
|
||||
// When the `fieldName` property is not specified,
|
||||
// the <field></field> will be left out of the XML and
|
||||
// the scratch-blocks defaults for that field will be
|
||||
// used instead (e.g. default of 0 for number fields)
|
||||
fieldName: 'NUM'
|
||||
}
|
||||
};
|
||||
map[ArgumentType.COLOR] = {
|
||||
shadowType: 'colour_picker'
|
||||
shadow: {
|
||||
type: 'colour_picker',
|
||||
fieldName: 'COLOUR'
|
||||
}
|
||||
};
|
||||
map[ArgumentType.NUMBER] = {
|
||||
shadowType: 'math_number',
|
||||
fieldType: 'NUM'
|
||||
shadow: {
|
||||
type: 'math_number',
|
||||
fieldName: 'NUM'
|
||||
}
|
||||
};
|
||||
map[ArgumentType.STRING] = {
|
||||
shadowType: 'text',
|
||||
fieldType: 'TEXT'
|
||||
shadow: {
|
||||
type: 'text',
|
||||
fieldName: 'TEXT'
|
||||
}
|
||||
};
|
||||
map[ArgumentType.BOOLEAN] = {
|
||||
check: 'Boolean'
|
||||
};
|
||||
map[ArgumentType.MATRIX] = {
|
||||
shadowType: 'matrix',
|
||||
fieldType: 'MATRIX'
|
||||
shadow: {
|
||||
type: 'matrix',
|
||||
fieldName: 'MATRIX'
|
||||
}
|
||||
};
|
||||
map[ArgumentType.NOTE] = {
|
||||
shadowType: 'note',
|
||||
fieldType: 'NOTE'
|
||||
shadow: {
|
||||
type: 'note',
|
||||
fieldName: 'NOTE'
|
||||
}
|
||||
};
|
||||
map[ArgumentType.IMAGE] = {
|
||||
// Inline images are weird because they're not actually "arguments".
|
||||
// They are more analagous to the label on a block.
|
||||
fieldType: 'field_image'
|
||||
};
|
||||
return map;
|
||||
})();
|
||||
|
@ -1152,7 +1177,7 @@ class Runtime extends EventEmitter {
|
|||
src: './static/blocks-media/repeat.svg', // TODO: use a constant or make this configurable?
|
||||
width: 24,
|
||||
height: 24,
|
||||
alt: '*',
|
||||
alt: '*', // TODO remove this since we don't use collapsed blocks in scratch
|
||||
flip_rtl: true
|
||||
}];
|
||||
++outLineNum;
|
||||
|
@ -1206,6 +1231,29 @@ class Runtime extends EventEmitter {
|
|||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper for _convertPlaceholdes which handles inline images which are a specialized case of block "arguments".
|
||||
* @param {object} argInfo Metadata about the inline image as specified by the extension
|
||||
* @return {object} JSON blob for a scratch-blocks image field.
|
||||
* @private
|
||||
*/
|
||||
_constructInlineImageJson (argInfo) {
|
||||
if (!argInfo.dataURI) {
|
||||
log.warn('Missing data URI in extension block with argument type IMAGE');
|
||||
}
|
||||
return {
|
||||
type: 'field_image',
|
||||
src: argInfo.dataURI || '',
|
||||
// TODO these probably shouldn't be hardcoded...?
|
||||
width: 24,
|
||||
height: 24,
|
||||
// Whether or not the inline image should be flipped horizontally
|
||||
// in RTL languages. Defaults to false, indicating that the
|
||||
// image will not be flipped.
|
||||
flip_rtl: argInfo.flipRTL || false
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper for _convertForScratchBlocks which handles linearization of argument placeholders. Called as a callback
|
||||
* from string#replace. In addition to the return value the JSON and XML items in the context will be filled.
|
||||
|
@ -1219,11 +1267,7 @@ class Runtime extends EventEmitter {
|
|||
// Sanitize the placeholder to ensure valid XML
|
||||
placeholder = placeholder.replace(/[<"&]/, '_');
|
||||
|
||||
const argJSON = {
|
||||
type: 'input_value',
|
||||
name: placeholder
|
||||
};
|
||||
|
||||
// Determine whether the argument type is one of the known standard field types
|
||||
const argInfo = context.blockInfo.arguments[placeholder] || {};
|
||||
let argTypeInfo = ArgumentTypeMap[argInfo.type] || {};
|
||||
|
||||
|
@ -1232,63 +1276,85 @@ class Runtime extends EventEmitter {
|
|||
argTypeInfo = context.categoryInfo.customFieldTypes[argInfo.type].argumentTypeInfo;
|
||||
}
|
||||
|
||||
const defaultValue =
|
||||
typeof argInfo.defaultValue === 'undefined' ? '' :
|
||||
xmlEscape(maybeFormatMessage(argInfo.defaultValue, this.makeMessageContextForTarget()).toString());
|
||||
// Start to construct the scratch-blocks style JSON defining how the block should be
|
||||
// laid out
|
||||
let argJSON;
|
||||
|
||||
if (argTypeInfo.check) {
|
||||
argJSON.check = argTypeInfo.check;
|
||||
}
|
||||
|
||||
let valueName;
|
||||
let shadowType;
|
||||
let fieldName;
|
||||
if (argInfo.menu) {
|
||||
const menuInfo = context.categoryInfo.menuInfo[argInfo.menu];
|
||||
if (menuInfo.acceptReporters) {
|
||||
valueName = placeholder;
|
||||
shadowType = this._makeExtensionMenuId(argInfo.menu, context.categoryInfo.id);
|
||||
fieldName = argInfo.menu;
|
||||
} else {
|
||||
argJSON.type = 'field_dropdown';
|
||||
argJSON.options = this._convertMenuItems(menuInfo.items);
|
||||
valueName = null;
|
||||
shadowType = null;
|
||||
fieldName = placeholder;
|
||||
}
|
||||
// Most field types are inputs (slots on the block that can have other blocks plugged into them)
|
||||
// check if this is not one of those cases. E.g. an inline image on a block.
|
||||
if (argTypeInfo.fieldType === 'field_image') {
|
||||
argJSON = this._constructInlineImageJson(argInfo);
|
||||
} else {
|
||||
valueName = placeholder;
|
||||
shadowType = argTypeInfo.shadowType;
|
||||
fieldName = argTypeInfo.fieldType;
|
||||
}
|
||||
// Construct input value
|
||||
|
||||
// <value> is the ScratchBlocks name for a block input.
|
||||
if (valueName) {
|
||||
context.inputList.push(`<value name="${placeholder}">`);
|
||||
}
|
||||
// Layout a block argument (e.g. an input slot on the block)
|
||||
argJSON = {
|
||||
type: 'input_value',
|
||||
name: placeholder
|
||||
};
|
||||
|
||||
// The <shadow> is a placeholder for a reporter and is visible when there's no reporter in this input.
|
||||
// Boolean inputs don't need to specify a shadow in the XML.
|
||||
if (shadowType) {
|
||||
context.inputList.push(`<shadow type="${shadowType}">`);
|
||||
}
|
||||
const defaultValue =
|
||||
typeof argInfo.defaultValue === 'undefined' ? '' :
|
||||
xmlEscape(maybeFormatMessage(argInfo.defaultValue, this.makeMessageContextForTarget()).toString());
|
||||
|
||||
// A <field> displays a dynamic value: a user-editable text field, a drop-down menu, etc.
|
||||
if (fieldName) {
|
||||
context.inputList.push(`<field name="${fieldName}">${defaultValue}</field>`);
|
||||
}
|
||||
if (argTypeInfo.check) {
|
||||
// Right now the only type of 'check' we have specifies that the
|
||||
// input slot on the block accepts Boolean reporters, so it should be
|
||||
// shaped like a hexagon
|
||||
argJSON.check = argTypeInfo.check;
|
||||
}
|
||||
|
||||
if (shadowType) {
|
||||
context.inputList.push('</shadow>');
|
||||
}
|
||||
let valueName;
|
||||
let shadowType;
|
||||
let fieldName;
|
||||
if (argInfo.menu) {
|
||||
const menuInfo = context.categoryInfo.menuInfo[argInfo.menu];
|
||||
if (menuInfo.acceptReporters) {
|
||||
valueName = placeholder;
|
||||
shadowType = this._makeExtensionMenuId(argInfo.menu, context.categoryInfo.id);
|
||||
fieldName = argInfo.menu;
|
||||
} else {
|
||||
argJSON.type = 'field_dropdown';
|
||||
argJSON.options = this._convertMenuItems(menuInfo.items);
|
||||
valueName = null;
|
||||
shadowType = null;
|
||||
fieldName = placeholder;
|
||||
}
|
||||
} else {
|
||||
valueName = placeholder;
|
||||
shadowType = (argTypeInfo.shadow && argTypeInfo.shadow.type) || null;
|
||||
fieldName = (argTypeInfo.shadow && argTypeInfo.shadow.fieldName) || null;
|
||||
}
|
||||
|
||||
if (valueName) {
|
||||
context.inputList.push('</value>');
|
||||
// <value> is the ScratchBlocks name for a block input.
|
||||
if (valueName) {
|
||||
context.inputList.push(`<value name="${placeholder}">`);
|
||||
}
|
||||
|
||||
// The <shadow> is a placeholder for a reporter and is visible when there's no reporter in this input.
|
||||
// Boolean inputs don't need to specify a shadow in the XML.
|
||||
if (shadowType) {
|
||||
context.inputList.push(`<shadow type="${shadowType}">`);
|
||||
}
|
||||
|
||||
// A <field> displays a dynamic value: a user-editable text field, a drop-down menu, etc.
|
||||
// Leave out the field if defaultValue or fieldName are not specified
|
||||
if (defaultValue && fieldName) {
|
||||
context.inputList.push(`<field name="${fieldName}">${defaultValue}</field>`);
|
||||
}
|
||||
|
||||
if (shadowType) {
|
||||
context.inputList.push('</shadow>');
|
||||
}
|
||||
|
||||
if (valueName) {
|
||||
context.inputList.push('</value>');
|
||||
}
|
||||
}
|
||||
|
||||
const argsName = `args${context.outLineNum}`;
|
||||
const blockArgs = (context.blockJSON[argsName] = context.blockJSON[argsName] || []);
|
||||
blockArgs.push(argJSON);
|
||||
if (argJSON) blockArgs.push(argJSON);
|
||||
const argNum = blockArgs.length;
|
||||
context.argsMap[placeholder] = argNum;
|
||||
|
||||
|
|
|
@ -36,7 +36,12 @@ const ArgumentType = {
|
|||
/**
|
||||
* MIDI note number with note picker (piano) field
|
||||
*/
|
||||
NOTE: 'note'
|
||||
NOTE: 'note',
|
||||
|
||||
/**
|
||||
* Inline image on block (as part of the label)
|
||||
*/
|
||||
IMAGE: 'image'
|
||||
};
|
||||
|
||||
module.exports = ArgumentType;
|
||||
|
|
|
@ -101,13 +101,16 @@ test('load sync', t => {
|
|||
t.equal(vm.runtime._blockInfo.length, 1);
|
||||
|
||||
// blocks should be an array of two items: a button pseudo-block and a reporter block.
|
||||
t.equal(vm.runtime._blockInfo[0].blocks.length, 2);
|
||||
t.equal(vm.runtime._blockInfo[0].blocks.length, 3);
|
||||
t.type(vm.runtime._blockInfo[0].blocks[0].info, 'object');
|
||||
t.type(vm.runtime._blockInfo[0].blocks[0].info.func, 'MAKE_A_VARIABLE');
|
||||
t.equal(vm.runtime._blockInfo[0].blocks[0].info.blockType, 'button');
|
||||
t.type(vm.runtime._blockInfo[0].blocks[1].info, 'object');
|
||||
t.equal(vm.runtime._blockInfo[0].blocks[1].info.opcode, 'exampleOpcode');
|
||||
t.equal(vm.runtime._blockInfo[0].blocks[1].info.blockType, 'reporter');
|
||||
t.type(vm.runtime._blockInfo[0].blocks[2].info, 'object');
|
||||
t.equal(vm.runtime._blockInfo[0].blocks[2].info.opcode, 'exampleWithInlineImage');
|
||||
t.equal(vm.runtime._blockInfo[0].blocks[2].info.blockType, 'command');
|
||||
|
||||
// Test the opcode function
|
||||
t.equal(vm.runtime._blockInfo[0].blocks[1].info.func(), 'no stage yet');
|
||||
|
|
|
@ -26,14 +26,29 @@ const testExtensionInfo = {
|
|||
text: 'simple text',
|
||||
blockIconURI: 'invalid icon URI' // trigger the 'scratch_extension' path
|
||||
},
|
||||
{
|
||||
opcode: 'inlineImage',
|
||||
blockType: BlockType.REPORTER,
|
||||
text: 'text and [IMAGE]',
|
||||
arguments: {
|
||||
IMAGE: {
|
||||
type: ArgumentType.IMAGE,
|
||||
dataURI: 'invalid image URI'
|
||||
}
|
||||
}
|
||||
},
|
||||
'---', // separator between groups of blocks in an extension
|
||||
{
|
||||
opcode: 'command',
|
||||
blockType: BlockType.COMMAND,
|
||||
text: 'text with [ARG]',
|
||||
text: 'text with [ARG] [ARG_WITH_DEFAULT]',
|
||||
arguments: {
|
||||
ARG: {
|
||||
type: ArgumentType.STRING
|
||||
},
|
||||
ARG_WITH_DEFAULT: {
|
||||
type: ArgumentType.STRING,
|
||||
defaultValue: 'default text'
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -108,6 +123,31 @@ const testReporter = function (t, reporter) {
|
|||
t.equal(reporter.xml, '<block type="test_reporter"></block>');
|
||||
};
|
||||
|
||||
const testInlineImage = function (t, inlineImage) {
|
||||
t.equal(inlineImage.json.type, 'test_inlineImage');
|
||||
testCategoryInfo(t, inlineImage);
|
||||
t.equal(inlineImage.json.checkboxInFlyout, true);
|
||||
t.equal(inlineImage.json.outputShape, ScratchBlocksConstants.OUTPUT_SHAPE_ROUND);
|
||||
t.equal(inlineImage.json.output, 'String');
|
||||
t.notOk(inlineImage.json.hasOwnProperty('previousStatement'));
|
||||
t.notOk(inlineImage.json.hasOwnProperty('nextStatement'));
|
||||
t.notOk(inlineImage.json.extensions && inlineImage.json.extensions.length); // OK if it's absent or empty
|
||||
t.equal(inlineImage.json.message0, 'text and %1'); // block text followed by inline image
|
||||
t.notOk(inlineImage.json.hasOwnProperty('message1'));
|
||||
t.same(inlineImage.json.args0, [
|
||||
// %1 in message0: the block icon
|
||||
{
|
||||
type: 'field_image',
|
||||
src: 'invalid image URI',
|
||||
width: 24,
|
||||
height: 24,
|
||||
flip_rtl: false // False by default
|
||||
}
|
||||
]);
|
||||
t.notOk(inlineImage.json.hasOwnProperty('args1'));
|
||||
t.equal(inlineImage.xml, '<block type="test_inlineImage"></block>');
|
||||
};
|
||||
|
||||
const testSeparator = function (t, separator) {
|
||||
t.same(separator.json, null); // should be null or undefined
|
||||
t.equal(separator.xml, '<sep gap="36"/>');
|
||||
|
@ -120,7 +160,7 @@ const testCommand = function (t, command) {
|
|||
t.assert(command.json.hasOwnProperty('previousStatement'));
|
||||
t.assert(command.json.hasOwnProperty('nextStatement'));
|
||||
t.notOk(command.json.extensions && command.json.extensions.length); // OK if it's absent or empty
|
||||
t.equal(command.json.message0, 'text with %1');
|
||||
t.equal(command.json.message0, 'text with %1 %2');
|
||||
t.notOk(command.json.hasOwnProperty('message1'));
|
||||
t.strictSame(command.json.args0[0], {
|
||||
type: 'input_value',
|
||||
|
@ -128,8 +168,9 @@ const testCommand = function (t, command) {
|
|||
});
|
||||
t.notOk(command.json.hasOwnProperty('args1'));
|
||||
t.equal(command.xml,
|
||||
'<block type="test_command"><value name="ARG"><shadow type="text"><field name="TEXT">' +
|
||||
'</field></shadow></value></block>');
|
||||
'<block type="test_command"><value name="ARG"><shadow type="text"></shadow></value>' +
|
||||
'<value name="ARG_WITH_DEFAULT"><shadow type="text"><field name="TEXT">' +
|
||||
'default text</field></shadow></value></block>');
|
||||
};
|
||||
|
||||
const testConditional = function (t, conditional) {
|
||||
|
@ -186,8 +227,7 @@ const testLoop = function (t, loop) {
|
|||
t.equal(loop.json.args2[0].flip_rtl, true);
|
||||
t.notOk(loop.json.hasOwnProperty('args3'));
|
||||
t.equal(loop.xml,
|
||||
'<block type="test_loop"><value name="MANY"><shadow type="math_number"><field name="NUM">' +
|
||||
'</field></shadow></value></block>');
|
||||
'<block type="test_loop"><value name="MANY"><shadow type="math_number"></shadow></value></block>');
|
||||
};
|
||||
|
||||
test('registerExtensionPrimitives', t => {
|
||||
|
@ -203,10 +243,11 @@ test('registerExtensionPrimitives', t => {
|
|||
});
|
||||
|
||||
// Note that this also implicitly tests that block order is preserved
|
||||
const [button, reporter, separator, command, conditional, loop] = blocksInfo;
|
||||
const [button, reporter, inlineImage, separator, command, conditional, loop] = blocksInfo;
|
||||
|
||||
testButton(t, button);
|
||||
testReporter(t, reporter);
|
||||
testInlineImage(t, inlineImage);
|
||||
testSeparator(t, separator);
|
||||
testCommand(t, command);
|
||||
testConditional(t, conditional);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue