"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;