diff --git a/.travis.yml b/.travis.yml index f836d9861..091fe53fe 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,6 +6,9 @@ sudo: false cache: directories: - node_modules +before_install: +# Install the most up to date scratch-* dependencies +- rm -rf node_modules/scratch-* after_script: - | # RELEASE_BRANCHES and NPM_TOKEN defined in Travis settings panel diff --git a/playground/index.html b/playground/index.html index 173ab844e..804d31f8e 100644 --- a/playground/index.html +++ b/playground/index.html @@ -437,7 +437,7 @@ - + diff --git a/src/blocks/scratch3_motion.js b/src/blocks/scratch3_motion.js index 10679750d..8b8e7356e 100644 --- a/src/blocks/scratch3_motion.js +++ b/src/blocks/scratch3_motion.js @@ -22,6 +22,7 @@ Scratch3MotionBlocks.prototype.getPrimitives = function() { 'motion_turnleft': this.turnLeft, 'motion_pointindirection': this.pointInDirection, 'motion_glidesecstoxy': this.glide, + 'motion_setrotationstyle': this.setRotationStyle, 'motion_changexby': this.changeX, 'motion_setx': this.setX, '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) { var dx = Cast.toNumber(args.DX); util.target.setXY(util.target.x + dx, util.target.y); diff --git a/src/blocks/scratch3_procedures.js b/src/blocks/scratch3_procedures.js new file mode 100644 index 000000000..8d43c4c92 --- /dev/null +++ b/src/blocks/scratch3_procedures.js @@ -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.} 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; diff --git a/src/engine/adapter.js b/src/engine/adapter.js index 26f52be90..327aae5e3 100644 --- a/src/engine/adapter.js +++ b/src/engine/adapter.js @@ -1,3 +1,4 @@ +var mutationAdapter = require('./mutation-adapter'); var html = require('htmlparser2'); /** @@ -138,6 +139,9 @@ function domToBlock (blockDOM, blocks, isTopBlock, parent) { // Link next block to this block. block.next = childBlockNode.attribs.id; break; + case 'mutation': + block.mutation = mutationAdapter(xmlChild); + break; } } } diff --git a/src/engine/blocks.js b/src/engine/blocks.js index 99f52477d..f96c916ad 100644 --- a/src/engine/blocks.js +++ b/src/engine/blocks.js @@ -1,4 +1,5 @@ var adapter = require('./adapter'); +var mutationAdapter = require('./mutation-adapter'); var xmlEscape = require('../util/xml-escape'); /** @@ -116,6 +117,16 @@ Blocks.prototype.getInputs = function (id) { 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. * @param {?string} id ID of block to query. @@ -130,6 +141,23 @@ Blocks.prototype.getTopLevelScript = function (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) { // 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].fields[args.name] === 'undefined') return; - // Update block value - this._blocks[args.id].fields[args.name].value = args.value; + if (args.element == 'field') { + // 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 + '"' + xy + '>'; + // Add any mutation. Must come before inputs. + if (block.mutation) { + xmlString += this.mutationToXML(block.mutation); + } // Add any inputs on this block. for (var input in block.inputs) { var blockInput = block.inputs[input]; @@ -389,6 +425,25 @@ Blocks.prototype.blockToXML = function (blockId) { 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 += ''; + return mutationString; +}; + // --------------------------------------------------------------------- /** diff --git a/src/engine/execute.js b/src/engine/execute.js index 29116d5e5..59b4a3cbf 100644 --- a/src/engine/execute.js +++ b/src/engine/execute.js @@ -132,6 +132,12 @@ var execute = function (sequencer, thread) { 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, // and `argValues` is fully populated. So, execute the block primitive. // First, clear `currentStackFrame.reported`, so any subsequent execution @@ -155,6 +161,9 @@ var execute = function (sequencer, thread) { startBranch: function (branchNum) { sequencer.stepToBranch(thread, branchNum); }, + startProcedure: function (procedureName) { + sequencer.stepToProcedure(thread, procedureName); + }, startHats: function(requestedHat, opt_matchFields, opt_target) { return ( runtime.startHats(requestedHat, opt_matchFields, opt_target) diff --git a/src/engine/mutation-adapter.js b/src/engine/mutation-adapter.js new file mode 100644 index 000000000..12dc123e1 --- /dev/null +++ b/src/engine/mutation-adapter.js @@ -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; +} diff --git a/src/engine/runtime.js b/src/engine/runtime.js index 2792dab2b..bad95bdf7 100644 --- a/src/engine/runtime.js +++ b/src/engine/runtime.js @@ -16,7 +16,8 @@ var defaultBlockPackages = { 'scratch3_operators': require('../blocks/scratch3_operators'), 'scratch3_sound': require('../blocks/scratch3_sound'), 'scratch3_sensing': require('../blocks/scratch3_sensing'), - 'scratch3_data': require('../blocks/scratch3_data') + 'scratch3_data': require('../blocks/scratch3_data'), + 'scratch3_procedures': require('../blocks/scratch3_procedures') }; /** diff --git a/src/engine/sequencer.js b/src/engine/sequencer.js index 690ab61a7..85575c804 100644 --- a/src/engine/sequencer.js +++ b/src/engine/sequencer.js @@ -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. * @param {!Thread} thread Thread object to step to reporter. diff --git a/src/import/sb2import.js b/src/import/sb2import.js index 5d383cc74..50911c83e 100644 --- a/src/import/sb2import.js +++ b/src/import/sb2import.js @@ -6,6 +6,7 @@ */ var Blocks = require('../engine/blocks'); +var Clone = require('../sprites/clone'); var Sprite = require('../sprites/sprite'); var Color = require('../util/color.js'); var uid = require('../util/uid'); @@ -123,7 +124,16 @@ function parseScratchObject (object, runtime, topLevel) { target.visible = object.visible; } 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.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; } diff --git a/src/import/sb2specmap.js b/src/import/sb2specmap.js index 693d82f81..76e9683e1 100644 --- a/src/import/sb2specmap.js +++ b/src/import/sb2specmap.js @@ -1374,15 +1374,20 @@ var specMap = { ] }, 'procDef':{ - 'opcode':'proc_def', - 'argMap':[] + 'opcode':'procedures_defnoreturn', + 'argMap':[ + { + 'type':'field', + 'fieldName':'NAME' + } + ] }, 'getParam':{ 'opcode':'proc_param', 'argMap':[] }, 'call':{ - 'opcode':'proc_call', + 'opcode':'procedures_callnoreturn', 'argMap':[] } }; diff --git a/src/sprites/clone.js b/src/sprites/clone.js index 5b3d2def2..c1b3d42cf 100644 --- a/src/sprites/clone.js +++ b/src/sprites/clone.js @@ -29,6 +29,20 @@ function Clone(sprite, runtime) { * @type {?Number} */ this.drawableID = null; + + /** + * Map of current graphic effect values. + * @type {!Object.} + */ + this.effects = { + 'color': 0, + 'fisheye': 0, + 'whirl': 0, + 'pixelate': 0, + 'mosaic': 0, + 'brightness': 0, + 'ghost': 0 + }; } util.inherits(Clone, Target); @@ -38,7 +52,6 @@ util.inherits(Clone, Target); Clone.prototype.initDrawable = function () { if (this.renderer) { this.drawableID = this.renderer.createDrawable(); - this.updateAllDrawableProperties(); } // If we're a clone, start the hats. if (!this.isOriginal) { @@ -99,18 +112,29 @@ Clone.prototype.size = 100; Clone.prototype.currentCostume = 0; /** - * Map of current graphic effect values. - * @type {!Object.} + * Rotation style for "all around"/spinning. + * @enum */ -Clone.prototype.effects = { - 'color': 0, - 'fisheye': 0, - 'whirl': 0, - 'pixelate': 0, - 'mosaic': 0, - 'brightness': 0, - 'ghost': 0 -}; +Clone.ROTATION_STYLE_ALL_AROUND = 'all around'; + +/** + * Rotation style for "left-right"/flipping. + * @enum + */ +Clone.ROTATION_STYLE_LEFT_RIGHT = 'left-right'; + +/** + * 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. /** @@ -131,6 +155,26 @@ Clone.prototype.setXY = function (x, y) { } }; +/** + * Get the rendered direction and scale, after applying rotation style. + * @return {Object} 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. * @param {!number} direction New direction of clone. @@ -142,8 +186,10 @@ Clone.prototype.setDirection = function (direction) { // Keep direction between -179 and +180. this.direction = MathUtil.wrapClamp(direction, -179, 180); if (this.renderer) { + var renderedDirectionScale = this._getRenderedDirectionAndScale(); 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%. this.size = MathUtil.clamp(size, 5, 535); if (this.renderer) { + var renderedDirectionScale = this._getRenderedDirectionAndScale(); 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) { // Keep the costume index within possible values. + index = Math.round(index); this.currentCostume = MathUtil.wrapClamp( 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. * @param {?string} costumeName Name of a costume. @@ -275,10 +345,11 @@ Clone.prototype.getSoundIndexByName = function (soundName) { */ Clone.prototype.updateAllDrawableProperties = function () { if (this.renderer) { + var renderedDirectionScale = this._getRenderedDirectionAndScale(); this.renderer.updateDrawableProperties(this.drawableID, { position: [this.x, this.y], - direction: this.direction, - scale: [this.size, this.size], + direction: renderedDirectionScale.direction, + scale: renderedDirectionScale.scale, visible: this.visible, skin: this.sprite.costumes[this.currentCostume].skin }); @@ -340,6 +411,7 @@ Clone.prototype.makeClone = function () { newClone.visible = this.visible; newClone.size = this.size; newClone.currentCostume = this.currentCostume; + newClone.rotationStyle = this.rotationStyle; newClone.effects = JSON.parse(JSON.stringify(this.effects)); newClone.variables = JSON.parse(JSON.stringify(this.variables)); newClone.lists = JSON.parse(JSON.stringify(this.lists)); diff --git a/src/sprites/sprite.js b/src/sprites/sprite.js index cfa8de90d..53f16e286 100644 --- a/src/sprites/sprite.js +++ b/src/sprites/sprite.js @@ -54,7 +54,6 @@ Sprite.prototype.createClone = function () { this.clones.push(newClone); if (newClone.isOriginal) { newClone.initDrawable(); - newClone.updateAllDrawableProperties(); } return newClone; }; diff --git a/test/unit/adapter.js b/test/unit/engine_adapter.js similarity index 100% rename from test/unit/adapter.js rename to test/unit/engine_adapter.js diff --git a/test/unit/blocks.js b/test/unit/engine_blocks.js similarity index 100% rename from test/unit/blocks.js rename to test/unit/engine_blocks.js diff --git a/test/unit/runtime.js b/test/unit/engine_runtime.js similarity index 100% rename from test/unit/runtime.js rename to test/unit/engine_runtime.js diff --git a/test/unit/sequencer.js b/test/unit/engine_sequencer.js similarity index 100% rename from test/unit/sequencer.js rename to test/unit/engine_sequencer.js diff --git a/test/unit/thread.js b/test/unit/engine_thread.js similarity index 100% rename from test/unit/thread.js rename to test/unit/engine_thread.js diff --git a/test/unit/sprites_clone.js b/test/unit/sprites_clone.js new file mode 100644 index 000000000..246e1b955 --- /dev/null +++ b/test/unit/sprites_clone.js @@ -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(); +}); diff --git a/test/unit/util_cast.js b/test/unit/util_cast.js new file mode 100644 index 000000000..1d372213e --- /dev/null +++ b/test/unit/util_cast.js @@ -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(); +}); diff --git a/test/unit/util_color.js b/test/unit/util_color.js new file mode 100644 index 000000000..ba7fa059c --- /dev/null +++ b/test/unit/util_color.js @@ -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(); +}); diff --git a/test/unit/util_math.js b/test/unit/util_math.js new file mode 100644 index 000000000..b282e90cc --- /dev/null +++ b/test/unit/util_math.js @@ -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(); +}); diff --git a/test/unit/timer.js b/test/unit/util_timer.js similarity index 100% rename from test/unit/timer.js rename to test/unit/util_timer.js diff --git a/test/unit/util_xml.js b/test/unit/util_xml.js new file mode 100644 index 000000000..1906a2ab6 --- /dev/null +++ b/test/unit/util_xml.js @@ -0,0 +1,9 @@ +var test = require('tap').test; +var xml = require('../../src/util/xml-escape'); + +test('escape', function (t) { + var input = ''; + var output = '<foo bar="he & llo '"></foo>'; + t.strictEqual(xml(input), output); + t.end(); +});