mirror of
https://github.com/scratchfoundation/scratch-vm.git
synced 2025-06-10 12:31:21 -04:00
Add tests for converting extension metadata
These tests convert some extension metadata into scratch-blocks JSON and XML, then verify that the JSON and XML are as expected.
This commit is contained in:
parent
8ef4aec7fa
commit
53b5ba93f5
5 changed files with 206 additions and 35 deletions
src
engine
extension-support
util
test/unit
|
@ -5,10 +5,11 @@ const escapeHtml = require('escape-html');
|
|||
const ArgumentType = require('../extension-support/argument-type');
|
||||
const Blocks = require('./blocks');
|
||||
const BlockType = require('../extension-support/block-type');
|
||||
const Profiler = require('./profiler');
|
||||
const Sequencer = require('./sequencer');
|
||||
const ScratchBlocksConstants = require('./scratch-blocks-constants');
|
||||
const TargetType = require('../extension-support/target-type');
|
||||
const Thread = require('./thread');
|
||||
const Profiler = require('./profiler');
|
||||
const log = require('../util/log');
|
||||
const maybeFormatMessage = require('../util/maybe-format-message');
|
||||
|
||||
|
@ -68,32 +69,6 @@ const ConvertedSeparator = {
|
|||
xml: '<sep gap="36"/>'
|
||||
};
|
||||
|
||||
/**
|
||||
* These constants are copied from scratch-blocks/core/constants.js
|
||||
* @TODO find a way to require() these... maybe make a scratch-blocks/dist/constants.js or something like that?
|
||||
* @readonly
|
||||
* @enum {int}
|
||||
*/
|
||||
const ScratchBlocksConstants = {
|
||||
/**
|
||||
* ENUM for output shape: hexagonal (booleans/predicates).
|
||||
* @const
|
||||
*/
|
||||
OUTPUT_SHAPE_HEXAGONAL: 1,
|
||||
|
||||
/**
|
||||
* ENUM for output shape: rounded (numbers).
|
||||
* @const
|
||||
*/
|
||||
OUTPUT_SHAPE_ROUND: 2,
|
||||
|
||||
/**
|
||||
* ENUM for output shape: squared (any/all values; strings).
|
||||
* @const
|
||||
*/
|
||||
OUTPUT_SHAPE_SQUARE: 3
|
||||
};
|
||||
|
||||
/**
|
||||
* Numeric ID for Runtime._step in Profiler instances.
|
||||
* @type {number}
|
||||
|
|
27
src/engine/scratch-blocks-constants.js
Normal file
27
src/engine/scratch-blocks-constants.js
Normal file
|
@ -0,0 +1,27 @@
|
|||
/**
|
||||
* These constants are copied from scratch-blocks/core/constants.js
|
||||
* @TODO find a way to require() these straight from scratch-blocks... maybe make a scratch-blocks/dist/constants.js?
|
||||
* @readonly
|
||||
* @enum {int}
|
||||
*/
|
||||
const ScratchBlocksConstants = {
|
||||
/**
|
||||
* ENUM for output shape: hexagonal (booleans/predicates).
|
||||
* @const
|
||||
*/
|
||||
OUTPUT_SHAPE_HEXAGONAL: 1,
|
||||
|
||||
/**
|
||||
* ENUM for output shape: rounded (numbers).
|
||||
* @const
|
||||
*/
|
||||
OUTPUT_SHAPE_ROUND: 2,
|
||||
|
||||
/**
|
||||
* ENUM for output shape: squared (any/all values; strings).
|
||||
* @const
|
||||
*/
|
||||
OUTPUT_SHAPE_SQUARE: 3
|
||||
};
|
||||
|
||||
module.exports = ScratchBlocksConstants;
|
|
@ -2,12 +2,12 @@
|
|||
* @typedef {object} ExtensionMetadata
|
||||
* All the metadata needed to register an extension.
|
||||
* @property {string} id - a unique alphanumeric identifier for this extension. No special characters allowed.
|
||||
* @property {string} name - the human-readable name of this extension.
|
||||
* @property {string} blockIconURI - URI for an image to be placed on each block in this extension. Data URI ok.
|
||||
* @property {string} menuIconURI - URI for an image to be placed on this extension's category menu entry. Data URI ok.
|
||||
* @property {string} docsURI - link to documentation content for this extension.
|
||||
* @property {string} [name] - the human-readable name of this extension.
|
||||
* @property {string} [blockIconURI] - URI for an image to be placed on each block in this extension. Data URI ok.
|
||||
* @property {string} [menuIconURI] - URI for an image to be placed on this extension's category menu item. Data URI ok.
|
||||
* @property {string} [docsURI] - link to documentation content for this extension.
|
||||
* @property {Array.<ExtensionBlockMetadata|string>} blocks - the blocks provided by this extension, plus separators.
|
||||
* @property {Object.<ExtensionMenuMetadata>} menus - map of menu name to metadata about each of this extension's menus.
|
||||
* @property {Object.<ExtensionMenuMetadata>} [menus] - map of menu name to metadata for each of this extension's menus.
|
||||
*/
|
||||
|
||||
/**
|
||||
|
@ -30,8 +30,8 @@
|
|||
* @typedef {object} ExtensionArgumentMetadata
|
||||
* All the metadata needed to register an argument for an extension block.
|
||||
* @property {ArgumentType} type - the type of the argument (number, string, etc.)
|
||||
* @property {*} defaultValue - the default value of this argument.
|
||||
* @property {string} menu - the name of the menu to use for this argument, if any.
|
||||
* @property {*} [defaultValue] - the default value of this argument.
|
||||
* @property {string} [menu] - the name of the menu to use for this argument, if any.
|
||||
*/
|
||||
|
||||
/**
|
||||
|
|
|
@ -9,7 +9,7 @@ const formatMessage = require('format-message');
|
|||
* @return {string|*} - the formatted message OR the original `maybeMessage` input.
|
||||
*/
|
||||
const maybeFormatMessage = function (maybeMessage, args, locale) {
|
||||
if (maybeMessage.id && maybeMessage.default) {
|
||||
if (maybeMessage && maybeMessage.id && maybeMessage.default) {
|
||||
return formatMessage(maybeMessage, args, locale);
|
||||
}
|
||||
return maybeMessage;
|
||||
|
|
169
test/unit/extension_conversion.js
Normal file
169
test/unit/extension_conversion.js
Normal file
|
@ -0,0 +1,169 @@
|
|||
const test = require('tap').test;
|
||||
|
||||
const ArgumentType = require('../../src/extension-support/argument-type');
|
||||
const BlockType = require('../../src/extension-support/block-type');
|
||||
const Runtime = require('../../src/engine/runtime');
|
||||
const ScratchBlocksConstants = require('../../src/engine/scratch-blocks-constants');
|
||||
|
||||
/**
|
||||
* @type {ExtensionMetadata}
|
||||
*/
|
||||
const testExtensionInfo = {
|
||||
id: 'test',
|
||||
name: 'fake test extension',
|
||||
blocks: [
|
||||
{
|
||||
opcode: 'reporter',
|
||||
blockType: BlockType.REPORTER,
|
||||
text: 'simple text'
|
||||
},
|
||||
'---', // separator between groups of blocks in an extension
|
||||
{
|
||||
opcode: 'command',
|
||||
blockType: BlockType.COMMAND,
|
||||
text: 'text with [ARG]',
|
||||
arguments: {
|
||||
ARG: {
|
||||
type: ArgumentType.STRING
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
opcode: 'ifElse',
|
||||
blockType: BlockType.CONDITIONAL,
|
||||
branchCount: 2,
|
||||
text: [
|
||||
'test if [THING] is spiffy and if so then',
|
||||
'or elsewise'
|
||||
],
|
||||
arguments: {
|
||||
THING: {
|
||||
type: ArgumentType.BOOLEAN
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
opcode: 'loop',
|
||||
blockType: BlockType.LOOP, // implied branchCount of 1 unless otherwise stated
|
||||
isTerminal: true,
|
||||
text: [
|
||||
'loopty [MANY] loops'
|
||||
],
|
||||
arguments: {
|
||||
MANY: {
|
||||
type: ArgumentType.NUMBER
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
const testReporter = function (t, reporter) {
|
||||
t.equal(reporter.json.type, 'test.reporter');
|
||||
t.equal(reporter.json.outputShape, ScratchBlocksConstants.OUTPUT_SHAPE_ROUND);
|
||||
t.equal(reporter.json.output, 'String');
|
||||
t.notOk(reporter.json.hasOwnProperty('previousStatement'));
|
||||
t.notOk(reporter.json.hasOwnProperty('nextStatement'));
|
||||
t.equal(reporter.json.message0, 'simple text');
|
||||
t.notOk(reporter.json.hasOwnProperty('message1'));
|
||||
t.notOk(reporter.json.hasOwnProperty('args0'));
|
||||
t.notOk(reporter.json.hasOwnProperty('args1'));
|
||||
t.equal(reporter.xml, '<block type="test.reporter"></block>');
|
||||
};
|
||||
|
||||
const testSeparator = function (t, separator) {
|
||||
t.equal(separator.json, null);
|
||||
t.equal(separator.xml, '<sep gap="36"/>');
|
||||
};
|
||||
|
||||
const testCommand = function (t, command) {
|
||||
t.equal(command.json.type, 'test.command');
|
||||
t.equal(command.json.outputShape, ScratchBlocksConstants.OUTPUT_SHAPE_SQUARE);
|
||||
t.assert(command.json.hasOwnProperty('previousStatement'));
|
||||
t.assert(command.json.hasOwnProperty('nextStatement'));
|
||||
t.equal(command.json.message0, 'text with %1');
|
||||
t.notOk(command.json.hasOwnProperty('message1'));
|
||||
t.strictSame(command.json.args0[0], {
|
||||
type: 'input_value',
|
||||
name: 'ARG'
|
||||
});
|
||||
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>');
|
||||
};
|
||||
|
||||
const testConditional = function (t, conditional) {
|
||||
t.equal(conditional.json.type, 'test.ifElse');
|
||||
t.equal(conditional.json.outputShape, ScratchBlocksConstants.OUTPUT_SHAPE_SQUARE);
|
||||
t.ok(conditional.json.hasOwnProperty('previousStatement'));
|
||||
t.ok(conditional.json.hasOwnProperty('nextStatement'));
|
||||
t.equal(conditional.json.message0, 'test if %1 is spiffy and if so then');
|
||||
t.equal(conditional.json.message1, '%1'); // placeholder for substack #1
|
||||
t.equal(conditional.json.message2, 'or elsewise');
|
||||
t.equal(conditional.json.message3, '%1'); // placeholder for substack #2
|
||||
t.notOk(conditional.json.hasOwnProperty('message4'));
|
||||
t.strictSame(conditional.json.args0[0], {
|
||||
type: 'input_value',
|
||||
name: 'THING',
|
||||
check: 'Boolean'
|
||||
});
|
||||
t.strictSame(conditional.json.args1[0], {
|
||||
type: 'input_statement',
|
||||
name: 'SUBSTACK'
|
||||
});
|
||||
t.notOk(conditional.json.hasOwnProperty(conditional.json.args2));
|
||||
t.strictSame(conditional.json.args3[0], {
|
||||
type: 'input_statement',
|
||||
name: 'SUBSTACK2'
|
||||
});
|
||||
t.notOk(conditional.json.hasOwnProperty('args4'));
|
||||
t.equal(conditional.xml, '<block type="test.ifElse"><value name="THING"></value></block>');
|
||||
};
|
||||
|
||||
const testLoop = function (t, loop) {
|
||||
t.equal(loop.json.type, 'test.loop');
|
||||
t.equal(loop.json.outputShape, ScratchBlocksConstants.OUTPUT_SHAPE_SQUARE);
|
||||
t.ok(loop.json.hasOwnProperty('previousStatement'));
|
||||
t.notOk(loop.json.hasOwnProperty('nextStatement')); // isTerminal is set on this block
|
||||
t.equal(loop.json.message0, 'loopty %1 loops');
|
||||
t.equal(loop.json.message1, '%1'); // placeholder for substack
|
||||
t.equal(loop.json.message2, '%1'); // placeholder for loop arrow
|
||||
t.notOk(loop.json.hasOwnProperty('message3'));
|
||||
t.strictSame(loop.json.args0[0], {
|
||||
type: 'input_value',
|
||||
name: 'MANY'
|
||||
});
|
||||
t.strictSame(loop.json.args1[0], {
|
||||
type: 'input_statement',
|
||||
name: 'SUBSTACK'
|
||||
});
|
||||
t.equal(loop.json.lastDummyAlign2, 'RIGHT'); // move loop arrow to right side
|
||||
t.equal(loop.json.args2[0].type, 'field_image');
|
||||
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>');
|
||||
};
|
||||
|
||||
test('registerExtensionPrimitives', t => {
|
||||
const runtime = new Runtime();
|
||||
|
||||
runtime.on(Runtime.EXTENSION_ADDED, blocksInfo => {
|
||||
t.equal(blocksInfo.length, testExtensionInfo.blocks.length);
|
||||
|
||||
// Note that this also implicitly tests that block order is preserved
|
||||
const [reporter, separator, command, conditional, loop] = blocksInfo;
|
||||
|
||||
testReporter(t, reporter);
|
||||
testSeparator(t, separator);
|
||||
testCommand(t, command);
|
||||
testConditional(t, conditional);
|
||||
testLoop(t, loop);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
runtime._registerExtensionPrimitives(testExtensionInfo);
|
||||
});
|
Loading…
Add table
Add a link
Reference in a new issue