Repository: chaijs/chai
Pull request #744 (2016-07-02 23:38:29)

Breaking: Change `.not.property(name, val)` behavior
- Previously, `expect(obj).not.property(name, val)` would throw an Error
if `obj` didn't have a property named `name`. This change causes the
assertion to pass instead.
- assert.propertyNotVal renamed to assert.notPropertyVal
- assert.deepPropertyNotVal renamed to assert.notDeepPropertyVal

Notes: 

- This PR is in reference to the issue discussed in https://github.com/chaijs/chai/issues/743#issuecomment-230112615
- It supersedes the decision made in https://github.com/chaijs/chai/issues/16
- It's a prerequisite for completing #743 in such a way that `.include` internally leverages `.property` when performing property assertions while providing a consistent, intuitive experience.



Patch:
diff --git a/lib/chai/core/assertions.js b/lib/chai/core/assertions.js
index f3bc6d6..9c995a7 100644
--- a/lib/chai/core/assertions.js
+++ b/lib/chai/core/assertions.js
@@ -835,6 +835,9 @@ module.exports = function (chai, _) {
    *     var obj = { foo: 'bar' };
    *     expect(obj).to.have.property('foo');
    *     expect(obj).to.have.property('foo', 'bar');
+   *     expect(obj).to.not.have.property('baz');
+   *     expect(obj).to.not.have.property('foo', 'baz');
+   *     expect(obj).to.not.have.property('baz', 'bar');
    *
    *     // deep referencing
    *     var deepObj = {
@@ -910,12 +913,12 @@ module.exports = function (chai, _) {
         ? pathInfo.value
         : obj[name];
 
-    if (negate && arguments.length > 1) {
-      if (undefined === value) {
-        msg = (msg != null) ? msg + ': ' : '';
-        throw new Error(msg + _.inspect(obj) + ' has no ' + descriptor + _.inspect(name));
-      }
-    } else {
+    // When performing a negated assertion for both name and val, merely having
+    // a property with the given name isn't enough to cause the assertion to
+    // fail. It must both have a property with the given name, and the value of
+    // that property must equal the given val. Therefore, skip this assertion in
+    // favor of the next.
+    if (!negate || arguments.length === 1) {
       this.assert(
           hasProperty
         , 'expected #{this} to have a ' + descriptor + _.inspect(name)
@@ -924,7 +927,7 @@ module.exports = function (chai, _) {
 
     if (arguments.length > 1) {
       this.assert(
-          val === value
+          hasProperty && val === value
         , 'expected #{this} to have a ' + descriptor + _.inspect(name) + ' of #{exp}, but got #{act}'
         , 'expected #{this} to not have a ' + descriptor + _.inspect(name) + ' of #{act}'
         , val
diff --git a/lib/chai/interface/assert.js b/lib/chai/interface/assert.js
index 850ac9f..aff29c8 100644
--- a/lib/chai/interface/assert.js
+++ b/lib/chai/interface/assert.js
@@ -1003,14 +1003,15 @@ module.exports = function (chai, util) {
   };
 
   /**
-   * ### .propertyNotVal(object, property, value, [message])
+   * ### .notPropertyVal(object, property, value, [message])
    *
-   * Asserts that `object` has a property named by `property`, but with a value
-   * different from that given by `value`.
+   * Asserts that `object` does _not_ have a property named by `property` with
+   * value given by `value`.
    *
-   *     assert.propertyNotVal({ tea: 'is good' }, 'tea', 'is bad');
+   *     assert.notPropertyVal({ tea: 'is good' }, 'tea', 'is bad');
+   *     assert.notPropertyVal({ tea: 'is good' }, 'coffee', 'is good');
    *
-   * @name propertyNotVal
+   * @name notPropertyVal
    * @param {Object} object
    * @param {String} property
    * @param {Mixed} value
@@ -1019,7 +1020,7 @@ module.exports = function (chai, util) {
    * @api public
    */
 
-  assert.propertyNotVal = function (obj, prop, val, msg) {
+  assert.notPropertyVal = function (obj, prop, val, msg) {
     new Assertion(obj, msg).to.not.have.property(prop, val);
   };
 
@@ -1046,15 +1047,16 @@ module.exports = function (chai, util) {
   };
 
   /**
-   * ### .deepPropertyNotVal(object, property, value, [message])
+   * ### .notDeepPropertyVal(object, property, value, [message])
    *
-   * Asserts that `object` has a property named by `property`, but with a value
-   * different from that given by `value`. `property` can use dot- and
-   * bracket-notation for deep reference.
+   * Asserts that `object` does _not_ have a property named by `property` with
+   * value given by `value`. `property` can use dot- and bracket-notation for deep
+   * reference.
    *
-   *     assert.deepPropertyNotVal({ tea: { green: 'matcha' }}, 'tea.green', 'konacha');
+   *     assert.notDeepPropertyVal({ tea: { green: 'matcha' }}, 'tea.green', 'konacha');
+   *     assert.notDeepPropertyVal({ tea: { green: 'matcha' }}, 'coffee.green', 'matcha');
    *
-   * @name deepPropertyNotVal
+   * @name notDeepPropertyVal
    * @param {Object} object
    * @param {String} property
    * @param {Mixed} value
@@ -1063,7 +1065,7 @@ module.exports = function (chai, util) {
    * @api public
    */
 
-  assert.deepPropertyNotVal = function (obj, prop, val, msg) {
+  assert.notDeepPropertyVal = function (obj, prop, val, msg) {
     new Assertion(obj, msg).to.not.have.deep.property(prop, val);
   };
 


Test patch:
diff --git a/test/assert.js b/test/assert.js
index 594bdc1..7a51de7 100644
--- a/test/assert.js
+++ b/test/assert.js
@@ -945,9 +945,12 @@ describe('assert', function () {
     assert.deepProperty(obj, 'foo.bar');
     assert.notProperty(obj, 'baz');
     assert.notProperty(obj, 'foo.bar');
+    assert.notPropertyVal(simpleObj, 'foo', 'flow');
+    assert.notPropertyVal(simpleObj, 'flow', 'bar');
     assert.notDeepProperty(obj, 'foo.baz');
     assert.deepPropertyVal(obj, 'foo.bar', 'baz');
-    assert.deepPropertyNotVal(obj, 'foo.bar', 'flow');
+    assert.notDeepPropertyVal(obj, 'foo.bar', 'flow');
+    assert.notDeepPropertyVal(obj, 'foo.flow', 'baz');
 
     err(function () {
       assert.property(obj, 'baz');
@@ -978,11 +981,11 @@ describe('assert', function () {
     }, "expected { foo: { bar: 'baz' } } to have a deep property 'foo.bar' of 'ball', but got 'baz'");
 
     err(function () {
-      assert.propertyNotVal(simpleObj, 'foo', 'bar');
+      assert.notPropertyVal(simpleObj, 'foo', 'bar');
     }, "expected { foo: 'bar' } to not have a property 'foo' of 'bar'");
 
     err(function () {
-      assert.deepPropertyNotVal(obj, 'foo.bar', 'baz');
+      assert.notDeepPropertyVal(obj, 'foo.bar', 'baz');
     }, "expected { foo: { bar: 'baz' } } to not have a deep property 'foo.bar' of 'baz'");
   });
 
diff --git a/test/expect.js b/test/expect.js
index 0595076..4c93c14 100644
--- a/test/expect.js
+++ b/test/expect.js
@@ -532,6 +532,8 @@ describe('expect', function () {
   it('property(name, val)', function(){
     expect('test').to.have.property('length', 4);
     expect('asd').to.have.property('constructor', String);
+    expect('test').to.not.have.property('length', 3);
+    expect('test').to.not.have.property('foo', 4);
 
     var deepObj = {
         green: { tea: 'matcha' }
@@ -582,10 +584,6 @@ describe('expect', function () {
       expect('asd').to.not.have.property('length', 3, 'blah');
     }, "blah: expected 'asd' to not have a property 'length' of 3");
 
-    err(function(){
-      expect('asd').to.not.have.property('foo', 3, 'blah');
-    }, "blah: 'asd' has no property 'foo'");
-
     err(function(){
       expect('asd').to.have.property('constructor', Number, 'blah');
     }, "blah: expected 'asd' to have a property 'constructor' of [Function: Number], but got [Function: String]");
@@ -594,6 +592,10 @@ describe('expect', function () {
   it('deep.property(name, val)', function(){
     expect({ foo: { bar: 'baz' } })
       .to.have.deep.property('foo.bar', 'baz');
+    expect({ foo: { bar: 'baz' } })
+      .to.not.have.deep.property('foo.bar', 'quux');
+    expect({ foo: { bar: 'baz' } })
+      .to.not.have.deep.property('foo.quux', 'baz');
 
     err(function(){
       expect({ foo: { bar: 'baz' } })
@@ -603,10 +605,6 @@ describe('expect', function () {
       expect({ foo: { bar: 'baz' } })
         .to.not.have.deep.property('foo.bar', 'baz', 'blah');
     }, "blah: expected { foo: { bar: 'baz' } } to not have a deep property 'foo.bar' of 'baz'");
-    err(function(){
-      expect({ foo: 5 })
-        .to.not.have.deep.property('foo.bar', 'baz', 'blah');
-    }, "blah: { foo: 5 } has no deep property 'foo.bar'");
   });
 
   it('ownProperty(name)', function(){
diff --git a/test/should.js b/test/should.js
index 41cf8b1..22dc2b2 100644
--- a/test/should.js
+++ b/test/should.js
@@ -438,10 +438,25 @@ describe('should', function() {
     }, "expected 'asd' to have a property 'foo'");
   });
 
+  it('deep.property(name)', function(){
+    ({ 'foo.bar': 'baz'}).should.not.have.deep.property('foo.bar');
+    ({ foo: { bar: 'baz' } }).should.have.deep.property('foo.bar');
+
+    ({ 'foo': [1, 2, 3] }).should.have.deep.property('foo[1]');
+
+    ({ 'foo.bar[]': 'baz'}).should.have.deep.property('foo\\.bar\\[\\]');
+
+    err(function(){
+      ({ 'foo.bar': 'baz' }).should.have.deep.property('foo.bar');
+    }, "expected { 'foo.bar': 'baz' } to have a deep property 'foo.bar'");
+  });
+
   it('property(name, val)', function(){
     'test'.should.have.property('length', 4);
     'asd'.should.have.property('constructor', String);
     ({ 1: 1 }).should.have.property(1, 1);
+    'test'.should.not.have.property('length', 3);
+    'test'.should.not.have.property('foo', 4);
 
     err(function(){
       'asd'.should.have.property('length', 4, 'blah');
@@ -451,15 +466,24 @@ describe('should', function() {
       'asd'.should.not.have.property('length', 3, 'blah');
     }, "blah: expected 'asd' to not have a property 'length' of 3");
 
-    err(function(){
-      'asd'.should.not.have.property('foo', 3, 'blah');
-    }, "blah: 'asd' has no property 'foo'");
-
     err(function(){
       'asd'.should.have.property('constructor', Number, 'blah');
     }, "blah: expected 'asd' to have a property 'constructor' of [Function: Number], but got [Function: String]");
   });
 
+  it('deep.property(name, val)', function(){
+    ({ foo: { bar: 'baz' } }).should.have.deep.property('foo.bar', 'baz');
+    ({ foo: { bar: 'baz' } }).should.not.have.deep.property('foo.bar', 'quux');
+    ({ foo: { bar: 'baz' } }).should.not.have.deep.property('foo.quux', 'baz');
+
+    err(function(){
+      ({ foo: { bar: 'baz' } }).should.have.deep.property('foo.bar', 'quux', 'blah');
+    }, "blah: expected { foo: { bar: 'baz' } } to have a deep property 'foo.bar' of 'quux', but got 'baz'");
+    err(function(){
+      ({ foo: { bar: 'baz' } }).should.not.have.deep.property('foo.bar', 'baz', 'blah');
+    }, "blah: expected { foo: { bar: 'baz' } } to not have a deep property 'foo.bar' of 'baz'");
+  });
+
   it('ownProperty(name)', function(){
     'test'.should.have.ownProperty('length');
     'test'.should.haveOwnProperty('length');