153 lines
5.3 KiB
Plaintext
153 lines
5.3 KiB
Plaintext
/*!
|
|
* Chai - addChainingMethod utility
|
|
* Copyright(c) 2012-2014 Jake Luer <jake@alogicalparadox.com>
|
|
* MIT Licensed
|
|
*/
|
|
|
|
/*!
|
|
* Module dependencies
|
|
*/
|
|
|
|
var addLengthGuard = require('./addLengthGuard');
|
|
var chai = require('../../chai');
|
|
var flag = require('./flag');
|
|
var proxify = require('./proxify');
|
|
var transferFlags = require('./transferFlags');
|
|
|
|
/*!
|
|
* Module variables
|
|
*/
|
|
|
|
// Check whether `Object.setPrototypeOf` is supported
|
|
var canSetPrototype = typeof Object.setPrototypeOf === 'function';
|
|
|
|
// Without `Object.setPrototypeOf` support, this module will need to add properties to a function.
|
|
// However, some of functions' own props are not configurable and should be skipped.
|
|
var testFn = function() {};
|
|
var excludeNames = Object.getOwnPropertyNames(testFn).filter(function(name) {
|
|
var propDesc = Object.getOwnPropertyDescriptor(testFn, name);
|
|
|
|
// Note: PhantomJS 1.x includes `callee` as one of `testFn`'s own properties,
|
|
// but then returns `undefined` as the property descriptor for `callee`. As a
|
|
// workaround, we perform an otherwise unnecessary type-check for `propDesc`,
|
|
// and then filter it out if it's not an object as it should be.
|
|
if (typeof propDesc !== 'object')
|
|
return true;
|
|
|
|
return !propDesc.configurable;
|
|
});
|
|
|
|
// Cache `Function` properties
|
|
var call = Function.prototype.call,
|
|
apply = Function.prototype.apply;
|
|
|
|
/**
|
|
* ### .addChainableMethod(ctx, name, method, chainingBehavior)
|
|
*
|
|
* Adds a method to an object, such that the method can also be chained.
|
|
*
|
|
* utils.addChainableMethod(chai.Assertion.prototype, 'foo', function (str) {
|
|
* var obj = utils.flag(this, 'object');
|
|
* new chai.Assertion(obj).to.be.equal(str);
|
|
* });
|
|
*
|
|
* Can also be accessed directly from `chai.Assertion`.
|
|
*
|
|
* chai.Assertion.addChainableMethod('foo', fn, chainingBehavior);
|
|
*
|
|
* The result can then be used as both a method assertion, executing both `method` and
|
|
* `chainingBehavior`, or as a language chain, which only executes `chainingBehavior`.
|
|
*
|
|
* expect(fooStr).to.be.foo('bar');
|
|
* expect(fooStr).to.be.foo.equal('foo');
|
|
*
|
|
* @param {Object} ctx object to which the method is added
|
|
* @param {String} name of method to add
|
|
* @param {Function} method function to be used for `name`, when called
|
|
* @param {Function} chainingBehavior function to be called every time the property is accessed
|
|
* @namespace Utils
|
|
* @name addChainableMethod
|
|
* @api public
|
|
*/
|
|
|
|
module.exports = function addChainableMethod(ctx, name, method, chainingBehavior) {
|
|
if (typeof chainingBehavior !== 'function') {
|
|
chainingBehavior = function () { };
|
|
}
|
|
|
|
var chainableBehavior = {
|
|
method: method
|
|
, chainingBehavior: chainingBehavior
|
|
};
|
|
|
|
// save the methods so we can overwrite them later, if we need to.
|
|
if (!ctx.__methods) {
|
|
ctx.__methods = {};
|
|
}
|
|
ctx.__methods[name] = chainableBehavior;
|
|
|
|
Object.defineProperty(ctx, name,
|
|
{ get: function chainableMethodGetter() {
|
|
chainableBehavior.chainingBehavior.call(this);
|
|
|
|
var chainableMethodWrapper = function () {
|
|
// Setting the `ssfi` flag to `chainableMethodWrapper` causes this
|
|
// function to be the starting point for removing implementation
|
|
// frames from the stack trace of a failed assertion.
|
|
//
|
|
// However, we only want to use this function as the starting point if
|
|
// the `lockSsfi` flag isn't set.
|
|
//
|
|
// If the `lockSsfi` flag is set, then this assertion is being
|
|
// invoked from inside of another assertion. In this case, the `ssfi`
|
|
// flag has already been set by the outer assertion.
|
|
//
|
|
// Note that overwriting a chainable method merely replaces the saved
|
|
// methods in `ctx.__methods` instead of completely replacing the
|
|
// overwritten assertion. Therefore, an overwriting assertion won't
|
|
// set the `ssfi` or `lockSsfi` flags.
|
|
if (!flag(this, 'lockSsfi')) {
|
|
flag(this, 'ssfi', chainableMethodWrapper);
|
|
}
|
|
|
|
var result = chainableBehavior.method.apply(this, arguments);
|
|
if (result !== undefined) {
|
|
return result;
|
|
}
|
|
|
|
var newAssertion = new chai.Assertion();
|
|
transferFlags(this, newAssertion);
|
|
return newAssertion;
|
|
};
|
|
|
|
addLengthGuard(chainableMethodWrapper, name, true);
|
|
|
|
// Use `Object.setPrototypeOf` if available
|
|
if (canSetPrototype) {
|
|
// Inherit all properties from the object by replacing the `Function` prototype
|
|
var prototype = Object.create(this);
|
|
// Restore the `call` and `apply` methods from `Function`
|
|
prototype.call = call;
|
|
prototype.apply = apply;
|
|
Object.setPrototypeOf(chainableMethodWrapper, prototype);
|
|
}
|
|
// Otherwise, redefine all properties (slow!)
|
|
else {
|
|
var asserterNames = Object.getOwnPropertyNames(ctx);
|
|
asserterNames.forEach(function (asserterName) {
|
|
if (excludeNames.indexOf(asserterName) !== -1) {
|
|
return;
|
|
}
|
|
|
|
var pd = Object.getOwnPropertyDescriptor(ctx, asserterName);
|
|
Object.defineProperty(chainableMethodWrapper, asserterName, pd);
|
|
});
|
|
}
|
|
|
|
transferFlags(this, chainableMethodWrapper);
|
|
return proxify(chainableMethodWrapper);
|
|
}
|
|
, configurable: true
|
|
});
|
|
};
|