Skip to content

Commit

Permalink
Wrap non-chainable methods in proxies
Browse files Browse the repository at this point in the history
  • Loading branch information
meeber committed Sep 10, 2016
1 parent e5e6f5c commit 6d2c663
Show file tree
Hide file tree
Showing 5 changed files with 82 additions and 10 deletions.
7 changes: 5 additions & 2 deletions lib/chai/utils/addMethod.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

var chai = require('../../chai');
var flag = require('./flag');
var proxify = require('./proxify');
var transferFlags = require('./transferFlags');

/**
Expand Down Expand Up @@ -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)
Expand All @@ -49,4 +50,6 @@ module.exports = function (ctx, name, method) {
transferFlags(this, newAssertion);
return newAssertion;
};

ctx[name] = proxify(fn, name);
};
20 changes: 17 additions & 3 deletions lib/chai/utils/proxify.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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;
Expand Down
23 changes: 21 additions & 2 deletions test/expect.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
23 changes: 21 additions & 2 deletions test/should.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
19 changes: 18 additions & 1 deletion test/utilities.js
Original file line number Diff line number Diff line change
Expand Up @@ -862,14 +862,31 @@ 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 () {
pizza.mushrooms;
}).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});

Expand Down

0 comments on commit 6d2c663

Please sign in to comment.