src/edgar.js
/**
* Js.Edgar is a lightweight Spy/Mock library for testing JavaScript.
*
* @package Js.Edgar
* @version 1.0.3
* @author Jordan Hawker <hawker.jordan@gmail.com>
* www.JordanHawker.com
* @license MIT
*/
(function (root, factory) {
if (typeof define === 'function' && define.amd) { // AMD module
define([], factory);
} else if (typeof exports === 'object') {
if (typeof module === 'object' && module.exports) {
module.exports = factory();
}
exports.Edgar = factory();
} else { // Browser globals (root is window)
root.Edgar = factory();
}
}(this, function () {
var Edgar = {
spies: {},
mocks: {},
/**
* Adds the Spy to a hash so it is accessible later when needed
*
* @param {Spy} spy - The Spy to be tracked
* @param {string} method - The name of the method the Spy is watching
*/
addSpy: function (spy, method) {
var spies = this.spies[method];
if (!spies) {
this.spies[method] = spies = [];
}
spies.push(spy);
},
/**
* Retrieves a Spy from the hash lookup
*
* @param {object} obj - The object being watched
* @param {string} method - The name of the method being watched
* @returns {Spy} The Spy that corresponds to the given object/method
*/
getSpy: function (obj, method) {
if (typeof method !== 'string') {
throw 'Failed to find spy: method name was invalid.';
}
if (this.spies.hasOwnProperty(method)) {
var spies = this.spies[method];
if (spies && spies.length) {
return spies.filter(function (spy) {
return spy.obj === obj;
})[0];
}
}
},
/**
* External API to create a Spy
*
* @param {object} obj - The object to watch
* @param {string} method - The name of the method to watch
* @param {*} [value] - A mock value to return or method to invoke
* @returns {Spy} A Spy watching the given object/method
*/
createSpy: function (obj, method, value) {
var type = typeof obj;
var spy;
if (type === 'undefined') { // Create a callback
throw 'Mocking without objects is not yet supported' + (typeof method === 'string' ? (': ' + method) : '');
// return this.createMock(obj);
} else if ((type === 'object' && obj !== null) || type === 'function') {
if (typeof method === 'string') { // Create a normal spy
spy = this.getSpy(obj, method);
if (!spy) {
// Don't create a new spy if one already exists for this method
spy = new this.Spy(obj, method, value);
this.addSpy(spy, method);
} else if (value !== undefined) {
spy.value = value; // Update the value if a new one was passed in
}
return spy;
}
throw 'Spy Creation Failed: Method name must be a string.';
} else {
throw 'Spy Creation Failed: Object was not a valid type.';
}
},
// /**
// *
// */
// createMock: function(method) {
// if (typeof method === 'function') {
// var mock = new this.Mock(method);
// this.mocks.push(mock);
// return mock;
// }
// throw 'Mock Creation Failed: Input must be a function.'
// },
/**
* Releases every Spy tracked by Edgar
*/
releaseAll: function () {
var spies = this.spies;
var spyList;
var i;
for (var key in spies) {
if (spies.hasOwnProperty(key)) {
spyList = spies[key];
for (i = 0; i < spyList.length; i++) {
spyList[i].release();
}
}
}
},
/**
* Resets Edgar's Spy tracking
*/
removeSpies: function () {
this.spies = {};
},
/**
* Setup cleanup callback for QUnit tests
*/
setupQUnitCleanup: function (qunitObj) {
qunitObj.testDone(function () {
Edgar.releaseAll();
Edgar.removeSpies();
});
},
/**
* Setup cleanup callback for Mocha tests
*/
setupMochaCleanup: function (mochaObj) {
mochaObj.afterEach(function () {
Edgar.releaseAll();
Edgar.removeSpies();
});
}
};
Edgar.Spy = (function () {
/**
* Spy constructor
*
* @param {Object} obj - The object to watch
* @param {String} method - The name of the method to watch
* @param {*} [value] - A mock value to return or method to invoke
* @constructor
*/
function Spy(obj, method, value) {
if (!(this instanceof Spy)) {
return new Spy(obj, method, value);
}
var self = this;
/**
* Track a new call to the watched method,
* mocking or executing it as needed
*
* @returns {*} The return value from the mock or real method
*/
self.mock = function () {
var args = arguments;
var id = self.calls.length;
var returned = self.value;
var context = self.obj;
if (this !== self) {
context = this;
}
self.calls.push({
args: args,
context: context
});
if (self.execute) {
returned = self.method.apply(context, args);
} else if (self.invoke) {
returned = self.value.apply(context, args);
}
self.calls[id].returned = returned;
return returned;
};
/**
* Setup the Spy to invoke its mocked value
* rather than just returning it
*
* @returns {Spy} Itself
*/
self.andInvoke = self.startInvoking = function () {
if (typeof this.value === 'function') {
self.invoke = true;
return self;
}
throw 'Cannot invoke value that is not a function.';
};
/**
* Setup the Spy to call the live method being watched
*
* @returns {Spy} Itself
*/
self.andExecute = self.startExecuting = function () {
self.execute = true;
return self;
};
/**
* Setup the Spy to mock the method being watched
*
* @param {*} [value] - The value to return/invoke for the mock
* @returns {Spy} Itself
*/
self.andMock = self.startMocking = function (value) {
if (value !== undefined) {
self.value = value;
}
self.execute = false;
return self;
};
/**
* Find out how many calls have been made to the watched method
*
* @returns {number} The number of calls made
*/
self.called = function () {
return self.calls.length;
};
/**
* Get the arguments passed to the watched method,
* defaulting to the most recent call if no id is passed
*
* @param {number} [id] - The id of the call, in case of multiple
* @returns {array} The arguments array for that call
*/
self.calledWith = function (id) {
if (id !== undefined && id !== null) {
if (id >= 0 && id < self.calls.length) {
return self.calls[id].args;
}
throw 'Cannot get arguments for invalid call index.';
} else if (self.calls.length) {
return self.calls[self.calls.length - 1].args;
}
throw 'Cannot get arguments, spy has not been called.';
};
/**
* Get the return value passed from Spy.mock(),
* defaulting to the most recent call if no id is passed
*
* @param {number} [id] The id of the call, in case of multiple
* @returns {*} The return value for that call
*/
self.returnedWith = function (id) {
if (id !== undefined && id !== null) {
if (id >= 0 && id < self.calls.length) {
return self.calls[id].returned;
}
throw 'Cannot get return value for invalid call index.';
} else {
return self.calls[self.calls.length - 1].returned;
}
};
/**
* Get the context of 'this' passed to Spy.mock(),
* defaulting to the spied object if 'this' was the Spy itself
*
* @param {number} [id] The id of the call, in case of multiple
* @returns {*} The context of that call
*/
self.getContext = function (id) {
if (id !== undefined && id !== null) {
if (id >= 0 && id < self.calls.length) {
return self.calls[id].context;
}
throw 'Cannot get context for invalid call index.';
} else {
return self.calls[self.calls.length - 1].context;
}
};
/**
* Reset the calls tracked by the Spy
*
* @returns {Spy} Itself
*/
self.reset = function () {
var calls = self.calls;
self.calls = [];
return self;
};
/**
* Release the watched method back
* to its original functionality
*
* @returns {Spy} Itself
*/
self.release = function () {
self.obj[self.name] = self.method;
return self;
};
/**
* Resumes mocking the watched method
*
* @returns {Spy} Itself
*/
self.resume = function () {
self.obj[self.name] = self.mock;
return self;
};
// Defaults and Storage
self.obj = obj;
self.name = method;
self.method = obj[method];
self.value = value;
self.execute = false;
self.invoke = null;
self.calls = [];
obj[method] = self.mock;
}
return Spy;
})();
/**
*
* @param method
* @constructor
*/
// Edgar.prototype.Mock = function(method) {
// var spy = new Edgar.Spy({}, 'mock', method, true),
// mock = spy.mock;
//
// mock.prototype = spy.prototype;
//
// return mock;
// };
if (typeof QUnit !== 'undefined') {
Edgar.setupQUnitCleanup(QUnit);
}
if (typeof mocha !== 'undefined') {
Edgar.setupMochaCleanup(mocha);
}
return Edgar;
}));