diff --git a/lib/chai/assertion.js b/lib/chai/assertion.js index 107eb083c..491add77f 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 1b20333dd..a79a707ab 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 6f6bf75fd..a5d32060a 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 48e1b2dad..f58400e5d 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 071de9ca8..708ff28c0 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 8fa7f1fd0..4b0bd4564 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 ef13236e0..ffb48f068 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 e2f28faa8..2b5b3fa5f 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 dedd8f9d8..1c9744feb 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 b17f085e4..d4f329c75 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 000000000..f0ef041db --- /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 + * 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 68142581a..d87d938e1 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 007ed125a..4d28f5aa7 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 66f55a70f..5d2306e8e 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 155ccbdab..f988ced32 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 a1e41c127..85c6cbf08 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/test.js b/lib/chai/utils/test.js index 453d4c72f..4afc5fc5c 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/lib/chai/utils/transferFlags.js b/lib/chai/utils/transferFlags.js index fa2c379d7..ea1316b54 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) { diff --git a/test/configuration.js b/test/configuration.js index d0399c4a4..d53f164fc 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 a1098f3d9..98a30d3ed 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; + }); + }); });