Merge remote-tracking branch 'LLK/develop' into develop

This commit is contained in:
Eric Rosenbaum 2016-10-05 17:16:13 -04:00
commit ed650ba487
25 changed files with 579 additions and 27 deletions

View file

@ -6,6 +6,9 @@ sudo: false
cache: cache:
directories: directories:
- node_modules - node_modules
before_install:
# Install the most up to date scratch-* dependencies
- rm -rf node_modules/scratch-*
after_script: after_script:
- | - |
# RELEASE_BRANCHES and NPM_TOKEN defined in Travis settings panel # RELEASE_BRANCHES and NPM_TOKEN defined in Travis settings panel

View file

@ -437,7 +437,7 @@
<category name="Data" colour="#FF8C1A" custom="VARIABLE"> <category name="Data" colour="#FF8C1A" custom="VARIABLE">
</category> </category>
<category name="Lists" colour="#FF8C1A"> <category name="Lists" colour="#FF8C1A">
<block type="data_list"></block> <block type="data_listcontents"></block>
<block type="data_addtolist"> <block type="data_addtolist">
<value name="ITEM"> <value name="ITEM">
<shadow type="text"> <shadow type="text">

View file

@ -22,6 +22,7 @@ Scratch3MotionBlocks.prototype.getPrimitives = function() {
'motion_turnleft': this.turnLeft, 'motion_turnleft': this.turnLeft,
'motion_pointindirection': this.pointInDirection, 'motion_pointindirection': this.pointInDirection,
'motion_glidesecstoxy': this.glide, 'motion_glidesecstoxy': this.glide,
'motion_setrotationstyle': this.setRotationStyle,
'motion_changexby': this.changeX, 'motion_changexby': this.changeX,
'motion_setx': this.setX, 'motion_setx': this.setX,
'motion_changeyby': this.changeY, 'motion_changeyby': this.changeY,
@ -96,6 +97,10 @@ Scratch3MotionBlocks.prototype.glide = function (args, util) {
} }
}; };
Scratch3MotionBlocks.prototype.setRotationStyle = function (args, util) {
util.target.setRotationStyle(args.STYLE);
};
Scratch3MotionBlocks.prototype.changeX = function (args, util) { Scratch3MotionBlocks.prototype.changeX = function (args, util) {
var dx = Cast.toNumber(args.DX); var dx = Cast.toNumber(args.DX);
util.target.setXY(util.target.x + dx, util.target.y); util.target.setXY(util.target.x + dx, util.target.y);

View file

@ -0,0 +1,32 @@
function Scratch3ProcedureBlocks(runtime) {
/**
* The runtime instantiating this block package.
* @type {Runtime}
*/
this.runtime = runtime;
}
/**
* Retrieve the block primitives implemented by this package.
* @return {Object.<string, Function>} Mapping of opcode to Function.
*/
Scratch3ProcedureBlocks.prototype.getPrimitives = function() {
return {
'procedures_defnoreturn': this.defNoReturn,
'procedures_callnoreturn': this.callNoReturn
};
};
Scratch3ProcedureBlocks.prototype.defNoReturn = function () {
// No-op: execute the blocks.
};
Scratch3ProcedureBlocks.prototype.callNoReturn = function (args, util) {
if (!util.stackFrame.executed) {
var procedureName = args.mutation.name;
util.stackFrame.executed = true;
util.startProcedure(procedureName);
}
};
module.exports = Scratch3ProcedureBlocks;

View file

@ -1,3 +1,4 @@
var mutationAdapter = require('./mutation-adapter');
var html = require('htmlparser2'); var html = require('htmlparser2');
/** /**
@ -138,6 +139,9 @@ function domToBlock (blockDOM, blocks, isTopBlock, parent) {
// Link next block to this block. // Link next block to this block.
block.next = childBlockNode.attribs.id; block.next = childBlockNode.attribs.id;
break; break;
case 'mutation':
block.mutation = mutationAdapter(xmlChild);
break;
} }
} }
} }

View file

@ -1,4 +1,5 @@
var adapter = require('./adapter'); var adapter = require('./adapter');
var mutationAdapter = require('./mutation-adapter');
var xmlEscape = require('../util/xml-escape'); var xmlEscape = require('../util/xml-escape');
/** /**
@ -116,6 +117,16 @@ Blocks.prototype.getInputs = function (id) {
return inputs; return inputs;
}; };
/**
* Get mutation data for a block.
* @param {?string} id ID of block to query.
* @return {!Object} Mutation for the block.
*/
Blocks.prototype.getMutation = function (id) {
if (typeof this._blocks[id] === 'undefined') return null;
return this._blocks[id].mutation;
};
/** /**
* Get the top-level script for a given block. * Get the top-level script for a given block.
* @param {?string} id ID of block to query. * @param {?string} id ID of block to query.
@ -130,6 +141,23 @@ Blocks.prototype.getTopLevelScript = function (id) {
return block.id; return block.id;
}; };
/**
* Get the procedure definition for a given name.
* @param {?string} name Name of procedure to query.
* @return {?string} ID of procedure definition.
*/
Blocks.prototype.getProcedureDefinition = function (name) {
for (var id in this._blocks) {
var block = this._blocks[id];
if ((block.opcode == 'procedures_defnoreturn' ||
block.opcode == 'procedures_defreturn') &&
block.fields['NAME'].value == name) {
return id;
}
}
return null;
};
// --------------------------------------------------------------------- // ---------------------------------------------------------------------
/** /**
@ -226,12 +254,16 @@ Blocks.prototype.createBlock = function (block, opt_isFlyoutBlock) {
*/ */
Blocks.prototype.changeBlock = function (args) { Blocks.prototype.changeBlock = function (args) {
// Validate // Validate
if (args.element !== 'field') return; if (args.element !== 'field' && args.element !== 'mutation') return;
if (typeof this._blocks[args.id] === 'undefined') return; if (typeof this._blocks[args.id] === 'undefined') return;
if (typeof this._blocks[args.id].fields[args.name] === 'undefined') return;
// Update block value if (args.element == 'field') {
this._blocks[args.id].fields[args.name].value = args.value; // Update block value
if (!this._blocks[args.id].fields[args.name]) return;
this._blocks[args.id].fields[args.name].value = args.value;
} else if (args.element == 'mutation') {
this._blocks[args.id].mutation = mutationAdapter(args.value);
}
}; };
/** /**
@ -355,6 +387,10 @@ Blocks.prototype.blockToXML = function (blockId) {
' type="' + block.opcode + '"' + ' type="' + block.opcode + '"' +
xy + xy +
'>'; '>';
// Add any mutation. Must come before inputs.
if (block.mutation) {
xmlString += this.mutationToXML(block.mutation);
}
// Add any inputs on this block. // Add any inputs on this block.
for (var input in block.inputs) { for (var input in block.inputs) {
var blockInput = block.inputs[input]; var blockInput = block.inputs[input];
@ -389,6 +425,25 @@ Blocks.prototype.blockToXML = function (blockId) {
return xmlString; return xmlString;
}; };
/**
* Recursively encode a mutation object to XML.
* @param {!Object} mutation Object representing a mutation.
* @return {string} XML string representing a mutation.
*/
Blocks.prototype.mutationToXML = function (mutation) {
var mutationString = '<' + mutation.tagName;
for (var prop in mutation) {
if (prop == 'children' || prop == 'tagName') continue;
mutationString += ' ' + prop + '="' + mutation[prop] + '"';
}
mutationString += '>';
for (var i = 0; i < mutation.children.length; i++) {
mutationString += this.mutationToXML(mutation.children[i]);
}
mutationString += '</' + mutation.tagName + '>';
return mutationString;
};
// --------------------------------------------------------------------- // ---------------------------------------------------------------------
/** /**

View file

@ -132,6 +132,12 @@ var execute = function (sequencer, thread) {
argValues[inputName] = currentStackFrame.reported[inputName]; argValues[inputName] = currentStackFrame.reported[inputName];
} }
// Add any mutation to args (e.g., for procedures).
var mutation = target.blocks.getMutation(currentBlockId);
if (mutation) {
argValues.mutation = mutation;
}
// If we've gotten this far, all of the input blocks are evaluated, // If we've gotten this far, all of the input blocks are evaluated,
// and `argValues` is fully populated. So, execute the block primitive. // and `argValues` is fully populated. So, execute the block primitive.
// First, clear `currentStackFrame.reported`, so any subsequent execution // First, clear `currentStackFrame.reported`, so any subsequent execution
@ -155,6 +161,9 @@ var execute = function (sequencer, thread) {
startBranch: function (branchNum) { startBranch: function (branchNum) {
sequencer.stepToBranch(thread, branchNum); sequencer.stepToBranch(thread, branchNum);
}, },
startProcedure: function (procedureName) {
sequencer.stepToProcedure(thread, procedureName);
},
startHats: function(requestedHat, opt_matchFields, opt_target) { startHats: function(requestedHat, opt_matchFields, opt_target) {
return ( return (
runtime.startHats(requestedHat, opt_matchFields, opt_target) runtime.startHats(requestedHat, opt_matchFields, opt_target)

View file

@ -0,0 +1,39 @@
var html = require('htmlparser2');
/**
* Adapter between mutator XML or DOM and block representation which can be
* used by the Scratch runtime.
* @param {(Object|string)} mutation Mutation XML string or DOM.
* @return {Object} Object representing the mutation.
*/
module.exports = function (mutation) {
var mutationParsed;
// Check if the mutation is already parsed; if not, parse it.
if (typeof mutation === 'object') {
mutationParsed = mutation;
} else {
mutationParsed = html.parseDOM(mutation)[0];
}
return mutatorTagToObject(mutationParsed);
};
/**
* Convert a part of a mutation DOM to a mutation VM object, recursively.
* @param {Object} dom DOM object for mutation tag.
* @return {Object} Object representing useful parts of this mutation.
*/
function mutatorTagToObject (dom) {
var obj = Object.create(null);
obj.tagName = dom.name;
obj.children = [];
for (var prop in dom.attribs) {
if (prop == 'xmlns') continue;
obj[prop] = dom.attribs[prop];
}
for (var i = 0; i < dom.children.length; i++) {
obj.children.push(
mutatorTagToObject(dom.children[i])
);
}
return obj;
}

View file

@ -16,7 +16,8 @@ var defaultBlockPackages = {
'scratch3_operators': require('../blocks/scratch3_operators'), 'scratch3_operators': require('../blocks/scratch3_operators'),
'scratch3_sound': require('../blocks/scratch3_sound'), 'scratch3_sound': require('../blocks/scratch3_sound'),
'scratch3_sensing': require('../blocks/scratch3_sensing'), 'scratch3_sensing': require('../blocks/scratch3_sensing'),
'scratch3_data': require('../blocks/scratch3_data') 'scratch3_data': require('../blocks/scratch3_data'),
'scratch3_procedures': require('../blocks/scratch3_procedures')
}; };
/** /**

View file

@ -124,6 +124,16 @@ Sequencer.prototype.stepToBranch = function (thread, branchNum) {
} }
}; };
/**
* Step a procedure.
* @param {!Thread} thread Thread object to step to procedure.
* @param {!string} procedureName Name of procedure defined in this target.
*/
Sequencer.prototype.stepToProcedure = function (thread, procedureName) {
var definition = thread.target.blocks.getProcedureDefinition(procedureName);
thread.pushStack(definition);
};
/** /**
* Step a thread into an input reporter, and manage its status appropriately. * Step a thread into an input reporter, and manage its status appropriately.
* @param {!Thread} thread Thread object to step to reporter. * @param {!Thread} thread Thread object to step to reporter.

View file

@ -6,6 +6,7 @@
*/ */
var Blocks = require('../engine/blocks'); var Blocks = require('../engine/blocks');
var Clone = require('../sprites/clone');
var Sprite = require('../sprites/sprite'); var Sprite = require('../sprites/sprite');
var Color = require('../util/color.js'); var Color = require('../util/color.js');
var uid = require('../util/uid'); var uid = require('../util/uid');
@ -123,7 +124,16 @@ function parseScratchObject (object, runtime, topLevel) {
target.visible = object.visible; target.visible = object.visible;
} }
if (object.hasOwnProperty('currentCostumeIndex')) { if (object.hasOwnProperty('currentCostumeIndex')) {
target.currentCostume = object.currentCostumeIndex; target.currentCostume = Math.round(object.currentCostumeIndex);
}
if (object.hasOwnProperty('rotationStyle')) {
if (object.rotationStyle == 'none') {
target.rotationStyle = Clone.ROTATION_STYLE_NONE;
} else if (object.rotationStyle == 'leftRight') {
target.rotationStyle = Clone.ROTATION_STYLE_LEFT_RIGHT;
} else if (object.rotationStyle == 'normal') {
target.rotationStyle = Clone.ROTATION_STYLE_ALL_AROUND;
}
} }
target.isStage = topLevel; target.isStage = topLevel;
target.updateAllDrawableProperties(); target.updateAllDrawableProperties();
@ -332,6 +342,14 @@ function parseBlock (sb2block) {
}; };
} }
} }
// Special cases to generate mutations.
if (oldOpcode == 'call') {
activeBlock.mutation = {
tagName: 'mutation',
children: [],
name: sb2block[1]
};
}
return activeBlock; return activeBlock;
} }

View file

@ -1374,15 +1374,20 @@ var specMap = {
] ]
}, },
'procDef':{ 'procDef':{
'opcode':'proc_def', 'opcode':'procedures_defnoreturn',
'argMap':[] 'argMap':[
{
'type':'field',
'fieldName':'NAME'
}
]
}, },
'getParam':{ 'getParam':{
'opcode':'proc_param', 'opcode':'proc_param',
'argMap':[] 'argMap':[]
}, },
'call':{ 'call':{
'opcode':'proc_call', 'opcode':'procedures_callnoreturn',
'argMap':[] 'argMap':[]
} }
}; };

View file

@ -29,6 +29,20 @@ function Clone(sprite, runtime) {
* @type {?Number} * @type {?Number}
*/ */
this.drawableID = null; this.drawableID = null;
/**
* Map of current graphic effect values.
* @type {!Object.<string, number>}
*/
this.effects = {
'color': 0,
'fisheye': 0,
'whirl': 0,
'pixelate': 0,
'mosaic': 0,
'brightness': 0,
'ghost': 0
};
} }
util.inherits(Clone, Target); util.inherits(Clone, Target);
@ -38,7 +52,6 @@ util.inherits(Clone, Target);
Clone.prototype.initDrawable = function () { Clone.prototype.initDrawable = function () {
if (this.renderer) { if (this.renderer) {
this.drawableID = this.renderer.createDrawable(); this.drawableID = this.renderer.createDrawable();
this.updateAllDrawableProperties();
} }
// If we're a clone, start the hats. // If we're a clone, start the hats.
if (!this.isOriginal) { if (!this.isOriginal) {
@ -99,18 +112,29 @@ Clone.prototype.size = 100;
Clone.prototype.currentCostume = 0; Clone.prototype.currentCostume = 0;
/** /**
* Map of current graphic effect values. * Rotation style for "all around"/spinning.
* @type {!Object.<string, number>} * @enum
*/ */
Clone.prototype.effects = { Clone.ROTATION_STYLE_ALL_AROUND = 'all around';
'color': 0,
'fisheye': 0, /**
'whirl': 0, * Rotation style for "left-right"/flipping.
'pixelate': 0, * @enum
'mosaic': 0, */
'brightness': 0, Clone.ROTATION_STYLE_LEFT_RIGHT = 'left-right';
'ghost': 0
}; /**
* Rotation style for "no rotation."
* @enum
*/
Clone.ROTATION_STYLE_NONE = 'don\'t rotate';
/**
* Current rotation style.
* @type {!string}
*/
Clone.prototype.rotationStyle = Clone.ROTATION_STYLE_ALL_AROUND;
// End clone-level properties. // End clone-level properties.
/** /**
@ -131,6 +155,26 @@ Clone.prototype.setXY = function (x, y) {
} }
}; };
/**
* Get the rendered direction and scale, after applying rotation style.
* @return {Object<string, number>} Direction and scale to render.
*/
Clone.prototype._getRenderedDirectionAndScale = function () {
// Default: no changes to `this.direction` or `this.scale`.
var finalDirection = this.direction;
var finalScale = [this.size, this.size];
if (this.rotationStyle == Clone.ROTATION_STYLE_NONE) {
// Force rendered direction to be 90.
finalDirection = 90;
} else if (this.rotationStyle === Clone.ROTATION_STYLE_LEFT_RIGHT) {
// Force rendered direction to be 90, and flip drawable if needed.
finalDirection = 90;
var scaleFlip = (this.direction < 0) ? -1 : 1;
finalScale = [scaleFlip * this.size, this.size];
}
return {direction: finalDirection, scale: finalScale};
};
/** /**
* Set the direction of a clone. * Set the direction of a clone.
* @param {!number} direction New direction of clone. * @param {!number} direction New direction of clone.
@ -142,8 +186,10 @@ Clone.prototype.setDirection = function (direction) {
// Keep direction between -179 and +180. // Keep direction between -179 and +180.
this.direction = MathUtil.wrapClamp(direction, -179, 180); this.direction = MathUtil.wrapClamp(direction, -179, 180);
if (this.renderer) { if (this.renderer) {
var renderedDirectionScale = this._getRenderedDirectionAndScale();
this.renderer.updateDrawableProperties(this.drawableID, { this.renderer.updateDrawableProperties(this.drawableID, {
direction: this.direction direction: renderedDirectionScale.direction,
scale: renderedDirectionScale.scale
}); });
} }
}; };
@ -192,8 +238,10 @@ Clone.prototype.setSize = function (size) {
// Keep size between 5% and 535%. // Keep size between 5% and 535%.
this.size = MathUtil.clamp(size, 5, 535); this.size = MathUtil.clamp(size, 5, 535);
if (this.renderer) { if (this.renderer) {
var renderedDirectionScale = this._getRenderedDirectionAndScale();
this.renderer.updateDrawableProperties(this.drawableID, { this.renderer.updateDrawableProperties(this.drawableID, {
scale: [this.size, this.size] direction: renderedDirectionScale.direction,
scale: renderedDirectionScale.scale
}); });
} }
}; };
@ -231,6 +279,7 @@ Clone.prototype.clearEffects = function () {
*/ */
Clone.prototype.setCostume = function (index) { Clone.prototype.setCostume = function (index) {
// Keep the costume index within possible values. // Keep the costume index within possible values.
index = Math.round(index);
this.currentCostume = MathUtil.wrapClamp( this.currentCostume = MathUtil.wrapClamp(
index, 0, this.sprite.costumes.length - 1 index, 0, this.sprite.costumes.length - 1
); );
@ -241,6 +290,27 @@ Clone.prototype.setCostume = function (index) {
} }
}; };
/**
* Update the rotation style for this clone.
* @param {!string} rotationStyle New rotation style.
*/
Clone.prototype.setRotationStyle = function (rotationStyle) {
if (rotationStyle == Clone.ROTATION_STYLE_NONE) {
this.rotationStyle = Clone.ROTATION_STYLE_NONE;
} else if (rotationStyle == Clone.ROTATION_STYLE_ALL_AROUND) {
this.rotationStyle = Clone.ROTATION_STYLE_ALL_AROUND;
} else if (rotationStyle == Clone.ROTATION_STYLE_LEFT_RIGHT) {
this.rotationStyle = Clone.ROTATION_STYLE_LEFT_RIGHT;
}
if (this.renderer) {
var renderedDirectionScale = this._getRenderedDirectionAndScale();
this.renderer.updateDrawableProperties(this.drawableID, {
direction: renderedDirectionScale.direction,
scale: renderedDirectionScale.scale
});
}
};
/** /**
* Get a costume index of this clone, by name of the costume. * Get a costume index of this clone, by name of the costume.
* @param {?string} costumeName Name of a costume. * @param {?string} costumeName Name of a costume.
@ -275,10 +345,11 @@ Clone.prototype.getSoundIndexByName = function (soundName) {
*/ */
Clone.prototype.updateAllDrawableProperties = function () { Clone.prototype.updateAllDrawableProperties = function () {
if (this.renderer) { if (this.renderer) {
var renderedDirectionScale = this._getRenderedDirectionAndScale();
this.renderer.updateDrawableProperties(this.drawableID, { this.renderer.updateDrawableProperties(this.drawableID, {
position: [this.x, this.y], position: [this.x, this.y],
direction: this.direction, direction: renderedDirectionScale.direction,
scale: [this.size, this.size], scale: renderedDirectionScale.scale,
visible: this.visible, visible: this.visible,
skin: this.sprite.costumes[this.currentCostume].skin skin: this.sprite.costumes[this.currentCostume].skin
}); });
@ -340,6 +411,7 @@ Clone.prototype.makeClone = function () {
newClone.visible = this.visible; newClone.visible = this.visible;
newClone.size = this.size; newClone.size = this.size;
newClone.currentCostume = this.currentCostume; newClone.currentCostume = this.currentCostume;
newClone.rotationStyle = this.rotationStyle;
newClone.effects = JSON.parse(JSON.stringify(this.effects)); newClone.effects = JSON.parse(JSON.stringify(this.effects));
newClone.variables = JSON.parse(JSON.stringify(this.variables)); newClone.variables = JSON.parse(JSON.stringify(this.variables));
newClone.lists = JSON.parse(JSON.stringify(this.lists)); newClone.lists = JSON.parse(JSON.stringify(this.lists));

View file

@ -54,7 +54,6 @@ Sprite.prototype.createClone = function () {
this.clones.push(newClone); this.clones.push(newClone);
if (newClone.isOriginal) { if (newClone.isOriginal) {
newClone.initDrawable(); newClone.initDrawable();
newClone.updateAllDrawableProperties();
} }
return newClone; return newClone;
}; };

View file

@ -0,0 +1,13 @@
var test = require('tap').test;
var Clone = require('../../src/sprites/clone');
var Sprite = require('../../src/sprites/sprite');
test('clone effects', function (t) {
// Create two clones and ensure they have different graphic effect objects.
// Regression test for Github issue #224
var spr = new Sprite();
var a = new Clone(spr, null);
var b = new Clone(spr, null);
t.ok(a.effects !== b.effects);
t.end();
});

179
test/unit/util_cast.js Normal file
View file

@ -0,0 +1,179 @@
var test = require('tap').test;
var cast = require('../../src/util/cast');
test('toNumber', function (t) {
// Numeric
t.strictEqual(cast.toNumber(0), 0);
t.strictEqual(cast.toNumber(1), 1);
t.strictEqual(cast.toNumber(3.14), 3.14);
// String
t.strictEqual(cast.toNumber('0'), 0);
t.strictEqual(cast.toNumber('1'), 1);
t.strictEqual(cast.toNumber('3.14'), 3.14);
t.strictEqual(cast.toNumber('0.1e10'), 1000000000);
t.strictEqual(cast.toNumber('foobar'), 0);
// Boolean
t.strictEqual(cast.toNumber(true), 1);
t.strictEqual(cast.toNumber(false), 0);
t.strictEqual(cast.toNumber('true'), 0);
t.strictEqual(cast.toNumber('false'), 0);
// Undefined & object
t.strictEqual(cast.toNumber(undefined), 0);
t.strictEqual(cast.toNumber({}), 0);
t.strictEqual(cast.toNumber(NaN), 0);
t.end();
});
test('toBoolean', function (t) {
// Numeric
t.strictEqual(cast.toBoolean(0), false);
t.strictEqual(cast.toBoolean(1), true);
t.strictEqual(cast.toBoolean(3.14), true);
// String
t.strictEqual(cast.toBoolean('0'), false);
t.strictEqual(cast.toBoolean('1'), true);
t.strictEqual(cast.toBoolean('3.14'), true);
t.strictEqual(cast.toBoolean('0.1e10'), true);
t.strictEqual(cast.toBoolean('foobar'), true);
// Boolean
t.strictEqual(cast.toBoolean(true), true);
t.strictEqual(cast.toBoolean(false), false);
// Undefined & object
t.strictEqual(cast.toBoolean(undefined), false);
t.strictEqual(cast.toBoolean({}), true);
t.end();
});
test('toString', function (t) {
// Numeric
t.strictEqual(cast.toString(0), '0');
t.strictEqual(cast.toString(1), '1');
t.strictEqual(cast.toString(3.14), '3.14');
// String
t.strictEqual(cast.toString('0'), '0');
t.strictEqual(cast.toString('1'), '1');
t.strictEqual(cast.toString('3.14'), '3.14');
t.strictEqual(cast.toString('0.1e10'), '0.1e10');
t.strictEqual(cast.toString('foobar'), 'foobar');
// Boolean
t.strictEqual(cast.toString(true), 'true');
t.strictEqual(cast.toString(false), 'false');
// Undefined & object
t.strictEqual(cast.toString(undefined), 'undefined');
t.strictEqual(cast.toString({}), '[object Object]');
t.end();
});
test('toRbgColorList', function (t) {
// Hex (minimal, see "color" util tests)
t.deepEqual(cast.toRgbColorList('#000'), [0,0,0]);
t.deepEqual(cast.toRgbColorList('#000000'), [0,0,0]);
t.deepEqual(cast.toRgbColorList('#fff'), [255,255,255]);
t.deepEqual(cast.toRgbColorList('#ffffff'), [255,255,255]);
// Decimal (minimal, see "color" util tests)
t.deepEqual(cast.toRgbColorList(0), [0,0,0]);
t.deepEqual(cast.toRgbColorList(1), [0,0,1]);
t.deepEqual(cast.toRgbColorList(16777215), [255,255,255]);
// Malformed
t.deepEqual(cast.toRgbColorList('ffffff'), [0,0,0]);
t.deepEqual(cast.toRgbColorList('foobar'), [0,0,0]);
t.end();
});
test('compare', function (t) {
// Numeric
t.strictEqual(cast.compare(0, 0), 0);
t.strictEqual(cast.compare(1, 0), 1);
t.strictEqual(cast.compare(0, 1), -1);
t.strictEqual(cast.compare(1, 1), 0);
// String
t.strictEqual(cast.compare('0', '0'), 0);
t.strictEqual(cast.compare('0.1e10', '1000000000'), 0);
t.strictEqual(cast.compare('foobar', 'FOOBAR'), 0);
t.ok(cast.compare('dog', 'cat') > 0);
// Boolean
t.strictEqual(cast.compare(true, true), 0);
t.strictEqual(cast.compare(true, false), 1);
t.strictEqual(cast.compare(false, true), -1);
t.strictEqual(cast.compare(true, true), 0);
// Undefined & object
t.strictEqual(cast.compare(undefined, undefined), 0);
t.strictEqual(cast.compare(undefined, 'undefined'), 0);
t.strictEqual(cast.compare({}, {}), 0);
t.strictEqual(cast.compare({}, '[object Object]'), 0);
t.end();
});
test('isInt', function (t) {
// Numeric
t.strictEqual(cast.isInt(0), true);
t.strictEqual(cast.isInt(1), true);
t.strictEqual(cast.isInt(0.0), true);
t.strictEqual(cast.isInt(3.14), false);
t.strictEqual(cast.isInt(NaN), true);
// String
t.strictEqual(cast.isInt('0'), true);
t.strictEqual(cast.isInt('1'), true);
t.strictEqual(cast.isInt('0.0'), false);
t.strictEqual(cast.isInt('0.1e10'), false);
t.strictEqual(cast.isInt('3.14'), false);
// Boolean
t.strictEqual(cast.isInt(true), true);
t.strictEqual(cast.isInt(false), true);
// Undefined & object
t.strictEqual(cast.isInt(undefined), false);
t.strictEqual(cast.isInt({}), false);
t.end();
});
test('toListIndex', function (t) {
var list = [0,1,2,3,4,5];
var empty = [];
// Valid
t.strictEqual(cast.toListIndex(1, list.length), 1);
t.strictEqual(cast.toListIndex(6, list.length), 6);
// Invalid
t.strictEqual(cast.toListIndex(-1, list.length), cast.LIST_INVALID);
t.strictEqual(cast.toListIndex(0.1, list.length), cast.LIST_INVALID);
t.strictEqual(cast.toListIndex(0, list.length), cast.LIST_INVALID);
t.strictEqual(cast.toListIndex(7, list.length), cast.LIST_INVALID);
// "all"
t.strictEqual(cast.toListIndex('all', list.length), cast.LIST_ALL);
// "last"
t.strictEqual(cast.toListIndex('last', list.length), list.length);
t.strictEqual(cast.toListIndex('last', empty.length), cast.LIST_INVALID);
// "random"
var random = cast.toListIndex('random', list.length);
t.ok(random <= list.length);
t.ok(random > 0);
t.strictEqual(cast.toListIndex('random', empty.length), cast.LIST_INVALID);
// "any" (alias for "random")
var any = cast.toListIndex('any', list.length);
t.ok(any <= list.length);
t.ok(any > 0);
t.strictEqual(cast.toListIndex('any', empty.length), cast.LIST_INVALID);
t.end();
});

62
test/unit/util_color.js Normal file
View file

@ -0,0 +1,62 @@
var test = require('tap').test;
var color = require('../../src/util/color');
test('decimalToHex', function (t) {
t.strictEqual(color.decimalToHex(0), '#000000');
t.strictEqual(color.decimalToHex(1), '#000001');
t.strictEqual(color.decimalToHex(16777215), '#ffffff');
t.strictEqual(color.decimalToHex(-16777215), '#000001');
t.strictEqual(color.decimalToHex(99999999), '#5f5e0ff');
t.end();
});
test('decimalToRgb', function (t) {
t.deepEqual(color.decimalToRgb(0), {r:0,g:0,b:0});
t.deepEqual(color.decimalToRgb(1), {r:0,g:0,b:1});
t.deepEqual(color.decimalToRgb(16777215), {r:255,g:255,b:255});
t.deepEqual(color.decimalToRgb(-16777215), {r:0,g:0,b:1});
t.deepEqual(color.decimalToRgb(99999999), {r:245,g:224,b:255});
t.end();
});
test('hexToRgb', function (t) {
t.deepEqual(color.hexToRgb('#000'), {r:0,g:0,b:0});
t.deepEqual(color.hexToRgb('#000000'), {r:0,g:0,b:0});
t.deepEqual(color.hexToRgb('#fff'), {r:255,g:255,b:255});
t.deepEqual(color.hexToRgb('#ffffff'), {r:255,g:255,b:255});
t.deepEqual(color.hexToRgb('#0fa'), {r:0,g:255,b:170});
t.deepEqual(color.hexToRgb('#00ffaa'), {r:0,g:255,b:170});
t.deepEqual(color.hexToRgb('000'), {r:0,g:0,b:0});
t.deepEqual(color.hexToRgb('fff'), {r:255,g:255,b:255});
t.deepEqual(color.hexToRgb('00ffaa'), {r:0,g:255,b:170});
t.deepEqual(color.hexToRgb('0'), null);
t.deepEqual(color.hexToRgb('hello world'), null);
t.end();
});
test('rgbToHex', function (t) {
t.strictEqual(color.rgbToHex({r:0,g:0,b:0}), '#000000');
t.strictEqual(color.rgbToHex({r:255,g:255,b:255}), '#ffffff');
t.strictEqual(color.rgbToHex({r:0,g:255,b:170}), '#00ffaa');
t.end();
});
test('rgbToDecimal', function (t) {
t.strictEqual(color.rgbToDecimal({r:0,g:0,b:0}), 0);
t.strictEqual(color.rgbToDecimal({r:255,g:255,b:255}), 16777215);
t.strictEqual(color.rgbToDecimal({r:0,g:255,b:170}), 65450);
t.end();
});
test('hexToDecimal', function (t) {
t.strictEqual(color.hexToDecimal('#000'), 0);
t.strictEqual(color.hexToDecimal('#000000'), 0);
t.strictEqual(color.hexToDecimal('#fff'), 16777215);
t.strictEqual(color.hexToDecimal('#ffffff'), 16777215);
t.strictEqual(color.hexToDecimal('#0fa'), 65450);
t.strictEqual(color.hexToDecimal('#00ffaa'), 65450);
t.end();
});

37
test/unit/util_math.js Normal file
View file

@ -0,0 +1,37 @@
var test = require('tap').test;
var math = require('../../src/util/math-util');
test('degToRad', function (t) {
// @todo This is incorrect
t.strictEqual(math.degToRad(0), 1.5707963267948966);
t.strictEqual(math.degToRad(1), 1.5533430342749535);
t.strictEqual(math.degToRad(180), -1.5707963267948966);
t.strictEqual(math.degToRad(360), -4.71238898038469);
t.strictEqual(math.degToRad(720), -10.995574287564276);
t.end();
});
test('radToDeg', function (t) {
t.strictEqual(math.radToDeg(0), 0);
t.strictEqual(math.radToDeg(1), 57.29577951308232);
t.strictEqual(math.radToDeg(180), 10313.240312354817);
t.strictEqual(math.radToDeg(360), 20626.480624709635);
t.strictEqual(math.radToDeg(720), 41252.96124941927);
t.end();
});
test('clamp', function (t) {
t.strictEqual(math.clamp(0, 0, 10), 0);
t.strictEqual(math.clamp(1, 0, 10), 1);
t.strictEqual(math.clamp(-10, 0, 10), 0);
t.strictEqual(math.clamp(100, 0, 10), 10);
t.end();
});
test('wrapClamp', function (t) {
t.strictEqual(math.wrapClamp(0, 0, 10), 0);
t.strictEqual(math.wrapClamp(1, 0, 10), 1);
t.strictEqual(math.wrapClamp(-10, 0, 10), 1);
t.strictEqual(math.wrapClamp(100, 0, 10), 1);
t.end();
});

9
test/unit/util_xml.js Normal file
View file

@ -0,0 +1,9 @@
var test = require('tap').test;
var xml = require('../../src/util/xml-escape');
test('escape', function (t) {
var input = '<foo bar="he & llo \'"></foo>';
var output = '&lt;foo bar=&quot;he &amp; llo &apos;&quot;&gt;&lt;/foo&gt;';
t.strictEqual(xml(input), output);
t.end();
});