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 are tracked under DEFAULT_SANDBOX

Fixes chaijs#38
  • Loading branch information
stalniy committed Dec 18, 2016
1 parent b522148 commit 6c68099
Show file tree
Hide file tree
Showing 2 changed files with 306 additions and 59 deletions.
237 changes: 209 additions & 28 deletions lib/spy.js
Expand Up @@ -13,6 +13,151 @@ module.exports = function (chai, _) {
var Assertion = chai.Assertion
, flag = _.flag
, i = _.inspect
, 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');
*
* const array = []
* const spy = chai.spy.sandbox();
* const [push, pop] = spy.on(array, ['push', 'pop']);
*
* @param {Object} object
* @param {String|String[]} method name or methods names to spy on
* @returns created spy or created spies
* @api public
*/

Sandbox.prototype.on = function (object, methodName) {
if (Array.isArray(methodName)) {
return methodName.map(function (name) {
return this.on(object, name);
}, this);
}

var isMethod = typeof object[methodName] === 'function'

if (methodName in object && !isMethod) {
throw new Error([
'Unable to spy property "', methodName,
'". Only methods and non-existing properties can be spied.'
].join(''))
}

if (isMethod && object[methodName].__spy) {
throw new Error('"' + methodName + '" is already a spy')
}

var method = chai.spy('object.' + methodName, object[methodName]);
var trackingId = ++spyAmount

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

return method;
};

/**
* # 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|String[]} [methods] method name or method names
* @return {Sandbox} Sandbox instance
* @api public
*/

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

if (methods && !Array.isArray(methods)) {
methods = [methods]
}

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 = !methods || methods.indexOf(tracked.methodName) !== -1;

delete sandbox[STATE_KEY][spyId];

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

sandbox.restoreTrackedObject(spy);

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

return this;
};

/**
* # 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) {
throw new Error('It is not possible to restore a non-tracked spy.')
}

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 +207,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 +221,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,53 +237,67 @@ 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)
* # chai.spy.interface (function)
*
* Creates an object interface with spied methods.
*
* Creates an object with spied methods.
* var events = chai.spy.interface('Events', ['trigger', 'on']);
*
* var object = chai.spy.object('Array', [ 'push', 'pop' ]);
* var array = chai.spy.interface({
* push(item) {
* this.items = this.items || [];
* return this.items.push(item);
* }
* });
*
* @param {String} [name] object name
* @param {String[]|Object} method names or method definitions
* @param {String|Object} name object or object name
* @param {String[]} [methods] method names
* @returns object with spied methods
* @api public
*/

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

if (name && typeof name === 'object') {
methods = name;
name = 'object';
}

if (methods && !Array.isArray(methods)) {
defs = methods;
methods = Object.keys(methods);
methods = Object.keys(name);
defs = name;
name = 'mock';
}

return methods.reduce(function (object, methodName) {
Expand All @@ -146,6 +306,27 @@ module.exports = function (chai, _) {
}, {});
};

/**
* # 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
*/

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

/**
* # chai.spy.returns (function)
*
Expand Down

0 comments on commit 6c68099

Please sign in to comment.