Skip to content

Commit

Permalink
Evaluate symbolic keys for deep equality assertion (#64)
Browse files Browse the repository at this point in the history
  • Loading branch information
BrandonE authored and mantoni committed Mar 7, 2019
1 parent 0a3fbe6 commit 91e1b85
Show file tree
Hide file tree
Showing 4 changed files with 149 additions and 83 deletions.
1 change: 1 addition & 0 deletions AUTHORS
Expand Up @@ -10,3 +10,4 @@ Magnar Sveen <magnars@gmail.com>
Mathias Schreck <schreck.mathias@googlemail.com>
Olle Jonsson <olle.jonsson@gmail.com>
Gunnar André Reinseth <gunnar.andre.reinseth@nrk.no>
Brandon Evans <contact@brandonmevans.com>
6 changes: 4 additions & 2 deletions docs/index.md
Expand Up @@ -53,15 +53,17 @@ observably distinguishable.
`NaN` is compared to itself.


### `deepEqual(obj1, obj2)`
### `deepEqual(actual, expectation)`

Deep equal comparison. Two values are "deep equal" if:

* They are 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 `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)`
Expand Down
182 changes: 105 additions & 77 deletions lib/deep-equal.js
Expand Up @@ -18,176 +18,204 @@ 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:
*
* - 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 ``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]) {
return true;
}

// 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) {
Expand Down
43 changes: 39 additions & 4 deletions lib/deep-equal.test.js
Expand Up @@ -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());
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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() {
Expand All @@ -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() {
Expand Down

0 comments on commit 91e1b85

Please sign in to comment.