Skip to content

Commit

Permalink
feat(sandbox): adds support for chai.spy.sandbox
Browse files Browse the repository at this point in the history
Also adds DEFAULT_SANDBOX and all spies from chai.spy.on and chai.spy.object are tracked under DEFAULT_SANDBOX

Fixes chaijs#38
  • Loading branch information
stalniy committed Dec 10, 2016
1 parent b522148 commit d7ad443
Show file tree
Hide file tree
Showing 2 changed files with 258 additions and 48 deletions.
245 changes: 211 additions & 34 deletions lib/spy.js
Expand Up @@ -13,6 +13,165 @@ module.exports = function (chai, _) {
var Assertion = chai.Assertion
, flag = _.flag
, i = _.inspect
, ID_KEY = typeof Symbol === 'undefined' ? '__id' : Symbol('id')
, STATE_KEY = typeof Symbol === 'undefined' ? '__state' : Symbol('state')
, spyAmount = 0
, DEFAULT_SANDBOX = new Sandbox()

/**
* # Sandbox constructor (function)
*
* Initialize new Sandbox instance
*
* @returns new sandbox
* @api private
*/

function Sandbox() {
this[STATE_KEY] = {};
}

/**
* # Sandbox.on (function)
*
* Wraps an object method into spy assigned to sandbox. All calls will
* pass through to the original function.
*
* var spy = chai.spy.sandbox();
* var isArray = spy.on(Array, 'isArray');
*
* @param {Object} object
* @param {String} method name to spy on
* @returns created spy
* @api public
*/

Sandbox.prototype.on = function (object, methodName) {
var method = chai.spy('object.' + methodName, object[methodName]);

method[ID_KEY] = ++spyAmount;
this[STATE_KEY][method[ID_KEY]] = method;
method.__spy.tracked = {
object: object
, methodName: methodName
, originalMethod: object[methodName]
, isOwnMethod: object.hasOwnProperty(methodName)
};
object[methodName] = method;

return method;
};

/**
* # Sandbox.object (function)
*
* Creates an object with spied methods if first argument is a String.
* Wraps passed object's methods with spies if first argument is an Object.
* In case, if first parameter is an object and second is omit wraps
* all enumerable methods of passed object.
*
* var spy = chai.spy.sandbox();
* var object = spy.object('Array', [ 'push', 'pop' ]);
* var array = spy.object([], [ 'push', 'pop' ]);
*
* @param {String|Object} name object or object name
* @param {String[]} [methods] method names
* @returns object with spied methods
* @api public
*/

Sandbox.prototype.object = function (name, methods) {
var defs = {};
var sandbox = this;
var object;

if (name && typeof name === 'object') {
object = name;
methods = methods || Object.keys(object);
methods.forEach(function (methodName) {
var method = object[methodName];

if (typeof method === 'function' && !method.__spy) {
sandbox.on(object, methodName);
}
})
} else {
object = methods.reduce(function (object, methodName) {
object[methodName] = chai.spy(name + '.' + methodName);
return object;
}, {});
}

return object;
};

/**
* # Sandbox.restore (function)
*
* Restores previously wrapped object's method.
* Restores all spied objects of a sandbox if called without parameters.
*
* var spy = chai.spy.sandbox();
* var object = spy.on(Array, 'isArray');
* spy.restore(Array, 'isArray'); // or spy.restore();
*
* @param {Object} [object]
* @param {String} [methodName] method name
* @api public
*/

Sandbox.prototype.restore = function (object, methodName) {
var hasFilter = Boolean(object && methodName);
var sandbox = this;

Object.keys(this[STATE_KEY]).some(function (spyId) {
var spy = sandbox[STATE_KEY][spyId];
var tracked = spy.__spy.tracked;
var isObjectSpied = !object || object === tracked.object;
var isMethodSpied = !methodName || methodName === tracked.methodName;

delete sandbox[STATE_KEY][spyId];

if (!isObjectSpied && !isMethodSpied) {
return false;
}

sandbox.restoreTrackedObject(spy);

if (hasFilter) {
return true;
}
});
};

/**
* # Sandbox.restoreTrackedObject (function)
*
* Restores tracked object's method
*
* var spy = chai.spy.sandbox();
* var isArray = spy.on(Array, 'isArray');
* spy.restoreTrackedObject(isArray);
*
* @param {Spy} spy
* @api private
*/

Sandbox.prototype.restoreTrackedObject = function (spy) {
var tracked = spy.__spy.tracked;

if (!tracked) {
return;
}

if (tracked.isOwnMethod) {
tracked.object[tracked.methodName] = tracked.originalMethod;
} else {
delete tracked.object[tracked.methodName];
}

spy.__spy.tracked = null;
};

/**
* # chai.spy (function)
Expand Down Expand Up @@ -62,10 +221,11 @@ module.exports = function (chai, _) {

proxy.prototype = fn.prototype;
proxy.toString = function toString() {
var l = this.__spy.calls.length;
var state = this.__spy;
var l = state.calls.length;
var s = "{ Spy";
if (this.__spy.name)
s += " '" + this.__spy.name + "'";
if (state.name)
s += " '" + state.name + "'";
if (l > 0)
s += ", " + l + " call" + (l > 1 ? 's' : '');
s += " }";
Expand All @@ -75,7 +235,7 @@ module.exports = function (chai, _) {
/**
* # proxy.reset (function)
*
* Resets __spy object parameters for instantiation and reuse
* Resets spy's state object parameters for instantiation and reuse
* @returns proxy spy object
*/
proxy.reset = function() {
Expand All @@ -91,59 +251,76 @@ module.exports = function (chai, _) {
}

/**
* # chai.spy.on (function)
* # chai.spy.sandbox (function)
*
* Wraps an object method into spy. All calls will
* pass through to the original function.
* Creates sandbox which allow to restore spied objects with spy.on.
* All calls will pass through to the original function.
*
* var spy = chai.spy.on(Array, 'isArray');
* var spy = chai.spy.sandbox();
* var isArray = spy.on(Array, 'isArray');
*
* @param {Object} object
* @param {...String} method names to spy on
* @param {String} method name to spy on
* @returns passed object
* @api public
*/

chai.spy.on = function (object) {
var methodNames = Array.prototype.slice.call(arguments, 1);
chai.spy.sandbox = function () {
return new Sandbox()
};

methodNames.forEach(function(methodName) {
object[methodName] = chai.spy(object[methodName]);
});
/**
* # chai.spy.on (function)
*
* The same as Sandbox.on.
* Assignes newly created spy to DEFAULT sandbox
*
* var isArray = chai.spy.on(Array, 'isArray');
*
* @see Sandbox.on
* @api public
*/

return object;
chai.spy.on = function () {
return DEFAULT_SANDBOX.on.apply(DEFAULT_SANDBOX, arguments)
};

/**
* # chai.spy.object (function)
*
* Creates an object with spied methods.
* The same as Sandbox.object.
* Assignes newly created spy to DEFAULT sandbox
*
* var object = chai.spy.object('Array', [ 'push', 'pop' ]);
* var array = chai.spy.object([], [ 'push', 'pop' ]);
*
* @param {String} [name] object name
* @param {String[]|Object} method names or method definitions
* @returns object with spied methods
* @see Sandbox.object
* @api public
*/

chai.spy.object = function (name, methods) {
var defs = {};

if (name && typeof name === 'object') {
methods = name;
name = 'object';
}
chai.spy.object = function () {
return DEFAULT_SANDBOX.object.apply(DEFAULT_SANDBOX, arguments)
};

if (methods && !Array.isArray(methods)) {
defs = methods;
methods = Object.keys(methods);
}
/**
* # chai.spy.restore (function)
*
* The same as Sandbox.restore.
* Restores spy assigned to DEFAULT sandbox
*
* var array = []
* chai.spy.on(array, 'push');
* expect(array.push).to.be.spy // true
*
* chai.spy.restore()
* expect(array.push).to.be.spy // false
*
* @see Sandbox.restore
* @api public
*/

return methods.reduce(function (object, methodName) {
object[methodName] = chai.spy(name + '.' + methodName, defs[methodName]);
return object;
}, {});
chai.spy.restore = function () {
return DEFAULT_SANDBOX.restore.apply(DEFAULT_SANDBOX, arguments)
};

/**
Expand Down
61 changes: 47 additions & 14 deletions test/spies.js
Expand Up @@ -209,7 +209,8 @@ describe('Chai Spies', function () {
});

it('should spy specified object method', function () {
var array = chai.spy.on([], 'push');
var array = []
chai.spy.on(array, 'push');
array.push(1, 2);

array.push.should.be.a.spy;
Expand All @@ -225,7 +226,7 @@ describe('Chai Spies', function () {
});

it('should spy multiple object methods passed as array', function () {
var array = chai.spy.on([], 'push', 'pop');
var array = chai.spy.object([], ['push', 'pop']);

array.push.should.be.a.spy;
array.pop.should.be.a.spy;
Expand Down Expand Up @@ -424,21 +425,18 @@ describe('Chai Spies', function () {
object.pop.should.be.a.spy;
});

it('should create an anonymous spy object', function () {
var object = chai.spy.object([ 'push' ]);

object.push.should.be.a.spy;
});

it('should create a spy object with specified method definitions', function () {
var object = chai.spy.object('array', {
push: function () {
return 'push';
it('should wrap each method in spy', function () {
var array = []
var object = chai.spy.object({
push: function() {
return array.push.apply(array, arguments)
}
});

object.push(1, 2, 3)

object.push.should.be.a.spy;
object.push().should.equal('push');
array.should.have.length(3)
});

it('should create an anonymous spy object with methods implementation', function () {
Expand All @@ -453,7 +451,7 @@ describe('Chai Spies', function () {
});
});

describe('reset method', function() {
describe('reset method', function () {
it('should reset spy object values to defaults when called', function() {
var name = 'proxy';
var spy = chai.spy(name);
Expand Down Expand Up @@ -482,4 +480,39 @@ describe('Chai Spies', function () {
spy.__spy.name.should.be.equal(name);
});
});

describe('spy restore', function () {
var array;

beforeEach(function () {
array = [];
chai.spy.on(array, 'push');
})

it('should restore all methods of tracked objects', function () {
chai.spy.restore();

array.push.should.not.be.spy;
});

it('should restore all methods on an object', function () {
chai.spy.on(array, 'pop');
chai.spy.restore(array);

array.push.should.not.be.spy;
array.pop.should.not.be.spy;
});

it('should restore a particular method on an particular object', function () {
chai.spy.restore(array, 'push');

array.push.should.not.be.spy;
});

it('should not throw if there are not tracked objects', function () {
chai.spy.restore();

chai.spy.restore.should.not.throw(Error);
});
});
});

0 comments on commit d7ad443

Please sign in to comment.