228 lines
7.9 KiB
JavaScript
228 lines
7.9 KiB
JavaScript
"use strict";
|
|
|
|
var valueToString = require("@sinonjs/commons").valueToString;
|
|
|
|
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 isNaN = require("./is-nan");
|
|
var isObject = require("./is-object");
|
|
var isSet = require("./is-set");
|
|
var isSubset = require("./is-subset");
|
|
var getClassName = require("./get-class-name");
|
|
|
|
var every = Array.prototype.every;
|
|
var getTime = Date.prototype.getTime;
|
|
var hasOwnProperty = Object.prototype.hasOwnProperty;
|
|
var indexOf = Array.prototype.indexOf;
|
|
var keys = Object.keys;
|
|
var getOwnPropertySymbols = Object.getOwnPropertySymbols;
|
|
|
|
/**
|
|
* @name samsam.deepEqual
|
|
* @param Object actual
|
|
* @param Object expectation
|
|
*
|
|
* Deep equal comparison. Two values are "deep equal" if:
|
|
*
|
|
* - 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.
|
|
*/
|
|
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
|
|
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;
|
|
|
|
// == null also matches undefined
|
|
if (
|
|
actualObj === expectationObj ||
|
|
isNaN(actualObj) ||
|
|
isNaN(expectationObj) ||
|
|
actualObj == null ||
|
|
expectationObj == null ||
|
|
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 = getClassName(actualObj);
|
|
var expectationName = getClassName(expectationObj);
|
|
var expectationSymbols =
|
|
typeof Object.getOwnPropertySymbols === "function"
|
|
? getOwnPropertySymbols(expectationObj)
|
|
: [];
|
|
var expectationKeysAndSymbols = expectationKeys.concat(
|
|
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);
|
|
}
|
|
|
|
return every.call(expectationKeysAndSymbols, function(key) {
|
|
if (!hasOwnProperty.call(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.call(actualObjects, actualValue)
|
|
: -1;
|
|
var expectationIndex = expectationObject
|
|
? indexOf.call(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) {
|
|
actualObjects.push(actualValue);
|
|
actualPaths.push(newActualPath);
|
|
}
|
|
if (expectationIndex === -1 && expectationObject) {
|
|
expectationObjects.push(expectationValue);
|
|
expectationPaths.push(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(a, b) {
|
|
return deepEqualCyclic(a, b, match);
|
|
};
|
|
};
|
|
|
|
module.exports = deepEqualCyclic;
|