| /*! |
| * 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 |
| }); |
| }; |