Repository: chaijs/chai
Pull request #1553 (2023-12-18 16:14:46)

Allow deepEqual fonction to be configured globally (4.x.x branch)
Identical change as https://github.com/chaijs/chai/pull/1551 on 4.x.x branch

forty: Thanks @43081j for the review!

Happy new year to the Chai team and congrats on the v5 release :clap: 

I assume you must be busy handling "after sales" of the release, but do you think this MR could be merged and released any time soon?

Thanks!

Patch:
diff --git a/chai.js b/chai.js
index 38efb8f..0396cb1 100644
--- a/chai.js
+++ b/chai.js
@@ -150,6 +150,8 @@ module.exports = function (_chai, util) {
    *   from within another assertion. It's also temporarily set to `true` before
    *   an overwritten assertion gets called by the overwriting assertion.
    *
+   * - `eql`: This flag contains the deepEqual function to be used by the assertion.
+   *
    * @param {Mixed} obj target of the assertion
    * @param {String} msg (optional) custom error message
    * @param {Function} ssfi (optional) starting point for removing stack frames
@@ -162,6 +164,7 @@ module.exports = function (_chai, util) {
     flag(this, 'lockSsfi', lockSsfi);
     flag(this, 'object', obj);
     flag(this, 'message', msg);
+    flag(this, 'eql', config.deepEqual ?? util.eql);
 
     return util.proxify(this);
   }
@@ -365,7 +368,33 @@ module.exports = {
    * @api public
    */
 
-  proxyExcludedKeys: ['then', 'catch', 'inspect', 'toJSON']
+  proxyExcludedKeys: ['then', 'catch', 'inspect', 'toJSON'],
+
+  /**
+   * ### config.deepEqual
+   *
+   * User configurable property, defines which a custom function to use for deepEqual
+   * comparisons.
+   * By default, the function used is the one from the `deep-eql` package without custom comparator.
+   *
+   *     // use a custom comparator
+   *     chai.config.deepEqual = (expected, actual) => {
+   *        return chai.util.eql(expected, actual, {
+   *           comparator: (expected, actual) => {
+   *              // for non number comparison, use the default behavior
+   *              if(typeof expected !== 'number') return null;
+   *              // allow a difference of 10 between compared numbers
+   *              return typeof actual === 'number' && Math.abs(actual - expected) < 10
+   *           }
+   *        })
+   *     };
+   *
+   * @param {Function}
+   * @api public
+   */
+
+  deepEqual: null
+
 };
 
 },{}],5:[function(require,module,exports){
@@ -849,7 +878,8 @@ module.exports = function (chai, _) {
       , negate = flag(this, 'negate')
       , ssfi = flag(this, 'ssfi')
       , isDeep = flag(this, 'deep')
-      , descriptor = isDeep ? 'deep ' : '';
+      , descriptor = isDeep ? 'deep ' : ''
+      , isEql = isDeep ? flag(this, 'eql') : SameValueZero;
 
     flagMsg = flagMsg ? flagMsg + ': ' : '';
 
@@ -873,7 +903,6 @@ module.exports = function (chai, _) {
         break;
 
       case 'map':
-        var isEql = isDeep ? _.eql : SameValueZero;
         obj.forEach(function (item) {
           included = included || isEql(item, val);
         });
@@ -882,7 +911,7 @@ module.exports = function (chai, _) {
       case 'set':
         if (isDeep) {
           obj.forEach(function (item) {
-            included = included || _.eql(item, val);
+            included = included || isEql(item, val);
           });
         } else {
           included = obj.has(val);
@@ -892,7 +921,7 @@ module.exports = function (chai, _) {
       case 'array':
         if (isDeep) {
           included = obj.some(function (item) {
-            return _.eql(item, val);
+            return isEql(item, val);
           })
         } else {
           included = obj.indexOf(val) !== -1;
@@ -1464,8 +1493,9 @@ module.exports = function (chai, _) {
 
   function assertEql(obj, msg) {
     if (msg) flag(this, 'message', msg);
+    var eql = flag(this, 'eql');
     this.assert(
-        _.eql(obj, flag(this, 'object'))
+        eql(obj, flag(this, 'object'))
       , 'expected #{this} to deeply equal #{exp}'
       , 'expected #{this} to not deeply equal #{exp}'
       , obj
@@ -2233,7 +2263,8 @@ module.exports = function (chai, _) {
     var isDeep = flag(this, 'deep')
       , negate = flag(this, 'negate')
       , pathInfo = isNested ? _.getPathInfo(obj, name) : null
-      , value = isNested ? pathInfo.value : obj[name];
+      , value = isNested ? pathInfo.value : obj[name]
+      , isEql = isDeep ? flag(this, 'eql') : (val1, val2) => val1 === val2;;
 
     var descriptor = '';
     if (isDeep) descriptor += 'deep ';
@@ -2260,7 +2291,7 @@ module.exports = function (chai, _) {
 
     if (arguments.length > 1) {
       this.assert(
-          hasProperty && (isDeep ? _.eql(val, value) : val === value)
+          hasProperty && isEql(val, value)
         , 'expected #{this} to have ' + descriptor + _.inspect(name) + ' of #{exp}, but got #{act}'
         , 'expected #{this} to not have ' + descriptor + _.inspect(name) + ' of #{act}'
         , val
@@ -2408,9 +2439,10 @@ module.exports = function (chai, _) {
     if (msg) flag(this, 'message', msg);
     var obj = flag(this, 'object');
     var actualDescriptor = Object.getOwnPropertyDescriptor(Object(obj), name);
+    var eql = flag(this, 'eql');
     if (actualDescriptor && descriptor) {
       this.assert(
-          _.eql(descriptor, actualDescriptor)
+          eql(descriptor, actualDescriptor)
         , 'expected the own property descriptor for ' + _.inspect(name) + ' on #{this} to match ' + _.inspect(descriptor) + ', got ' + _.inspect(actualDescriptor)
         , 'expected the own property descriptor for ' + _.inspect(name) + ' on #{this} to not match ' + _.inspect(descriptor)
         , descriptor
@@ -2764,7 +2796,8 @@ module.exports = function (chai, _) {
     var len = keys.length
       , any = flag(this, 'any')
       , all = flag(this, 'all')
-      , expected = keys;
+      , expected = keys
+      , isEql = isDeep ? flag(this, 'eql') : (val1, val2) => val1 === val2;
 
     if (!any && !all) {
       all = true;
@@ -2774,11 +2807,7 @@ module.exports = function (chai, _) {
     if (any) {
       ok = expected.some(function(expectedKey) {
         return actual.some(function(actualKey) {
-          if (isDeep) {
-            return _.eql(expectedKey, actualKey);
-          } else {
-            return expectedKey === actualKey;
-          }
+          return isEql(expectedKey, actualKey);
         });
       });
     }
@@ -2787,11 +2816,7 @@ module.exports = function (chai, _) {
     if (all) {
       ok = expected.every(function(expectedKey) {
         return actual.some(function(actualKey) {
-          if (isDeep) {
-            return _.eql(expectedKey, actualKey);
-          } else {
-            return expectedKey === actualKey;
-          }
+          return isEql(expectedKey, actualKey);
         });
       });
 
@@ -3479,7 +3504,7 @@ module.exports = function (chai, _) {
       failNegateMsg = 'expected #{this} to not have the same ' + subject + ' as #{exp}';
     }
 
-    var cmp = flag(this, 'deep') ? _.eql : undefined;
+    var cmp = flag(this, 'deep') ? flag(this, 'eql') : undefined;
 
     this.assert(
         isSubsetOf(subset, obj, cmp, contains, ordered)
@@ -3535,7 +3560,8 @@ module.exports = function (chai, _) {
       , flagMsg = flag(this, 'message')
       , ssfi = flag(this, 'ssfi')
       , contains = flag(this, 'contains')
-      , isDeep = flag(this, 'deep');
+      , isDeep = flag(this, 'deep')
+      , eql = flag(this, 'eql');
     new Assertion(list, flagMsg, ssfi, true).to.be.an('array');
 
     if (contains) {
@@ -3549,7 +3575,7 @@ module.exports = function (chai, _) {
     } else {
       if (isDeep) {
         this.assert(
-          list.some(function(possibility) { return _.eql(expected, possibility) })
+          list.some(function(possibility) { return eql(expected, possibility) })
           , 'expected #{this} to deeply equal one of #{exp}'
           , 'expected #{this} to deeply equal one of #{exp}'
           , list
diff --git a/lib/chai/assertion.js b/lib/chai/assertion.js
index 8ed93ae..b54b64c 100644
--- a/lib/chai/assertion.js
+++ b/lib/chai/assertion.js
@@ -52,6 +52,8 @@ module.exports = function (_chai, util) {
    *   from within another assertion. It's also temporarily set to `true` before
    *   an overwritten assertion gets called by the overwriting assertion.
    *
+   * - `eql`: This flag contains the deepEqual function to be used by the assertion.
+   *
    * @param {Mixed} obj target of the assertion
    * @param {String} msg (optional) custom error message
    * @param {Function} ssfi (optional) starting point for removing stack frames
@@ -64,6 +66,7 @@ module.exports = function (_chai, util) {
     flag(this, 'lockSsfi', lockSsfi);
     flag(this, 'object', obj);
     flag(this, 'message', msg);
+    flag(this, 'eql', config.deepEqual ?? util.eql);
 
     return util.proxify(this);
   }
diff --git a/lib/chai/config.js b/lib/chai/config.js
index 412a0c8..d7cbdb9 100644
--- a/lib/chai/config.js
+++ b/lib/chai/config.js
@@ -90,5 +90,31 @@ module.exports = {
    * @api public
    */
 
-  proxyExcludedKeys: ['then', 'catch', 'inspect', 'toJSON']
+  proxyExcludedKeys: ['then', 'catch', 'inspect', 'toJSON'],
+
+  /**
+   * ### config.deepEqual
+   *
+   * User configurable property, defines which a custom function to use for deepEqual
+   * comparisons.
+   * By default, the function used is the one from the `deep-eql` package without custom comparator.
+   *
+   *     // use a custom comparator
+   *     chai.config.deepEqual = (expected, actual) => {
+   *        return chai.util.eql(expected, actual, {
+   *           comparator: (expected, actual) => {
+   *              // for non number comparison, use the default behavior
+   *              if(typeof expected !== 'number') return null;
+   *              // allow a difference of 10 between compared numbers
+   *              return typeof actual === 'number' && Math.abs(actual - expected) < 10
+   *           }
+   *        })
+   *     };
+   *
+   * @param {Function}
+   * @api public
+   */
+
+  deepEqual: null
+
 };
diff --git a/lib/chai/core/assertions.js b/lib/chai/core/assertions.js
index 990664b..19bdbbc 100644
--- a/lib/chai/core/assertions.js
+++ b/lib/chai/core/assertions.js
@@ -478,7 +478,8 @@ module.exports = function (chai, _) {
       , negate = flag(this, 'negate')
       , ssfi = flag(this, 'ssfi')
       , isDeep = flag(this, 'deep')
-      , descriptor = isDeep ? 'deep ' : '';
+      , descriptor = isDeep ? 'deep ' : ''
+      , isEql = isDeep ? flag(this, 'eql') : SameValueZero;
 
     flagMsg = flagMsg ? flagMsg + ': ' : '';
 
@@ -502,7 +503,6 @@ module.exports = function (chai, _) {
         break;
 
       case 'map':
-        var isEql = isDeep ? _.eql : SameValueZero;
         obj.forEach(function (item) {
           included = included || isEql(item, val);
         });
@@ -511,7 +511,7 @@ module.exports = function (chai, _) {
       case 'set':
         if (isDeep) {
           obj.forEach(function (item) {
-            included = included || _.eql(item, val);
+            included = included || isEql(item, val);
           });
         } else {
           included = obj.has(val);
@@ -521,7 +521,7 @@ module.exports = function (chai, _) {
       case 'array':
         if (isDeep) {
           included = obj.some(function (item) {
-            return _.eql(item, val);
+            return isEql(item, val);
           })
         } else {
           included = obj.indexOf(val) !== -1;
@@ -1093,8 +1093,9 @@ module.exports = function (chai, _) {
 
   function assertEql(obj, msg) {
     if (msg) flag(this, 'message', msg);
+    var eql = flag(this, 'eql');
     this.assert(
-        _.eql(obj, flag(this, 'object'))
+        eql(obj, flag(this, 'object'))
       , 'expected #{this} to deeply equal #{exp}'
       , 'expected #{this} to not deeply equal #{exp}'
       , obj
@@ -1862,7 +1863,8 @@ module.exports = function (chai, _) {
     var isDeep = flag(this, 'deep')
       , negate = flag(this, 'negate')
       , pathInfo = isNested ? _.getPathInfo(obj, name) : null
-      , value = isNested ? pathInfo.value : obj[name];
+      , value = isNested ? pathInfo.value : obj[name]
+      , isEql = isDeep ? flag(this, 'eql') : (val1, val2) => val1 === val2;;
 
     var descriptor = '';
     if (isDeep) descriptor += 'deep ';
@@ -1889,7 +1891,7 @@ module.exports = function (chai, _) {
 
     if (arguments.length > 1) {
       this.assert(
-          hasProperty && (isDeep ? _.eql(val, value) : val === value)
+          hasProperty && isEql(val, value)
         , 'expected #{this} to have ' + descriptor + _.inspect(name) + ' of #{exp}, but got #{act}'
         , 'expected #{this} to not have ' + descriptor + _.inspect(name) + ' of #{act}'
         , val
@@ -2037,9 +2039,10 @@ module.exports = function (chai, _) {
     if (msg) flag(this, 'message', msg);
     var obj = flag(this, 'object');
     var actualDescriptor = Object.getOwnPropertyDescriptor(Object(obj), name);
+    var eql = flag(this, 'eql');
     if (actualDescriptor && descriptor) {
       this.assert(
-          _.eql(descriptor, actualDescriptor)
+          eql(descriptor, actualDescriptor)
         , 'expected the own property descriptor for ' + _.inspect(name) + ' on #{this} to match ' + _.inspect(descriptor) + ', got ' + _.inspect(actualDescriptor)
         , 'expected the own property descriptor for ' + _.inspect(name) + ' on #{this} to not match ' + _.inspect(descriptor)
         , descriptor
@@ -2393,7 +2396,8 @@ module.exports = function (chai, _) {
     var len = keys.length
       , any = flag(this, 'any')
       , all = flag(this, 'all')
-      , expected = keys;
+      , expected = keys
+      , isEql = isDeep ? flag(this, 'eql') : (val1, val2) => val1 === val2;
 
     if (!any && !all) {
       all = true;
@@ -2403,11 +2407,7 @@ module.exports = function (chai, _) {
     if (any) {
       ok = expected.some(function(expectedKey) {
         return actual.some(function(actualKey) {
-          if (isDeep) {
-            return _.eql(expectedKey, actualKey);
-          } else {
-            return expectedKey === actualKey;
-          }
+          return isEql(expectedKey, actualKey);
         });
       });
     }
@@ -2416,11 +2416,7 @@ module.exports = function (chai, _) {
     if (all) {
       ok = expected.every(function(expectedKey) {
         return actual.some(function(actualKey) {
-          if (isDeep) {
-            return _.eql(expectedKey, actualKey);
-          } else {
-            return expectedKey === actualKey;
-          }
+          return isEql(expectedKey, actualKey);
         });
       });
 
@@ -3108,7 +3104,7 @@ module.exports = function (chai, _) {
       failNegateMsg = 'expected #{this} to not have the same ' + subject + ' as #{exp}';
     }
 
-    var cmp = flag(this, 'deep') ? _.eql : undefined;
+    var cmp = flag(this, 'deep') ? flag(this, 'eql') : undefined;
 
     this.assert(
         isSubsetOf(subset, obj, cmp, contains, ordered)
@@ -3164,7 +3160,8 @@ module.exports = function (chai, _) {
       , flagMsg = flag(this, 'message')
       , ssfi = flag(this, 'ssfi')
       , contains = flag(this, 'contains')
-      , isDeep = flag(this, 'deep');
+      , isDeep = flag(this, 'deep')
+      , eql = flag(this, 'eql');
     new Assertion(list, flagMsg, ssfi, true).to.be.an('array');
 
     if (contains) {
@@ -3178,7 +3175,7 @@ module.exports = function (chai, _) {
     } else {
       if (isDeep) {
         this.assert(
-          list.some(function(possibility) { return _.eql(expected, possibility) })
+          list.some(function(possibility) { return eql(expected, possibility) })
           , 'expected #{this} to deeply equal one of #{exp}'
           , 'expected #{this} to deeply equal one of #{exp}'
           , list


Test patch:
diff --git a/test/configuration.js b/test/configuration.js
index 7127314..e0e6689 100644
--- a/test/configuration.js
+++ b/test/configuration.js
@@ -810,4 +810,23 @@ describe('configuration', function () {
       }
     });
   });
+
+  describe('deepEqual', function() {
+    it('should use custom deepEqual function for deepEqual comparison', function(){
+      chai.config.deepEqual = (expected, actual) => {
+        return chai.util.eql(expected, actual, {
+          comparator: (expected, actual) => {
+              // for non number comparison, use the default behavior
+              if(typeof expected !== 'number') return null;
+              // allow a difference of 10 between compared numbers
+              return typeof actual === 'number' && Math.abs(actual - expected) < 10
+          }
+        })
+      };
+      assert.deepEqual({v: 1}, {v: 10});
+      err(function() {
+        assert.deepEqual({v: 1}, {v: 100});
+      }, "expected { v: 1 } to deeply equal { v: 100 }");
+    })
+  })
 });