Repository: chaijs/chai
Pull request #884 (2016-12-11 22:28:43)

Remove proxy frames from stack traces and improve docs/tests
This PR is intentionally split into a number of commits, each one of which has a description within the commit message.

Highlights of this PR:

- Remove Chai's internal proxy frames from stack traces of failed property assertions (note: they were already correctly removed from the stack traces of failed method and chainable method assertions)
- Improve tests and inline documentation related to Chai's stack trace wizardry
- Remove dead code related to Chai's stack trace wizardry

Note that this doesn't address #878 regarding extra frames in stack traces when using the `assert` interface.

lucasfcosta: This LGTM!

I wasn't really aware of how this whole Stack Trace manipulation worked, so I did a bit of research and I found it to be very interesting.

Just in case anyone wants to read more about it, take a look at the [`AssertionError` constructor](https://github.com/chaijs/assertion-error/blob/master/index.js#L55) and the [`Error.captureStackTrace` docs](https://nodejs.org/api/errors.html#errors_error_capturestacktrace_targetobject_constructoropt).

I made a few comments on the source just to make sure I got it right and added a little consideration to improve the readability of the code regarding the `ssfi` flag we're using, just to make it more clear to new contributors.

Awesome job @meeber! 😄 
meeber: Pushed another commit:

- Rename third parameter of Assertion constructor from `stack` to
  `ssfi` for consistency's sake.
- Add documentation to Assertion constructor explaining what the `object`,
  `message`, and `ssfi` flags are for.
lucasfcosta: @meeber awesome job! Chai's code is becoming even more easy and pleasurable to read than it was before with all these useful and well written comments.

I'm in love with this codebase ❤️ 
lucasfcosta: Hi friends, sorry for pinging everyone right after holidays, but I've seen we have many open Pull Requests so I thought it would be a good idea for us to start reviewing and approving in order to avoid accumulating too much work.

cc @keithamus @shvaikalesh @vieiralucas 
shvaikalesh: Awesome changes. Makes code so much clearer. LGTM.
keithamus: @meeber think you could resolve the conflicts for this, so we can merge. All LGTM 😄 

Patch:
diff --git a/lib/chai/assertion.js b/lib/chai/assertion.js
index 107eb08..491add7 100644
--- a/lib/chai/assertion.js
+++ b/lib/chai/assertion.js
@@ -26,11 +26,34 @@ module.exports = function (_chai, util) {
    *
    * Creates object for chaining.
    *
+   * `Assertion` objects contain metadata in the form of flags. Three flags can
+   * be assigned during instantiation by passing arguments to this constructor:
+   *
+   * - `object`: This flag contains the target of the assertion. For example, in
+   *   the assertion `expect(numKittens).to.equal(7);`, the `object` flag will
+   *   contain `numKittens` so that the `equal` assertion can reference it when
+   *   needed.
+   *
+   * - `message`: This flag contains an optional custom error message to be
+   *   prepended to the error message that's generated by the assertion when it
+   *   fails.
+   *
+   * - `ssfi`: This flag stands for "start stack function indicator". It
+   *   contains a function reference that serves as the starting point for
+   *   removing frames from the stack trace of the error that's created by the
+   *   assertion when it fails. The goal is to provide a cleaner stack trace to
+   *   end users by removing Chai's internal functions. Note that it only works
+   *   in environments that support `Error.captureStackTrace`, and only when
+   *   `Chai.config.includeStack` hasn't been set to `false`.
+   *
+   * @param {Mixed} obj target of the assertion
+   * @param {String} msg (optional) custom error message
+   * @param {Function} stack (optional) starting point for removing stack frames
    * @api private
    */
 
-  function Assertion (obj, msg, stack) {
-    flag(this, 'ssfi', stack || Assertion);
+  function Assertion (obj, msg, ssfi) {
+    flag(this, 'ssfi', ssfi || Assertion);
     flag(this, 'object', obj);
     flag(this, 'message', msg);
 
diff --git a/lib/chai/utils/addChainableMethod.js b/lib/chai/utils/addChainableMethod.js
index 1b20333..a79a707 100644
--- a/lib/chai/utils/addChainableMethod.js
+++ b/lib/chai/utils/addChainableMethod.js
@@ -59,7 +59,7 @@ var call  = Function.prototype.call,
  * @api public
  */
 
-module.exports = function (ctx, name, method, chainingBehavior) {
+module.exports = function addChainableMethod(ctx, name, method, chainingBehavior) {
   if (typeof chainingBehavior !== 'function') {
     chainingBehavior = function () { };
   }
@@ -76,13 +76,18 @@ module.exports = function (ctx, name, method, chainingBehavior) {
   ctx.__methods[name] = chainableBehavior;
 
   Object.defineProperty(ctx, name,
-    { get: function () {
+    { get: function chainableMethodGetter() {
         chainableBehavior.chainingBehavior.call(this);
 
-        var assert = function assert() {
-          var old_ssfi = flag(this, 'ssfi');
-          if (old_ssfi)
-            flag(this, 'ssfi', assert);
+        var chainableMethodWrapper = function () {
+          // Use this chainable method wrapper as the starting point for
+          // removing implementation frames from the stack trace of a failed
+          // assertion. Note that this is the correct starting point even if
+          // this assertion has been overwritten since overwriting a chainable
+          // method merely replaces the saved methods in `ctx.__methods` instead
+          // of completely replacing the overwritten assertion.
+          flag(this, 'ssfi', chainableMethodWrapper);
+
           var result = chainableBehavior.method.apply(this, arguments);
 
           if (result !== undefined) {
@@ -94,12 +99,12 @@ module.exports = function (ctx, name, method, chainingBehavior) {
           return newAssertion;
         };
 
-        addLengthGuard(assert, name, true);
+        addLengthGuard(chainableMethodWrapper, name, true);
 
         // Use `__proto__` if available
         if (hasProtoSupport) {
           // Inherit all properties from the object by replacing the `Function` prototype
-          var prototype = assert.__proto__ = Object.create(this);
+          var prototype = chainableMethodWrapper.__proto__ = Object.create(this);
           // Restore the `call` and `apply` methods from `Function`
           prototype.call = call;
           prototype.apply = apply;
@@ -110,13 +115,13 @@ module.exports = function (ctx, name, method, chainingBehavior) {
           asserterNames.forEach(function (asserterName) {
             if (!excludeNames.test(asserterName)) {
               var pd = Object.getOwnPropertyDescriptor(ctx, asserterName);
-              Object.defineProperty(assert, asserterName, pd);
+              Object.defineProperty(chainableMethodWrapper, asserterName, pd);
             }
           });
         }
 
-        transferFlags(this, assert);
-        return proxify(assert);
+        transferFlags(this, chainableMethodWrapper);
+        return proxify(chainableMethodWrapper);
       }
     , configurable: true
   });
diff --git a/lib/chai/utils/addMethod.js b/lib/chai/utils/addMethod.js
index 6f6bf75..a5d3206 100644
--- a/lib/chai/utils/addMethod.js
+++ b/lib/chai/utils/addMethod.js
@@ -36,12 +36,19 @@ var transferFlags = require('./transferFlags');
  * @api public
  */
 
-module.exports = function (ctx, name, method) {
-  var fn = function () {
-    var keep_ssfi = flag(this, 'keep_ssfi');
-    var old_ssfi = flag(this, 'ssfi');
-    if (!keep_ssfi && old_ssfi)
-      flag(this, 'ssfi', fn);
+module.exports = function addMethod(ctx, name, method) {
+  var methodWrapper = function () {
+    // If this assertion hasn't been overwritten, then use this method wrapper
+    // as the starting point for removing implementation frames from the stack
+    // trace of a failed assertion.
+    //
+    // Note: If this assertion has been overwritten, and thus the `keep_ssfi`
+    // flag is set, then the overwriting method wrapper is used as the starting
+    // point instead. This prevents the overwriting method wrapper from showing
+    // up in the stack trace since it's invoked before this method wrapper.
+    if (!flag(this, 'keep_ssfi')) {
+      flag(this, 'ssfi', methodWrapper);
+    }
 
     var result = method.apply(this, arguments);
     if (result !== undefined)
@@ -52,6 +59,6 @@ module.exports = function (ctx, name, method) {
     return newAssertion;
   };
 
-  addLengthGuard(fn, name, false);
-  ctx[name] = proxify(fn, name);
+  addLengthGuard(methodWrapper, name, false);
+  ctx[name] = proxify(methodWrapper, name);
 };
diff --git a/lib/chai/utils/addProperty.js b/lib/chai/utils/addProperty.js
index 48e1b2d..f58400e 100644
--- a/lib/chai/utils/addProperty.js
+++ b/lib/chai/utils/addProperty.js
@@ -6,6 +6,7 @@
 
 var chai = require('../../chai');
 var flag = require('./flag');
+var isProxyEnabled = require('./isProxyEnabled');
 var transferFlags = require('./transferFlags');
 
 /**
@@ -34,15 +35,31 @@ var transferFlags = require('./transferFlags');
  * @api public
  */
 
-module.exports = function (ctx, name, getter) {
+module.exports = function addProperty(ctx, name, getter) {
   getter = getter === undefined ? new Function() : getter;
 
   Object.defineProperty(ctx, name,
-    { get: function addProperty() {
-        var keep_ssfi = flag(this, 'keep_ssfi');
-        var old_ssfi = flag(this, 'ssfi');
-        if (!keep_ssfi && old_ssfi)
-          flag(this, 'ssfi', addProperty);
+    { get: function propertyGetter() {
+        // If proxy protection is disabled and this assertion hasn't been
+        // overwritten, then use this property getter as the starting point for
+        // removing implementation frames from the stack trace of a failed
+        // assertion.
+        //
+        // Notes:
+        //
+        //  - If proxy protection is enabled, then the proxy getter is used as
+        //    the starting point instead. This prevents the proxy getter from
+        //    showing up in the stack trace since it's invoked before this
+        //    property getter.
+        //
+        //  - If proxy protection is disabled but this assertion has been
+        //    overwritten, and thus the `keep_ssfi` flag is set, then the
+        //    overwriting property getter is used as the starting point instead.
+        //    This prevents the overwriting property getter from showing up in
+        //    the stack trace since it's invoked before this property getter.
+        if (!isProxyEnabled() && !flag(this, 'keep_ssfi')) {
+          flag(this, 'ssfi', propertyGetter);
+        }
 
         var result = getter.call(this);
         if (result !== undefined)
diff --git a/lib/chai/utils/compareByInspect.js b/lib/chai/utils/compareByInspect.js
index 071de9c..708ff28 100644
--- a/lib/chai/utils/compareByInspect.js
+++ b/lib/chai/utils/compareByInspect.js
@@ -26,6 +26,6 @@ var inspect = require('./inspect');
  * @api public
  */
 
-module.exports = function (a, b) {
+module.exports = function compareByInspect(a, b) {
   return inspect(a) < inspect(b) ? -1 : 1;
 };
diff --git a/lib/chai/utils/expectTypes.js b/lib/chai/utils/expectTypes.js
index 8fa7f1f..4b0bd45 100644
--- a/lib/chai/utils/expectTypes.js
+++ b/lib/chai/utils/expectTypes.js
@@ -22,7 +22,7 @@ var AssertionError = require('assertion-error');
 var flag = require('./flag');
 var type = require('type-detect');
 
-module.exports = function (obj, types) {
+module.exports = function expectTypes(obj, types) {
   obj = flag(obj, 'object');
   types = types.map(function (t) { return t.toLowerCase(); });
   types.sort();
diff --git a/lib/chai/utils/flag.js b/lib/chai/utils/flag.js
index ef13236..ffb48f0 100644
--- a/lib/chai/utils/flag.js
+++ b/lib/chai/utils/flag.js
@@ -23,7 +23,7 @@
  * @api private
  */
 
-module.exports = function (obj, key, value) {
+module.exports = function flag(obj, key, value) {
   var flags = obj.__flags || (obj.__flags = Object.create(null));
   if (arguments.length === 3) {
     flags[key] = value;
diff --git a/lib/chai/utils/getActual.js b/lib/chai/utils/getActual.js
index e2f28fa..2b5b3fa 100644
--- a/lib/chai/utils/getActual.js
+++ b/lib/chai/utils/getActual.js
@@ -15,6 +15,6 @@
  * @name getActual
  */
 
-module.exports = function (obj, args) {
+module.exports = function getActual(obj, args) {
   return args.length > 4 ? args[4] : obj._obj;
 };
diff --git a/lib/chai/utils/getMessage.js b/lib/chai/utils/getMessage.js
index dedd8f9..1c9744f 100644
--- a/lib/chai/utils/getMessage.js
+++ b/lib/chai/utils/getMessage.js
@@ -32,7 +32,7 @@ var flag = require('./flag')
  * @api public
  */
 
-module.exports = function (obj, args) {
+module.exports = function getMessage(obj, args) {
   var negate = flag(obj, 'negate')
     , val = flag(obj, 'object')
     , expected = args[3]
diff --git a/lib/chai/utils/index.js b/lib/chai/utils/index.js
index b17f085..d4f329c 100644
--- a/lib/chai/utils/index.js
+++ b/lib/chai/utils/index.js
@@ -154,11 +154,17 @@ exports.checkError = require('check-error');
 exports.proxify = require('./proxify');
 
 /*!
- * Proxify util
+ * addLengthGuard util
  */
 
 exports.addLengthGuard = require('./addLengthGuard');
 
+/*!
+ * isProxyEnabled helper
+ */
+
+exports.isProxyEnabled = require('./isProxyEnabled');
+
 /*!
  * isNaN method
  */
diff --git a/lib/chai/utils/isProxyEnabled.js b/lib/chai/utils/isProxyEnabled.js
new file mode 100644
index 0000000..f0ef041
--- /dev/null
+++ b/lib/chai/utils/isProxyEnabled.js
@@ -0,0 +1,24 @@
+var config = require('../config');
+
+/*!
+ * Chai - isProxyEnabled helper
+ * Copyright(c) 2012-2014 Jake Luer <jake@alogicalparadox.com>
+ * MIT Licensed
+ */
+
+/**
+ * # isProxyEnabled()
+ *
+ * Helper function to check if Chai's proxy protection feature is enabled. If
+ * proxies are unsupported or disabled via the user's Chai config, then return
+ * false. Otherwise, return true.
+ *
+ * @namespace Utils
+ * @name isProxyEnabled
+ */
+
+module.exports = function isProxyEnabled() {
+  return config.useProxy && 
+    typeof Proxy !== 'undefined' &&
+    typeof Reflect !== 'undefined';
+};
diff --git a/lib/chai/utils/objDisplay.js b/lib/chai/utils/objDisplay.js
index 6814258..d87d938 100644
--- a/lib/chai/utils/objDisplay.js
+++ b/lib/chai/utils/objDisplay.js
@@ -24,7 +24,7 @@ var config = require('../config');
  * @api public
  */
 
-module.exports = function (obj) {
+module.exports = function objDisplay(obj) {
   var str = inspect(obj)
     , type = Object.prototype.toString.call(obj);
 
diff --git a/lib/chai/utils/overwriteChainableMethod.js b/lib/chai/utils/overwriteChainableMethod.js
index 007ed12..4d28f5a 100644
--- a/lib/chai/utils/overwriteChainableMethod.js
+++ b/lib/chai/utils/overwriteChainableMethod.js
@@ -40,11 +40,11 @@ var transferFlags = require('./transferFlags');
  * @api public
  */
 
-module.exports = function (ctx, name, method, chainingBehavior) {
+module.exports = function overwriteChainableMethod(ctx, name, method, chainingBehavior) {
   var chainableBehavior = ctx.__methods[name];
 
   var _chainingBehavior = chainableBehavior.chainingBehavior;
-  chainableBehavior.chainingBehavior = function () {
+  chainableBehavior.chainingBehavior = function overwritingChainableMethodGetter() {
     var result = chainingBehavior(_chainingBehavior).call(this);
     if (result !== undefined) {
       return result;
@@ -56,7 +56,7 @@ module.exports = function (ctx, name, method, chainingBehavior) {
   };
 
   var _method = chainableBehavior.method;
-  chainableBehavior.method = function () {
+  chainableBehavior.method = function overwritingChainableMethodWrapper() {
     var result = method(_method).apply(this, arguments);
     if (result !== undefined) {
       return result;
diff --git a/lib/chai/utils/overwriteMethod.js b/lib/chai/utils/overwriteMethod.js
index 66f55a7..5d2306e 100644
--- a/lib/chai/utils/overwriteMethod.js
+++ b/lib/chai/utils/overwriteMethod.js
@@ -44,7 +44,7 @@ var transferFlags = require('./transferFlags');
  * @api public
  */
 
-module.exports = function (ctx, name, method) {
+module.exports = function overwriteMethod(ctx, name, method) {
   var _method = ctx[name]
     , _super = function () {
       throw new Error(name + ' is not a function');
@@ -53,12 +53,24 @@ module.exports = function (ctx, name, method) {
   if (_method && 'function' === typeof _method)
     _super = _method;
 
-  var fn = function () {
-    var keep_ssfi = flag(this, 'keep_ssfi');
-    var old_ssfi = flag(this, 'ssfi');
-    if (!keep_ssfi && old_ssfi)
-      flag(this, 'ssfi', fn);
+  var overwritingMethodWrapper = function () {
+    // If proxy protection is disabled and this overwriting assertion hasn't
+    // been overwritten again by yet another assertion, then use this method
+    // wrapper as the starting point for removing implementation frames from the
+    // stack trace of a failed assertion.
+    //
+    // Note: If this assertion has been overwritten, and thus the `keep_ssfi`
+    // flag is set, then the overwriting method wrapper is used as the starting
+    // point instead. This prevents the overwriting method wrapper from showing
+    // up in the stack trace since it's invoked before this method wrapper.
+    if (!flag(this, 'keep_ssfi')) {
+      flag(this, 'ssfi', overwritingMethodWrapper);
+    }
 
+    // The `keep_ssfi` flag is set so that if this assertion ends up calling
+    // the overwritten assertion, then the overwritten assertion doesn't attempt
+    // to use itself as the starting point for removing implementation frames
+    // from the stack trace of a failed assertion.
     flag(this, 'keep_ssfi', true);
     var result = method(_super).apply(this, arguments);
     flag(this, 'keep_ssfi', false);
@@ -72,6 +84,6 @@ module.exports = function (ctx, name, method) {
     return newAssertion;
   }
 
-  addLengthGuard(fn, name, false);
-  ctx[name] = proxify(fn, name);
+  addLengthGuard(overwritingMethodWrapper, name, false);
+  ctx[name] = proxify(overwritingMethodWrapper, name);
 };
diff --git a/lib/chai/utils/overwriteProperty.js b/lib/chai/utils/overwriteProperty.js
index 155ccbd..f988ced 100644
--- a/lib/chai/utils/overwriteProperty.js
+++ b/lib/chai/utils/overwriteProperty.js
@@ -6,6 +6,7 @@
 
 var chai = require('../../chai');
 var flag = require('./flag');
+var isProxyEnabled = require('./isProxyEnabled');
 var transferFlags = require('./transferFlags');
 
 /**
@@ -42,7 +43,7 @@ var transferFlags = require('./transferFlags');
  * @api public
  */
 
-module.exports = function (ctx, name, getter) {
+module.exports = function overwriteProperty(ctx, name, getter) {
   var _get = Object.getOwnPropertyDescriptor(ctx, name)
     , _super = function () {};
 
@@ -50,12 +51,32 @@ module.exports = function (ctx, name, getter) {
     _super = _get.get
 
   Object.defineProperty(ctx, name,
-    { get: function overwriteProperty() {
-        var keep_ssfi = flag(this, 'keep_ssfi');
-        var old_ssfi = flag(this, 'ssfi');
-        if (!keep_ssfi && old_ssfi)
-          flag(this, 'ssfi', overwriteProperty);
+    { get: function overwritingPropertyGetter() {
+        // If proxy protection is disabled and this overwriting assertion hasn't
+        // been overwritten again by yet another assertion, then use this
+        // property getter as the starting point for removing implementation
+        // frames from the stack trace of a failed assertion.
+        //
+        // Notes:
+        //
+        //  - If proxy protection is enabled, then the proxy getter is used as
+        //    the starting point instead. This prevents the proxy getter from
+        //    showing up in the stack trace since it's invoked before this
+        //    property getter.
+        //
+        //  - If proxy protection is disabled but this assertion has been
+        //    overwritten, and thus the `keep_ssfi` flag is set, then the
+        //    overwriting property getter is used as the starting point instead.
+        //    This prevents the overwriting property getter from showing up in
+        //    the stack trace since it's invoked before this property getter.
+        if (!isProxyEnabled() && !flag(this, 'keep_ssfi')) {
+          flag(this, 'ssfi', overwritingPropertyGetter);
+        }
 
+        // The `keep_ssfi` flag is set so that if this assertion ends up calling
+        // the overwritten assertion, then the overwritten assertion doesn't
+        // attempt to use itself as the starting point for removing
+        // implementation frames from the stack trace of a failed assertion.
         flag(this, 'keep_ssfi', true);
         var result = getter(_super).call(this);
         flag(this, 'keep_ssfi', false);
diff --git a/lib/chai/utils/proxify.js b/lib/chai/utils/proxify.js
index a1e41c1..85c6cbf 100644
--- a/lib/chai/utils/proxify.js
+++ b/lib/chai/utils/proxify.js
@@ -1,5 +1,7 @@
 var config = require('../config');
+var flag = require('./flag');
 var getProperties = require('./getProperties');
+var isProxyEnabled = require('./isProxyEnabled');
 
 /*!
  * Chai - proxify utility
@@ -26,12 +28,13 @@ var getProperties = require('./getProperties');
  * @name proxify
  */
 
-module.exports = function proxify (obj, nonChainableMethodName) {
-  if (!config.useProxy || typeof Proxy === 'undefined' || typeof Reflect === 'undefined')
-    return obj;
+var builtins = ['__flags', '__methods', '_obj', 'assert'];
+
+module.exports = function proxify(obj, nonChainableMethodName) {
+  if (!isProxyEnabled()) return obj;
 
   return new Proxy(obj, {
-    get: function getProperty (target, property) {
+    get: function proxyGetter(target, property) {
       // This check is here because we should not throw errors on Symbol properties
       // such as `Symbol.toStringTag`.
       // The values for which an error should be thrown can be configured using
@@ -48,7 +51,7 @@ module.exports = function proxify (obj, nonChainableMethodName) {
 
         var orderedProperties = getProperties(target).filter(function(property) {
           return !Object.prototype.hasOwnProperty(property) &&
-            ['__flags', '__methods', '_obj', 'assert'].indexOf(property) === -1;
+            builtins.indexOf(property) === -1;
         }).sort(function(a, b) {
           return stringDistance(property, a) - stringDistance(property, b);
         });
@@ -64,7 +67,20 @@ module.exports = function proxify (obj, nonChainableMethodName) {
         }
       }
 
-      return target[property];
+      // Use this proxy getter as the starting point for removing implementation
+      // frames from the stack trace of a failed assertion. For property
+      // assertions, this prevents the proxy getter from showing up in the stack
+      // trace since it's invoked before the property getter. For method and
+      // chainable method assertions, this flag will end up getting changed to
+      // the method wrapper, which is good since this frame will no longer be in
+      // the stack once the method is invoked. Note that Chai builtin assertion
+      // properties such as `__flags` are skipped since this is only meant to
+      // capture the starting point of an assertion.
+      if (builtins.indexOf(property) === -1) {
+        flag(target, 'ssfi', proxyGetter);
+      }
+
+      return Reflect.get(target, property);
     }
   });
 };
diff --git a/lib/chai/utils/transferFlags.js b/lib/chai/utils/transferFlags.js
index fa2c379..ea1316b 100644
--- a/lib/chai/utils/transferFlags.js
+++ b/lib/chai/utils/transferFlags.js
@@ -27,7 +27,7 @@
  * @api private
  */
 
-module.exports = function (assertion, object, includeAll) {
+module.exports = function transferFlags(assertion, object, includeAll) {
   var flags = assertion.__flags || (assertion.__flags = Object.create(null));
 
   if (!object.__flags) {


Test patch:
diff --git a/lib/chai/utils/test.js b/lib/chai/utils/test.js
index 453d4c7..4afc5fc 100644
--- a/lib/chai/utils/test.js
+++ b/lib/chai/utils/test.js
@@ -21,7 +21,7 @@ var flag = require('./flag');
  * @name test
  */
 
-module.exports = function (obj, args) {
+module.exports = function test(obj, args) {
   var negate = flag(obj, 'negate')
     , expr = args[0];
   return negate ? !expr : expr;
diff --git a/test/configuration.js b/test/configuration.js
index d0399c4..d53f164 100644
--- a/test/configuration.js
+++ b/test/configuration.js
@@ -19,78 +19,660 @@ describe('configuration', function () {
     });
   });
 
-  function fooThrows () {
-    chai.expect('foo').to.be.equal('bar');
-  }
-  function fooPropThrows () {
-    chai.expect('foo').to.not.exist;
-  }
-
   describe('includeStack', function() {
-    it('is true for method assertions', function () {
-      chai.config.includeStack = true;
-
-      try {
-        fooThrows();
-        assert.ok(false, 'should not get here because error thrown');
-      } catch (err) {
-        // not all browsers support err.stack
-        if ('undefined' !== typeof err.stack) {
-          assert.include(err.stack, 'assertEqual', 'should have internal stack trace in error message');
-          assert.include(err.stack, 'fooThrows', 'should have user stack trace in error message');
-        }
-      }
+    // Skip tests if `Error.captureStackTrace` is unsupported
+    if (typeof Error.captureStackTrace === 'undefined') return;
+
+    try {
+      throw Error();
+    } catch (err) {
+      // Skip tests if `err.stack` is unsupported
+      if (typeof err.stack === 'undefined') return;
+    }
 
-    });
+    // Create overwritten assertions that always fail
+    before(function () {
+      chai.util.addProperty(chai.Assertion.prototype, 'tmpProperty', function () {});
+      chai.util.overwriteProperty(chai.Assertion.prototype, 'tmpProperty', function () {
+        return function () {
+          this.assert(false);
+        };
+      });
 
-    it('is false for method assertions', function () {
-      chai.config.includeStack = false;
+      chai.util.addMethod(chai.Assertion.prototype, 'tmpMethod', function () {});
+      chai.util.overwriteMethod(chai.Assertion.prototype, 'tmpMethod', function () {
+        return function () {
+          this.assert(false);
+        };
+      });
 
-      try {
-        fooThrows();
-        assert.ok(false, 'should not get here because error thrown');
-      } catch (err) {
-        // IE 10 supports err.stack in Chrome format, but without
-        // `Error.captureStackTrace` support that allows tuning of the error
-        // message.
-        if ('undefined' !== typeof err.stack && 'undefined' !== typeof Error.captureStackTrace) {
-          assert.notInclude(err.stack, 'assertEqual', 'should not have internal stack trace in error message');
-          assert.include(err.stack, 'fooThrows', 'should have user stack trace in error message');
-        }
-      }
+      chai.util.addChainableMethod(chai.Assertion.prototype, 'tmpChainableMethod', function () {}, function () {});
+      chai.util.overwriteChainableMethod(chai.Assertion.prototype, 'tmpChainableMethod', function (_super) {
+        return function () {
+          this.assert(false);
+        };
+      }, function () {
+        return function () {};
+      });
     });
 
-    it('is true for property assertions', function () {
-      chai.config.includeStack = true;
+    // Delete overwritten assertions
+    after(function () {
+      delete chai.Assertion.prototype.tmpProperty;
+      delete chai.Assertion.prototype.tmpMethod;
+      delete chai.Assertion.prototype.tmpChainableMethod;
+    });
 
-      try {
-        fooPropThrows();
-        assert.ok(false, 'should not get here because error thrown');
-      } catch (err) {
-        // not all browsers support err.stack
-        // Phantom does not include function names for getter exec
-        if ('undefined' !== typeof err.stack && 'undefined' !== typeof Error.captureStackTrace) {
-          assert.include(err.stack, 'addProperty', 'should have internal stack trace in error message');
-          assert.include(err.stack, 'fooPropThrows', 'should have user stack trace in error message');
-        }
+    describe('expect interface', function () {
+      // Functions that always throw an error
+      function badPropertyAssertion() {
+        expect(42).to.be.false;
+      }
+      function badOverwrittenPropertyAssertion() {
+        expect(42).tmpProperty;
+      }
+      function badMethodAssertion() {
+        expect(42).to.equal(false);
+      }
+      function badOverwrittenMethodAssertion() {
+        expect(42).tmpMethod();
+      }
+      function badChainableMethodAssertion() {
+        expect(42).to.be.a('string');
+      }
+      function badOverwrittenChainableMethodAssertion() {
+        expect(42).tmpChainableMethod();
       }
-    });
 
-    it('is false for property assertions', function () {
-      chai.config.includeStack = false;
+      describe('when true', function () {
+        describe('failed property assertions', function () {
+          var caughtErr = '__PRETEST__';
+  
+          before(function () {
+            chai.config.includeStack = true;
+  
+            try {
+              badPropertyAssertion();
+            } catch (err) {
+              caughtErr = err;
+            }
+          });
+  
+          it('should include Chai frames in stack trace', function () {
+            expect(caughtErr.stack).to.contain('propertyGetter');
+  
+            if (typeof Proxy !== 'undefined' && typeof Reflect !== 'undefined') {
+              expect(caughtErr.stack).to.contain('proxyGetter');
+            }
+          });
+  
+          it('should include user frames in stack trace', function () {
+            expect(caughtErr.stack).to.contain('badPropertyAssertion');
+          });
+        });
+  
+        describe('failed overwritten property assertions', function () {
+          var caughtErr = '__PRETEST__';
+  
+          before(function () {
+            chai.config.includeStack = true;
+  
+            try {
+              badOverwrittenPropertyAssertion();
+            } catch (err) {
+              caughtErr = err;
+            }
+          });
+  
+          it('should include Chai frames in stack trace', function () {
+            expect(caughtErr.stack).to.contain('overwritingPropertyGetter');
+  
+            if (typeof Proxy !== 'undefined' && typeof Reflect !== 'undefined') {
+              expect(caughtErr.stack).to.contain('proxyGetter');
+            }
+          });
+  
+          it('should include user frames in stack trace', function () {
+            expect(caughtErr.stack).to.contain('badOverwrittenPropertyAssertion');
+          });
+        });
+  
+        describe('failed method assertions', function () {
+          var caughtErr = '__PRETEST__';
+  
+          before(function () {
+            chai.config.includeStack = true;
+  
+            try {
+              badMethodAssertion();
+            } catch (err) {
+              caughtErr = err;
+            }
+          });
+  
+          it('should include Chai frames in stack trace', function () {
+            expect(caughtErr.stack).to.contain('methodWrapper');
+          });
+  
+          it('should include user frames in stack trace', function () {
+            expect(caughtErr.stack).to.contain('badMethodAssertion');
+          });
+        });
+  
+        describe('failed overwritten method assertions', function () {
+          var caughtErr = '__PRETEST__';
+  
+          before(function () {
+            chai.config.includeStack = true;
+  
+            try {
+              badOverwrittenMethodAssertion();
+            } catch (err) {
+              caughtErr = err;
+            }
+          });
+  
+          it('should include Chai frames in stack trace', function () {
+            expect(caughtErr.stack).to.contain('overwritingMethodWrapper');
+          });
+  
+          it('should include user frames in stack trace', function () {
+            expect(caughtErr.stack).to.contain('badOverwrittenMethodAssertion');
+          });
+        });
+  
+        describe('failed chainable method assertions', function () {
+          var caughtErr = '__PRETEST__';
+  
+          before(function () {
+            chai.config.includeStack = true;
+  
+            try {
+              badChainableMethodAssertion();
+            } catch (err) {
+              caughtErr = err;
+            }
+          });
+  
+          it('should include Chai frames in stack trace', function () {
+            expect(caughtErr.stack).to.contain('chainableMethodWrapper');
+          });
+  
+          it('should include user frames in stack trace', function () {
+            expect(caughtErr.stack).to.contain('badChainableMethodAssertion');
+          });
+        });
+  
+        describe('failed overwritten chainable method assertions', function () {
+          var caughtErr = '__PRETEST__';
+  
+          before(function () {
+            chai.config.includeStack = true;
+  
+            try {
+              badOverwrittenChainableMethodAssertion();
+            } catch (err) {
+              caughtErr = err;
+            }
+          });
+  
+          it('should include Chai frames in stack trace', function () {
+            expect(caughtErr.stack).to.contain('overwritingChainableMethodWrapper');
+          });
+  
+          it('should include user frames in stack trace', function () {
+            expect(caughtErr.stack).to.contain('badOverwrittenChainableMethodAssertion');
+          });
+        });
+      });
+  
+      describe('when false', function () {
+        describe('failed property assertions', function () {
+          var caughtErr = '__PRETEST__';
+  
+          before(function () {
+            chai.config.includeStack = false;
+  
+            try {
+              badPropertyAssertion();
+            } catch (err) {
+              caughtErr = err;
+            }
+          });
+  
+          it('should not include Chai frames in stack trace', function () {
+            expect(caughtErr.stack).to.not.contain('propertyGetter');
+  
+            if (typeof Proxy !== 'undefined' && typeof Reflect !== 'undefined') {
+              expect(caughtErr.stack).to.not.contain('proxyGetter');
+            }
+          });
+  
+          it('should include user frames in stack trace', function () {
+            expect(caughtErr.stack).to.contain('badPropertyAssertion');
+          });
+        });
+  
+        describe('failed overwritten property assertions', function () {
+          var caughtErr = '__PRETEST__';
+  
+          before(function () {
+            chai.config.includeStack = false;
+  
+            try {
+              badOverwrittenPropertyAssertion();
+            } catch (err) {
+              caughtErr = err;
+            }
+          });
+  
+          it('should not include Chai frames in stack trace', function () {
+            expect(caughtErr.stack).to.not.contain('overwritingPropertyGetter');
+  
+            if (typeof Proxy !== 'undefined' && typeof Reflect !== 'undefined') {
+              expect(caughtErr.stack).to.not.contain('proxyGetter');
+            }
+          });
+  
+          it('should include user frames in stack trace', function () {
+            expect(caughtErr.stack).to.contain('badOverwrittenPropertyAssertion');
+          });
+        });
+  
+        describe('failed method assertions', function () {
+          var caughtErr = '__PRETEST__';
+  
+          before(function () {
+            chai.config.includeStack = false;
+  
+            try {
+              badMethodAssertion();
+            } catch (err) {
+              caughtErr = err;
+            }
+          });
+  
+          it('should not include Chai frames in stack trace', function () {
+            expect(caughtErr.stack).to.not.contain('methodWrapper');
+          });
+  
+          it('should include user frames in stack trace', function () {
+            expect(caughtErr.stack).to.contain('badMethodAssertion');
+          });
+        });
+  
+        describe('failed overwritten method assertions', function () {
+          var caughtErr = '__PRETEST__';
+  
+          before(function () {
+            chai.config.includeStack = false;
+  
+            try {
+              badOverwrittenMethodAssertion();
+            } catch (err) {
+              caughtErr = err;
+            }
+          });
+  
+          it('should not include Chai frames in stack trace', function () {
+            expect(caughtErr.stack).to.not.contain('overwritingMethodWrapper');
+          });
+  
+          it('should include user frames in stack trace', function () {
+            expect(caughtErr.stack).to.contain('badOverwrittenMethodAssertion');
+          });
+        });
+  
+        describe('failed chainable method assertions', function () {
+          var caughtErr = '__PRETEST__';
+  
+          before(function () {
+            chai.config.includeStack = false;
+  
+            try {
+              badChainableMethodAssertion();
+            } catch (err) {
+              caughtErr = err;
+            }
+          });
+  
+          it('should not include Chai frames in stack trace', function () {
+            expect(caughtErr.stack).to.not.contain('chainableMethodWrapper');
+          });
+  
+          it('should include user frames in stack trace', function () {
+            expect(caughtErr.stack).to.contain('badChainableMethodAssertion');
+          });
+        });
+  
+        describe('failed overwritten chainable method assertions', function () {
+          var caughtErr = '__PRETEST__';
+  
+          before(function () {
+            chai.config.includeStack = false;
+  
+            try {
+              badOverwrittenChainableMethodAssertion();
+            } catch (err) {
+              caughtErr = err;
+            }
+          });
+  
+          it('should not include Chai frames in stack trace', function () {
+            expect(caughtErr.stack).to.not.contain('overwritingChainableMethodWrapper');
+          });
+  
+          it('should include user frames in stack trace', function () {
+            expect(caughtErr.stack).to.contain('badOverwrittenChainableMethodAssertion');
+          });
+        });
+      });
+    });
 
-      try {
-        fooPropThrows();
-        assert.ok(false, 'should not get here because error thrown');
-      } catch (err) {
-        // IE 10 supports err.stack in Chrome format, but without
-        // `Error.captureStackTrace` support that allows tuning of the error
-        // message.
-        if ('undefined' !== typeof err.stack && 'undefined' !== typeof Error.captureStackTrace) {
-          assert.notInclude(err.stack, 'addProperty', 'should not have internal stack trace in error message');
-          assert.include(err.stack, 'fooPropThrows', 'should have user stack trace in error message');
-        }
+    describe('should interface', function () {
+      // Functions that always throw an error
+      function badPropertyAssertion() {
+        (42).should.be.false;
+      }
+      function badOverwrittenPropertyAssertion() {
+        (42).should.tmpProperty;
       }
+      function badMethodAssertion() {
+        (42).should.equal(false);
+      }
+      function badOverwrittenMethodAssertion() {
+        (42).should.tmpMethod();
+      }
+      function badChainableMethodAssertion() {
+        (42).should.be.a('string');
+      }
+      function badOverwrittenChainableMethodAssertion() {
+        (42).should.tmpChainableMethod();
+      }
+
+      describe('when true', function () {
+        describe('failed property assertions', function () {
+          var caughtErr = '__PRETEST__';
+  
+          before(function () {
+            chai.config.includeStack = true;
+  
+            try {
+              badPropertyAssertion();
+            } catch (err) {
+              caughtErr = err;
+            }
+          });
+  
+          it('should include Chai frames in stack trace', function () {
+            expect(caughtErr.stack).to.contain('propertyGetter');
+  
+            if (typeof Proxy !== 'undefined' && typeof Reflect !== 'undefined') {
+              expect(caughtErr.stack).to.contain('proxyGetter');
+            }
+          });
+  
+          it('should include user frames in stack trace', function () {
+            expect(caughtErr.stack).to.contain('badPropertyAssertion');
+          });
+        });
+  
+        describe('failed overwritten property assertions', function () {
+          var caughtErr = '__PRETEST__';
+  
+          before(function () {
+            chai.config.includeStack = true;
+  
+            try {
+              badOverwrittenPropertyAssertion();
+            } catch (err) {
+              caughtErr = err;
+            }
+          });
+  
+          it('should include Chai frames in stack trace', function () {
+            expect(caughtErr.stack).to.contain('overwritingPropertyGetter');
+  
+            if (typeof Proxy !== 'undefined' && typeof Reflect !== 'undefined') {
+              expect(caughtErr.stack).to.contain('proxyGetter');
+            }
+          });
+  
+          it('should include user frames in stack trace', function () {
+            expect(caughtErr.stack).to.contain('badOverwrittenPropertyAssertion');
+          });
+        });
+  
+        describe('failed method assertions', function () {
+          var caughtErr = '__PRETEST__';
+  
+          before(function () {
+            chai.config.includeStack = true;
+  
+            try {
+              badMethodAssertion();
+            } catch (err) {
+              caughtErr = err;
+            }
+          });
+  
+          it('should include Chai frames in stack trace', function () {
+            expect(caughtErr.stack).to.contain('methodWrapper');
+          });
+  
+          it('should include user frames in stack trace', function () {
+            expect(caughtErr.stack).to.contain('badMethodAssertion');
+          });
+        });
+  
+        describe('failed overwritten method assertions', function () {
+          var caughtErr = '__PRETEST__';
+  
+          before(function () {
+            chai.config.includeStack = true;
+  
+            try {
+              badOverwrittenMethodAssertion();
+            } catch (err) {
+              caughtErr = err;
+            }
+          });
+  
+          it('should include Chai frames in stack trace', function () {
+            expect(caughtErr.stack).to.contain('overwritingMethodWrapper');
+          });
+  
+          it('should include user frames in stack trace', function () {
+            expect(caughtErr.stack).to.contain('badOverwrittenMethodAssertion');
+          });
+        });
+  
+        describe('failed chainable method assertions', function () {
+          var caughtErr = '__PRETEST__';
+  
+          before(function () {
+            chai.config.includeStack = true;
+  
+            try {
+              badChainableMethodAssertion();
+            } catch (err) {
+              caughtErr = err;
+            }
+          });
+  
+          it('should include Chai frames in stack trace', function () {
+            expect(caughtErr.stack).to.contain('chainableMethodWrapper');
+          });
+  
+          it('should include user frames in stack trace', function () {
+            expect(caughtErr.stack).to.contain('badChainableMethodAssertion');
+          });
+        });
+  
+        describe('failed overwritten chainable method assertions', function () {
+          var caughtErr = '__PRETEST__';
+  
+          before(function () {
+            chai.config.includeStack = true;
+  
+            try {
+              badOverwrittenChainableMethodAssertion();
+            } catch (err) {
+              caughtErr = err;
+            }
+          });
+  
+          it('should include Chai frames in stack trace', function () {
+            expect(caughtErr.stack).to.contain('overwritingChainableMethodWrapper');
+          });
+  
+          it('should include user frames in stack trace', function () {
+            expect(caughtErr.stack).to.contain('badOverwrittenChainableMethodAssertion');
+          });
+        });
+      });
+  
+      describe('when false', function () {
+        describe('failed property assertions', function () {
+          var caughtErr = '__PRETEST__';
+  
+          before(function () {
+            chai.config.includeStack = false;
+  
+            try {
+              badPropertyAssertion();
+            } catch (err) {
+              caughtErr = err;
+            }
+          });
+  
+          it('should not include Chai frames in stack trace', function () {
+            expect(caughtErr.stack).to.not.contain('propertyGetter');
+  
+            if (typeof Proxy !== 'undefined' && typeof Reflect !== 'undefined') {
+              expect(caughtErr.stack).to.not.contain('proxyGetter');
+            }
+          });
+  
+          it('should include user frames in stack trace', function () {
+            expect(caughtErr.stack).to.contain('badPropertyAssertion');
+          });
+        });
+  
+        describe('failed overwritten property assertions', function () {
+          var caughtErr = '__PRETEST__';
+  
+          before(function () {
+            chai.config.includeStack = false;
+  
+            try {
+              badOverwrittenPropertyAssertion();
+            } catch (err) {
+              caughtErr = err;
+            }
+          });
+  
+          it('should not include Chai frames in stack trace', function () {
+            expect(caughtErr.stack).to.not.contain('overwritingPropertyGetter');
+  
+            if (typeof Proxy !== 'undefined' && typeof Reflect !== 'undefined') {
+              expect(caughtErr.stack).to.not.contain('proxyGetter');
+            }
+          });
+  
+          it('should include user frames in stack trace', function () {
+            expect(caughtErr.stack).to.contain('badOverwrittenPropertyAssertion');
+          });
+        });
+  
+        describe('failed method assertions', function () {
+          var caughtErr = '__PRETEST__';
+  
+          before(function () {
+            chai.config.includeStack = false;
+  
+            try {
+              badMethodAssertion();
+            } catch (err) {
+              caughtErr = err;
+            }
+          });
+  
+          it('should not include Chai frames in stack trace', function () {
+            expect(caughtErr.stack).to.not.contain('methodWrapper');
+          });
+  
+          it('should include user frames in stack trace', function () {
+            expect(caughtErr.stack).to.contain('badMethodAssertion');
+          });
+        });
+  
+        describe('failed overwritten method assertions', function () {
+          var caughtErr = '__PRETEST__';
+  
+          before(function () {
+            chai.config.includeStack = false;
+  
+            try {
+              badOverwrittenMethodAssertion();
+            } catch (err) {
+              caughtErr = err;
+            }
+          });
+  
+          it('should not include Chai frames in stack trace', function () {
+            expect(caughtErr.stack).to.not.contain('overwritingMethodWrapper');
+          });
+  
+          it('should include user frames in stack trace', function () {
+            expect(caughtErr.stack).to.contain('badOverwrittenMethodAssertion');
+          });
+        });
+  
+        describe('failed chainable method assertions', function () {
+          var caughtErr = '__PRETEST__';
+  
+          before(function () {
+            chai.config.includeStack = false;
+  
+            try {
+              badChainableMethodAssertion();
+            } catch (err) {
+              caughtErr = err;
+            }
+          });
+  
+          it('should not include Chai frames in stack trace', function () {
+            expect(caughtErr.stack).to.not.contain('chainableMethodWrapper');
+          });
+  
+          it('should include user frames in stack trace', function () {
+            expect(caughtErr.stack).to.contain('badChainableMethodAssertion');
+          });
+        });
+  
+        describe('failed overwritten chainable method assertions', function () {
+          var caughtErr = '__PRETEST__';
+  
+          before(function () {
+            chai.config.includeStack = false;
+  
+            try {
+              badOverwrittenChainableMethodAssertion();
+            } catch (err) {
+              caughtErr = err;
+            }
+          });
+  
+          it('should not include Chai frames in stack trace', function () {
+            expect(caughtErr.stack).to.not.contain('overwritingChainableMethodWrapper');
+          });
+  
+          it('should include user frames in stack trace', function () {
+            expect(caughtErr.stack).to.contain('badOverwrittenChainableMethodAssertion');
+          });
+        });
+      });
     });
   });
 
diff --git a/test/utilities.js b/test/utilities.js
index a1098f3..98a30d3 100644
--- a/test/utilities.js
+++ b/test/utilities.js
@@ -1118,4 +1118,83 @@ describe('utilities', function () {
       }).to.throw('Invalid Chai property: hoagie.length. Due to a compatibility issue, "length" cannot directly follow "hoagie". Use "hoagie.lengthOf" instead.');
     });
   });
+
+  describe("isProxyEnabled", function () {
+    if (typeof Proxy === 'undefined' || typeof Reflect === 'undefined') return;
+
+    var origProxy, origReflect, origUseProxy, isProxyEnabled;
+
+    before(function () {
+      chai.use(function (_chai, _) {
+        isProxyEnabled = _.isProxyEnabled;
+      });
+
+      origProxy = Proxy;
+      origReflect = Reflect;
+      origUseProxy = chai.config.useProxy;
+    });
+
+    beforeEach(function () {
+      Proxy = origProxy;
+      Reflect = origReflect;
+      chai.config.useProxy = true;
+    });
+
+    after(function () {
+      Proxy = origProxy;
+      Reflect = origReflect;
+      chai.config.useProxy = origUseProxy;
+    });
+
+    it("returns true if Proxy is defined, Reflect is defined, and useProxy is true", function () {
+      expect(isProxyEnabled()).to.be.true;
+    });
+
+    it("returns false if Proxy is defined, Reflect is defined, and useProxy is false", function () {
+      chai.config.useProxy = false;
+
+      expect(isProxyEnabled()).to.be.false;
+    });
+
+    it("returns false if Proxy is defined, Reflect is undefined, and useProxy is true", function () {
+      Reflect = undefined;
+
+      expect(isProxyEnabled()).to.be.false;
+    });
+
+    it("returns false if Proxy is defined, Reflect is undefined, and useProxy is false", function () {
+      Reflect = undefined;
+      chai.config.useProxy = false;
+
+      expect(isProxyEnabled()).to.be.false;
+    });
+
+    it("returns false if Proxy is undefined, Reflect is defined, and useProxy is true", function () {
+      Proxy = undefined;
+
+      expect(isProxyEnabled()).to.be.false;
+    });
+
+    it("returns false if Proxy is undefined, Reflect is defined, and useProxy is false", function () {
+      Proxy = undefined;
+      chai.config.useProxy = false;
+
+      expect(isProxyEnabled()).to.be.false;
+    });
+
+    it("returns false if Proxy is undefined, Reflect is undefined, and useProxy is true", function () {
+      Proxy = undefined;
+      Reflect = undefined;
+
+      expect(isProxyEnabled()).to.be.false;
+    });
+
+    it("returns false if Proxy is undefined, Reflect is undefined, and useProxy is false", function () {
+      Proxy = undefined;
+      Reflect = undefined;
+      chai.config.useProxy = false;
+
+      expect(isProxyEnabled()).to.be.false;
+    });
+  });
 });