Repository: chaijs/chai
Pull request #607 (2016-02-01 22:35:34)

Change function for value
This is my first draft for #544.
(EDIT: I think there's nothing more to be done, I read the code again and it seems to match all the orientations on #544)

* I have added checks on the assert interface in order to maintain compatibility and allow different arguments
* `assertChanges`, `assertIncreases` and `assertDecreases` now accept only a `function` as the `object` argument instead of needing both `object` and `prop`
* Tests were added on all three interfaces

I tried to avoid code repetition, but all assertions have basically the same inner workings, so this is the cleanest way I could code them.

Is this looking like what you had expected @keithamus? If it is not feel free to point out the changes you'd like me to make.

@timruffles and @matthijsgroen are also welcome to take a look on this and share their opinions :smiley: .


Patch:
diff --git a/lib/chai/core/assertions.js b/lib/chai/core/assertions.js
index d6b9f34..330dc03 100644
--- a/lib/chai/core/assertions.js
+++ b/lib/chai/core/assertions.js
@@ -1635,7 +1635,7 @@ module.exports = function (chai, _) {
    * @alias changes
    * @alias Change
    * @param {String} object
-   * @param {String} property name
+   * @param {String} property name _optional_
    * @param {String} message _optional_
    * @namespace BDD
    * @api public
@@ -1644,16 +1644,26 @@ module.exports = function (chai, _) {
   function assertChanges (object, prop, msg) {
     if (msg) flag(this, 'message', msg);
     var fn = flag(this, 'object');
-    new Assertion(object, msg).to.have.property(prop);
     new Assertion(fn).is.a('function');
 
-    var initial = object[prop];
+    var initial;
+    if (!prop) {
+      new Assertion(object).is.a('function');
+      initial = object();
+    } else {
+      new Assertion(object, msg).to.have.property(prop);
+      initial = object[prop];
+    }
+
     fn();
 
+    var final = prop === undefined ? object() : object[prop];
+    var msgObj = prop === undefined ? initial : '.' + prop;
+
     this.assert(
-      initial !== object[prop]
-      , 'expected .' + prop + ' to change'
-      , 'expected .' + prop + ' to not change'
+      initial !== final
+      , 'expected ' + msgObj + ' to change'
+      , 'expected ' + msgObj + ' to not change'
     );
   }
 
@@ -1682,16 +1692,26 @@ module.exports = function (chai, _) {
   function assertIncreases (object, prop, msg) {
     if (msg) flag(this, 'message', msg);
     var fn = flag(this, 'object');
-    new Assertion(object, msg).to.have.property(prop);
     new Assertion(fn).is.a('function');
 
-    var initial = object[prop];
+    var initial;
+    if (!prop) {
+      new Assertion(object).is.a('function');
+      initial = object();
+    } else {
+      new Assertion(object, msg).to.have.property(prop);
+      initial = object[prop];
+    }
+
     fn();
 
+    var final = prop === undefined ? object() : object[prop];
+    var msgObj = prop === undefined ? initial : '.' + prop;
+
     this.assert(
-      object[prop] - initial > 0
-      , 'expected .' + prop + ' to increase'
-      , 'expected .' + prop + ' to not increase'
+      final - initial > 0
+      , 'expected ' + msgObj + ' to increase'
+      , 'expected ' + msgObj + ' to not increase'
     );
   }
 
@@ -1720,16 +1740,26 @@ module.exports = function (chai, _) {
   function assertDecreases (object, prop, msg) {
     if (msg) flag(this, 'message', msg);
     var fn = flag(this, 'object');
-    new Assertion(object, msg).to.have.property(prop);
     new Assertion(fn).is.a('function');
 
-    var initial = object[prop];
+    var initial;
+    if (!prop) {
+      new Assertion(object).is.a('function');
+      initial = object();
+    } else {
+      new Assertion(object, msg).to.have.property(prop);
+      initial = object[prop];
+    }
+
     fn();
 
+    var final = prop === undefined ? object() : object[prop];
+    var msgObj = prop === undefined ? initial : '.' + prop;
+
     this.assert(
-      object[prop] - initial < 0
-      , 'expected .' + prop + ' to decrease'
-      , 'expected .' + prop + ' to not decrease'
+      final - initial < 0
+      , 'expected ' + msgObj + ' to decrease'
+      , 'expected ' + msgObj + ' to not decrease'
     );
   }
 
diff --git a/lib/chai/interface/assert.js b/lib/chai/interface/assert.js
index 5099ea1..4152645 100644
--- a/lib/chai/interface/assert.js
+++ b/lib/chai/interface/assert.js
@@ -1354,14 +1354,19 @@ module.exports = function (chai, util) {
    *
    * @name changes
    * @param {Function} modifier function
-   * @param {Object} object
-   * @param {String} property name
+   * @param {Object} object or getter function
+   * @param {String} property name _optional_
    * @param {String} message _optional_
    * @namespace Assert
    * @api public
    */
 
   assert.changes = function (fn, obj, prop, msg) {
+    if (arguments.length === 3 && typeof obj === 'function') {
+      msg = prop;
+      prop = null;
+    }
+
     new Assertion(fn, msg).to.change(obj, prop);
   }
 
@@ -1376,14 +1381,19 @@ module.exports = function (chai, util) {
    *
    * @name doesNotChange
    * @param {Function} modifier function
-   * @param {Object} object
-   * @param {String} property name
+   * @param {Object} object or getter function
+   * @param {String} property name _optional_
    * @param {String} message _optional_
    * @namespace Assert
    * @api public
    */
 
   assert.doesNotChange = function (fn, obj, prop, msg) {
+    if (arguments.length === 3 && typeof obj === 'function') {
+      msg = prop;
+      prop = null;
+    }
+
     new Assertion(fn, msg).to.not.change(obj, prop);
   }
 
@@ -1398,14 +1408,19 @@ module.exports = function (chai, util) {
    *
    * @name increases
    * @param {Function} modifier function
-   * @param {Object} object
-   * @param {String} property name
+   * @param {Object} object or getter function
+   * @param {String} property name _optional_
    * @param {String} message _optional_
    * @namespace Assert
    * @api public
    */
 
   assert.increases = function (fn, obj, prop, msg) {
+    if (arguments.length === 3 && typeof obj === 'function') {
+      msg = prop;
+      prop = null;
+    }
+
     new Assertion(fn, msg).to.increase(obj, prop);
   }
 
@@ -1420,14 +1435,19 @@ module.exports = function (chai, util) {
    *
    * @name doesNotIncrease
    * @param {Function} modifier function
-   * @param {Object} object
-   * @param {String} property name
+   * @param {Object} object or getter function
+   * @param {String} property name _optional_
    * @param {String} message _optional_
    * @namespace Assert
    * @api public
    */
 
   assert.doesNotIncrease = function (fn, obj, prop, msg) {
+    if (arguments.length === 3 && typeof obj === 'function') {
+      msg = prop;
+      prop = null;
+    }
+
     new Assertion(fn, msg).to.not.increase(obj, prop);
   }
 
@@ -1442,14 +1462,19 @@ module.exports = function (chai, util) {
    *
    * @name decreases
    * @param {Function} modifier function
-   * @param {Object} object
-   * @param {String} property name
+   * @param {Object} object or getter function
+   * @param {String} property name _optional_
    * @param {String} message _optional_
    * @namespace Assert
    * @api public
    */
 
   assert.decreases = function (fn, obj, prop, msg) {
+    if (arguments.length === 3 && typeof obj === 'function') {
+      msg = prop;
+      prop = null;
+    }
+
     new Assertion(fn, msg).to.decrease(obj, prop);
   }
 
@@ -1464,14 +1489,19 @@ module.exports = function (chai, util) {
    *
    * @name doesNotDecrease
    * @param {Function} modifier function
-   * @param {Object} object
-   * @param {String} property name
+   * @param {Object} object or getter function
+   * @param {String} property name _optional_
    * @param {String} message _optional_
    * @namespace Assert
    * @api public
    */
 
   assert.doesNotDecrease = function (fn, obj, prop, msg) {
+    if (arguments.length === 3 && typeof obj === 'function') {
+      msg = prop;
+      prop = null;
+    }
+
     new Assertion(fn, msg).to.not.decrease(obj, prop);
   }
 


Test patch:
diff --git a/test/assert.js b/test/assert.js
index 2432d64..29b98fb 100644
--- a/test/assert.js
+++ b/test/assert.js
@@ -873,17 +873,26 @@ describe('assert', function () {
 
   it('change', function() {
     var obj = { value: 10, str: 'foo' },
+        heroes = ['spiderman', 'superman'],
         fn     = function() { obj.value += 5 },
         bangFn = function() { obj.str += '!' },
-        smFn   = function() { 'foo' + 'bar' };
+        smFn   = function() { 'foo' + 'bar' },
+        batFn  = function() { heroes.push('batman') },
+        lenFn  = function() { return heroes.length };
 
     assert.changes(fn, obj, 'value');
     assert.doesNotChange(smFn, obj, 'value');
     assert.changes(bangFn, obj, 'str');
+    assert.changes(batFn, lenFn);
+    assert.doesNotChange(smFn, lenFn);
   });
 
   it('increase, decrease', function() {
     var obj = { value: 10 },
+        arr = ['one', 'two'],
+        pFn   = function() { arr.push('three') },
+        popFn  = function() { arr.pop() },
+        lenFn = function() { return arr.length },
         incFn = function() { obj.value += 2 },
         decFn = function() { obj.value -= 3 },
         smFn  = function() { obj.value += 0 };
@@ -893,6 +902,12 @@ describe('assert', function () {
 
     assert.increases(incFn, obj, 'value');
     assert.doesNotIncrease(smFn, obj, 'value');
+
+    assert.decreases(popFn, lenFn);
+    assert.doesNotDecrease(pFn, lenFn);
+
+    assert.increases(pFn, lenFn);
+    assert.doesNotIncrease(popFn, lenFn);
   });
 
   it('isExtensible / extensible', function() {
diff --git a/test/expect.js b/test/expect.js
index 79068e4..df4d142 100644
--- a/test/expect.js
+++ b/test/expect.js
@@ -1121,18 +1121,28 @@ describe('expect', function () {
 
   it('change', function() {
     var obj = { value: 10, str: 'foo' },
+        heroes = ['spiderman', 'superman'],
         fn     = function() { obj.value += 5 },
         sameFn = function() { 'foo' + 'bar' },
-        bangFn = function() { obj.str += '!' };
+        bangFn = function() { obj.str += '!' },
+        batFn  = function() { heroes.push('batman') },
+        lenFn  = function() { return heroes.length };
 
     expect(fn).to.change(obj, 'value');
     expect(sameFn).to.not.change(obj, 'value');
     expect(sameFn).to.not.change(obj, 'str');
     expect(bangFn).to.change(obj, 'str');
+    expect(batFn).to.change(lenFn);
+    expect(sameFn).to.not.change(lenFn);
   });
 
   it('increase, decrease', function() {
     var obj = { value: 10 },
+        arr = ['one', 'two'],
+        pFn   = function() { arr.push('three') },
+        popFn = function() { arr.pop() },
+        nFn   = function() { return null },
+        lenFn = function() { return arr.length },
         incFn = function() { obj.value += 2 },
         decFn = function() { obj.value -= 3 },
         smFn  = function() { obj.value += 0 };
@@ -1144,6 +1154,14 @@ describe('expect', function () {
     expect(smFn).to.not.decrease(obj, 'value');
     expect(incFn).to.not.decrease(obj, 'value');
     expect(decFn).to.decrease(obj, 'value');
+
+    expect(popFn).to.not.increase(lenFn);
+    expect(nFn).to.not.increase(lenFn);
+    expect(pFn).to.increase(lenFn);
+
+    expect(popFn).to.decrease(lenFn);
+    expect(nFn).to.not.decrease(lenFn);
+    expect(pFn).to.not.decrease(lenFn);
   });
 
   it('extensible', function() {
diff --git a/test/should.js b/test/should.js
index 0fde10b..ced2965 100644
--- a/test/should.js
+++ b/test/should.js
@@ -950,19 +950,30 @@ describe('should', function() {
 
   it('change', function() {
     var obj = { value: 10, str: 'foo' },
+        heroes = ['spiderman', 'superman'],
         fn     = function() { obj.value += 5 },
         sameFn = function() { obj.value += 0 },
         decFn  = function() { obj.value -= 3 },
-        bangFn = function() { obj.str += '!' };
+        bangFn = function() { obj.str += '!' },
+        batFn  = function() { heroes.push('batman') },
+        lenFn  = function() { return heroes.length },
+        noFn   = function() { return null };
 
     fn.should.change(obj, 'value');
     sameFn.should.not.change(obj, 'value');
     sameFn.should.not.change(obj, 'str');
     bangFn.should.change(obj, 'str');
+    batFn.should.change(lenFn);
+    noFn.should.not.change(lenFn);
   });
 
   it('increase, decrease', function() {
     var obj = { value: 10 },
+        arr = ['one', 'two'],
+        pFn   = function() { arr.push('three') },
+        popFn = function() { arr.pop() },
+        lenFn = function() { return arr.length },
+        nFn   = function() { return null },
         incFn = function() { obj.value += 2 },
         decFn = function() { obj.value -= 3 },
         smFn  = function() { obj.value += 0 };
@@ -974,6 +985,14 @@ describe('should', function() {
     smFn.should.not.decrease(obj, 'value');
     incFn.should.not.decrease(obj, 'value');
     decFn.should.decrease(obj, 'value');
+
+    nFn.should.not.increase(lenFn);
+    popFn.should.not.increase(lenFn);
+    pFn.should.increase(lenFn);
+
+    nFn.should.not.decrease(lenFn);
+    pFn.should.not.decrease(lenFn);
+    popFn.should.decrease(lenFn);
   });
 
   it('extensible', function() {