Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add fix suggestions when accessing a nonexistent property in proxy mode #782

Merged
merged 1 commit into from
Sep 8, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
57 changes: 55 additions & 2 deletions lib/chai/utils/proxify.js
@@ -1,4 +1,5 @@
var config = require('../config');
var getProperties = require('./getProperties');

/*!
* Chai - proxify utility
Expand Down Expand Up @@ -30,10 +31,62 @@ module.exports = function proxify (obj) {
// the `config.proxyExcludedKeys` setting.
if (typeof property === 'string' &&
config.proxyExcludedKeys.indexOf(property) === -1 &&
!Reflect.has(target, property))
throw Error('Invalid Chai property: ' + property);
!Reflect.has(target, property)) {
var orderedProperties = getProperties(target).filter(function(property) {
return !Object.prototype.hasOwnProperty(property) &&
['__flags', '__methods', '_obj', 'assert'].indexOf(property) === -1;
}).sort(function(a, b) {
return stringDistance(property, a) - stringDistance(property, b);
});

if (orderedProperties.length &&
stringDistance(orderedProperties[0], property) < 4) {
// If the property is reasonably close to an existing Chai property,
// suggest that property to the user.
throw Error('Invalid Chai property: ' + property +
'. Did you mean "' + orderedProperties[0] + '"?');
} else {
throw Error('Invalid Chai property: ' + property);
}
}

return target[property];
}
});
};

/**
* # stringDistance(strA, strB)
* Return the Levenshtein distance between two strings.
* @param {string} strA
* @param {string} strB
* @return {number} the string distance between strA and strB
* @api private
*/

function stringDistance(strA, strB, memo) {
if (!memo) {
// `memo` is a two-dimensional array containing a cache of distances
// memo[i][j] is the distance between strA.slice(0, i) and
// strB.slice(0, j).
memo = [];
for (var i = 0; i <= strA.length; i++) {
memo[i] = [];
}
}

if (!memo[strA.length] || !memo[strA.length][strB.length]) {
if (strA.length === 0 || strB.length === 0) {
memo[strA.length][strB.length] = Math.max(strA.length, strB.length);
} else {
memo[strA.length][strB.length] = Math.min(
stringDistance(strA.slice(0, -1), strB, memo) + 1,
stringDistance(strA, strB.slice(0, -1), memo) + 1,
stringDistance(strA.slice(0, -1), strB.slice(0, -1), memo) +
(strA.slice(-1) === strB.slice(-1) ? 0 : 1)
);
}
}

return memo[strA.length][strB.length];
}
32 changes: 32 additions & 0 deletions test/utilities.js
Expand Up @@ -870,6 +870,38 @@ describe('utilities', function () {
}).to.throw('Invalid Chai property: mushrooms');
});

it('suggests a fix if a non-existent prop looks like a typo', function () {
var pizza = proxify({foo: 1, bar: 2, baz: 3});

expect(function () {
pizza.phoo;
}).to.throw('Invalid Chai property: phoo. Did you mean "foo"?');
});

it('doesn\'t take exponential time to find string distances', function () {
var pizza = proxify({veryLongPropertyNameWithLotsOfLetters: 1});

expect(function () {
pizza.extremelyLongPropertyNameWithManyLetters;
}).to.throw(
'Invalid Chai property: extremelyLongPropertyNameWithManyLetters'
);
});

it('doesn\'t suggest properties from Object.prototype', function () {
var pizza = proxify({string: 5});
expect(function () {
pizza.tostring;
}).to.throw('Invalid Chai property: tostring. Did you mean "string"?');
});

it('doesn\'t suggest internally properties', function () {
var pizza = proxify({flags: 5, __flags: 6});
expect(function () {
pizza.___flags; // 3 underscores; closer to '__flags' than 'flags'
}).to.throw('Invalid Chai property: ___flags. Did you mean "flags"?');
});

// .then is excluded from property validation for promise support
it('doesn\'t throw error if non-existent `then` is read', function () {
var pizza = proxify({});
Expand Down