From e28f04881f656b2ff4f3b056ae7e6d7b0645c0bb Mon Sep 17 00:00:00 2001 From: Grant Snodgrass Date: Thu, 8 Sep 2016 20:50:39 -0400 Subject: [PATCH] Wrap non-chainable methods in proxies --- lib/chai/utils/addMethod.js | 7 +++++-- lib/chai/utils/proxify.js | 20 +++++++++++++++++--- test/expect.js | 23 +++++++++++++++++++++-- test/should.js | 23 +++++++++++++++++++++-- test/utilities.js | 19 ++++++++++++++++++- 5 files changed, 82 insertions(+), 10 deletions(-) diff --git a/lib/chai/utils/addMethod.js b/lib/chai/utils/addMethod.js index 91c048d80..b4a51a6ba 100644 --- a/lib/chai/utils/addMethod.js +++ b/lib/chai/utils/addMethod.js @@ -6,6 +6,7 @@ var chai = require('../../chai'); var flag = require('./flag'); +var proxify = require('./proxify'); var transferFlags = require('./transferFlags'); /** @@ -35,11 +36,11 @@ var transferFlags = require('./transferFlags'); */ module.exports = function (ctx, name, method) { - ctx[name] = function () { + var fn = function () { var keep_ssfi = flag(this, 'keep_ssfi'); var old_ssfi = flag(this, 'ssfi'); if (!keep_ssfi && old_ssfi) - flag(this, 'ssfi', ctx[name]); + flag(this, 'ssfi', fn); var result = method.apply(this, arguments); if (result !== undefined) @@ -49,4 +50,6 @@ module.exports = function (ctx, name, method) { transferFlags(this, newAssertion); return newAssertion; }; + + ctx[name] = proxify(fn, name); }; diff --git a/lib/chai/utils/proxify.js b/lib/chai/utils/proxify.js index b08fe2638..a1e41c127 100644 --- a/lib/chai/utils/proxify.js +++ b/lib/chai/utils/proxify.js @@ -11,15 +11,22 @@ var getProperties = require('./getProperties'); * # proxify(object) * * Return a proxy of given object that throws an error when a non-existent - * property is read. (If Proxy or Reflect is undefined, then return object - * without modification.) + * property is read. By default, the root cause is assumed to be a misspelled + * property, and thus an attempt is made to offer a reasonable suggestion from + * the list of existing properties. However, if a nonChainableMethodName is + * provided, then the root cause is instead a failure to invoke a non-chainable + * method prior to reading the non-existent property. + * + * If proxies are unsupported or disabled via the user's Chai config, then + * return object without modification. * * @param {Object} obj + * @param {String} nonChainableMethodName * @namespace Utils * @name proxify */ -module.exports = function proxify (obj) { +module.exports = function proxify (obj, nonChainableMethodName) { if (!config.useProxy || typeof Proxy === 'undefined' || typeof Reflect === 'undefined') return obj; @@ -32,6 +39,13 @@ module.exports = function proxify (obj) { if (typeof property === 'string' && config.proxyExcludedKeys.indexOf(property) === -1 && !Reflect.has(target, property)) { + // Special message for invalid property access of non-chainable methods. + if (nonChainableMethodName) { + throw Error('Invalid Chai property: ' + nonChainableMethodName + '.' + + property + '. See docs for proper usage of "' + + nonChainableMethodName + '".'); + } + var orderedProperties = getProperties(target).filter(function(property) { return !Object.prototype.hasOwnProperty(property) && ['__flags', '__methods', '_obj', 'assert'].indexOf(property) === -1; diff --git a/test/expect.js b/test/expect.js index be4d92f04..e45d5587f 100644 --- a/test/expect.js +++ b/test/expect.js @@ -13,20 +13,39 @@ describe('expect', function () { it('invalid property', function () { if (typeof Proxy === 'undefined' || typeof Reflect === 'undefined') return; + // expect first err(function () { expect(42).pizza; }, 'Invalid Chai property: pizza'); + // language chain first err(function () { expect(42).to.pizza; }, 'Invalid Chai property: pizza'); + // property assertion first err(function () { - expect(42).to.be.a.pizza; + expect(42).ok.pizza; }, 'Invalid Chai property: pizza'); + // uncalled method assertion first err(function () { - expect(42).to.equal(42).pizza; + expect(42).equal.pizza; + }, 'Invalid Chai property: equal.pizza. See docs for proper usage of "equal".'); + + // called method assertion first + err(function () { + expect(42).equal(42).pizza; + }, 'Invalid Chai property: pizza'); + + // uncalled chainable method assertion first + err(function () { + expect(42).a.pizza; + }, 'Invalid Chai property: pizza'); + + // called chainable method assertion first + err(function () { + expect(42).a('number').pizza; }, 'Invalid Chai property: pizza'); // .then is excluded from property validation for promise support diff --git a/test/should.js b/test/should.js index 5282baccb..c07aa13da 100644 --- a/test/should.js +++ b/test/should.js @@ -10,22 +10,41 @@ describe('should', function() { it('invalid property', function () { if (typeof Proxy === 'undefined' || typeof Reflect === 'undefined') return; + // should first err(function () { (42).should.pizza; }, 'Invalid Chai property: pizza'); + // language chain first err(function () { - (42).should.be.pizza; + (42).should.to.pizza; }, 'Invalid Chai property: pizza'); + // property assertion first err(function () { - (42).should.be.a.pizza; + (42).should.ok.pizza; }, 'Invalid Chai property: pizza'); + // uncalled method assertion first + err(function () { + (42).should.equal.pizza; + }, 'Invalid Chai property: equal.pizza. See docs for proper usage of "equal".'); + + // called method assertion first err(function () { (42).should.equal(42).pizza; }, 'Invalid Chai property: pizza'); + // uncalled chainable method assertion first + err(function () { + (42).should.a.pizza; + }, 'Invalid Chai property: pizza'); + + // called chainable method assertion first + err(function () { + (42).should.a('number').pizza; + }, 'Invalid Chai property: pizza'); + // .then is excluded from property validation for promise support (function () { (42).should.then; diff --git a/test/utilities.js b/test/utilities.js index 7201eef6f..451421b20 100644 --- a/test/utilities.js +++ b/test/utilities.js @@ -938,7 +938,16 @@ describe('utilities', function () { expect(pizza.mushrooms).to.equal(42); }); - it('throws error if a non-existent property is read', function () { + it('returns property value if an existing property is read when nonChainableMethodName is set', function () { + var bake = function () {}; + bake.numPizzas = 2; + + var bakeProxy = proxify(bake, 'bake'); + + expect(bakeProxy.numPizzas).to.equal(2); + }); + + it('throws invalid property error if a non-existent property is read', function () { var pizza = proxify({}); expect(function () { @@ -946,6 +955,14 @@ describe('utilities', function () { }).to.throw('Invalid Chai property: mushrooms'); }); + it('throws invalid use error if a non-existent property is read when nonChainableMethodName is set', function () { + var bake = proxify(function () {}, 'bake'); + + expect(function () { + bake.numPizzas; + }).to.throw('Invalid Chai property: bake.numPizzas. See docs for proper usage of "bake".'); + }); + it('suggests a fix if a non-existent prop looks like a typo', function () { var pizza = proxify({foo: 1, bar: 2, baz: 3});