From 3c6696a78fe9a17f9dc9b53558d65ccd627bbaae Mon Sep 17 00:00:00 2001 From: Andrew Stewart Date: Mon, 10 Mar 2014 12:00:48 -0700 Subject: [PATCH 1/3] Reorganize Utils for easier testing --- lib/utils.js | 311 ++++++++++++++++++++++++++------------------------- 1 file changed, 159 insertions(+), 152 deletions(-) diff --git a/lib/utils.js b/lib/utils.js index b83afc7..f285374 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -6,158 +6,6 @@ * Licensed under the Apache 2.0 license. */ -// Public: Alias to setInterval, combined with Number monkeypatches below to -// create an artoo-like syntax. -// -// interval - interval to run action on -// action - action to perform at interval -// -// Examples -// -// every 5.seconds(), -> console.log("hello world (and again in 5 seconds)!") -// -// Returns an interval -global.every = function(interval, action) { - return setInterval(action, interval); -}; - -// Public: Alias to setTimeout, combined with Number monkeypatches below to -// create an artoo-like syntax. -// -// interval - interval to run action on -// action - action to perform at interval -// -// Examples -// -// after 10.seconds(), -> console.log("hello world from ten seconds ago!") -// -// Returns an interval -global.after = function(delay, action) { - return setTimeout(action, delay); -}; - -// Public: Alias to the `every` function, but passing 0 -// Examples -// -// constantly -> console.log("hello world (and again and again)!") -// -// Returns an interval -global.constantly = function(action) { - return every(0, action); -}; - -// Public: Sleep - do nothing for some duration of time. -// -// ms - number of ms to sleep for -// -// Returns a function -// Examples: -// sleep 1.second() -global.sleep = function(ms) { - var start = Date.now(); - - while(Date.now() < start + ms) { - var i = 0; - } -}; - -// Copies -global.slice = [].slice; -global.hasProp = {}.hasOwnProperty; - -// Public: Function to use for class inheritance. Copy of a CoffeeScript helper -// function. -// -// Example -// -// var Sphero = (function(klass) { -// subclass(Sphero, klass); -// // Sphero is now a subclass of Parent, and can access it's methods through -// // Sphero.__super__ -// })(Parent); -// -// Returns subclass -global.subclass = function(child, parent) { - var ctor = function() { this.constructor = child; }; - - for (var key in parent) { - if (hasProp.call(parent, key)) { child[key] = parent[key]; } - } - - ctor.prototype = parent.prototype; - child.prototype = new ctor(); - child.__super__ = parent.prototype; - return child; -}; - -// Public: Proxies a list of methods from one object to another. It will not -// overwrite existing methods unless told to. -// -// methods - array of functions to proxy -// target - object to proxy the functions to -// base - (optional) object that proxied functions will be declared on. Defaults -// to this -// force - (optional) boolean - whether or not to force method assignment -// -// Returns base -global.proxyFunctionsToObject = function(methods, target, base, force) { - if (base == null) { base = this; } - if (force == null) { force = false; } - - var fn = function(method) { - return base[method] = function() { - var args = arguments.length >= 1 ? [].slice.call(arguments, 0) : []; - return target[method].apply(target, args); - }; - }; - - for (var i = 0; i < methods.length; i++) { - var method = methods[i]; - if (!force) { - if (typeof base[method] === 'function') { continue; } - } - - fn(method); - } - return base; -}; - -// Public: Proxies a list of methods for test stubbing. -// -// methods - array of functions to proxy -// base - (optional) object that proxied functions will be declared on. Defaults -// to this -// -// Returns base -global.proxyTestStubs = function(methods, base) { - if (base == null) { base = this; } - - methods.forEach(function(method) { - base[method] = function() { return true; }; - base.commandList.push(method); - }); - - return base; -}; - -// Public: Binds an argument to a caller -// -// fn - function to bind -// me - value for 'this' scope inside the function -// -// Examples -// -// var me = { hello: "Hello World" }; -// var proxy = { boundMethod: function() { return this.hello; } }; -// proxy.boundMethod = bind(proxy.boundMethod, me); -// proxy.boundMethod(); -// //=> "Hello World" -// -// Returns a function wrapper -global.bind = function(fn, me) { - return function() { return fn.apply(me, arguments); }; -}; - // Public: Monkey-patches Number to have Rails-like //seconds() function. Warning, // due to the way the Javascript parser works, applying functions on numbers is // kind of weird. See examples for details. @@ -219,3 +67,162 @@ Number.prototype.fromScale = function(start, end) { Number.prototype.toScale = function(start, end) { return Math.ceil(this * (Math.max(start, end) - Math.min(start, end)) + Math.min(start, end)); }; + +var Utils = { + // Public: Alias to setInterval, combined with Number monkeypatches below to + // create an artoo-like syntax. + // + // interval - interval to run action on + // action - action to perform at interval + // + // Examples + // + // every 5.seconds(), -> console.log("hello world (and again in 5 seconds)!") + // + // Returns an interval + every: function(interval, action) { + return setInterval(action, interval); + }, + + // Public: Alias to setTimeout, combined with Number monkeypatches below to + // create an artoo-like syntax. + // + // interval - interval to run action on + // action - action to perform at interval + // + // Examples + // + // after 10.seconds(), -> console.log("hello world from ten seconds ago!") + // + // Returns an interval + after: function(delay, action) { + return setTimeout(action, delay); + }, + + // Public: Alias to the `every` function, but passing 0 + // Examples + // + // constantly -> console.log("hello world (and again and again)!") + // + // Returns an interval + constantly: function(action) { + return every(0, action); + }, + + // Public: Sleep - do nothing for some duration of time. + // + // ms - number of ms to sleep for + // + // Returns a function + // Examples: + // sleep 1.second() + sleep: function(ms) { + var start = Date.now(); + + while(Date.now() < start + ms) { + var i = 0; + } + }, + + // Copies + slice: [].slice, + hasProp: {}.hasOwnProperty, + + // Public: Function to use for class inheritance. Copy of a CoffeeScript helper + // function. + // + // Example + // + // var Sphero = (function(klass) { + // subclass(Sphero, klass); + // // Sphero is now a subclass of Parent, and can access it's methods through + // // Sphero.__super__ + // })(Parent); + // + // Returns subclass + subclass: function(child, parent) { + var ctor = function() { this.constructor = child; }; + + for (var key in parent) { + if (hasProp.call(parent, key)) { child[key] = parent[key]; } + } + + ctor.prototype = parent.prototype; + child.prototype = new ctor(); + child.__super__ = parent.prototype; + return child; + }, + + // Public: Proxies a list of methods from one object to another. It will not + // overwrite existing methods unless told to. + // + // methods - array of functions to proxy + // target - object to proxy the functions to + // base - (optional) object that proxied functions will be declared on. Defaults + // to this + // force - (optional) boolean - whether or not to force method assignment + // + // Returns base + proxyFunctionsToObject: function(methods, target, base, force) { + if (base == null) { base = this; } + if (force == null) { force = false; } + + var fn = function(method) { + return base[method] = function() { + var args = arguments.length >= 1 ? [].slice.call(arguments, 0) : []; + return target[method].apply(target, args); + }; + }; + + for (var i = 0; i < methods.length; i++) { + var method = methods[i]; + if (!force) { + if (typeof base[method] === 'function') { continue; } + } + + fn(method); + } + return base; + }, + + // Public: Proxies a list of methods for test stubbing. + // + // methods - array of functions to proxy + // base - (optional) object that proxied functions will be declared on. Defaults + // to this + // + // Returns base + proxyTestStubs: function(methods, base) { + if (base == null) { base = this; } + + methods.forEach(function(method) { + base[method] = function() { return true; }; + base.commandList.push(method); + }); + + return base; + }, + + // Public: Binds an argument to a caller + // + // fn - function to bind + // me - value for 'this' scope inside the function + // + // Examples + // + // var me = { hello: "Hello World" }; + // var proxy = { boundMethod: function() { return this.hello; } }; + // proxy.boundMethod = bind(proxy.boundMethod, me); + // proxy.boundMethod(); + // //=> "Hello World" + // + // Returns a function wrapper + bind: function(fn, me) { + return function() { return fn.apply(me, arguments); }; + } +}; + +// Add all utility functions to global namespace +for (var util in Utils) { global[util] = Utils[util]; } + +module.exports = Utils; From 4c464f0d88d4b991914289a05e609ff17ae798de Mon Sep 17 00:00:00 2001 From: Andrew Stewart Date: Mon, 10 Mar 2014 15:25:39 -0700 Subject: [PATCH 2/3] Remove Math.ceil from #toScale, JS has no integers/floats anyways --- lib/utils.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/utils.js b/lib/utils.js index f285374..e28aef9 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -65,7 +65,7 @@ Number.prototype.fromScale = function(start, end) { // // Returns an integer representing the scaled value Number.prototype.toScale = function(start, end) { - return Math.ceil(this * (Math.max(start, end) - Math.min(start, end)) + Math.min(start, end)); + return this * (Math.max(start, end) - Math.min(start, end)) + Math.min(start, end); }; var Utils = { From 0ebd4153d446cbfe70b2e9eef3da044d46527d2e Mon Sep 17 00:00:00 2001 From: Andrew Stewart Date: Mon, 10 Mar 2014 15:26:19 -0700 Subject: [PATCH 3/3] Improve/expand test coverage for utils --- test/specs/utils.spec.js | 187 +++++++++++++++++++++++++++++---------- 1 file changed, 142 insertions(+), 45 deletions(-) diff --git a/test/specs/utils.spec.js b/test/specs/utils.spec.js index 86c25d3..42dd442 100644 --- a/test/specs/utils.spec.js +++ b/test/specs/utils.spec.js @@ -1,38 +1,148 @@ -'use strict'; -source("utils"); +"use strict"; + +var utils = source("utils"); describe("Utils", function() { - describe("Monkeypatches Number", function() { - it("adds seconds() method", function() { - 5..seconds().should.be.equal(5000); + describe("Monkey-patches", function() { + describe("Number", function() { + describe("#seconds", function() { + it("allows for expressing time in seconds", function() { + expect((5).seconds()).to.be.eql(5000); + }); + }); + + describe("#second", function() { + it("allows for expressing time in seconds", function() { + expect((1).second()).to.be.eql(1000); + }); + }); + + describe("#fromScale", function() { + it("converts a value from one scale to 0-1 scale", function() { + expect((5).fromScale(0, 10)).to.be.eql(0.5); + }); + + it("converts floats", function() { + expect(2.5.fromScale(0, 10)).to.be.eql(0.25); + }); + }); + + describe("#toScale", function() { + it("converts a value from 0-1 scale to another", function() { + expect((0.5).toScale(0, 10)).to.be.eql(5); + }); + + it("converts to floats", function() { + expect(0.25.toScale(0, 10)).to.be.eql(2.5); + }); + + it("can be chained with #fromScale", function() { + var num = (5).fromScale(0, 20).toScale(0, 10); + expect(num).to.be.eql(2.5); + }); + }); + }); + }); + + describe("#every", function() { + before(function() { + this.clock = sinon.useFakeTimers(); }); - it("adds second() method", function() { - 1..second().should.be.equal(1000); + after(function() { + this.clock.restore(); }); - it("scales an Integer", function() { - 2..fromScale(1, 10).toScale(1, 20).should.be.equal(4); + it("sets a function to be called every time an interval passes", function() { + var func = spy(); + utils.every(10, func); + this.clock.tick(25); + expect(func).to.be.calledTwice; + }); + }); + + describe("#after", function() { + before(function() { + this.clock = sinon.useFakeTimers(); }); - it("scales a right angle", function() { - 90..fromScale(1, 180).toScale(-90, 90).should.be.equal(0); + after(function() { + this.clock.restore(); }); - it("scales an acute angle", function() { - 45..fromScale(1, 180).toScale(0, 90).should.be.equal(23); + it("sets a function to be called after time an interval passes", function() { + var func = spy(); + utils.after(10, func); + this.clock.tick(15); + expect(func).to.be.called; + }); + }); + + describe("constantly", function() { + before(function() { + stub(global, 'every').returns(0); }); - it("scales a Float", function() { - 2.5.fromScale(1, 10).toScale(1, 20).should.be.equal(5); + after(function() { + global.every.restore(); + }); + + it("schedules a task to run continuously with #every", function() { + var func = function() {}; + utils.constantly(func); + + expect(global.every).to.be.calledWith(0, func); + }); + }); + + describe("#slice", function() { + it("performs array slices", function() { + var arr = [1, 2, 3, 4, 5]; + expect(slice.call(arr, 1)).to.be.eql([2, 3, 4, 5]); + }); + }); + + describe("hasProp", function() { + it("checks objects have properties", function() { + var obj = { test: 'test' }; + expect(hasProp.call(obj, 'test')).to.be.true; + }); + }); + + describe("#subclass", function() { + var BaseClass = (function() { + function BaseClass(opts) { + this.greeting = opts.greeting; + }; + + BaseClass.prototype.sayHi = function() { + return "Hi!"; + }; + + return BaseClass + })(); + + var SubClass = (function(klass) { + subclass(SubClass, klass); + + function SubClass(opts) { + SubClass.__super__.constructor.apply(this, arguments); + }; + + return SubClass; + })(BaseClass); + + it("adds inheritance to Javascript classes", function() { + var sub = new SubClass({greeting: "Hello World"}); + expect(sub.greeting).to.be.eql("Hello World"); + expect(sub.sayHi()).to.be.eql("Hi!"); }); }); describe("#proxyFunctionsToObject", function() { - var ProxyClass, TestClass, methods; - methods = ['asString', 'toString', 'returnString']; + var methods = ['asString', 'toString', 'returnString']; - ProxyClass = (function() { + var ProxyClass = (function() { function ProxyClass() {} ProxyClass.prototype.asString = function() { @@ -50,7 +160,7 @@ describe("Utils", function() { return ProxyClass; })(); - TestClass = (function() { + var TestClass = (function() { function TestClass() { this.self = this; this.testInstance = new ProxyClass; @@ -60,54 +170,41 @@ describe("Utils", function() { return TestClass; })(); + var testclass = new TestClass(); + it('can alias methods', function() { - var testclass; - testclass = new TestClass; - assert(typeof testclass.asString === 'function'); - testclass.asString().should.be.equal("[object ProxyClass]"); + expect(testclass.asString()).to.be.eql("[object ProxyClass]"); }); it('can alias existing methods if forced to', function() { - var testclass; - testclass = new TestClass; - assert(typeof testclass.toString === 'function'); - testclass.toString().should.be.equal("[object ProxyClass]"); + expect(testclass.toString()).to.be.eql("[object ProxyClass]"); }); it('can alias methods with arguments', function() { - var testclass; - testclass = new TestClass; - assert(typeof testclass.returnString === 'function'); - testclass.returnString("testString").should.be.equal("testString"); + expect(testclass.returnString).to.be.a('function'); }); }); describe("#proxyTestStubs", function() { it("proxies methods to an object's commandList", function() { - var base, methods; - methods = ["hello", "goodbye"]; - base = { - commandList: [] - }; + var methods = ["hello", "goodbye"], + base = { commandList: [] }; + proxyTestStubs(methods, base); expect(base.commandList).to.be.eql(methods); }); it("returns the object methods have been proxied to", function() { - var base, methods; - methods = ["hello", "goodbye"]; - base = { - commandList: [] - }; + var methods = ["hello", "goodbye"], + base = { commandList: [] }; + expect(proxyTestStubs(methods, base)).to.be.eql(base); }); }); describe("#bind", function() { - var me = { hello: "Hello World" }; - var proxy = { - boundMethod: function() { return this.hello; } - }; + var me = { hello: "Hello World" }, + proxy = { boundMethod: function() { return this.hello; } }; it("binds the 'this' scope for the method", function() { proxy.boundMethod = function() { return this.hello; };