diff --git a/.eslintrc b/.eslintrc index 4ff2c50..4fb86e1 100644 --- a/.eslintrc +++ b/.eslintrc @@ -16,6 +16,7 @@ "files": ["example/**", "test/**"], "rules": { "array-bracket-newline": 0, + "max-lines": 0, "max-params": 0, "max-statements": 0, "no-console": 0, diff --git a/index.js b/index.js index 61307f3..ff30525 100644 --- a/index.js +++ b/index.js @@ -3,9 +3,14 @@ var isArguments = require('is-arguments'); var is = require('object-is'); var isRegex = require('is-regex'); var flags = require('regexp.prototype.flags'); +var isArray = require('isarray'); var isDate = require('is-date-object'); +var isBoxedPrimitive = require('is-boxed-primitive'); +var toPrimitive = require('es-to-primitive/es2015'); // TODO: replace this with ES2020 once updated var getTime = Date.prototype.getTime; +var gPO = Object.getPrototypeOf; +var objToString = Object.prototype.toString; function deepEqual(actual, expected, options) { var opts = options || {}; @@ -15,6 +20,12 @@ function deepEqual(actual, expected, options) { return true; } + var actualBoxed = isBoxedPrimitive(actual); + var expectedBoxed = isBoxedPrimitive(expected); + if (actualBoxed || expectedBoxed) { + return deepEqual(toPrimitive(actual), toPrimitive(expected), opts); + } + // 7.3. Other pairs that do not both pass typeof value == 'object', equivalence is determined by ==. if (!actual || !expected || (typeof actual !== 'object' && typeof expected !== 'object')) { return opts.strict ? is(actual, expected) : actual == expected; @@ -50,16 +61,31 @@ function isBuffer(x) { } function objEquiv(a, b, opts) { - /* eslint max-statements: [2, 50] */ + /* eslint max-statements: [2, 70], max-lines-per-function: [2, 80] */ var i, key; + if (typeof a !== typeof b) { return false; } if (isUndefinedOrNull(a) || isUndefinedOrNull(b)) { return false; } // an identical 'prototype' property. if (a.prototype !== b.prototype) { return false; } + if (objToString.call(a) !== objToString.call(b)) { return false; } + if (isArguments(a) !== isArguments(b)) { return false; } + var aIsArray = isArray(a); + var bIsArray = isArray(b); + if (aIsArray !== bIsArray) { return false; } + + // TODO: replace when a cross-realm brand check is available + var aIsError = a instanceof Error; + var bIsError = b instanceof Error; + if (aIsError !== bIsError) { return false; } + if (aIsError || bIsError) { + if (a.name !== b.name || a.message !== b.message) { return false; } + } + var aIsRegex = isRegex(a); var bIsRegex = isRegex(b); if (aIsRegex !== bIsRegex) { return false; } @@ -67,9 +93,13 @@ function objEquiv(a, b, opts) { return a.source === b.source && flags(a) === flags(b); } - if (isDate(a) && isDate(b)) { - return getTime.call(a) === getTime.call(b); - } + var aIsDate = isDate(a); + var bIsDate = isDate(b); + if (aIsDate !== bIsDate) { return false; } + if (aIsDate || bIsDate) { // && would work too, because both are true or both false here + if (getTime.call(a) !== getTime.call(b)) { return false; } + if (opts.strict && gPO && gPO(a) !== gPO(b)) { return false; } + } else if (gPO && gPO(a) !== gPO(b)) { return false; } // non-Dates always compare [[Prototype]]s var aIsBuffer = isBuffer(a); var bIsBuffer = isBuffer(b); @@ -100,6 +130,7 @@ function objEquiv(a, b, opts) { for (i = ka.length - 1; i >= 0; i--) { if (ka[i] != kb[i]) { return false; } } + // equivalent values for every corresponding key, and ~~~possibly expensive deep test for (i = ka.length - 1; i >= 0; i--) { key = ka[i]; diff --git a/package.json b/package.json index f580769..89dbe23 100644 --- a/package.json +++ b/package.json @@ -15,9 +15,12 @@ "test": "npm run tests-only" }, "dependencies": { + "es-to-primitive": "^1.2.0", "is-arguments": "^1.0.4", + "is-boxed-primitive": "^1.0.0", "is-date-object": "^1.0.1", "is-regex": "^1.0.4", + "isarray": "^2.0.5", "object-is": "^1.0.1", "object-keys": "^1.1.1", "regexp.prototype.flags": "^1.2.0" @@ -25,6 +28,8 @@ "devDependencies": { "@ljharb/eslint-config": "^13.1.1", "eslint": "^5.16.0", + "has-symbols": "^1.0.0", + "object.assign": "^4.1.0", "tape": "^4.11.0" }, "repository": { diff --git a/test/cmp.js b/test/cmp.js index b377743..4d6871f 100644 --- a/test/cmp.js +++ b/test/cmp.js @@ -1,5 +1,7 @@ var test = require('tape'); require('./_tape'); +var assign = require('object.assign'); +var hasSymbols = require('has-symbols')(); var equal = require('../'); @@ -90,7 +92,13 @@ test('arguments class', function (t) { test('dates', function (t) { var d0 = new Date(1387585278000); var d1 = new Date('Fri Dec 20 2013 16:21:18 GMT-0800 (PST)'); - t.deepEqualTest(d0, d1, 'equivalent Dates', true, true); + + t.deepEqualTest(d0, d1, 'two Dates with the same timestamp', true, true); + + d1.a = true; + + t.deepEqualTest(d0, d1, 'two Dates with the same timestamp but different own properties', false, false); + t.end(); }); @@ -327,9 +335,9 @@ test('regexen', function (t) { }); test('arrays and objects', function (t) { - t.deepEqualTest([], {}, 'empty array and empty object', true, true); + t.deepEqualTest([], {}, 'empty array and empty object', false, false); t.deepEqualTest([], { length: 0 }, 'empty array and empty arraylike object', false, false); - t.deepEqualTest([1], { 0: 1 }, 'array and similar object', true, true); + t.deepEqualTest([1], { 0: 1 }, 'array and similar object', false, false); t.end(); }); @@ -342,3 +350,96 @@ test('functions', function (t) { t.end(); }); + +test('Errors', function (t) { + t.deepEqualTest(new Error('xyz'), new Error('xyz'), 'two errors of the same type with the same message', true, true, false); + t.deepEqualTest(new Error('xyz'), new TypeError('xyz'), 'two errors of different types with the same message', false, false); + t.deepEqualTest(new Error('xyz'), new Error('zyx'), 'two errors of the same type with a different message', false, false); + + t.deepEqualTest( + new Error('a'), + assign(new Error('a'), { code: 10 }), + 'two otherwise equal errors with different own properties', + false, + false + ); + + t.end(); +}); + +test('errors', function (t) { + + t.end(); +}); + +test('error = Object', function (t) { + t.notOk(equal(new Error('a'), { message: 'a' })); + t.end(); +}); + +test('[[Prototypes]]', { skip: !Object.getPrototypeOf }, function (t) { + function C() {} + var instance = new C(); + delete instance.constructor; + + t.deepEqualTest({}, instance, 'two identical objects with different [[Prototypes]]', false, false); + + t.test('Dates with different prototypes', { skip: !Object.setPrototypeOf }, function (st) { + var d1 = new Date(0); + var d2 = new Date(0); + + t.deepEqualTest(d1, d2, 'two dates with the same timestamp', true, true); + + var newProto = {}; + Object.setPrototypeOf(newProto, Date.prototype); + Object.setPrototypeOf(d2, newProto); + st.ok(d2 instanceof Date, 'd2 is still a Date instance after tweaking [[Prototype]]'); + + t.deepEqualTest(d1, d2, 'two dates with the same timestamp and different [[Prototype]]', true, false); + + st.end(); + }); + + t.end(); +}); + +test('toStringTag', { skip: !hasSymbols || !Symbol.toStringTag }, function (t) { + var o1 = {}; + t.equal(Object.prototype.toString.call(o1), '[object Object]', 'o1: Symbol.toStringTag works'); + + var o2 = {}; + t.equal(Object.prototype.toString.call(o2), '[object Object]', 'o2: original Symbol.toStringTag works'); + + t.deepEqualTest(o1, o2, 'two normal empty objects', true, true); + + o2[Symbol.toStringTag] = 'jifasnif'; + t.equal(Object.prototype.toString.call(o2), '[object jifasnif]', 'o2: modified Symbol.toStringTag works'); + + t.deepEqualTest(o1, o2, 'two normal empty objects with different toStringTags', false, false); + + t.end(); +}); + +test('boxed primitives', function (t) { + t.deepEqualTest(Object(false), false, 'boxed and primitive `false`', true, true); + t.deepEqualTest(Object(true), true, 'boxed and primitive `true`', true, true); + t.deepEqualTest(Object(3), 3, 'boxed and primitive `3`', true, true); + t.deepEqualTest(Object(NaN), NaN, 'boxed and primitive `NaN`', false, true); + t.deepEqualTest(Object(''), '', 'boxed and primitive `""`', true, true); + t.deepEqualTest(Object('str'), 'str', 'boxed and primitive `"str"`', true, true); + + t.test('symbol', { skip: !hasSymbols }, function (st) { + var s = Symbol(''); + st.deepEqualTest(Object(s), s, 'boxed and primitive `Symbol()`', true, true); + st.end(); + }); + + /* globals BigInt: false */ + t.test('bigint', { skip: typeof BigInt !== 'function' }, function (st) { + var hhgtg = BigInt(42); + st.deepEqualTest(Object(hhgtg), hhgtg, 'boxed and primitive `BigInt(42)`', true, true); + st.end(); + }); + + t.end(); +});