addChainableMethod.js 3.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111
  1. /*!
  2. * Chai - addChainingMethod utility
  3. * Copyright(c) 2012-2014 Jake Luer <jake@alogicalparadox.com>
  4. * MIT Licensed
  5. */
  6. /*!
  7. * Module dependencies
  8. */
  9. var transferFlags = require('./transferFlags');
  10. var flag = require('./flag');
  11. var config = require('../config');
  12. /*!
  13. * Module variables
  14. */
  15. // Check whether `__proto__` is supported
  16. var hasProtoSupport = '__proto__' in Object;
  17. // Without `__proto__` support, this module will need to add properties to a function.
  18. // However, some Function.prototype methods cannot be overwritten,
  19. // and there seems no easy cross-platform way to detect them (@see chaijs/chai/issues/69).
  20. var excludeNames = /^(?:length|name|arguments|caller)$/;
  21. // Cache `Function` properties
  22. var call = Function.prototype.call,
  23. apply = Function.prototype.apply;
  24. /**
  25. * ### addChainableMethod (ctx, name, method, chainingBehavior)
  26. *
  27. * Adds a method to an object, such that the method can also be chained.
  28. *
  29. * utils.addChainableMethod(chai.Assertion.prototype, 'foo', function (str) {
  30. * var obj = utils.flag(this, 'object');
  31. * new chai.Assertion(obj).to.be.equal(str);
  32. * });
  33. *
  34. * Can also be accessed directly from `chai.Assertion`.
  35. *
  36. * chai.Assertion.addChainableMethod('foo', fn, chainingBehavior);
  37. *
  38. * The result can then be used as both a method assertion, executing both `method` and
  39. * `chainingBehavior`, or as a language chain, which only executes `chainingBehavior`.
  40. *
  41. * expect(fooStr).to.be.foo('bar');
  42. * expect(fooStr).to.be.foo.equal('foo');
  43. *
  44. * @param {Object} ctx object to which the method is added
  45. * @param {String} name of method to add
  46. * @param {Function} method function to be used for `name`, when called
  47. * @param {Function} chainingBehavior function to be called every time the property is accessed
  48. * @name addChainableMethod
  49. * @api public
  50. */
  51. module.exports = function (ctx, name, method, chainingBehavior) {
  52. if (typeof chainingBehavior !== 'function') {
  53. chainingBehavior = function () { };
  54. }
  55. var chainableBehavior = {
  56. method: method
  57. , chainingBehavior: chainingBehavior
  58. };
  59. // save the methods so we can overwrite them later, if we need to.
  60. if (!ctx.__methods) {
  61. ctx.__methods = {};
  62. }
  63. ctx.__methods[name] = chainableBehavior;
  64. Object.defineProperty(ctx, name,
  65. { get: function () {
  66. chainableBehavior.chainingBehavior.call(this);
  67. var assert = function assert() {
  68. var old_ssfi = flag(this, 'ssfi');
  69. if (old_ssfi && config.includeStack === false)
  70. flag(this, 'ssfi', assert);
  71. var result = chainableBehavior.method.apply(this, arguments);
  72. return result === undefined ? this : result;
  73. };
  74. // Use `__proto__` if available
  75. if (hasProtoSupport) {
  76. // Inherit all properties from the object by replacing the `Function` prototype
  77. var prototype = assert.__proto__ = Object.create(this);
  78. // Restore the `call` and `apply` methods from `Function`
  79. prototype.call = call;
  80. prototype.apply = apply;
  81. }
  82. // Otherwise, redefine all properties (slow!)
  83. else {
  84. var asserterNames = Object.getOwnPropertyNames(ctx);
  85. asserterNames.forEach(function (asserterName) {
  86. if (!excludeNames.test(asserterName)) {
  87. var pd = Object.getOwnPropertyDescriptor(ctx, asserterName);
  88. Object.defineProperty(assert, asserterName, pd);
  89. }
  90. });
  91. }
  92. transferFlags(this, assert);
  93. return assert;
  94. }
  95. , configurable: true
  96. });
  97. };