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:
Christopher Willis-Ford 2018-04-09 15:34:46 -07:00
parent 8ef4aec7fa
commit 53b5ba93f5
5 changed files with 206 additions and 35 deletions

View file

@ -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}

View 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;

View file

@ -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.
*/
/**

View file

@ -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;

View 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);
});