| 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]; |
| } |