import assert from 'node:assert'; import lodashStable from 'lodash'; import { noop, create, args, realm, arrayViews, map, promise, set, defineProperty, document, stubFalse, } from './utils'; import isEqual from '../src/isEqual'; describe('isEqual', () => { const symbol1 = Symbol ? Symbol('a') : true, symbol2 = Symbol ? Symbol('b') : false; it('should compare primitives', () => { const pairs = [ [1, 1, true], [1, Object(1), true], [1, '1', false], [1, 2, false], [-0, -0, true], [0, 0, true], [0, Object(0), true], [Object(0), Object(0), true], [-0, 0, true], [0, '0', false], [0, null, false], [NaN, NaN, true], [NaN, Object(NaN), true], [Object(NaN), Object(NaN), true], [NaN, 'a', false], [NaN, Infinity, false], ['a', 'a', true], ['a', Object('a'), true], [Object('a'), Object('a'), true], ['a', 'b', false], ['a', ['a'], false], [true, true, true], [true, Object(true), true], [Object(true), Object(true), true], [true, 1, false], [true, 'a', false], [false, false, true], [false, Object(false), true], [Object(false), Object(false), true], [false, 0, false], [false, '', false], [symbol1, symbol1, true], [symbol1, Object(symbol1), true], [Object(symbol1), Object(symbol1), true], [symbol1, symbol2, false], [null, null, true], [null, undefined, false], [null, {}, false], [null, '', false], [undefined, undefined, true], [undefined, null, false], [undefined, '', false], ]; const expected = lodashStable.map(pairs, (pair) => pair[2]); const actual = lodashStable.map(pairs, (pair) => isEqual(pair[0], pair[1])); assert.deepStrictEqual(actual, expected); }); it('should compare arrays', () => { let array1 = [true, null, 1, 'a', undefined], array2 = [true, null, 1, 'a', undefined]; assert.strictEqual(isEqual(array1, array2), true); array1 = [[1, 2, 3], new Date(2012, 4, 23), /x/, { e: 1 }]; array2 = [[1, 2, 3], new Date(2012, 4, 23), /x/, { e: 1 }]; assert.strictEqual(isEqual(array1, array2), true); array1 = [1]; array1[2] = 3; array2 = [1]; array2[1] = undefined; array2[2] = 3; assert.strictEqual(isEqual(array1, array2), true); array1 = [ Object(1), false, Object('a'), /x/, new Date(2012, 4, 23), ['a', 'b', [Object('c')]], { a: 1 }, ]; array2 = [ 1, Object(false), 'a', /x/, new Date(2012, 4, 23), ['a', Object('b'), ['c']], { a: 1 }, ]; assert.strictEqual(isEqual(array1, array2), true); array1 = [1, 2, 3]; array2 = [3, 2, 1]; assert.strictEqual(isEqual(array1, array2), false); array1 = [1, 2]; array2 = [1, 2, 3]; assert.strictEqual(isEqual(array1, array2), false); }); it('should treat arrays with identical values but different non-index properties as equal', () => { let array1 = [1, 2, 3], array2 = [1, 2, 3]; array1.every = array1.filter = array1.forEach = array1.indexOf = array1.lastIndexOf = array1.map = array1.some = array1.reduce = array1.reduceRight = null; array2.concat = array2.join = array2.pop = array2.reverse = array2.shift = array2.slice = array2.sort = array2.splice = array2.unshift = null; assert.strictEqual(isEqual(array1, array2), true); array1 = [1, 2, 3]; array1.a = 1; array2 = [1, 2, 3]; array2.b = 1; assert.strictEqual(isEqual(array1, array2), true); array1 = /c/.exec('abcde'); array2 = ['c']; assert.strictEqual(isEqual(array1, array2), true); }); it('should compare sparse arrays', () => { const array = Array(1); assert.strictEqual(isEqual(array, Array(1)), true); assert.strictEqual(isEqual(array, [undefined]), true); assert.strictEqual(isEqual(array, Array(2)), false); }); it('should compare plain objects', () => { let object1 = { a: true, b: null, c: 1, d: 'a', e: undefined }, object2 = { a: true, b: null, c: 1, d: 'a', e: undefined }; assert.strictEqual(isEqual(object1, object2), true); object1 = { a: [1, 2, 3], b: new Date(2012, 4, 23), c: /x/, d: { e: 1 } }; object2 = { a: [1, 2, 3], b: new Date(2012, 4, 23), c: /x/, d: { e: 1 } }; assert.strictEqual(isEqual(object1, object2), true); object1 = { a: 1, b: 2, c: 3 }; object2 = { a: 3, b: 2, c: 1 }; assert.strictEqual(isEqual(object1, object2), false); object1 = { a: 1, b: 2, c: 3 }; object2 = { d: 1, e: 2, f: 3 }; assert.strictEqual(isEqual(object1, object2), false); object1 = { a: 1, b: 2 }; object2 = { a: 1, b: 2, c: 3 }; assert.strictEqual(isEqual(object1, object2), false); }); it('should compare objects regardless of key order', () => { const object1 = { a: 1, b: 2, c: 3 }, object2 = { c: 3, a: 1, b: 2 }; assert.strictEqual(isEqual(object1, object2), true); }); it('should compare nested objects', () => { const object1 = { a: [1, 2, 3], b: true, c: Object(1), d: 'a', e: { f: ['a', Object('b'), 'c'], g: Object(false), h: new Date(2012, 4, 23), i: noop, j: 'a', }, }; const object2 = { a: [1, Object(2), 3], b: Object(true), c: 1, d: Object('a'), e: { f: ['a', 'b', 'c'], g: false, h: new Date(2012, 4, 23), i: noop, j: 'a', }, }; assert.strictEqual(isEqual(object1, object2), true); }); it('should compare object instances', () => { function Foo() { this.a = 1; } Foo.prototype.a = 1; function Bar() { this.a = 1; } Bar.prototype.a = 2; assert.strictEqual(isEqual(new Foo(), new Foo()), true); assert.strictEqual(isEqual(new Foo(), new Bar()), false); assert.strictEqual(isEqual({ a: 1 }, new Foo()), false); assert.strictEqual(isEqual({ a: 2 }, new Bar()), false); }); it('should compare objects with constructor properties', () => { assert.strictEqual(isEqual({ constructor: 1 }, { constructor: 1 }), true); assert.strictEqual(isEqual({ constructor: 1 }, { constructor: '1' }), false); assert.strictEqual(isEqual({ constructor: [1] }, { constructor: [1] }), true); assert.strictEqual(isEqual({ constructor: [1] }, { constructor: ['1'] }), false); assert.strictEqual(isEqual({ constructor: Object }, {}), false); }); it('should compare arrays with circular references', () => { let array1 = [], array2 = []; array1.push(array1); array2.push(array2); assert.strictEqual(isEqual(array1, array2), true); array1.push('b'); array2.push('b'); assert.strictEqual(isEqual(array1, array2), true); array1.push('c'); array2.push('d'); assert.strictEqual(isEqual(array1, array2), false); array1 = ['a', 'b', 'c']; array1[1] = array1; array2 = ['a', ['a', 'b', 'c'], 'c']; assert.strictEqual(isEqual(array1, array2), false); }); it('should have transitive equivalence for circular references of arrays', () => { const array1 = [], array2 = [array1], array3 = [array2]; array1[0] = array1; assert.strictEqual(isEqual(array1, array2), true); assert.strictEqual(isEqual(array2, array3), true); assert.strictEqual(isEqual(array1, array3), true); }); it('should compare objects with circular references', () => { let object1 = {}, object2 = {}; object1.a = object1; object2.a = object2; assert.strictEqual(isEqual(object1, object2), true); object1.b = 0; object2.b = Object(0); assert.strictEqual(isEqual(object1, object2), true); object1.c = Object(1); object2.c = Object(2); assert.strictEqual(isEqual(object1, object2), false); object1 = { a: 1, b: 2, c: 3 }; object1.b = object1; object2 = { a: 1, b: { a: 1, b: 2, c: 3 }, c: 3 }; assert.strictEqual(isEqual(object1, object2), false); }); it('should have transitive equivalence for circular references of objects', () => { const object1 = {}, object2 = { a: object1 }, object3 = { a: object2 }; object1.a = object1; assert.strictEqual(isEqual(object1, object2), true); assert.strictEqual(isEqual(object2, object3), true); assert.strictEqual(isEqual(object1, object3), true); }); it('should compare objects with multiple circular references', () => { const array1 = [{}], array2 = [{}]; (array1[0].a = array1).push(array1); (array2[0].a = array2).push(array2); assert.strictEqual(isEqual(array1, array2), true); array1[0].b = 0; array2[0].b = Object(0); assert.strictEqual(isEqual(array1, array2), true); array1[0].c = Object(1); array2[0].c = Object(2); assert.strictEqual(isEqual(array1, array2), false); }); it('should compare objects with complex circular references', () => { const object1 = { foo: { b: { c: { d: {} } } }, bar: { a: 2 }, }; const object2 = { foo: { b: { c: { d: {} } } }, bar: { a: 2 }, }; object1.foo.b.c.d = object1; object1.bar.b = object1.foo.b; object2.foo.b.c.d = object2; object2.bar.b = object2.foo.b; assert.strictEqual(isEqual(object1, object2), true); }); it('should compare objects with shared property values', () => { const object1 = { a: [1, 2], }; const object2 = { a: [1, 2], b: [1, 2], }; object1.b = object1.a; assert.strictEqual(isEqual(object1, object2), true); }); it('should treat objects created by `Object.create(null)` like plain objects', () => { function Foo() { this.a = 1; } Foo.prototype.constructor = null; const object1 = create(null); object1.a = 1; const object2 = { a: 1 }; assert.strictEqual(isEqual(object1, object2), true); assert.strictEqual(isEqual(new Foo(), object2), false); }); it('should avoid common type coercions', () => { assert.strictEqual(isEqual(true, Object(false)), false); assert.strictEqual(isEqual(Object(false), Object(0)), false); assert.strictEqual(isEqual(false, Object('')), false); assert.strictEqual(isEqual(Object(36), Object('36')), false); assert.strictEqual(isEqual(0, ''), false); assert.strictEqual(isEqual(1, true), false); assert.strictEqual(isEqual(1337756400000, new Date(2012, 4, 23)), false); assert.strictEqual(isEqual('36', 36), false); assert.strictEqual(isEqual(36, '36'), false); }); it('should compare `arguments` objects', () => { const args1 = (function () { return arguments; })(), args2 = (function () { return arguments; })(), args3 = (function () { return arguments; })(1, 2); assert.strictEqual(isEqual(args1, args2), true); assert.strictEqual(isEqual(args1, args3), false); }); it('should treat `arguments` objects like `Object` objects', () => { const object = { '0': 1, '1': 2, '2': 3 }; function Foo() {} Foo.prototype = object; assert.strictEqual(isEqual(args, object), true); assert.strictEqual(isEqual(object, args), true); assert.strictEqual(isEqual(args, new Foo()), false); assert.strictEqual(isEqual(new Foo(), args), false); }); it('should compare array buffers', () => { if (ArrayBuffer) { const buffer = new Int8Array([-1]).buffer; assert.strictEqual(isEqual(buffer, new Uint8Array([255]).buffer), true); assert.strictEqual(isEqual(buffer, new ArrayBuffer(1)), false); } }); it('should compare array views', () => { lodashStable.times(2, (index) => { const ns = index ? realm : root; const pairs = lodashStable.map(arrayViews, (type, viewIndex) => { const otherType = arrayViews[(viewIndex + 1) % arrayViews.length], CtorA = ns[type] || function (n) { this.n = n; }, CtorB = ns[otherType] || function (n) { this.n = n; }, bufferA = ns[type] ? new ns.ArrayBuffer(8) : 8, bufferB = ns[otherType] ? new ns.ArrayBuffer(8) : 8, bufferC = ns[otherType] ? new ns.ArrayBuffer(16) : 16; return [ new CtorA(bufferA), new CtorA(bufferA), new CtorB(bufferB), new CtorB(bufferC), ]; }); const expected = lodashStable.map(pairs, lodashStable.constant([true, false, false])); const actual = lodashStable.map(pairs, (pair) => [ isEqual(pair[0], pair[1]), isEqual(pair[0], pair[2]), isEqual(pair[2], pair[3]), ]); assert.deepStrictEqual(actual, expected); }); }); it('should compare buffers', () => { if (Buffer) { const buffer = new Buffer([1]); assert.strictEqual(isEqual(buffer, new Buffer([1])), true); assert.strictEqual(isEqual(buffer, new Buffer([2])), false); assert.strictEqual(isEqual(buffer, new Uint8Array([1])), false); } }); it('should compare date objects', () => { const date = new Date(2012, 4, 23); assert.strictEqual(isEqual(date, new Date(2012, 4, 23)), true); assert.strictEqual(isEqual(new Date('a'), new Date('b')), true); assert.strictEqual(isEqual(date, new Date(2013, 3, 25)), false); assert.strictEqual(isEqual(date, { getTime: lodashStable.constant(+date) }), false); }); it('should compare error objects', () => { const pairs = lodashStable.map( [ 'Error', 'EvalError', 'RangeError', 'ReferenceError', 'SyntaxError', 'TypeError', 'URIError', ], (type, index, errorTypes) => { const otherType = errorTypes[++index % errorTypes.length], CtorA = root[type], CtorB = root[otherType]; return [new CtorA('a'), new CtorA('a'), new CtorB('a'), new CtorB('b')]; }, ); const expected = lodashStable.map(pairs, lodashStable.constant([true, false, false])); const actual = lodashStable.map(pairs, (pair) => [ isEqual(pair[0], pair[1]), isEqual(pair[0], pair[2]), isEqual(pair[2], pair[3]), ]); assert.deepStrictEqual(actual, expected); }); it('should compare functions', () => { function a() { return 1 + 2; } function b() { return 1 + 2; } assert.strictEqual(isEqual(a, a), true); assert.strictEqual(isEqual(a, b), false); }); it('should compare maps', () => { if (Map) { lodashStable.each( [ [map, new Map()], [map, realm.map], ], (maps) => { const map1 = maps[0], map2 = maps[1]; map1.set('a', 1); map2.set('b', 2); assert.strictEqual(isEqual(map1, map2), false); map1.set('b', 2); map2.set('a', 1); assert.strictEqual(isEqual(map1, map2), true); map1.delete('a'); map1.set('a', 1); assert.strictEqual(isEqual(map1, map2), true); map2.delete('a'); assert.strictEqual(isEqual(map1, map2), false); map1.clear(); map2.clear(); }, ); } }); it('should compare maps with circular references', () => { if (Map) { const map1 = new Map(), map2 = new Map(); map1.set('a', map1); map2.set('a', map2); assert.strictEqual(isEqual(map1, map2), true); map1.set('b', 1); map2.set('b', 2); assert.strictEqual(isEqual(map1, map2), false); } }); it('should compare promises by reference', () => { if (promise) { lodashStable.each( [ [promise, Promise.resolve(1)], [promise, realm.promise], ], (promises) => { const promise1 = promises[0], promise2 = promises[1]; assert.strictEqual(isEqual(promise1, promise2), false); assert.strictEqual(isEqual(promise1, promise1), true); }, ); } }); it('should compare regexes', () => { assert.strictEqual(isEqual(/x/gim, /x/gim), true); assert.strictEqual(isEqual(/x/gim, /x/gim), true); assert.strictEqual(isEqual(/x/gi, /x/g), false); assert.strictEqual(isEqual(/x/, /y/), false); assert.strictEqual( isEqual(/x/g, { global: true, ignoreCase: false, multiline: false, source: 'x' }), false, ); }); it('should compare sets', () => { if (Set) { lodashStable.each( [ [set, new Set()], [set, realm.set], ], (sets) => { const set1 = sets[0], set2 = sets[1]; set1.add(1); set2.add(2); assert.strictEqual(isEqual(set1, set2), false); set1.add(2); set2.add(1); assert.strictEqual(isEqual(set1, set2), true); set1.delete(1); set1.add(1); assert.strictEqual(isEqual(set1, set2), true); set2.delete(1); assert.strictEqual(isEqual(set1, set2), false); set1.clear(); set2.clear(); }, ); } }); it('should compare sets with circular references', () => { if (Set) { const set1 = new Set(), set2 = new Set(); set1.add(set1); set2.add(set2); assert.strictEqual(isEqual(set1, set2), true); set1.add(1); set2.add(2); assert.strictEqual(isEqual(set1, set2), false); } }); it('should compare symbol properties', () => { if (Symbol) { const object1 = { a: 1 }, object2 = { a: 1 }; object1[symbol1] = { a: { b: 2 } }; object2[symbol1] = { a: { b: 2 } }; defineProperty(object2, symbol2, { configurable: true, enumerable: false, writable: true, value: 2, }); assert.strictEqual(isEqual(object1, object2), true); object2[symbol1] = { a: 1 }; assert.strictEqual(isEqual(object1, object2), false); delete object2[symbol1]; object2[Symbol('a')] = { a: { b: 2 } }; assert.strictEqual(isEqual(object1, object2), false); } }); it('should compare wrapped values', () => { const stamp = +new Date(); const values = [ [ [1, 2], [1, 2], [1, 2, 3], ], [true, true, false], [new Date(stamp), new Date(stamp), new Date(stamp - 100)], [ { a: 1, b: 2 }, { a: 1, b: 2 }, { a: 1, b: 1 }, ], [1, 1, 2], [NaN, NaN, Infinity], [/x/, /x/, /x/i], ['a', 'a', 'A'], ]; lodashStable.each(values, (vals) => { let wrapped1 = _(vals[0]), wrapped2 = _(vals[1]), actual = wrapped1.isEqual(wrapped2); assert.strictEqual(actual, true); assert.strictEqual(isEqual(_(actual), _(true)), true); wrapped1 = _(vals[0]); wrapped2 = _(vals[2]); actual = wrapped1.isEqual(wrapped2); assert.strictEqual(actual, false); assert.strictEqual(isEqual(_(actual), _(false)), true); }); }); it('should compare wrapped and non-wrapped values', () => { let object1 = _({ a: 1, b: 2 }), object2 = { a: 1, b: 2 }; assert.strictEqual(object1.isEqual(object2), true); assert.strictEqual(isEqual(object1, object2), true); object1 = _({ a: 1, b: 2 }); object2 = { a: 1, b: 1 }; assert.strictEqual(object1.isEqual(object2), false); assert.strictEqual(isEqual(object1, object2), false); }); it('should work as an iteratee for `_.every`', () => { const actual = lodashStable.every([1, 1, 1], lodashStable.partial(isEqual, 1)); assert.ok(actual); }); it('should not error on DOM elements', () => { if (document) { const element1 = document.createElement('div'), element2 = element1.cloneNode(true); try { assert.strictEqual(isEqual(element1, element2), false); } catch (e) { assert.ok(false, e.message); } } }); it('should return `true` for like-objects from different documents', () => { if (realm.object) { assert.strictEqual(isEqual([1], realm.array), true); assert.strictEqual(isEqual([2], realm.array), false); assert.strictEqual(isEqual({ a: 1 }, realm.object), true); assert.strictEqual(isEqual({ a: 2 }, realm.object), false); } }); it('should return `false` for objects with custom `toString` methods', () => { let primitive, object = { toString: function () { return primitive; }, }, values = [true, null, 1, 'a', undefined], expected = lodashStable.map(values, stubFalse); const actual = lodashStable.map(values, (value) => { primitive = value; return isEqual(object, value); }); assert.deepStrictEqual(actual, expected); }); it('should return an unwrapped value when implicitly chaining', () => { assert.strictEqual(_('a').isEqual('a'), true); }); it('should return a wrapped value when explicitly chaining', () => { assert.ok(_('a').chain().isEqual('a') instanceof _); }); });