mirror of
https://github.com/scratchfoundation/scratch-vm.git
synced 2025-06-03 09:04:40 -04:00
Merge pull request #57 from tmickel/feature/full-tree
Update for VM block management
This commit is contained in:
commit
2be27cbc7d
10 changed files with 1049 additions and 372 deletions
50
README.md
50
README.md
|
@ -52,35 +52,33 @@ The Virtual Machine constructs and maintains the state of an [Abstract Syntax Tr
|
||||||
#### Anatomy of a Block
|
#### Anatomy of a Block
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"id": "^1r~63Gdl7;Dh?I*OP3_",
|
"7AJZR#NA;m*b}R]pdq63": {
|
||||||
"opcode": "wedo_motorclockwise",
|
"id": "7AJZR#NA;m*b}R]pdq63",
|
||||||
"next": null,
|
"opcode": "control_wait",
|
||||||
"fields": {
|
"inputs": {
|
||||||
"DURATION": {
|
"DURATION": {
|
||||||
"name": "DURATION",
|
"name": "DURATION",
|
||||||
"value": null,
|
"block": ",xA8/S!Z6+kR,9dph.rO"
|
||||||
"blocks": {
|
|
||||||
"1?P=eV(OiDY3vMk!24Ip": {
|
|
||||||
"id": "1?P=eV(OiDY3vMk!24Ip",
|
|
||||||
"opcode": "math_number",
|
|
||||||
"next": null,
|
|
||||||
"fields": {
|
|
||||||
"NUM": {
|
|
||||||
"name": "NUM",
|
|
||||||
"value": "10",
|
|
||||||
"blocks": null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"SUBSTACK": {
|
|
||||||
"name": "SUBSTACK",
|
|
||||||
"value": "@1ln(HsUO4!]*2*%BrE|",
|
|
||||||
"blocks": null
|
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"fields": {},
|
||||||
|
"next": null,
|
||||||
|
"topLevel": true
|
||||||
|
},
|
||||||
|
",xA8/S!Z6+kR,9dph.rO": {
|
||||||
|
"id": ",xA8/S!Z6+kR,9dph.rO",
|
||||||
|
"opcode": "math_number",
|
||||||
|
"inputs": {},
|
||||||
|
"fields": {
|
||||||
|
"NUM": {
|
||||||
|
"name": "NUM",
|
||||||
|
"value": "1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"next": null,
|
||||||
|
"topLevel": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## Testing
|
## Testing
|
||||||
|
|
|
@ -9,79 +9,123 @@ var parseDOM = memoize(html.parseDOM, {
|
||||||
/**
|
/**
|
||||||
* Adapter between block creation events and block representation which can be
|
* Adapter between block creation events and block representation which can be
|
||||||
* used by the Scratch runtime.
|
* used by the Scratch runtime.
|
||||||
*
|
* @param {Object} e `Blockly.events.create`
|
||||||
* @param {Object} `Blockly.events.create`
|
* @return {Array.<Object>} List of blocks from this CREATE event.
|
||||||
*
|
|
||||||
* @return {Object}
|
|
||||||
*/
|
*/
|
||||||
module.exports = function (e) {
|
module.exports = function (e) {
|
||||||
// Validate input
|
// Validate input
|
||||||
if (typeof e !== 'object') return;
|
if (typeof e !== 'object') return;
|
||||||
if (typeof e.blockId !== 'string') return;
|
|
||||||
if (typeof e.xml !== 'object') return;
|
if (typeof e.xml !== 'object') return;
|
||||||
|
|
||||||
// Storage object
|
return domToBlocks(parseDOM(e.xml.outerHTML));
|
||||||
var obj = {
|
|
||||||
id: e.blockId,
|
|
||||||
opcode: null,
|
|
||||||
next: null,
|
|
||||||
fields: {}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Set opcode
|
|
||||||
if (typeof e.xml.attributes === 'object') {
|
|
||||||
obj.opcode = e.xml.attributes.type.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extract fields from event's `innerHTML`
|
|
||||||
if (typeof e.xml.innerHTML !== 'string') return obj;
|
|
||||||
if (e.xml.innerHTML === '') return obj;
|
|
||||||
obj.fields = extract(parseDOM(e.xml.innerHTML));
|
|
||||||
|
|
||||||
return obj;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Extracts fields from a block's innerHTML.
|
* Convert outer blocks DOM from a Blockly CREATE event
|
||||||
* @todo Extend this to support vertical grammar / nested blocks.
|
* to a usable form for the Scratch runtime.
|
||||||
*
|
* This structure is based on Blockly xml.js:`domToWorkspace` and `domToBlock`.
|
||||||
* @param {Object} DOM representation of block's innerHTML
|
* @param {Element} blocksDOM DOM tree for this event.
|
||||||
*
|
* @return {Array.<Object>} Usable list of blocks from this CREATE event.
|
||||||
* @return {Object}
|
|
||||||
*/
|
*/
|
||||||
function extract (dom) {
|
function domToBlocks (blocksDOM) {
|
||||||
// Storage object
|
// At this level, there could be multiple blocks adjacent in the DOM tree.
|
||||||
var fields = {};
|
var blocks = {};
|
||||||
|
for (var i = 0; i < blocksDOM.length; i++) {
|
||||||
// Field
|
var block = blocksDOM[i];
|
||||||
var field = dom[0];
|
if (!block.name || !block.attribs) {
|
||||||
var fieldName = field.attribs.name;
|
continue;
|
||||||
fields[fieldName] = {
|
}
|
||||||
name: fieldName,
|
var tagName = block.name.toLowerCase();
|
||||||
value: null,
|
if (tagName == 'block' || tagName == 'shadow') {
|
||||||
blocks: {}
|
domToBlock(block, blocks, true);
|
||||||
};
|
}
|
||||||
|
}
|
||||||
// Shadow block
|
// Flatten blocks object into a list.
|
||||||
var shadow = field.children[0];
|
var blocksList = [];
|
||||||
var shadowId = shadow.attribs.id;
|
for (var b in blocks) {
|
||||||
var shadowOpcode = shadow.attribs.type;
|
blocksList.push(blocks[b]);
|
||||||
fields[fieldName].blocks[shadowId] = {
|
}
|
||||||
id: shadowId,
|
return blocksList;
|
||||||
opcode: shadowOpcode,
|
}
|
||||||
next: null,
|
|
||||||
fields: {}
|
/**
|
||||||
};
|
* Convert and an individual block DOM to the representation tree.
|
||||||
|
* Based on Blockly's `domToBlockHeadless_`.
|
||||||
// Primitive
|
* @param {Element} blockDOM DOM tree for an individual block.
|
||||||
var primitive = shadow.children[0];
|
* @param {Boolean} isTopBlock Whether blocks at this level are "top blocks."
|
||||||
var primitiveName = primitive.attribs.name;
|
* @param {Object} blocks Collection of blocks to add to.
|
||||||
var primitiveValue = primitive.children[0].data;
|
*/
|
||||||
fields[fieldName].blocks[shadowId].fields[primitiveName] = {
|
function domToBlock (blockDOM, blocks, isTopBlock) {
|
||||||
name: primitiveName,
|
// Block skeleton.
|
||||||
value: primitiveValue,
|
var block = {
|
||||||
blocks: null
|
id: blockDOM.attribs.id, // Block ID
|
||||||
};
|
opcode: blockDOM.attribs.type, // For execution, "event_whengreenflag".
|
||||||
|
inputs: {}, // Inputs to this block and the blocks they point to.
|
||||||
return fields;
|
fields: {}, // Fields on this block and their values.
|
||||||
|
next: null, // Next block in the stack, if one exists.
|
||||||
|
topLevel: isTopBlock // If this block starts a stack.
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add the block to the representation tree.
|
||||||
|
blocks[block.id] = block;
|
||||||
|
|
||||||
|
// Process XML children and find enclosed blocks, fields, etc.
|
||||||
|
for (var i = 0; i < blockDOM.children.length; i++) {
|
||||||
|
var xmlChild = blockDOM.children[i];
|
||||||
|
// Enclosed blocks and shadows
|
||||||
|
var childBlockNode = null;
|
||||||
|
var childShadowNode = null;
|
||||||
|
for (var j = 0; j < xmlChild.children.length; j++) {
|
||||||
|
var grandChildNode = xmlChild.children[j];
|
||||||
|
if (!grandChildNode.name) {
|
||||||
|
// Non-XML tag node.
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
var grandChildNodeName = grandChildNode.name.toLowerCase();
|
||||||
|
if (grandChildNodeName == 'block') {
|
||||||
|
childBlockNode = grandChildNode;
|
||||||
|
} else if (grandChildNodeName == 'shadow') {
|
||||||
|
childShadowNode = grandChildNode;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use shadow block only if there's no real block node.
|
||||||
|
if (!childBlockNode && childShadowNode) {
|
||||||
|
childBlockNode = childShadowNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Not all Blockly-type blocks are handled here,
|
||||||
|
// as we won't be using all of them for Scratch.
|
||||||
|
switch (xmlChild.name.toLowerCase()) {
|
||||||
|
case 'field':
|
||||||
|
// Add the field to this block.
|
||||||
|
var fieldName = xmlChild.attribs.name;
|
||||||
|
block.fields[fieldName] = {
|
||||||
|
name: fieldName,
|
||||||
|
value: xmlChild.children[0].data
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
case 'value':
|
||||||
|
case 'statement':
|
||||||
|
// Recursively generate block structure for input block.
|
||||||
|
domToBlock(childBlockNode, blocks, false);
|
||||||
|
// Link this block's input to the child block.
|
||||||
|
var inputName = xmlChild.attribs.name;
|
||||||
|
block.inputs[inputName] = {
|
||||||
|
name: inputName,
|
||||||
|
block: childBlockNode.attribs.id
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
case 'next':
|
||||||
|
if (!childBlockNode || !childBlockNode.attribs) {
|
||||||
|
// Invalid child block.
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// Recursively generate block structure for next block.
|
||||||
|
domToBlock(childBlockNode, blocks, false);
|
||||||
|
// Link next block to this block.
|
||||||
|
block.next = childBlockNode.attribs.id;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
207
src/engine/blocks.js
Normal file
207
src/engine/blocks.js
Normal file
|
@ -0,0 +1,207 @@
|
||||||
|
/**
|
||||||
|
* @fileoverview
|
||||||
|
* Store and mutate the VM block representation,
|
||||||
|
* and handle updates from Scratch Blocks events.
|
||||||
|
*/
|
||||||
|
|
||||||
|
function Blocks () {
|
||||||
|
/**
|
||||||
|
* All blocks in the workspace.
|
||||||
|
* Keys are block IDs, values are metadata about the block.
|
||||||
|
* @type {Object.<string, Object>}
|
||||||
|
*/
|
||||||
|
this._blocks = {};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* All stacks in the workspace.
|
||||||
|
* A list of block IDs that represent stacks (first block in stack).
|
||||||
|
* @type {Array.<String>}
|
||||||
|
*/
|
||||||
|
this._stacks = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provide an object with metadata for the requested block ID.
|
||||||
|
* @param {!string} blockId ID of block we have stored.
|
||||||
|
* @return {?Object} Metadata about the block, if it exists.
|
||||||
|
*/
|
||||||
|
Blocks.prototype.getBlock = function (blockId) {
|
||||||
|
return this._blocks[blockId];
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all known top-level blocks that start stacks.
|
||||||
|
* @return {Array.<string>} List of block IDs.
|
||||||
|
*/
|
||||||
|
Blocks.prototype.getStacks = function () {
|
||||||
|
return this._stacks;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the next block for a particular block
|
||||||
|
* @param {?string} id ID of block to get the next block for
|
||||||
|
* @return {?string} ID of next block in the sequence
|
||||||
|
*/
|
||||||
|
Blocks.prototype.getNextBlock = function (id) {
|
||||||
|
if (typeof this._blocks[id] === 'undefined') return null;
|
||||||
|
return this._blocks[id].next;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the substack for a particular C-shaped block
|
||||||
|
* @param {?string} id ID for block to get the substack for
|
||||||
|
* @param {?number} substackNum Which substack to select (e.g. for if-else)
|
||||||
|
* @return {?string} ID of block in the substack
|
||||||
|
*/
|
||||||
|
Blocks.prototype.getSubstack = function (id, substackNum) {
|
||||||
|
var block = this._blocks[id];
|
||||||
|
if (typeof block === 'undefined') return null;
|
||||||
|
if (!substackNum) substackNum = 1;
|
||||||
|
|
||||||
|
var inputName = 'SUBSTACK';
|
||||||
|
if (substackNum > 1) {
|
||||||
|
inputName += substackNum;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Empty C-block?
|
||||||
|
if (!(inputName in block.inputs)) return null;
|
||||||
|
return block.inputs[inputName].block;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the opcode for a particular block
|
||||||
|
* @param {?string} id ID of block to query
|
||||||
|
* @return {?string} the opcode corresponding to that block
|
||||||
|
*/
|
||||||
|
Blocks.prototype.getOpcode = function (id) {
|
||||||
|
if (typeof this._blocks[id] === 'undefined') return null;
|
||||||
|
return this._blocks[id].opcode;
|
||||||
|
};
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Block management: create blocks and stacks from a `create` event
|
||||||
|
* @param {!Object} block Blockly create event to be processed
|
||||||
|
*/
|
||||||
|
Blocks.prototype.createBlock = function (block, opt_isFlyoutBlock) {
|
||||||
|
// Create new block
|
||||||
|
this._blocks[block.id] = block;
|
||||||
|
|
||||||
|
// Push block id to stacks array.
|
||||||
|
// Blocks are added as a top-level stack if they are marked as a top-block
|
||||||
|
// (if they were top-level XML in the event) and if they are not
|
||||||
|
// flyout blocks.
|
||||||
|
if (!opt_isFlyoutBlock && block.topLevel) {
|
||||||
|
this._addStack(block.id);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Block management: change block field values
|
||||||
|
* @param {!Object} args Blockly change event to be processed
|
||||||
|
*/
|
||||||
|
Blocks.prototype.changeBlock = function (args) {
|
||||||
|
// Validate
|
||||||
|
if (args.element !== 'field') return;
|
||||||
|
if (typeof this._blocks[args.id] === 'undefined') return;
|
||||||
|
if (typeof this._blocks[args.id].fields[args.name] === 'undefined') return;
|
||||||
|
|
||||||
|
// Update block value
|
||||||
|
this._blocks[args.id].fields[args.name].value = args.value;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Block management: move blocks from parent to parent
|
||||||
|
* @param {!Object} e Blockly move event to be processed
|
||||||
|
*/
|
||||||
|
Blocks.prototype.moveBlock = function (e) {
|
||||||
|
// Remove from any old parent.
|
||||||
|
if (e.oldParent !== undefined) {
|
||||||
|
var oldParent = this._blocks[e.oldParent];
|
||||||
|
if (e.oldInput !== undefined &&
|
||||||
|
oldParent.inputs[e.oldInput].block === e.id) {
|
||||||
|
// This block was connected to the old parent's input.
|
||||||
|
oldParent.inputs[e.oldInput].block = null;
|
||||||
|
} else if (oldParent.next === e.id) {
|
||||||
|
// This block was connected to the old parent's next connection.
|
||||||
|
oldParent.next = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Has the block become a top-level block?
|
||||||
|
if (e.newParent === undefined) {
|
||||||
|
this._addStack(e.id);
|
||||||
|
} else {
|
||||||
|
// Remove stack, if one exists.
|
||||||
|
this._deleteStack(e.id);
|
||||||
|
// Otherwise, try to connect it in its new place.
|
||||||
|
if (e.newInput !== undefined) {
|
||||||
|
// Moved to the new parent's input.
|
||||||
|
this._blocks[e.newParent].inputs[e.newInput] = {
|
||||||
|
name: e.newInput,
|
||||||
|
block: e.id
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
// Moved to the new parent's next connection.
|
||||||
|
this._blocks[e.newParent].next = e.id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Block management: delete blocks and their associated stacks
|
||||||
|
* @param {!Object} e Blockly delete event to be processed
|
||||||
|
*/
|
||||||
|
Blocks.prototype.deleteBlock = function (e) {
|
||||||
|
// @todo In runtime, stop threads running on this stack
|
||||||
|
|
||||||
|
// Get block
|
||||||
|
var block = this._blocks[e.id];
|
||||||
|
|
||||||
|
// Delete children
|
||||||
|
if (block.next !== null) {
|
||||||
|
this.deleteBlock({id: block.next});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete inputs (including substacks)
|
||||||
|
for (var input in block.inputs) {
|
||||||
|
// If it's null, the block in this input moved away.
|
||||||
|
if (block.inputs[input].block !== null) {
|
||||||
|
this.deleteBlock({id: block.inputs[input].block});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete stack
|
||||||
|
this._deleteStack(e.id);
|
||||||
|
|
||||||
|
// Delete block
|
||||||
|
delete this._blocks[e.id];
|
||||||
|
};
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper to add a stack to `this._stacks`
|
||||||
|
* @param {?string} id ID of block that starts the stack
|
||||||
|
*/
|
||||||
|
Blocks.prototype._addStack = function (id) {
|
||||||
|
var i = this._stacks.indexOf(id);
|
||||||
|
if (i > -1) return; // Already in stacks.
|
||||||
|
this._stacks.push(id);
|
||||||
|
// Update `topLevel` property on the top block.
|
||||||
|
this._blocks[id].topLevel = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper to remove a stack from `this._stacks`
|
||||||
|
* @param {?string} id ID of block that starts the stack
|
||||||
|
*/
|
||||||
|
Blocks.prototype._deleteStack = function (id) {
|
||||||
|
var i = this._stacks.indexOf(id);
|
||||||
|
if (i > -1) this._stacks.splice(i, 1);
|
||||||
|
// Update `topLevel` property on the top block.
|
||||||
|
if (this._blocks[id]) this._blocks[id].topLevel = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = Blocks;
|
|
@ -10,25 +10,18 @@ var defaultBlockPackages = {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Manages blocks, stacks, and the sequencer.
|
* Manages blocks, stacks, and the sequencer.
|
||||||
|
* @param blocks Blocks instance for this runtime.
|
||||||
*/
|
*/
|
||||||
function Runtime () {
|
function Runtime (blocks) {
|
||||||
// Bind event emitter
|
// Bind event emitter
|
||||||
EventEmitter.call(this);
|
EventEmitter.call(this);
|
||||||
|
|
||||||
// State for the runtime
|
// State for the runtime
|
||||||
/**
|
|
||||||
* All blocks in the workspace.
|
|
||||||
* Keys are block IDs, values are metadata about the block.
|
|
||||||
* @type {Object.<string, Object>}
|
|
||||||
*/
|
|
||||||
this.blocks = {};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* All stacks in the workspace.
|
* Block management and storage
|
||||||
* A list of block IDs that represent stacks (first block in stack).
|
|
||||||
* @type {Array.<String>}
|
|
||||||
*/
|
*/
|
||||||
this.stacks = [];
|
this.blocks = blocks;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A list of threads that are currently running in the VM.
|
* A list of threads that are currently running in the VM.
|
||||||
|
@ -83,118 +76,6 @@ util.inherits(Runtime, EventEmitter);
|
||||||
*/
|
*/
|
||||||
Runtime.THREAD_STEP_INTERVAL = 1000 / 30;
|
Runtime.THREAD_STEP_INTERVAL = 1000 / 30;
|
||||||
|
|
||||||
/**
|
|
||||||
* Block management: create blocks and stacks from a `create` event
|
|
||||||
* @param {!Object} block Blockly create event to be processed
|
|
||||||
*/
|
|
||||||
Runtime.prototype.createBlock = function (block, opt_isFlyoutBlock) {
|
|
||||||
// Create new block
|
|
||||||
this.blocks[block.id] = block;
|
|
||||||
|
|
||||||
// Walk each field and add any shadow blocks
|
|
||||||
// @todo Expand this to cover vertical / nested blocks
|
|
||||||
for (var i in block.fields) {
|
|
||||||
var shadows = block.fields[i].blocks;
|
|
||||||
for (var y in shadows) {
|
|
||||||
var shadow = shadows[y];
|
|
||||||
this.blocks[shadow.id] = shadow;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Push block id to stacks array. New blocks are always a stack even if only
|
|
||||||
// momentary. If the new block is added to an existing stack this stack will
|
|
||||||
// be removed by the `moveBlock` method below.
|
|
||||||
if (!opt_isFlyoutBlock) {
|
|
||||||
this.stacks.push(block.id);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Block management: change block field values
|
|
||||||
* @param {!Object} args Blockly change event to be processed
|
|
||||||
*/
|
|
||||||
Runtime.prototype.changeBlock = function (args) {
|
|
||||||
// Validate
|
|
||||||
if (args.element !== 'field') return;
|
|
||||||
if (typeof this.blocks[args.id] === 'undefined') return;
|
|
||||||
if (typeof this.blocks[args.id].fields[args.name] === 'undefined') return;
|
|
||||||
|
|
||||||
// Update block value
|
|
||||||
this.blocks[args.id].fields[args.name].value = args.value;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Block management: move blocks from parent to parent
|
|
||||||
* @param {!Object} e Blockly move event to be processed
|
|
||||||
*/
|
|
||||||
Runtime.prototype.moveBlock = function (e) {
|
|
||||||
var _this = this;
|
|
||||||
|
|
||||||
// Block was removed from parent
|
|
||||||
if (e.newParent === undefined && e.oldParent !== undefined) {
|
|
||||||
// Add stack
|
|
||||||
_this.stacks.push(e.id);
|
|
||||||
|
|
||||||
// Update old parent
|
|
||||||
if (e.oldField === undefined) {
|
|
||||||
_this.blocks[e.oldParent].next = null;
|
|
||||||
} else {
|
|
||||||
delete _this.blocks[e.oldParent].fields[e.oldField];
|
|
||||||
}
|
|
||||||
} else if (e.newParent !== undefined) {
|
|
||||||
// Block was moved to a new parent
|
|
||||||
// Either happens because it was previously parentless
|
|
||||||
// (e.oldParent === undefined)
|
|
||||||
// or because a block was moved in front of it.
|
|
||||||
|
|
||||||
// Remove stack
|
|
||||||
_this._deleteStack(e.id);
|
|
||||||
|
|
||||||
// Update new parent
|
|
||||||
if (e.newField === undefined) {
|
|
||||||
_this.blocks[e.newParent].next = e.id;
|
|
||||||
} else {
|
|
||||||
_this.blocks[e.newParent].fields[e.newField] = {
|
|
||||||
name: e.newField,
|
|
||||||
value: e.id,
|
|
||||||
blocks: {}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Block management: delete blocks and their associated stacks
|
|
||||||
* @param {!Object} e Blockly delete event to be processed
|
|
||||||
*/
|
|
||||||
Runtime.prototype.deleteBlock = function (e) {
|
|
||||||
// @todo Stop threads running on this stack
|
|
||||||
|
|
||||||
// Get block
|
|
||||||
var block = this.blocks[e.id];
|
|
||||||
|
|
||||||
// Delete children
|
|
||||||
if (block.next !== null) {
|
|
||||||
this.deleteBlock({id: block.next});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete substacks and fields
|
|
||||||
for (var field in block.fields) {
|
|
||||||
if (field === 'SUBSTACK') {
|
|
||||||
this.deleteBlock({id: block.fields[field].value});
|
|
||||||
} else {
|
|
||||||
for (var shadow in block.fields[field].blocks) {
|
|
||||||
this.deleteBlock({id: shadow});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete stack
|
|
||||||
this._deleteStack(e.id);
|
|
||||||
|
|
||||||
// Delete block
|
|
||||||
delete this.blocks[e.id];
|
|
||||||
};
|
|
||||||
|
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
|
@ -280,10 +161,11 @@ Runtime.prototype.greenFlag = function () {
|
||||||
this._removeThread(this.threads[i]);
|
this._removeThread(this.threads[i]);
|
||||||
}
|
}
|
||||||
// Add all top stacks with green flag
|
// Add all top stacks with green flag
|
||||||
for (var j = 0; j < this.stacks.length; j++) {
|
var stacks = this.blocks.getStacks();
|
||||||
var topBlock = this.stacks[j];
|
for (var j = 0; j < stacks.length; j++) {
|
||||||
if (this.blocks[topBlock].opcode === 'event_whenflagclicked') {
|
var topBlock = stacks[j];
|
||||||
this._pushThread(this.stacks[j]);
|
if (this.blocks.getBlock(topBlock).opcode === 'event_whenflagclicked') {
|
||||||
|
this._pushThread(stacks[j]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -293,9 +175,11 @@ Runtime.prototype.greenFlag = function () {
|
||||||
*/
|
*/
|
||||||
Runtime.prototype.startDistanceSensors = function () {
|
Runtime.prototype.startDistanceSensors = function () {
|
||||||
// Add all top stacks with distance sensor
|
// Add all top stacks with distance sensor
|
||||||
for (var j = 0; j < this.stacks.length; j++) {
|
var stacks = this.blocks.getStacks();
|
||||||
var topBlock = this.stacks[j];
|
for (var j = 0; j < stacks.length; j++) {
|
||||||
if (this.blocks[topBlock].opcode === 'wedo_whendistanceclose') {
|
var topBlock = stacks[j];
|
||||||
|
if (this.blocks.getBlock(topBlock).opcode ===
|
||||||
|
'wedo_whendistanceclose') {
|
||||||
var alreadyRunning = false;
|
var alreadyRunning = false;
|
||||||
for (var k = 0; k < this.threads.length; k++) {
|
for (var k = 0; k < this.threads.length; k++) {
|
||||||
if (this.threads[k].topBlock === topBlock) {
|
if (this.threads[k].topBlock === topBlock) {
|
||||||
|
@ -303,7 +187,7 @@ Runtime.prototype.startDistanceSensors = function () {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!alreadyRunning) {
|
if (!alreadyRunning) {
|
||||||
this._pushThread(this.stacks[j]);
|
this._pushThread(stacks[j]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -357,46 +241,4 @@ Runtime.prototype.start = function () {
|
||||||
}.bind(this), Runtime.THREAD_STEP_INTERVAL);
|
}.bind(this), Runtime.THREAD_STEP_INTERVAL);
|
||||||
};
|
};
|
||||||
|
|
||||||
// -----------------------------------------------------------------------------
|
|
||||||
// -----------------------------------------------------------------------------
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Helper to remove a stack from `this.stacks`
|
|
||||||
* @param {?string} id ID of block that starts the stack
|
|
||||||
*/
|
|
||||||
Runtime.prototype._deleteStack = function (id) {
|
|
||||||
var i = this.stacks.indexOf(id);
|
|
||||||
if (i > -1) this.stacks.splice(i, 1);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Helper to get the next block for a particular block
|
|
||||||
* @param {?string} id ID of block to get the next block for
|
|
||||||
* @return {?string} ID of next block in the sequence
|
|
||||||
*/
|
|
||||||
Runtime.prototype._getNextBlock = function (id) {
|
|
||||||
if (typeof this.blocks[id] === 'undefined') return null;
|
|
||||||
return this.blocks[id].next;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Helper to get the substack for a particular C-shaped block
|
|
||||||
* @param {?string} id ID for block to get the substack for
|
|
||||||
* @return {?string} ID of block in the substack
|
|
||||||
*/
|
|
||||||
Runtime.prototype._getSubstack = function (id) {
|
|
||||||
if (typeof this.blocks[id] === 'undefined') return null;
|
|
||||||
return this.blocks[id].fields['SUBSTACK'];
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Helper to get the opcode for a particular block
|
|
||||||
* @param {?string} id ID of block to query
|
|
||||||
* @return {?string} the opcode corresponding to that block
|
|
||||||
*/
|
|
||||||
Runtime.prototype._getOpcode = function (id) {
|
|
||||||
if (typeof this.blocks[id] === 'undefined') return null;
|
|
||||||
return this.blocks[id].opcode;
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = Runtime;
|
module.exports = Runtime;
|
||||||
|
|
|
@ -97,13 +97,13 @@ Sequencer.prototype.stepThread = function (thread) {
|
||||||
// If the primitive would like to do control flow,
|
// If the primitive would like to do control flow,
|
||||||
// it can overwrite nextBlock.
|
// it can overwrite nextBlock.
|
||||||
var currentBlock = thread.nextBlock;
|
var currentBlock = thread.nextBlock;
|
||||||
if (!currentBlock || !this.runtime.blocks[currentBlock]) {
|
if (!currentBlock || !this.runtime.blocks.getBlock(currentBlock)) {
|
||||||
thread.status = Thread.STATUS_DONE;
|
thread.status = Thread.STATUS_DONE;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
thread.nextBlock = this.runtime._getNextBlock(currentBlock);
|
thread.nextBlock = this.runtime.blocks.getNextBlock(currentBlock);
|
||||||
|
|
||||||
var opcode = this.runtime._getOpcode(currentBlock);
|
var opcode = this.runtime.blocks.getOpcode(currentBlock);
|
||||||
|
|
||||||
// Push the current block to the stack
|
// Push the current block to the stack
|
||||||
thread.stack.push(currentBlock);
|
thread.stack.push(currentBlock);
|
||||||
|
@ -130,7 +130,7 @@ Sequencer.prototype.stepThread = function (thread) {
|
||||||
var threadDoneCallback = function () {
|
var threadDoneCallback = function () {
|
||||||
thread.status = Thread.STATUS_DONE;
|
thread.status = Thread.STATUS_DONE;
|
||||||
// Refresh nextBlock in case it has changed during a yield.
|
// Refresh nextBlock in case it has changed during a yield.
|
||||||
thread.nextBlock = instance.runtime._getNextBlock(currentBlock);
|
thread.nextBlock = instance.runtime.blocks.getNextBlock(currentBlock);
|
||||||
// Pop the stack and stack frame
|
// Pop the stack and stack frame
|
||||||
thread.stack.pop();
|
thread.stack.pop();
|
||||||
thread.stackFrames.pop();
|
thread.stackFrames.pop();
|
||||||
|
@ -141,9 +141,10 @@ Sequencer.prototype.stepThread = function (thread) {
|
||||||
* @todo very hacked...
|
* @todo very hacked...
|
||||||
*/
|
*/
|
||||||
var startHats = function(callback) {
|
var startHats = function(callback) {
|
||||||
for (var i = 0; i < instance.runtime.stacks.length; i++) {
|
var stacks = instance.runtime.blocks.getStacks();
|
||||||
var stack = instance.runtime.stacks[i];
|
for (var i = 0; i < stacks.length; i++) {
|
||||||
var stackBlock = instance.runtime.blocks[stack];
|
var stack = stacks[i];
|
||||||
|
var stackBlock = instance.runtime.blocks.getBlock(stack);
|
||||||
var result = callback(stackBlock);
|
var result = callback(stackBlock);
|
||||||
if (result) {
|
if (result) {
|
||||||
// Check if the stack is already running
|
// Check if the stack is already running
|
||||||
|
@ -174,7 +175,7 @@ Sequencer.prototype.stepThread = function (thread) {
|
||||||
*/
|
*/
|
||||||
var threadStartSubstack = function () {
|
var threadStartSubstack = function () {
|
||||||
// Set nextBlock to the start of the substack
|
// Set nextBlock to the start of the substack
|
||||||
var substack = instance.runtime._getSubstack(currentBlock);
|
var substack = instance.runtime.blocks.getSubstack(currentBlock);
|
||||||
if (substack && substack.value) {
|
if (substack && substack.value) {
|
||||||
thread.nextBlock = substack.value;
|
thread.nextBlock = substack.value;
|
||||||
} else {
|
} else {
|
||||||
|
@ -185,7 +186,7 @@ Sequencer.prototype.stepThread = function (thread) {
|
||||||
|
|
||||||
// @todo extreme hack to get the single argument value for prototype
|
// @todo extreme hack to get the single argument value for prototype
|
||||||
var argValues = [];
|
var argValues = [];
|
||||||
var blockInputs = this.runtime.blocks[currentBlock].fields;
|
var blockInputs = this.runtime.blocks.getBlock(currentBlock).fields;
|
||||||
for (var bi in blockInputs) {
|
for (var bi in blockInputs) {
|
||||||
var outer = blockInputs[bi];
|
var outer = blockInputs[bi];
|
||||||
for (var b in outer.blocks) {
|
for (var b in outer.blocks) {
|
||||||
|
|
30
src/index.js
30
src/index.js
|
@ -1,6 +1,7 @@
|
||||||
var EventEmitter = require('events');
|
var EventEmitter = require('events');
|
||||||
var util = require('util');
|
var util = require('util');
|
||||||
|
|
||||||
|
var Blocks = require('./engine/blocks');
|
||||||
var Runtime = require('./engine/runtime');
|
var Runtime = require('./engine/runtime');
|
||||||
var adapter = require('./engine/adapter');
|
var adapter = require('./engine/adapter');
|
||||||
|
|
||||||
|
@ -15,7 +16,8 @@ function VirtualMachine () {
|
||||||
// Bind event emitter and runtime to VM instance
|
// Bind event emitter and runtime to VM instance
|
||||||
// @todo Post message (Web Worker) polyfill
|
// @todo Post message (Web Worker) polyfill
|
||||||
EventEmitter.call(instance);
|
EventEmitter.call(instance);
|
||||||
instance.runtime = new Runtime();
|
instance.blocks = new Blocks();
|
||||||
|
instance.runtime = new Runtime(instance.blocks);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Event listener for blocks. Handles validation and serves as a generic
|
* Event listener for blocks. Handles validation and serves as a generic
|
||||||
|
@ -37,10 +39,14 @@ function VirtualMachine () {
|
||||||
// Block create/update/destroy
|
// Block create/update/destroy
|
||||||
switch (e.type) {
|
switch (e.type) {
|
||||||
case 'create':
|
case 'create':
|
||||||
instance.runtime.createBlock(adapter(e), false);
|
var newBlocks = adapter(e);
|
||||||
|
// A create event can create many blocks. Add them all.
|
||||||
|
for (var i = 0; i < newBlocks.length; i++) {
|
||||||
|
instance.blocks.createBlock(newBlocks[i], false);
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case 'change':
|
case 'change':
|
||||||
instance.runtime.changeBlock({
|
instance.blocks.changeBlock({
|
||||||
id: e.blockId,
|
id: e.blockId,
|
||||||
element: e.element,
|
element: e.element,
|
||||||
name: e.name,
|
name: e.name,
|
||||||
|
@ -48,16 +54,16 @@ function VirtualMachine () {
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case 'move':
|
case 'move':
|
||||||
instance.runtime.moveBlock({
|
instance.blocks.moveBlock({
|
||||||
id: e.blockId,
|
id: e.blockId,
|
||||||
oldParent: e.oldParentId,
|
oldParent: e.oldParentId,
|
||||||
oldField: e.oldInputName,
|
oldInput: e.oldInputName,
|
||||||
newParent: e.newParentId,
|
newParent: e.newParentId,
|
||||||
newField: e.newInputName
|
newInput: e.newInputName
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case 'delete':
|
case 'delete':
|
||||||
instance.runtime.deleteBlock({
|
instance.blocks.deleteBlock({
|
||||||
id: e.blockId
|
id: e.blockId
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
|
@ -67,10 +73,14 @@ function VirtualMachine () {
|
||||||
instance.flyoutBlockListener = function (e) {
|
instance.flyoutBlockListener = function (e) {
|
||||||
switch (e.type) {
|
switch (e.type) {
|
||||||
case 'create':
|
case 'create':
|
||||||
instance.runtime.createBlock(adapter(e), true);
|
var newBlocks = adapter(e);
|
||||||
|
// A create event can create many blocks. Add them all.
|
||||||
|
for (var i = 0; i < newBlocks.length; i++) {
|
||||||
|
instance.blocks.createBlock(newBlocks[i], true);
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case 'change':
|
case 'change':
|
||||||
instance.runtime.changeBlock({
|
instance.blocks.changeBlock({
|
||||||
id: e.blockId,
|
id: e.blockId,
|
||||||
element: e.element,
|
element: e.element,
|
||||||
name: e.name,
|
name: e.name,
|
||||||
|
@ -78,7 +88,7 @@ function VirtualMachine () {
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case 'delete':
|
case 'delete':
|
||||||
instance.runtime.deleteBlock({
|
instance.blocks.deleteBlock({
|
||||||
id: e.blockId
|
id: e.blockId
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
|
|
51
test/fixtures/events.json
vendored
51
test/fixtures/events.json
vendored
|
@ -1,20 +1,57 @@
|
||||||
{
|
{
|
||||||
"create": {
|
"create": {
|
||||||
"blockId": "z!+#Nqr,_(V=xz0y7a@d",
|
|
||||||
"workspaceId": "7Luws3lyb*Z98~Kk+IG|",
|
"workspaceId": "7Luws3lyb*Z98~Kk+IG|",
|
||||||
"group": ";OswyM#@%`%,xOrhOXC=",
|
"group": ";OswyM#@%`%,xOrhOXC=",
|
||||||
"recordUndo": true,
|
"recordUndo": true,
|
||||||
|
"name": "block",
|
||||||
"xml": {
|
"xml": {
|
||||||
"attributes": {
|
"outerHTML": "<block type=\"wedo_motorclockwise\" id=\"z!+#Nqr,_(V=xz0y7a@d\"><value name=\"DURATION\"><shadow type=\"math_number\" id=\"!6Ahqg4f}Ljl}X5Hws?Z\"><field name=\"NUM\">10</field></shadow></value></block>"
|
||||||
"type": {
|
|
||||||
"value": "wedo_motorclockwise"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"innerHTML": "<value name=\"DURATION\"><shadow type=\"math_number\" id=\"!6Ahqg4f}Ljl}X5Hws?Z\"><field name=\"NUM\">10</field></shadow></value>"
|
|
||||||
},
|
},
|
||||||
"ids": [
|
"ids": [
|
||||||
"z!+#Nqr,_(V=xz0y7a@d",
|
"z!+#Nqr,_(V=xz0y7a@d",
|
||||||
"!6Ahqg4f}Ljl}X5Hws?Z"
|
"!6Ahqg4f}Ljl}X5Hws?Z"
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
"createsubstack": {
|
||||||
|
"name": "block",
|
||||||
|
"xml": {
|
||||||
|
"outerHTML": "<block type=\"control_forever\" id=\"r9`RpL74T6*SXPKv7}Dq\" x=\"61\" y=\"90\"><statement name=\"SUBSTACK\"><block type=\"control_wait\" id=\"{Rwt[LFtD1-JPAi-qf:.\"><value name=\"DURATION\"><shadow type=\"math_number\" id=\"VMDxt_9SYe5{*eNRe5dZ\"><field name=\"NUM\">1</field></shadow></value></block></statement></block>"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"createtwosubstacks": {
|
||||||
|
"name": "block",
|
||||||
|
"xml": {
|
||||||
|
"outerHTML": "<block type=\"control_if_else\" id=\"8W?lmIY!Tgnh)~0!G#9-\" x=\"87\" y=\"159\"><statement name=\"SUBSTACK\"><block type=\"event_broadcast\" id=\"lgU2GGtwlREuasCB02Vr\"></block></statement><statement name=\"SUBSTACK2\"><block type=\"event_broadcast\" id=\"Gb]N,2P;|J%F?pxSwz(2\"></block></statement></block>"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"createtoplevelshadow": {
|
||||||
|
"name": "shadow",
|
||||||
|
"xml": {
|
||||||
|
"outerHTML": "<shadow type=\"math_number\" id=\"z9d57=IUI5se;DBbyug)\"><field name=\"NUM\">4</field></shadow>"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"createwithnext": {
|
||||||
|
"name": "block",
|
||||||
|
"xml": {
|
||||||
|
"outerHTML": "<block type=\"wedo_setcolor\" id=\"*CT)7+UKjQIEtUw.OGT6\" x=\"89\" y=\"48\"><next><block type=\"wedo_motorspeed\" id=\"Er*:^o7yYL#dX+5)R^xq\"></block></next></block>"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"createinvalid": {
|
||||||
|
"name": "whatever",
|
||||||
|
"xml": {
|
||||||
|
"outerHTML": "<xml></xml>"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"createinvalidgrandchild": {
|
||||||
|
"name": "block",
|
||||||
|
"xml": {
|
||||||
|
"outerHTML": "<block type=\"control_forever\" id=\"r9`RpL74T6*SXPKv7}Dq\" x=\"61\" y=\"90\"><next><invalidgrandchild>xxx</invalidgrandchild></next></block>"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"createbadxml": {
|
||||||
|
"name": "whatever",
|
||||||
|
"xml": {
|
||||||
|
"outerHTML": "></xml>"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,14 +7,161 @@ test('spec', function (t) {
|
||||||
t.end();
|
t.end();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('invalid inputs', function(t) {
|
||||||
|
var nothing = adapter('not an object');
|
||||||
|
t.type(nothing, 'undefined');
|
||||||
|
nothing = adapter({noxmlproperty:true});
|
||||||
|
t.type(nothing, 'undefined');
|
||||||
|
t.end();
|
||||||
|
});
|
||||||
|
|
||||||
test('create event', function (t) {
|
test('create event', function (t) {
|
||||||
var result = adapter(events.create);
|
var result = adapter(events.create);
|
||||||
|
|
||||||
t.type(result, 'object');
|
t.ok(Array.isArray(result));
|
||||||
t.type(result.id, 'string');
|
t.equal(result.length, 2);
|
||||||
t.type(result.opcode, 'string');
|
|
||||||
t.type(result.fields, 'object');
|
// Outer block
|
||||||
t.type(result.fields['DURATION'], 'object');
|
t.type(result[0].id, 'string');
|
||||||
|
t.type(result[0].opcode, 'string');
|
||||||
|
t.type(result[0].fields, 'object');
|
||||||
|
t.type(result[0].inputs, 'object');
|
||||||
|
t.type(result[0].inputs['DURATION'], 'object');
|
||||||
|
t.type(result[0].topLevel, 'boolean');
|
||||||
|
t.equal(result[0].topLevel, true);
|
||||||
|
|
||||||
|
// Enclosed shadow block
|
||||||
|
t.type(result[1].id, 'string');
|
||||||
|
t.type(result[1].opcode, 'string');
|
||||||
|
t.type(result[1].fields, 'object');
|
||||||
|
t.type(result[1].inputs, 'object');
|
||||||
|
t.type(result[1].fields['NUM'], 'object');
|
||||||
|
t.type(result[1].fields['NUM'].value, '10');
|
||||||
|
t.type(result[1].topLevel, 'boolean');
|
||||||
|
t.equal(result[1].topLevel, false);
|
||||||
|
|
||||||
t.end();
|
t.end();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('create with substack', function (t) {
|
||||||
|
var result = adapter(events.createsubstack);
|
||||||
|
// Outer block
|
||||||
|
t.type(result[0].id, 'string');
|
||||||
|
t.type(result[0].opcode, 'string');
|
||||||
|
t.type(result[0].fields, 'object');
|
||||||
|
t.type(result[0].inputs, 'object');
|
||||||
|
t.type(result[0].inputs['SUBSTACK'], 'object');
|
||||||
|
t.type(result[0].topLevel, 'boolean');
|
||||||
|
t.equal(result[0].topLevel, true);
|
||||||
|
// In substack
|
||||||
|
var substackBlockId = result[0].inputs['SUBSTACK']['block'];
|
||||||
|
t.type(substackBlockId, 'string');
|
||||||
|
// Find actual substack block
|
||||||
|
var substackBlock = null;
|
||||||
|
for (var i = 0; i < result.length; i++) {
|
||||||
|
if (result[i].id == substackBlockId) {
|
||||||
|
substackBlock = result[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
t.type(substackBlock, 'object');
|
||||||
|
t.end();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('create with two substacks', function (t) {
|
||||||
|
var result = adapter(events.createtwosubstacks);
|
||||||
|
// Outer block
|
||||||
|
t.type(result[0].id, 'string');
|
||||||
|
t.type(result[0].opcode, 'string');
|
||||||
|
t.type(result[0].fields, 'object');
|
||||||
|
t.type(result[0].inputs, 'object');
|
||||||
|
t.type(result[0].inputs['SUBSTACK'], 'object');
|
||||||
|
t.type(result[0].inputs['SUBSTACK2'], 'object');
|
||||||
|
t.type(result[0].topLevel, 'boolean');
|
||||||
|
t.equal(result[0].topLevel, true);
|
||||||
|
// In substacks
|
||||||
|
var firstSubstackBlockId = result[0].inputs['SUBSTACK']['block'];
|
||||||
|
var secondSubstackBlockId = result[0].inputs['SUBSTACK2']['block'];
|
||||||
|
t.type(firstSubstackBlockId, 'string');
|
||||||
|
t.type(secondSubstackBlockId, 'string');
|
||||||
|
// Find actual substack blocks
|
||||||
|
var firstSubstackBlock = null;
|
||||||
|
var secondSubstackBlock = null;
|
||||||
|
for (var i = 0; i < result.length; i++) {
|
||||||
|
if (result[i].id == firstSubstackBlockId) {
|
||||||
|
firstSubstackBlock = result[i];
|
||||||
|
}
|
||||||
|
if (result[i].id == secondSubstackBlockId) {
|
||||||
|
secondSubstackBlock = result[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
t.type(firstSubstackBlock, 'object');
|
||||||
|
t.type(secondSubstackBlock, 'object');
|
||||||
|
t.end();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('create with top-level shadow', function (t) {
|
||||||
|
var result = adapter(events.createtoplevelshadow);
|
||||||
|
t.ok(Array.isArray(result));
|
||||||
|
t.equal(result.length, 1);
|
||||||
|
|
||||||
|
// Outer block
|
||||||
|
t.type(result[0].id, 'string');
|
||||||
|
t.type(result[0].opcode, 'string');
|
||||||
|
t.type(result[0].fields, 'object');
|
||||||
|
t.type(result[0].inputs, 'object');
|
||||||
|
t.type(result[0].topLevel, 'boolean');
|
||||||
|
t.equal(result[0].topLevel, true);
|
||||||
|
t.end();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('create with next connection', function (t) {
|
||||||
|
var result = adapter(events.createwithnext);
|
||||||
|
|
||||||
|
t.ok(Array.isArray(result));
|
||||||
|
t.equal(result.length, 2);
|
||||||
|
|
||||||
|
// First block
|
||||||
|
t.type(result[0].id, 'string');
|
||||||
|
t.type(result[0].opcode, 'string');
|
||||||
|
t.type(result[0].fields, 'object');
|
||||||
|
t.type(result[0].inputs, 'object');
|
||||||
|
t.type(result[0].topLevel, 'boolean');
|
||||||
|
t.equal(result[0].topLevel, true);
|
||||||
|
t.type(result[0].next, 'string');
|
||||||
|
t.equal(result[0].next, result[1].id);
|
||||||
|
|
||||||
|
// Second block
|
||||||
|
t.type(result[1].id, 'string');
|
||||||
|
t.type(result[1].opcode, 'string');
|
||||||
|
t.type(result[1].fields, 'object');
|
||||||
|
t.type(result[1].inputs, 'object');
|
||||||
|
t.type(result[1].topLevel, 'boolean');
|
||||||
|
t.equal(result[1].topLevel, false);
|
||||||
|
t.equal(result[1].next, null);
|
||||||
|
|
||||||
|
t.end();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('create with invalid block xml', function (t) {
|
||||||
|
// Entirely invalid block XML
|
||||||
|
var result = adapter(events.createinvalid);
|
||||||
|
t.ok(Array.isArray(result));
|
||||||
|
t.equal(result.length, 0);
|
||||||
|
|
||||||
|
// Invalid grandchild tag
|
||||||
|
var result2 = adapter(events.createinvalidgrandchild);
|
||||||
|
t.ok(Array.isArray(result2));
|
||||||
|
t.equal(result2.length, 1);
|
||||||
|
t.type(result2[0].id, 'string');
|
||||||
|
t.equal(Object.keys(result2[0].inputs).length, 0);
|
||||||
|
t.equal(Object.keys(result2[0].fields).length, 0);
|
||||||
|
|
||||||
|
t.end();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('create with invalid xml', function (t) {
|
||||||
|
var result = adapter(events.createbadxml);
|
||||||
|
t.ok(Array.isArray(result));
|
||||||
|
t.equal(result.length, 0);
|
||||||
|
t.end();
|
||||||
|
});
|
||||||
|
|
468
test/unit/blocks.js
Normal file
468
test/unit/blocks.js
Normal file
|
@ -0,0 +1,468 @@
|
||||||
|
var test = require('tap').test;
|
||||||
|
var Blocks = require('../../src/engine/blocks');
|
||||||
|
|
||||||
|
test('spec', function (t) {
|
||||||
|
var b = new Blocks();
|
||||||
|
|
||||||
|
t.type(Blocks, 'function');
|
||||||
|
t.type(b, 'object');
|
||||||
|
t.ok(b instanceof Blocks);
|
||||||
|
|
||||||
|
t.type(b._blocks, 'object');
|
||||||
|
t.type(b._stacks, 'object');
|
||||||
|
t.ok(Array.isArray(b._stacks));
|
||||||
|
|
||||||
|
t.type(b.createBlock, 'function');
|
||||||
|
t.type(b.moveBlock, 'function');
|
||||||
|
t.type(b.changeBlock, 'function');
|
||||||
|
t.type(b.deleteBlock, 'function');
|
||||||
|
t.type(b.getBlock, 'function');
|
||||||
|
t.type(b.getStacks, 'function');
|
||||||
|
t.type(b.getNextBlock, 'function');
|
||||||
|
t.type(b.getSubstack, 'function');
|
||||||
|
t.type(b.getOpcode, 'function');
|
||||||
|
|
||||||
|
|
||||||
|
t.end();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Getter tests
|
||||||
|
test('getBlock', function (t) {
|
||||||
|
var b = new Blocks();
|
||||||
|
b.createBlock({
|
||||||
|
id: 'foo',
|
||||||
|
opcode: 'TEST_BLOCK',
|
||||||
|
next: null,
|
||||||
|
fields: {},
|
||||||
|
inputs: {},
|
||||||
|
topLevel: true
|
||||||
|
});
|
||||||
|
var block = b.getBlock('foo');
|
||||||
|
t.type(block, 'object');
|
||||||
|
var notBlock = b.getBlock('?');
|
||||||
|
t.type(notBlock, 'undefined');
|
||||||
|
t.end();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('getStacks', function (t) {
|
||||||
|
var b = new Blocks();
|
||||||
|
var stacks = b.getStacks();
|
||||||
|
t.type(stacks, 'object');
|
||||||
|
t.equals(stacks.length, 0);
|
||||||
|
// Create two top-level blocks and one not.
|
||||||
|
b.createBlock({
|
||||||
|
id: 'foo',
|
||||||
|
opcode: 'TEST_BLOCK',
|
||||||
|
next: null,
|
||||||
|
fields: {},
|
||||||
|
inputs: {},
|
||||||
|
topLevel: true
|
||||||
|
});
|
||||||
|
b.createBlock({
|
||||||
|
id: 'foo2',
|
||||||
|
opcode: 'TEST_BLOCK',
|
||||||
|
next: null,
|
||||||
|
fields: {},
|
||||||
|
inputs: {},
|
||||||
|
topLevel: true
|
||||||
|
});
|
||||||
|
b.createBlock({
|
||||||
|
id: 'foo3',
|
||||||
|
opcode: 'TEST_BLOCK',
|
||||||
|
next: null,
|
||||||
|
fields: {},
|
||||||
|
inputs: {},
|
||||||
|
topLevel: false
|
||||||
|
});
|
||||||
|
|
||||||
|
stacks = b.getStacks();
|
||||||
|
t.type(stacks, 'object');
|
||||||
|
t.equals(stacks.length, 2);
|
||||||
|
t.ok(stacks.indexOf('foo') > -1);
|
||||||
|
t.ok(stacks.indexOf('foo2') > -1);
|
||||||
|
t.equals(stacks.indexOf('foo3'), -1);
|
||||||
|
t.end();
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
test('getNextBlock', function (t) {
|
||||||
|
var b = new Blocks();
|
||||||
|
b.createBlock({
|
||||||
|
id: 'foo',
|
||||||
|
opcode: 'TEST_BLOCK',
|
||||||
|
next: null,
|
||||||
|
fields: {},
|
||||||
|
inputs: {},
|
||||||
|
topLevel: true
|
||||||
|
});
|
||||||
|
|
||||||
|
var next = b.getNextBlock('foo');
|
||||||
|
t.equals(next, null);
|
||||||
|
|
||||||
|
// Add a block with "foo" as its next.
|
||||||
|
b.createBlock({
|
||||||
|
id: 'foo2',
|
||||||
|
opcode: 'TEST_BLOCK',
|
||||||
|
next: 'foo',
|
||||||
|
fields: {},
|
||||||
|
inputs: {},
|
||||||
|
topLevel: true
|
||||||
|
});
|
||||||
|
|
||||||
|
next = b.getNextBlock('foo2');
|
||||||
|
t.equals(next, 'foo');
|
||||||
|
|
||||||
|
// Block that doesn't exist.
|
||||||
|
var noBlock = b.getNextBlock('?');
|
||||||
|
t.equals(noBlock, null);
|
||||||
|
|
||||||
|
t.end();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('getSubstack', function (t) {
|
||||||
|
var b = new Blocks();
|
||||||
|
// Single substack
|
||||||
|
b.createBlock({
|
||||||
|
id: 'foo',
|
||||||
|
opcode: 'TEST_BLOCK',
|
||||||
|
next: null,
|
||||||
|
fields: {},
|
||||||
|
inputs: {
|
||||||
|
SUBSTACK: {
|
||||||
|
name: 'SUBSTACK',
|
||||||
|
block: 'foo2'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
topLevel: true
|
||||||
|
});
|
||||||
|
b.createBlock({
|
||||||
|
id: 'foo2',
|
||||||
|
opcode: 'TEST_BLOCK',
|
||||||
|
next: null,
|
||||||
|
fields: {},
|
||||||
|
inputs: {},
|
||||||
|
topLevel: false
|
||||||
|
});
|
||||||
|
|
||||||
|
var substack = b.getSubstack('foo');
|
||||||
|
t.equals(substack, 'foo2');
|
||||||
|
|
||||||
|
var notSubstack = b.getSubstack('?');
|
||||||
|
t.equals(notSubstack, null);
|
||||||
|
|
||||||
|
t.end();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('getSubstack2', function (t) {
|
||||||
|
var b = new Blocks();
|
||||||
|
// Second substack
|
||||||
|
b.createBlock({
|
||||||
|
id: 'foo',
|
||||||
|
opcode: 'TEST_BLOCK',
|
||||||
|
next: null,
|
||||||
|
fields: {},
|
||||||
|
inputs: {
|
||||||
|
SUBSTACK: {
|
||||||
|
name: 'SUBSTACK',
|
||||||
|
block: 'foo2'
|
||||||
|
},
|
||||||
|
SUBSTACK2: {
|
||||||
|
name: 'SUBSTACK2',
|
||||||
|
block: 'foo3'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
topLevel: true
|
||||||
|
});
|
||||||
|
b.createBlock({
|
||||||
|
id: 'foo2',
|
||||||
|
opcode: 'TEST_BLOCK',
|
||||||
|
next: null,
|
||||||
|
fields: {},
|
||||||
|
inputs: {},
|
||||||
|
topLevel: false
|
||||||
|
});
|
||||||
|
b.createBlock({
|
||||||
|
id: 'foo3',
|
||||||
|
opcode: 'TEST_BLOCK',
|
||||||
|
next: null,
|
||||||
|
fields: {},
|
||||||
|
inputs: {},
|
||||||
|
topLevel: false
|
||||||
|
});
|
||||||
|
|
||||||
|
var substack1 = b.getSubstack('foo', 1);
|
||||||
|
var substack2 = b.getSubstack('foo', 2);
|
||||||
|
t.equals(substack1, 'foo2');
|
||||||
|
t.equals(substack2, 'foo3');
|
||||||
|
|
||||||
|
t.end();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('getSubstack with none', function (t) {
|
||||||
|
var b = new Blocks();
|
||||||
|
b.createBlock({
|
||||||
|
id: 'foo',
|
||||||
|
opcode: 'TEST_BLOCK',
|
||||||
|
next: null,
|
||||||
|
fields: {},
|
||||||
|
inputs: {},
|
||||||
|
topLevel: true
|
||||||
|
});
|
||||||
|
var noSubstack = b.getSubstack('foo');
|
||||||
|
t.equals(noSubstack, null);
|
||||||
|
t.end();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('getOpcode', function (t) {
|
||||||
|
var b = new Blocks();
|
||||||
|
b.createBlock({
|
||||||
|
id: 'foo',
|
||||||
|
opcode: 'TEST_BLOCK',
|
||||||
|
next: null,
|
||||||
|
fields: {},
|
||||||
|
inputs: {},
|
||||||
|
topLevel: true
|
||||||
|
});
|
||||||
|
var opcode = b.getOpcode('foo');
|
||||||
|
t.equals(opcode, 'TEST_BLOCK');
|
||||||
|
var notOpcode = b.getOpcode('?');
|
||||||
|
t.equals(notOpcode, null);
|
||||||
|
t.end();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Block events tests
|
||||||
|
test('create', function (t) {
|
||||||
|
var b = new Blocks();
|
||||||
|
b.createBlock({
|
||||||
|
id: 'foo',
|
||||||
|
opcode: 'TEST_BLOCK',
|
||||||
|
next: null,
|
||||||
|
fields: {},
|
||||||
|
inputs: {},
|
||||||
|
topLevel: true
|
||||||
|
});
|
||||||
|
|
||||||
|
t.type(b._blocks['foo'], 'object');
|
||||||
|
t.equal(b._blocks['foo'].opcode, 'TEST_BLOCK');
|
||||||
|
t.notEqual(b._stacks.indexOf('foo'), -1);
|
||||||
|
t.end();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('move', function (t) {
|
||||||
|
var b = new Blocks();
|
||||||
|
b.createBlock({
|
||||||
|
id: 'foo',
|
||||||
|
opcode: 'TEST_BLOCK',
|
||||||
|
next: null,
|
||||||
|
fields: {},
|
||||||
|
inputs: {},
|
||||||
|
topLevel: true
|
||||||
|
});
|
||||||
|
b.createBlock({
|
||||||
|
id: 'bar',
|
||||||
|
opcode: 'TEST_BLOCK',
|
||||||
|
next: null,
|
||||||
|
fields: {},
|
||||||
|
inputs: {},
|
||||||
|
topLevel: true
|
||||||
|
});
|
||||||
|
|
||||||
|
// Attach 'bar' to the end of 'foo'
|
||||||
|
b.moveBlock({
|
||||||
|
id: 'bar',
|
||||||
|
newParent: 'foo'
|
||||||
|
});
|
||||||
|
t.equal(b._stacks.length, 1);
|
||||||
|
t.equal(Object.keys(b._blocks).length, 2);
|
||||||
|
t.equal(b._blocks['foo'].next, 'bar');
|
||||||
|
|
||||||
|
// Detach 'bar' from 'foo'
|
||||||
|
b.moveBlock({
|
||||||
|
id: 'bar',
|
||||||
|
oldParent: 'foo'
|
||||||
|
});
|
||||||
|
t.equal(b._stacks.length, 2);
|
||||||
|
t.equal(Object.keys(b._blocks).length, 2);
|
||||||
|
t.equal(b._blocks['foo'].next, null);
|
||||||
|
|
||||||
|
t.end();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('change', function (t) {
|
||||||
|
var b = new Blocks();
|
||||||
|
b.createBlock({
|
||||||
|
id: 'foo',
|
||||||
|
opcode: 'TEST_BLOCK',
|
||||||
|
next: null,
|
||||||
|
fields: {
|
||||||
|
someField: {
|
||||||
|
name: 'someField',
|
||||||
|
value: 'initial-value'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
inputs: {},
|
||||||
|
topLevel: true
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test that the field is updated
|
||||||
|
t.equal(b._blocks['foo'].fields.someField.value, 'initial-value');
|
||||||
|
|
||||||
|
b.changeBlock({
|
||||||
|
element: 'field',
|
||||||
|
id: 'foo',
|
||||||
|
name: 'someField',
|
||||||
|
value: 'final-value'
|
||||||
|
});
|
||||||
|
|
||||||
|
t.equal(b._blocks['foo'].fields.someField.value, 'final-value');
|
||||||
|
|
||||||
|
// Invalid cases
|
||||||
|
// No `element`
|
||||||
|
b.changeBlock({
|
||||||
|
id: 'foo',
|
||||||
|
name: 'someField',
|
||||||
|
value: 'invalid-value'
|
||||||
|
});
|
||||||
|
t.equal(b._blocks['foo'].fields.someField.value, 'final-value');
|
||||||
|
|
||||||
|
// No block ID
|
||||||
|
b.changeBlock({
|
||||||
|
element: 'field',
|
||||||
|
name: 'someField',
|
||||||
|
value: 'invalid-value'
|
||||||
|
});
|
||||||
|
t.equal(b._blocks['foo'].fields.someField.value, 'final-value');
|
||||||
|
|
||||||
|
// No such field
|
||||||
|
b.changeBlock({
|
||||||
|
element: 'field',
|
||||||
|
id: 'foo',
|
||||||
|
name: 'someWrongField',
|
||||||
|
value: 'final-value'
|
||||||
|
});
|
||||||
|
t.equal(b._blocks['foo'].fields.someField.value, 'final-value');
|
||||||
|
|
||||||
|
t.end();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('delete', function (t) {
|
||||||
|
var b = new Blocks();
|
||||||
|
b.createBlock({
|
||||||
|
id: 'foo',
|
||||||
|
opcode: 'TEST_BLOCK',
|
||||||
|
next: null,
|
||||||
|
fields: {},
|
||||||
|
inputs: {},
|
||||||
|
topLevel: true
|
||||||
|
});
|
||||||
|
b.deleteBlock({
|
||||||
|
id: 'foo'
|
||||||
|
});
|
||||||
|
|
||||||
|
t.type(b._blocks['foo'], 'undefined');
|
||||||
|
t.equal(b._stacks.indexOf('foo'), -1);
|
||||||
|
t.end();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('delete chain', function (t) {
|
||||||
|
// Create a chain of connected blocks and delete the top one.
|
||||||
|
// All of them should be deleted.
|
||||||
|
var b = new Blocks();
|
||||||
|
b.createBlock({
|
||||||
|
id: 'foo',
|
||||||
|
opcode: 'TEST_BLOCK',
|
||||||
|
next: 'foo2',
|
||||||
|
fields: {},
|
||||||
|
inputs: {},
|
||||||
|
topLevel: true
|
||||||
|
});
|
||||||
|
b.createBlock({
|
||||||
|
id: 'foo2',
|
||||||
|
opcode: 'TEST_BLOCK',
|
||||||
|
next: 'foo3',
|
||||||
|
fields: {},
|
||||||
|
inputs: {},
|
||||||
|
topLevel: false
|
||||||
|
});
|
||||||
|
b.createBlock({
|
||||||
|
id: 'foo3',
|
||||||
|
opcode: 'TEST_BLOCK',
|
||||||
|
next: null,
|
||||||
|
fields: {},
|
||||||
|
inputs: {},
|
||||||
|
topLevel: false
|
||||||
|
});
|
||||||
|
b.deleteBlock({
|
||||||
|
id: 'foo'
|
||||||
|
});
|
||||||
|
t.type(b._blocks['foo'], 'undefined');
|
||||||
|
t.type(b._blocks['foo2'], 'undefined');
|
||||||
|
t.type(b._blocks['foo3'], 'undefined');
|
||||||
|
t.equal(b._stacks.indexOf('foo'), -1);
|
||||||
|
t.equal(Object.keys(b._blocks).length, 0);
|
||||||
|
t.equal(b._stacks.length, 0);
|
||||||
|
t.end();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('delete inputs', function (t) {
|
||||||
|
// Create a block with two inputs, one of which has its own input.
|
||||||
|
// Delete the block - all of them should be deleted.
|
||||||
|
var b = new Blocks();
|
||||||
|
b.createBlock({
|
||||||
|
id: 'foo',
|
||||||
|
opcode: 'TEST_BLOCK',
|
||||||
|
next: null,
|
||||||
|
fields: {},
|
||||||
|
inputs: {
|
||||||
|
input1: {
|
||||||
|
name: 'input1',
|
||||||
|
block: 'foo2'
|
||||||
|
},
|
||||||
|
SUBSTACK: {
|
||||||
|
name: 'SUBSTACK',
|
||||||
|
block: 'foo3'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
topLevel: true
|
||||||
|
});
|
||||||
|
b.createBlock({
|
||||||
|
id: 'foo2',
|
||||||
|
opcode: 'TEST_BLOCK',
|
||||||
|
next: null,
|
||||||
|
fields: {},
|
||||||
|
inputs: {},
|
||||||
|
topLevel: false
|
||||||
|
});
|
||||||
|
b.createBlock({
|
||||||
|
id: 'foo3',
|
||||||
|
opcode: 'TEST_BLOCK',
|
||||||
|
next: null,
|
||||||
|
fields: {},
|
||||||
|
inputs: {
|
||||||
|
subinput: {
|
||||||
|
name: 'subinput',
|
||||||
|
block: 'foo4'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
topLevel: false
|
||||||
|
});
|
||||||
|
b.createBlock({
|
||||||
|
id: 'foo4',
|
||||||
|
opcode: 'TEST_BLOCK',
|
||||||
|
next: null,
|
||||||
|
fields: {},
|
||||||
|
inputs: {},
|
||||||
|
topLevel: false
|
||||||
|
});
|
||||||
|
b.deleteBlock({
|
||||||
|
id: 'foo'
|
||||||
|
});
|
||||||
|
t.type(b._blocks['foo'], 'undefined');
|
||||||
|
t.type(b._blocks['foo2'], 'undefined');
|
||||||
|
t.type(b._blocks['foo3'], 'undefined');
|
||||||
|
t.type(b._blocks['foo4'], 'undefined');
|
||||||
|
t.equal(b._stacks.indexOf('foo'), -1);
|
||||||
|
t.equal(Object.keys(b._blocks).length, 0);
|
||||||
|
t.equal(b._stacks.length, 0);
|
||||||
|
t.end();
|
||||||
|
});
|
|
@ -8,82 +8,5 @@ test('spec', function (t) {
|
||||||
t.type(r, 'object');
|
t.type(r, 'object');
|
||||||
t.ok(r instanceof Runtime);
|
t.ok(r instanceof Runtime);
|
||||||
|
|
||||||
t.type(r.blocks, 'object');
|
|
||||||
t.type(r.stacks, 'object');
|
|
||||||
t.ok(Array.isArray(r.stacks));
|
|
||||||
|
|
||||||
t.type(r.createBlock, 'function');
|
|
||||||
t.type(r.moveBlock, 'function');
|
|
||||||
t.type(r.changeBlock, 'function');
|
|
||||||
t.type(r.deleteBlock, 'function');
|
|
||||||
|
|
||||||
t.end();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('create', function (t) {
|
|
||||||
var r = new Runtime();
|
|
||||||
r.createBlock({
|
|
||||||
id: 'foo',
|
|
||||||
opcode: 'TEST_BLOCK',
|
|
||||||
next: null,
|
|
||||||
fields: {}
|
|
||||||
});
|
|
||||||
|
|
||||||
t.type(r.blocks['foo'], 'object');
|
|
||||||
t.equal(r.blocks['foo'].opcode, 'TEST_BLOCK');
|
|
||||||
t.notEqual(r.stacks.indexOf('foo'), -1);
|
|
||||||
t.end();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('move', function (t) {
|
|
||||||
var r = new Runtime();
|
|
||||||
r.createBlock({
|
|
||||||
id: 'foo',
|
|
||||||
opcode: 'TEST_BLOCK',
|
|
||||||
next: null,
|
|
||||||
fields: {}
|
|
||||||
});
|
|
||||||
r.createBlock({
|
|
||||||
id: 'bar',
|
|
||||||
opcode: 'TEST_BLOCK',
|
|
||||||
next: null,
|
|
||||||
fields: {}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Attach 'bar' to the end of 'foo'
|
|
||||||
r.moveBlock({
|
|
||||||
id: 'bar',
|
|
||||||
newParent: 'foo'
|
|
||||||
});
|
|
||||||
t.equal(r.stacks.length, 1);
|
|
||||||
t.equal(Object.keys(r.blocks).length, 2);
|
|
||||||
t.equal(r.blocks['foo'].next, 'bar');
|
|
||||||
|
|
||||||
// Detach 'bar' from 'foo'
|
|
||||||
r.moveBlock({
|
|
||||||
id: 'bar',
|
|
||||||
oldParent: 'foo'
|
|
||||||
});
|
|
||||||
t.equal(r.stacks.length, 2);
|
|
||||||
t.equal(Object.keys(r.blocks).length, 2);
|
|
||||||
t.equal(r.blocks['foo'].next, null);
|
|
||||||
|
|
||||||
t.end();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('delete', function (t) {
|
|
||||||
var r = new Runtime();
|
|
||||||
r.createBlock({
|
|
||||||
id: 'foo',
|
|
||||||
opcode: 'TEST_BLOCK',
|
|
||||||
next: null,
|
|
||||||
fields: {}
|
|
||||||
});
|
|
||||||
r.deleteBlock({
|
|
||||||
id: 'foo'
|
|
||||||
});
|
|
||||||
|
|
||||||
t.type(r.blocks['foo'], 'undefined');
|
|
||||||
t.equal(r.stacks.indexOf('foo'), -1);
|
|
||||||
t.end();
|
t.end();
|
||||||
});
|
});
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue