148 lines
5.1 KiB
Plaintext
148 lines
5.1 KiB
Plaintext
var config = require('../config');
|
|
var flag = require('./flag');
|
|
var getProperties = require('./getProperties');
|
|
var isProxyEnabled = require('./isProxyEnabled');
|
|
|
|
/*!
|
|
* Chai - proxify utility
|
|
* Copyright(c) 2012-2014 Jake Luer <jake@alogicalparadox.com>
|
|
* MIT Licensed
|
|
*/
|
|
|
|
/**
|
|
* ### .proxify(object)
|
|
*
|
|
* Return a proxy of given object that throws an error when a non-existent
|
|
* 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
|
|
*/
|
|
|
|
var builtins = ['__flags', '__methods', '_obj', 'assert'];
|
|
|
|
module.exports = function proxify(obj, nonChainableMethodName) {
|
|
if (!isProxyEnabled()) return obj;
|
|
|
|
return new Proxy(obj, {
|
|
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
|
|
// the `config.proxyExcludedKeys` setting.
|
|
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 + '".');
|
|
}
|
|
|
|
// If the property is reasonably close to an existing Chai property,
|
|
// suggest that property to the user. Only suggest properties with a
|
|
// distance less than 4.
|
|
var suggestion = null;
|
|
var suggestionDistance = 4;
|
|
getProperties(target).forEach(function(prop) {
|
|
if (
|
|
!Object.prototype.hasOwnProperty(prop) &&
|
|
builtins.indexOf(prop) === -1
|
|
) {
|
|
var dist = stringDistanceCapped(
|
|
property,
|
|
prop,
|
|
suggestionDistance
|
|
);
|
|
if (dist < suggestionDistance) {
|
|
suggestion = prop;
|
|
suggestionDistance = dist;
|
|
}
|
|
}
|
|
});
|
|
|
|
if (suggestion !== null) {
|
|
throw Error('Invalid Chai property: ' + property +
|
|
'. Did you mean "' + suggestion + '"?');
|
|
} else {
|
|
throw Error('Invalid Chai property: ' + 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. This step is also skipped
|
|
// if the `lockSsfi` flag is set, thus indicating that this assertion is
|
|
// being called from within another assertion. In that case, the `ssfi`
|
|
// flag is already set to the outer assertion's starting point.
|
|
if (builtins.indexOf(property) === -1 && !flag(target, 'lockSsfi')) {
|
|
flag(target, 'ssfi', proxyGetter);
|
|
}
|
|
|
|
return Reflect.get(target, property);
|
|
}
|
|
});
|
|
};
|
|
|
|
/**
|
|
* # stringDistanceCapped(strA, strB, cap)
|
|
* Return the Levenshtein distance between two strings, but no more than cap.
|
|
* @param {string} strA
|
|
* @param {string} strB
|
|
* @param {number} number
|
|
* @return {number} min(string distance between strA and strB, cap)
|
|
* @api private
|
|
*/
|
|
|
|
function stringDistanceCapped(strA, strB, cap) {
|
|
if (Math.abs(strA.length - strB.length) >= cap) {
|
|
return cap;
|
|
}
|
|
|
|
var memo = [];
|
|
// `memo` is a two-dimensional array containing distances.
|
|
// memo[i][j] is the distance between strA.slice(0, i) and
|
|
// strB.slice(0, j).
|
|
for (var i = 0; i <= strA.length; i++) {
|
|
memo[i] = Array(strB.length + 1).fill(0);
|
|
memo[i][0] = i;
|
|
}
|
|
for (var j = 0; j < strB.length; j++) {
|
|
memo[0][j] = j;
|
|
}
|
|
|
|
for (var i = 1; i <= strA.length; i++) {
|
|
var ch = strA.charCodeAt(i - 1);
|
|
for (var j = 1; j <= strB.length; j++) {
|
|
if (Math.abs(i - j) >= cap) {
|
|
memo[i][j] = cap;
|
|
continue;
|
|
}
|
|
memo[i][j] = Math.min(
|
|
memo[i - 1][j] + 1,
|
|
memo[i][j - 1] + 1,
|
|
memo[i - 1][j - 1] +
|
|
(ch === strB.charCodeAt(j - 1) ? 0 : 1)
|
|
);
|
|
}
|
|
}
|
|
|
|
return memo[strA.length][strB.length];
|
|
}
|