261 lines
9.2 KiB
JavaScript
261 lines
9.2 KiB
JavaScript
|
"use strict";
|
||
|
|
||
|
var valueToString = require("@sinonjs/commons").valueToString;
|
||
|
var className = require("@sinonjs/commons").className;
|
||
|
var typeOf = require("@sinonjs/commons").typeOf;
|
||
|
var arrayProto = require("@sinonjs/commons").prototypes.array;
|
||
|
var objectProto = require("@sinonjs/commons").prototypes.object;
|
||
|
var mapForEach = require("@sinonjs/commons").prototypes.map.forEach;
|
||
|
|
||
|
var getClass = require("./get-class");
|
||
|
var identical = require("./identical");
|
||
|
var isArguments = require("./is-arguments");
|
||
|
var isDate = require("./is-date");
|
||
|
var isElement = require("./is-element");
|
||
|
var isMap = require("./is-map");
|
||
|
var isNaN = require("./is-nan");
|
||
|
var isObject = require("./is-object");
|
||
|
var isSet = require("./is-set");
|
||
|
var isSubset = require("./is-subset");
|
||
|
|
||
|
var concat = arrayProto.concat;
|
||
|
var every = arrayProto.every;
|
||
|
var push = arrayProto.push;
|
||
|
|
||
|
var getTime = Date.prototype.getTime;
|
||
|
var hasOwnProperty = objectProto.hasOwnProperty;
|
||
|
var indexOf = arrayProto.indexOf;
|
||
|
var keys = Object.keys;
|
||
|
var getOwnPropertySymbols = Object.getOwnPropertySymbols;
|
||
|
|
||
|
/**
|
||
|
* Deep equal comparison. Two values are "deep equal" when:
|
||
|
*
|
||
|
* - They are equal, according to samsam.identical
|
||
|
* - They are both date objects representing the same time
|
||
|
* - They are both arrays containing elements that are all deepEqual
|
||
|
* - They are objects with the same set of properties, and each property
|
||
|
* in ``actual`` is deepEqual to the corresponding property in ``expectation``
|
||
|
*
|
||
|
* Supports cyclic objects.
|
||
|
*
|
||
|
* @alias module:samsam.deepEqual
|
||
|
* @param {*} actual The object to examine
|
||
|
* @param {*} expectation The object actual is expected to be equal to
|
||
|
* @param {object} match A value to match on
|
||
|
* @returns {boolean} Returns true when actual and expectation are considered equal
|
||
|
*/
|
||
|
function deepEqualCyclic(actual, expectation, match) {
|
||
|
// used for cyclic comparison
|
||
|
// contain already visited objects
|
||
|
var actualObjects = [];
|
||
|
var expectationObjects = [];
|
||
|
// contain pathes (position in the object structure)
|
||
|
// of the already visited objects
|
||
|
// indexes same as in objects arrays
|
||
|
var actualPaths = [];
|
||
|
var expectationPaths = [];
|
||
|
// contains combinations of already compared objects
|
||
|
// in the manner: { "$1['ref']$2['ref']": true }
|
||
|
var compared = {};
|
||
|
|
||
|
// does the recursion for the deep equal check
|
||
|
// eslint-disable-next-line complexity
|
||
|
return (function deepEqual(
|
||
|
actualObj,
|
||
|
expectationObj,
|
||
|
actualPath,
|
||
|
expectationPath
|
||
|
) {
|
||
|
// If both are matchers they must be the same instance in order to be
|
||
|
// considered equal If we didn't do that we would end up running one
|
||
|
// matcher against the other
|
||
|
if (match && match.isMatcher(expectationObj)) {
|
||
|
if (match.isMatcher(actualObj)) {
|
||
|
return actualObj === expectationObj;
|
||
|
}
|
||
|
return expectationObj.test(actualObj);
|
||
|
}
|
||
|
|
||
|
var actualType = typeof actualObj;
|
||
|
var expectationType = typeof expectationObj;
|
||
|
|
||
|
if (
|
||
|
actualObj === expectationObj ||
|
||
|
isNaN(actualObj) ||
|
||
|
isNaN(expectationObj) ||
|
||
|
actualObj === null ||
|
||
|
expectationObj === null ||
|
||
|
actualObj === undefined ||
|
||
|
expectationObj === undefined ||
|
||
|
actualType !== "object" ||
|
||
|
expectationType !== "object"
|
||
|
) {
|
||
|
return identical(actualObj, expectationObj);
|
||
|
}
|
||
|
|
||
|
// Elements are only equal if identical(expected, actual)
|
||
|
if (isElement(actualObj) || isElement(expectationObj)) {
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
var isActualDate = isDate(actualObj);
|
||
|
var isExpectationDate = isDate(expectationObj);
|
||
|
if (isActualDate || isExpectationDate) {
|
||
|
if (
|
||
|
!isActualDate ||
|
||
|
!isExpectationDate ||
|
||
|
getTime.call(actualObj) !== getTime.call(expectationObj)
|
||
|
) {
|
||
|
return false;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (actualObj instanceof RegExp && expectationObj instanceof RegExp) {
|
||
|
if (valueToString(actualObj) !== valueToString(expectationObj)) {
|
||
|
return false;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (actualObj instanceof Error && expectationObj instanceof Error) {
|
||
|
return actualObj === expectationObj;
|
||
|
}
|
||
|
|
||
|
var actualClass = getClass(actualObj);
|
||
|
var expectationClass = getClass(expectationObj);
|
||
|
var actualKeys = keys(actualObj);
|
||
|
var expectationKeys = keys(expectationObj);
|
||
|
var actualName = className(actualObj);
|
||
|
var expectationName = className(expectationObj);
|
||
|
var expectationSymbols =
|
||
|
typeOf(getOwnPropertySymbols) === "function"
|
||
|
? getOwnPropertySymbols(expectationObj)
|
||
|
: /* istanbul ignore next: cannot collect coverage for engine that doesn't support Symbol */
|
||
|
[];
|
||
|
var expectationKeysAndSymbols = concat(
|
||
|
expectationKeys,
|
||
|
expectationSymbols
|
||
|
);
|
||
|
|
||
|
if (isArguments(actualObj) || isArguments(expectationObj)) {
|
||
|
if (actualObj.length !== expectationObj.length) {
|
||
|
return false;
|
||
|
}
|
||
|
} else {
|
||
|
if (
|
||
|
actualType !== expectationType ||
|
||
|
actualClass !== expectationClass ||
|
||
|
actualKeys.length !== expectationKeys.length ||
|
||
|
(actualName &&
|
||
|
expectationName &&
|
||
|
actualName !== expectationName)
|
||
|
) {
|
||
|
return false;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (isSet(actualObj) || isSet(expectationObj)) {
|
||
|
if (
|
||
|
!isSet(actualObj) ||
|
||
|
!isSet(expectationObj) ||
|
||
|
actualObj.size !== expectationObj.size
|
||
|
) {
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
return isSubset(actualObj, expectationObj, deepEqual);
|
||
|
}
|
||
|
|
||
|
if (isMap(actualObj) || isMap(expectationObj)) {
|
||
|
if (
|
||
|
!isMap(actualObj) ||
|
||
|
!isMap(expectationObj) ||
|
||
|
actualObj.size !== expectationObj.size
|
||
|
) {
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
var mapsDeeplyEqual = true;
|
||
|
mapForEach(actualObj, function(value, key) {
|
||
|
mapsDeeplyEqual =
|
||
|
mapsDeeplyEqual &&
|
||
|
deepEqualCyclic(value, expectationObj.get(key));
|
||
|
});
|
||
|
|
||
|
return mapsDeeplyEqual;
|
||
|
}
|
||
|
|
||
|
return every(expectationKeysAndSymbols, function(key) {
|
||
|
if (!hasOwnProperty(actualObj, key)) {
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
var actualValue = actualObj[key];
|
||
|
var expectationValue = expectationObj[key];
|
||
|
var actualObject = isObject(actualValue);
|
||
|
var expectationObject = isObject(expectationValue);
|
||
|
// determines, if the objects were already visited
|
||
|
// (it's faster to check for isObject first, than to
|
||
|
// get -1 from getIndex for non objects)
|
||
|
var actualIndex = actualObject
|
||
|
? indexOf(actualObjects, actualValue)
|
||
|
: -1;
|
||
|
var expectationIndex = expectationObject
|
||
|
? indexOf(expectationObjects, expectationValue)
|
||
|
: -1;
|
||
|
// determines the new paths of the objects
|
||
|
// - for non cyclic objects the current path will be extended
|
||
|
// by current property name
|
||
|
// - for cyclic objects the stored path is taken
|
||
|
var newActualPath =
|
||
|
actualIndex !== -1
|
||
|
? actualPaths[actualIndex]
|
||
|
: actualPath + "[" + JSON.stringify(key) + "]";
|
||
|
var newExpectationPath =
|
||
|
expectationIndex !== -1
|
||
|
? expectationPaths[expectationIndex]
|
||
|
: expectationPath + "[" + JSON.stringify(key) + "]";
|
||
|
var combinedPath = newActualPath + newExpectationPath;
|
||
|
|
||
|
// stop recursion if current objects are already compared
|
||
|
if (compared[combinedPath]) {
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
// remember the current objects and their paths
|
||
|
if (actualIndex === -1 && actualObject) {
|
||
|
push(actualObjects, actualValue);
|
||
|
push(actualPaths, newActualPath);
|
||
|
}
|
||
|
if (expectationIndex === -1 && expectationObject) {
|
||
|
push(expectationObjects, expectationValue);
|
||
|
push(expectationPaths, newExpectationPath);
|
||
|
}
|
||
|
|
||
|
// remember that the current objects are already compared
|
||
|
if (actualObject && expectationObject) {
|
||
|
compared[combinedPath] = true;
|
||
|
}
|
||
|
|
||
|
// End of cyclic logic
|
||
|
|
||
|
// neither actualValue nor expectationValue is a cycle
|
||
|
// continue with next level
|
||
|
return deepEqual(
|
||
|
actualValue,
|
||
|
expectationValue,
|
||
|
newActualPath,
|
||
|
newExpectationPath
|
||
|
);
|
||
|
});
|
||
|
})(actual, expectation, "$1", "$2");
|
||
|
}
|
||
|
|
||
|
deepEqualCyclic.use = function(match) {
|
||
|
return function deepEqual(a, b) {
|
||
|
return deepEqualCyclic(a, b, match);
|
||
|
};
|
||
|
};
|
||
|
|
||
|
module.exports = deepEqualCyclic;
|