diff --git a/AUTHORS b/AUTHORS index 35200a9..4b0a569 100644 --- a/AUTHORS +++ b/AUTHORS @@ -10,3 +10,4 @@ Magnar Sveen Mathias Schreck Olle Jonsson Gunnar André Reinseth +Brandon Evans diff --git a/docs/index.md b/docs/index.md index e8c7a2d..a054883 100644 --- a/docs/index.md +++ b/docs/index.md @@ -53,7 +53,7 @@ observably distinguishable. `NaN` is compared to itself. -### `deepEqual(obj1, obj2)` +### `deepEqual(actual, expectation)` Deep equal comparison. Two values are "deep equal" if: @@ -61,7 +61,9 @@ Deep equal comparison. Two values are "deep equal" if: * 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 `obj1` is deepEqual to the corresponding property in `obj2` + in `actual` is deepEqual to the corresponding property in `expectation` + + * `actual` can have [symbolic properties](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol) that are missing from `expectation` ### `match(object, matcher)` diff --git a/lib/deep-equal.js b/lib/deep-equal.js index 012b0f6..abea178 100644 --- a/lib/deep-equal.js +++ b/lib/deep-equal.js @@ -18,11 +18,12 @@ 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 first - * @param Object second + * @param Object actual + * @param Object expectation * * Deep equal comparison. Two values are "deep equal" if: * @@ -30,136 +31,158 @@ var keys = Object.keys; * - 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 ``first`` is deepEqual to the corresponding property in ``second`` + * in ``actual`` is deepEqual to the corresponding property in ``expectation`` * * Supports cyclic objects. */ -function deepEqualCyclic(first, second, match) { +function deepEqualCyclic(actual, expectation, match) { // used for cyclic comparison // contain already visited objects - var objects1 = []; - var objects2 = []; + var actualObjects = []; + var expectationObjects = []; // contain pathes (position in the object structure) // of the already visited objects // indexes same as in objects arrays - var paths1 = []; - var paths2 = []; + 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(obj1, obj2, path1, path2) { + 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(obj2)) { - if (match.isMatcher(obj1)) { - return obj1 === obj2; + if (match && match.isMatcher(expectationObj)) { + if (match.isMatcher(actualObj)) { + return actualObj === expectationObj; } - return obj2.test(obj1); + return expectationObj.test(actualObj); } - var type1 = typeof obj1; - var type2 = typeof obj2; + var actualType = typeof actualObj; + var expectationType = typeof expectationObj; // == null also matches undefined if ( - obj1 === obj2 || - isNaN(obj1) || - isNaN(obj2) || - obj1 == null || - obj2 == null || - type1 !== "object" || - type2 !== "object" + actualObj === expectationObj || + isNaN(actualObj) || + isNaN(expectationObj) || + actualObj == null || + expectationObj == null || + actualType !== "object" || + expectationType !== "object" ) { - return identical(obj1, obj2); + return identical(actualObj, expectationObj); } // Elements are only equal if identical(expected, actual) - if (isElement(obj1) || isElement(obj2)) { + if (isElement(actualObj) || isElement(expectationObj)) { return false; } - var isDate1 = isDate(obj1); - var isDate2 = isDate(obj2); - if (isDate1 || isDate2) { + var isActualDate = isDate(actualObj); + var isExpectationDate = isDate(expectationObj); + if (isActualDate || isExpectationDate) { if ( - !isDate1 || - !isDate2 || - getTime.call(obj1) !== getTime.call(obj2) + !isActualDate || + !isExpectationDate || + getTime.call(actualObj) !== getTime.call(expectationObj) ) { return false; } } - if (obj1 instanceof RegExp && obj2 instanceof RegExp) { - if (valueToString(obj1) !== valueToString(obj2)) { + if (actualObj instanceof RegExp && expectationObj instanceof RegExp) { + if (valueToString(actualObj) !== valueToString(expectationObj)) { return false; } } - if (obj1 instanceof Error && obj2 instanceof Error) { - return obj1 === obj2; + if (actualObj instanceof Error && expectationObj instanceof Error) { + return actualObj === expectationObj; } - var class1 = getClass(obj1); - var class2 = getClass(obj2); - var keys1 = keys(obj1); - var keys2 = keys(obj2); - var name1 = getClassName(obj1); - var name2 = getClassName(obj2); - - if (isArguments(obj1) || isArguments(obj2)) { - if (obj1.length !== obj2.length) { + 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 ( - type1 !== type2 || - class1 !== class2 || - keys1.length !== keys2.length || - (name1 && name2 && name1 !== name2) + actualType !== expectationType || + actualClass !== expectationClass || + actualKeys.length !== expectationKeys.length || + (actualName && + expectationName && + actualName !== expectationName) ) { return false; } } - if (isSet(obj1) || isSet(obj2)) { - if (!isSet(obj1) || !isSet(obj2) || obj1.size !== obj2.size) { + if (isSet(actualObj) || isSet(expectationObj)) { + if ( + !isSet(actualObj) || + !isSet(expectationObj) || + actualObj.size !== expectationObj.size + ) { return false; } - return isSubset(obj1, obj2, deepEqual); + return isSubset(actualObj, expectationObj, deepEqual); } - return every.call(keys1, function(key) { - if (!hasOwnProperty.call(obj2, key)) { + return every.call(expectationKeysAndSymbols, function(key) { + if (!hasOwnProperty.call(actualObj, key)) { return false; } - var value1 = obj1[key]; - var value2 = obj2[key]; - var isObject1 = isObject(value1); - var isObject2 = isObject(value2); + 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 index1 = isObject1 ? indexOf.call(objects1, value1) : -1; - var index2 = isObject2 ? indexOf.call(objects2, value2) : -1; + 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 newPath1 = - index1 !== -1 - ? paths1[index1] - : path1 + "[" + JSON.stringify(key) + "]"; - var newPath2 = - index2 !== -1 - ? paths2[index2] - : path2 + "[" + JSON.stringify(key) + "]"; - var combinedPath = newPath1 + newPath2; + 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]) { @@ -167,27 +190,32 @@ function deepEqualCyclic(first, second, match) { } // remember the current objects and their paths - if (index1 === -1 && isObject1) { - objects1.push(value1); - paths1.push(newPath1); + if (actualIndex === -1 && actualObject) { + actualObjects.push(actualValue); + actualPaths.push(newActualPath); } - if (index2 === -1 && isObject2) { - objects2.push(value2); - paths2.push(newPath2); + if (expectationIndex === -1 && expectationObject) { + expectationObjects.push(expectationValue); + expectationPaths.push(newExpectationPath); } // remember that the current objects are already compared - if (isObject1 && isObject2) { + if (actualObject && expectationObject) { compared[combinedPath] = true; } // End of cyclic logic - // neither value1 nor value2 is a cycle + // neither actualValue nor expectationValue is a cycle // continue with next level - return deepEqual(value1, value2, newPath1, newPath2); + return deepEqual( + actualValue, + expectationValue, + newActualPath, + newExpectationPath + ); }); - })(first, second, "$1", "$2"); + })(actual, expectation, "$1", "$2"); } deepEqualCyclic.use = function(match) { diff --git a/lib/deep-equal.test.js b/lib/deep-equal.test.js index ca278cf..7dadf73 100644 --- a/lib/deep-equal.test.js +++ b/lib/deep-equal.test.js @@ -26,6 +26,7 @@ describe("deepEqual", function() { var func = function() {}; var obj = {}; var arr = []; + var symbol = Symbol("id"); var date = new Date(); var sameDate = new Date(date.getTime()); var sameDateWithProp = new Date(date.getTime()); @@ -364,6 +365,40 @@ describe("deepEqual", function() { assert.isFalse(checkDeep); }); + it("returns false if object has different symbolic properties", function() { + var obj1 = {}; + var obj2 = {}; + obj1[symbol] = 42; + obj2[symbol] = 43; + var checkDeep = samsam.deepEqual(obj1, obj2); + assert.isFalse(checkDeep); + }); + + it("returns true if object has same symbolic properties", function() { + var obj1 = {}; + var obj2 = {}; + obj1[symbol] = 42; + obj2[symbol] = 42; + var checkDeep = samsam.deepEqual(obj1, obj2); + assert.isTrue(checkDeep); + }); + + it("returns false if object missing expected symbolic properties", function() { + var obj1 = {}; + var obj2 = {}; + obj2[symbol] = 42; + var checkDeep = samsam.deepEqual(obj1, obj2); + assert.isFalse(checkDeep); + }); + + it("returns true if object contains additional symbolic properties", function() { + var obj1 = {}; + var obj2 = {}; + obj1[symbol] = 42; + var checkDeep = samsam.deepEqual(obj1, obj2); + assert.isTrue(checkDeep); + }); + it("returns false if object to null", function() { var checkDeep = samsam.deepEqual({}, null); assert.isFalse(checkDeep); @@ -414,12 +449,12 @@ describe("deepEqual", function() { assert.isFalse(checkDeep); }); - it("returns true if arguments to array", function() { + it("returns false if arguments to array", function() { var gather = function() { return arguments; }; var checkDeep = samsam.deepEqual([1, 2, {}, []], gather(1, 2, {}, [])); - assert.isTrue(checkDeep); + assert.isFalse(checkDeep); }); it("returns true if array to arguments", function() { @@ -430,13 +465,13 @@ describe("deepEqual", function() { assert.isTrue(checkDeep); }); - it("returns true if arguments to array like object", function() { + it("returns false if arguments to array like object", function() { var gather = function() { return arguments; }; var arrayLike = { length: 4, "0": 1, "1": 2, "2": {}, "3": [] }; var checkDeep = samsam.deepEqual(arrayLike, gather(1, 2, {}, [])); - assert.isTrue(checkDeep); + assert.isFalse(checkDeep); }); it("returns true for same error", function() {