Repository: chaijs/chai (js)

Issue:
Unexpected meaning of `deep` flag in `property` assertion
There's an inconsistency with the meaning of the `deep` flag. Consider these examples:

```js
const obj = {a: 1};
expect(obj).to.deep.equal({a: 1});  // Currently passes thanks to deep equality
expect([obj]).to.have.deep.members([{a: 1}]);  // Currently passes thanks to deep equality
expect([obj]).to.deep.include({a: 1});  // Will pass thanks to deep equality once Issue #743 is fixed
expect({foo: obj}).to.deep.include({foo: {a: 1}});  // Will pass thanks to deep equality once Issue #743 is fixed
expect({foo: obj}).to.have.deep.property('foo', {a: 1});  // Fails due to using strict equality
```

The reason the last example fails is because the `deep` flag has a different meaning when applied to the `.property` assertion than it does when applied to any other assertion. Instead of switching from strict to deep equality, the `deep` flag unlocks the ability for the developer to use dot or bracket notation in their `.property` assertion:

```js
const obj = {a: 1};
expect({foo: obj}).to.have.deep.property('foo.a', 1);
```

That's pretty cool, but the inconsistency of the `deep` flag creates a confusing developer experience and brings sadness to my soul. It also makes it impossible for other assertions such as `.include` to internally leverage `.property` by passing along the `deep` flag, as noted in https://github.com/chaijs/chai/issues/743#issuecomment-230186471.

I propose the following breaking change:

- Make the `deep` flag perform a deep equality comparison when used with the `.property` assertion.
- Add either a `nested` or `inner` flag to inherit the old behavior of the `deep` flag, allowing dot or bracket notation in the `.property` assertion.

Examples:

```js
const obj = {a: 1};
expect({foo: obj}).to.have.deep.property('foo', {a: 1});
expect({foo: obj}).to.have.nested.property('foo.a', 1);

const obj2 = {a: {b: 2}};
expect({foo: obj2}).to.have.deep.nested.property('foo.a', {b: 2});
```

Pull request description:
Add `.deep.property` for deep equality comparisons
Notes:
- #744 and #757 are prerequisites for this PR. I recommend reviewing them first, and then only reviewing the newest commit in this PR.
- This PR is a prerequisite for completing #743 in such a way that `.include` internally leverages `.property` when performing property assertions while providing a consistent, intuitive experience.
- Resolves #745

Patch:
diff --git a/lib/chai/core/assertions.js b/lib/chai/core/assertions.js
index f3bc6d6..dfafa8b 100644
--- a/lib/chai/core/assertions.js
+++ b/lib/chai/core/assertions.js
@@ -70,26 +70,44 @@ module.exports = function (chai, _) {
   /**
    * ### .deep
    *
-   * Sets the `deep` flag, later used by the `equal` and
-   * `property` assertions.
+   * Sets the `deep` flag, later used by the `equal`, `members`, and `property`
+   * assertions.
+   *
+   *     const obj = {a: 1};
+   *     expect(obj).to.deep.equal({a: 1});
+   *     expect([obj]).to.have.deep.members([{a: 1}]);
+   *     expect({foo: obj}).to.have.deep.property('foo', {a: 1});
+   *
+   * @name deep
+   * @namespace BDD
+   * @api public
+   */
+
+  Assertion.addProperty('deep', function () {
+    flag(this, 'deep', true);
+  });
+
+  /**
+   * ### .nested
+   *
+   * Sets the `nested` flag, later used by the `property` assertion.
    *
-   *     expect(foo).to.deep.equal({ bar: 'baz' });
    *     expect({ foo: { bar: { baz: 'quux' } } })
-   *       .to.have.deep.property('foo.bar.baz', 'quux');
+   *       .to.have.nested.property('foo.bar.baz', 'quux');
    *
-   * `.deep.property` special characters can be escaped
-   * by adding two slashes before the `.` or `[]`.
+   * `.nested.property` special characters can be escaped by adding two slashes
+   * before the `.` or `[]`.
    *
    *     var deepCss = { '.link': { '[target]': 42 }};
-   *     expect(deepCss).to.have.deep.property('\\.link.\\[target\\]', 42);
+   *     expect(deepCss).to.have.nested.property('\\.link.\\[target\\]', 42);
    *
-   * @name deep
+   * @name nested
    * @namespace BDD
    * @api public
    */
 
-  Assertion.addProperty('deep', function () {
-    flag(this, 'deep', true);
+  Assertion.addProperty('nested', function () {
+    flag(this, 'nested', true);
   });
 
   /**
@@ -827,26 +845,39 @@ module.exports = function (chai, _) {
    * ### .property(name, [value])
    *
    * Asserts that the target has a property `name`, optionally asserting that
-   * the value of that property is strictly equal to  `value`.
-   * If the `deep` flag is set, you can use dot- and bracket-notation for deep
-   * references into objects and arrays.
+   * the value of that property is strictly equal to `value`.
    *
-   *     // simple referencing
    *     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');
+   *
+   * If the `deep` flag is set, asserts that the value of the property is deeply
+   * equal to `value`.
+   *
+   *     var obj = { foo: { bar: 'baz' } };
+   *     expect(obj).to.have.deep.property('foo', { bar: 'baz' });
+   *     expect(obj).to.not.have.deep.property('foo', { bar: 'quux' });
+   *
+   * If the `nested` flag is set, you can use dot- and bracket-notation for
+   * nested references into objects and arrays.
    *
-   *     // deep referencing
    *     var deepObj = {
    *         green: { tea: 'matcha' }
    *       , teas: [ 'chai', 'matcha', { tea: 'konacha' } ]
    *     };
+   *     expect(deepObj).to.have.nested.property('green.tea', 'matcha');
+   *     expect(deepObj).to.have.nested.property('teas[1]', 'matcha');
+   *     expect(deepObj).to.have.nested.property('teas[2].tea', 'konacha');
+   *
+   * The `deep` and `nested` flags can be combined.
    *
-   *     expect(deepObj).to.have.deep.property('green.tea', 'matcha');
-   *     expect(deepObj).to.have.deep.property('teas[1]', 'matcha');
-   *     expect(deepObj).to.have.deep.property('teas[2].tea', 'konacha');
+   *    expect({ foo: { bar: { baz: 'quux' } } })
+   *      .to.have.deep.nested.property('foo.bar', { baz: 'quux' });
    *
-   * You can also use an array as the starting point of a `deep.property`
+   * You can also use an array as the starting point of a `nested.property`
    * assertion, or traverse nested arrays.
    *
    *     var arr = [
@@ -855,9 +886,8 @@ module.exports = function (chai, _) {
    *         , { tea: 'matcha' }
    *         , { tea: 'konacha' } ]
    *     ];
-   *
-   *     expect(arr).to.have.deep.property('[0][1]', 'matcha');
-   *     expect(arr).to.have.deep.property('[1][2].tea', 'konacha');
+   *     expect(arr).to.have.nested.property('[0][1]', 'matcha');
+   *     expect(arr).to.have.nested.property('[1][2].tea', 'konacha');
    *
    * Furthermore, `property` changes the subject of the assertion
    * to be the value of that property from the original object. This
@@ -870,23 +900,24 @@ module.exports = function (chai, _) {
    *       .that.deep.equals({ tea: 'matcha' });
    *     expect(deepObj).to.have.property('teas')
    *       .that.is.an('array')
-   *       .with.deep.property('[2]')
+   *       .with.nested.property('[2]')
    *         .that.deep.equals({ tea: 'konacha' });
    *
    * Note that dots and brackets in `name` must be backslash-escaped when
-   * the `deep` flag is set, while they must NOT be escaped when the `deep`
+   * the `nested` flag is set, while they must NOT be escaped when the `nested`
    * flag is not set.
    *
-   *     // simple referencing
+   *     // without nested referencing
    *     var css = { '.link[target]': 42 };
    *     expect(css).to.have.property('.link[target]', 42);
    *
-   *     // deep referencing
+   *     // with nested referencing
    *     var deepCss = { '.link': { '[target]': 42 }};
-   *     expect(deepCss).to.have.deep.property('\\.link.\\[target\\]', 42);
+   *     expect(deepCss).to.have.nested.property('\\.link.\\[target\\]', 42);
    *
    * @name property
    * @alias deep.property
+   * @alias nested.property
    * @param {String} name
    * @param {Mixed} value (optional)
    * @param {String} message _optional_
@@ -898,24 +929,27 @@ module.exports = function (chai, _) {
   Assertion.addMethod('property', function (name, val, msg) {
     if (msg) flag(this, 'message', msg);
 
-    var isDeep = !!flag(this, 'deep')
-      , descriptor = isDeep ? 'deep property ' : 'property '
+    var isNested = !!flag(this, 'nested')
+      , isDeep = !!flag(this, 'deep')
+      , descriptor = (isDeep ? 'deep ' : '')
+                   + (isNested ? 'nested ' : '')
+                   + 'property '
       , negate = flag(this, 'negate')
       , obj = flag(this, 'object')
-      , pathInfo = isDeep ? _.getPathInfo(name, obj) : null
-      , hasProperty = isDeep
+      , pathInfo = isNested ? _.getPathInfo(name, obj) : null
+      , hasProperty = isNested
         ? pathInfo.exists
         : _.hasProperty(name, obj)
-      , value = isDeep
+      , value = isNested
         ? 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 +958,7 @@ module.exports = function (chai, _) {
 
     if (arguments.length > 1) {
       this.assert(
-          val === value
+          hasProperty && (isDeep ? _.eql(val, value) : 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..a9d96e6 100644
--- a/lib/chai/interface/assert.js
+++ b/lib/chai/interface/assert.js
@@ -942,14 +942,14 @@ module.exports = function (chai, util) {
   };
 
   /**
-   * ### .deepProperty(object, property, [message])
+   * ### .nestedProperty(object, property, [message])
    *
    * Asserts that `object` has a property named by `property`, which can be a
-   * string using dot- and bracket-notation for deep reference.
+   * string using dot- and bracket-notation for nested reference.
    *
-   *     assert.deepProperty({ tea: { green: 'matcha' }}, 'tea.green');
+   *     assert.nestedProperty({ tea: { green: 'matcha' }}, 'tea.green');
    *
-   * @name deepProperty
+   * @name nestedProperty
    * @param {Object} object
    * @param {String} property
    * @param {String} message
@@ -957,19 +957,19 @@ module.exports = function (chai, util) {
    * @api public
    */
 
-  assert.deepProperty = function (obj, prop, msg) {
-    new Assertion(obj, msg).to.have.deep.property(prop);
+  assert.nestedProperty = function (obj, prop, msg) {
+    new Assertion(obj, msg).to.have.nested.property(prop);
   };
 
   /**
-   * ### .notDeepProperty(object, property, [message])
+   * ### .notNestedProperty(object, property, [message])
    *
    * Asserts that `object` does _not_ have a property named by `property`, which
-   * can be a string using dot- and bracket-notation for deep reference.
+   * can be a string using dot- and bracket-notation for nested reference.
    *
-   *     assert.notDeepProperty({ tea: { green: 'matcha' }}, 'tea.oolong');
+   *     assert.notNestedProperty({ tea: { green: 'matcha' }}, 'tea.oolong');
    *
-   * @name notDeepProperty
+   * @name notNestedProperty
    * @param {Object} object
    * @param {String} property
    * @param {String} message
@@ -977,15 +977,15 @@ module.exports = function (chai, util) {
    * @api public
    */
 
-  assert.notDeepProperty = function (obj, prop, msg) {
-    new Assertion(obj, msg).to.not.have.deep.property(prop);
+  assert.notNestedProperty = function (obj, prop, msg) {
+    new Assertion(obj, msg).to.not.have.nested.property(prop);
   };
 
   /**
    * ### .propertyVal(object, property, value, [message])
    *
-   * Asserts that `object` has a property named by `property` with value given
-   * by `value`.
+   * Asserts that `object` has a property named by `property` with a value given
+   * by `value`. Uses a strict equality check (===).
    *
    *     assert.propertyVal({ tea: 'is good' }, 'tea', 'is good');
    *
@@ -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`. Uses a strict equality check (===).
    *
-   *     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,18 +1020,17 @@ 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);
   };
 
   /**
    * ### .deepPropertyVal(object, property, value, [message])
    *
-   * Asserts that `object` has a property named by `property` with value given
-   * by `value`. `property` can use dot- and bracket-notation for deep
-   * reference.
+   * Asserts that `object` has a property named by `property` with a value given
+   * by `value`. Uses a deep equality check.
    *
-   *     assert.deepPropertyVal({ tea: { green: 'matcha' }}, 'tea.green', 'matcha');
+   *     assert.deepPropertyVal({ tea: { green: 'matcha' } }, 'tea', { green: 'matcha' });
    *
    * @name deepPropertyVal
    * @param {Object} object
@@ -1046,15 +1046,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`. Uses a deep equality check.
    *
-   *     assert.deepPropertyNotVal({ tea: { green: 'matcha' }}, 'tea.green', 'konacha');
+   *     assert.notDeepPropertyVal({ tea: { green: 'matcha' } }, 'tea', { black: 'matcha' });
+   *     assert.notDeepPropertyVal({ tea: { green: 'matcha' } }, 'tea', { green: 'oolong' });
+   *     assert.notDeepPropertyVal({ tea: { green: 'matcha' } }, 'coffee', { green: 'matcha' });
    *
-   * @name deepPropertyNotVal
+   * @name notDeepPropertyVal
    * @param {Object} object
    * @param {String} property
    * @param {Mixed} value
@@ -1063,10 +1064,101 @@ 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);
   };
 
+  /**
+   * ### .nestedPropertyVal(object, property, value, [message])
+   *
+   * Asserts that `object` has a property named by `property` with value given
+   * by `value`. `property` can use dot- and bracket-notation for nested
+   * reference. Uses a strict equality check (===).
+   *
+   *     assert.nestedPropertyVal({ tea: { green: 'matcha' }}, 'tea.green', 'matcha');
+   *
+   * @name nestedPropertyVal
+   * @param {Object} object
+   * @param {String} property
+   * @param {Mixed} value
+   * @param {String} message
+   * @namespace Assert
+   * @api public
+   */
+
+  assert.nestedPropertyVal = function (obj, prop, val, msg) {
+    new Assertion(obj, msg).to.have.nested.property(prop, val);
+  };
+
+  /**
+   * ### .notNestedPropertyVal(object, property, value, [message])
+   *
+   * Asserts that `object` does _not_ have a property named by `property` with
+   * value given by `value`. `property` can use dot- and bracket-notation for
+   * nested reference. Uses a strict equality check (===).
+   *
+   *     assert.notNestedPropertyVal({ tea: { green: 'matcha' }}, 'tea.green', 'konacha');
+   *     assert.notNestedPropertyVal({ tea: { green: 'matcha' }}, 'coffee.green', 'matcha');
+   *
+   * @name notNestedPropertyVal
+   * @param {Object} object
+   * @param {String} property
+   * @param {Mixed} value
+   * @param {String} message
+   * @namespace Assert
+   * @api public
+   */
+
+  assert.notNestedPropertyVal = function (obj, prop, val, msg) {
+    new Assertion(obj, msg).to.not.have.nested.property(prop, val);
+  };
+
+  /**
+   * ### .deepNestedPropertyVal(object, property, value, [message])
+   *
+   * Asserts that `object` has a property named by `property` with a value given
+   * by `value`. `property` can use dot- and bracket-notation for nested
+   * reference. Uses a deep equality check.
+   *
+   *     assert.deepNestedPropertyVal({ tea: { green: { matcha: 'yum' } } }, 'tea.green', { matcha: 'yum' });
+   *
+   * @name deepNestedPropertyVal
+   * @param {Object} object
+   * @param {String} property
+   * @param {Mixed} value
+   * @param {String} message
+   * @namespace Assert
+   * @api public
+   */
+
+  assert.deepNestedPropertyVal = function (obj, prop, val, msg) {
+    new Assertion(obj, msg).to.have.deep.nested.property(prop, val);
+  };
+
+  /**
+   * ### .notDeepNestedPropertyVal(object, property, value, [message])
+   *
+   * Asserts that `object` does _not_ have a property named by `property` with
+   * value given by `value`. `property` can use dot- and bracket-notation for
+   * nested reference. Uses a deep equality check.
+   *
+   *     assert.notDeepNestedPropertyVal({ tea: { green: { matcha: 'yum' } } }, 'tea.green', { oolong: 'yum' });
+   *     assert.notDeepNestedPropertyVal({ tea: { green: { matcha: 'yum' } } }, 'tea.green', { matcha: 'yuck' });
+   *     assert.notDeepNestedPropertyVal({ tea: { green: { matcha: 'yum' } } }, 'tea.black', { matcha: 'yum' });
+   *
+   * @name notDeepNestedPropertyVal
+   * @param {Object} object
+   * @param {String} property
+   * @param {Mixed} value
+   * @param {String} message
+   * @namespace Assert
+   * @api public
+   */
+
+  assert.notDeepNestedPropertyVal = function (obj, prop, val, msg) {
+    new Assertion(obj, msg).to.not.have.deep.nested.property(prop, val);
+  }
+
   /**
    * ### .lengthOf(object, length, [message])
    *


Test patch:
diff --git a/test/assert.js b/test/assert.js
index 594bdc1..defd4b0 100644
--- a/test/assert.js
+++ b/test/assert.js
@@ -942,28 +942,32 @@ describe('assert', function () {
     assert.property(obj, 'foo');
     assert.property(undefinedKeyObj, 'foo');
     assert.propertyVal(undefinedKeyObj, 'foo', undefined);
-    assert.deepProperty(obj, 'foo.bar');
+    assert.nestedProperty(obj, 'foo.bar');
     assert.notProperty(obj, 'baz');
     assert.notProperty(obj, 'foo.bar');
-    assert.notDeepProperty(obj, 'foo.baz');
-    assert.deepPropertyVal(obj, 'foo.bar', 'baz');
-    assert.deepPropertyNotVal(obj, 'foo.bar', 'flow');
+    assert.notPropertyVal(simpleObj, 'foo', 'flow');
+    assert.notPropertyVal(simpleObj, 'flow', 'bar');
+    assert.notPropertyVal(obj, 'foo', {bar: 'baz'});
+    assert.notNestedProperty(obj, 'foo.baz');
+    assert.nestedPropertyVal(obj, 'foo.bar', 'baz');
+    assert.notNestedPropertyVal(obj, 'foo.bar', 'flow');
+    assert.notNestedPropertyVal(obj, 'foo.flow', 'baz');
 
     err(function () {
       assert.property(obj, 'baz');
     }, "expected { foo: { bar: 'baz' } } to have a property 'baz'");
 
     err(function () {
-      assert.deepProperty(obj, 'foo.baz');
-    }, "expected { foo: { bar: 'baz' } } to have a deep property 'foo.baz'");
+      assert.nestedProperty(obj, 'foo.baz');
+    }, "expected { foo: { bar: 'baz' } } to have a nested property 'foo.baz'");
 
     err(function () {
       assert.notProperty(obj, 'foo');
     }, "expected { foo: { bar: 'baz' } } to not have property 'foo'");
 
     err(function () {
-      assert.notDeepProperty(obj, 'foo.bar');
-    }, "expected { foo: { bar: 'baz' } } to not have deep property 'foo.bar'");
+      assert.notNestedProperty(obj, 'foo.bar');
+    }, "expected { foo: { bar: 'baz' } } to not have nested property 'foo.bar'");
 
     err(function () {
       assert.propertyVal(simpleObj, 'foo', 'ball');
@@ -974,16 +978,56 @@ describe('assert', function () {
     }, "expected { foo: 'bar' } to have a property 'foo' of undefined, but got 'bar'");
 
     err(function () {
-      assert.deepPropertyVal(obj, 'foo.bar', 'ball');
-    }, "expected { foo: { bar: 'baz' } } to have a deep property 'foo.bar' of 'ball', but got 'baz'");
+      assert.nestedPropertyVal(obj, 'foo.bar', 'ball');
+    }, "expected { foo: { bar: 'baz' } } to have a nested 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');
-    }, "expected { foo: { bar: 'baz' } } to not have a deep property 'foo.bar' of 'baz'");
+      assert.notNestedPropertyVal(obj, 'foo.bar', 'baz');
+    }, "expected { foo: { bar: 'baz' } } to not have a nested property 'foo.bar' of 'baz'");
+  });
+
+  it('deepPropertyVal', function () {
+    var obj = {a: {b: 1}};
+    assert.deepPropertyVal(obj, 'a', {b: 1});
+    assert.notDeepPropertyVal(obj, 'a', {b: 7});
+    assert.notDeepPropertyVal(obj, 'a', {z: 1});
+    assert.notDeepPropertyVal(obj, 'z', {b: 1});
+
+    err(function () {
+      assert.deepPropertyVal(obj, 'a', {b: 7}, 'blah');
+    }, "blah: expected { a: { b: 1 } } to have a deep property 'a' of { b: 7 }, but got { b: 1 }");
+
+    err(function () {
+      assert.deepPropertyVal(obj, 'z', {b: 1}, 'blah');
+    }, "blah: expected { a: { b: 1 } } to have a deep property 'z'");
+
+    err(function () {
+      assert.notDeepPropertyVal(obj, 'a', {b: 1}, 'blah');
+    }, "blah: expected { a: { b: 1 } } to not have a deep property 'a' of { b: 1 }");
+  });
+
+  it('deepNestedPropertyVal', function () {
+    var obj = {a: {b: {c: 1}}};
+    assert.deepNestedPropertyVal(obj, 'a.b', {c: 1});
+    assert.notDeepNestedPropertyVal(obj, 'a.b', {c: 7});
+    assert.notDeepNestedPropertyVal(obj, 'a.b', {z: 1});
+    assert.notDeepNestedPropertyVal(obj, 'a.z', {c: 1});
+
+    err(function () {
+      assert.deepNestedPropertyVal(obj, 'a.b', {c: 7}, 'blah');
+    }, "blah: expected { a: { b: { c: 1 } } } to have a deep nested property 'a.b' of { c: 7 }, but got { c: 1 }");
+
+    err(function () {
+      assert.deepNestedPropertyVal(obj, 'a.z', {c: 1}, 'blah');
+    }, "blah: expected { a: { b: { c: 1 } } } to have a deep nested property 'a.z'");
+
+    err(function () {
+      assert.notDeepNestedPropertyVal(obj, 'a.b', {c: 1}, 'blah');
+    }, "blah: expected { a: { b: { c: 1 } } } to not have a deep nested property 'a.b' of { c: 1 }");
   });
 
   it('throws / throw / Throw', function() {
diff --git a/test/expect.js b/test/expect.js
index 0595076..4298879 100644
--- a/test/expect.js
+++ b/test/expect.js
@@ -511,50 +511,53 @@ describe('expect', function () {
     }, "expected { foo: { bar: 'baz' } } to have a property 'foo.bar'");
   });
 
-  it('deep.property(name)', function(){
+  it('nested.property(name)', function(){
     expect({ 'foo.bar': 'baz'})
-      .to.not.have.deep.property('foo.bar');
+      .to.not.have.nested.property('foo.bar');
     expect({ foo: { bar: 'baz' } })
-      .to.have.deep.property('foo.bar');
+      .to.have.nested.property('foo.bar');
 
     expect({ 'foo': [1, 2, 3] })
-      .to.have.deep.property('foo[1]');
+      .to.have.nested.property('foo[1]');
 
     expect({ 'foo.bar[]': 'baz'})
-      .to.have.deep.property('foo\\.bar\\[\\]');
+      .to.have.nested.property('foo\\.bar\\[\\]');
 
     err(function(){
       expect({ 'foo.bar': 'baz' })
-        .to.have.deep.property('foo.bar');
-    }, "expected { 'foo.bar': 'baz' } to have a deep property 'foo.bar'");
+        .to.have.nested.property('foo.bar');
+    }, "expected { 'foo.bar': 'baz' } to have a nested property 'foo.bar'");
   });
 
   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);
+    expect({a: {b: 1}}).to.not.have.property('a', {b: 1});
 
     var deepObj = {
         green: { tea: 'matcha' }
       , teas: [ 'chai', 'matcha', { tea: 'konacha' } ]
     };
-    expect(deepObj).to.have.deep.property('green.tea', 'matcha');
-    expect(deepObj).to.have.deep.property('teas[1]', 'matcha');
-    expect(deepObj).to.have.deep.property('teas[2].tea', 'konacha');
+    expect(deepObj).to.have.nested.property('green.tea', 'matcha');
+    expect(deepObj).to.have.nested.property('teas[1]', 'matcha');
+    expect(deepObj).to.have.nested.property('teas[2].tea', 'konacha');
 
     expect(deepObj).to.have.property('teas')
       .that.is.an('array')
-      .with.deep.property('[2]')
+      .with.nested.property('[2]')
         .that.deep.equals({tea: 'konacha'});
 
     err(function(){
-      expect(deepObj).to.have.deep.property('teas[3]');
-    }, "expected { Object (green, teas) } to have a deep property 'teas[3]'");
+      expect(deepObj).to.have.nested.property('teas[3]');
+    }, "expected { Object (green, teas) } to have a nested property 'teas[3]'");
     err(function(){
-      expect(deepObj).to.have.deep.property('teas[3]', 'bar');
-    }, "expected { Object (green, teas) } to have a deep property 'teas[3]'");
+      expect(deepObj).to.have.nested.property('teas[3]', 'bar');
+    }, "expected { Object (green, teas) } to have a nested property 'teas[3]'");
     err(function(){
-      expect(deepObj).to.have.deep.property('teas[3].tea', 'bar');
-    }, "expected { Object (green, teas) } to have a deep property 'teas[3].tea'");
+      expect(deepObj).to.have.nested.property('teas[3].tea', 'bar');
+    }, "expected { Object (green, teas) } to have a nested property 'teas[3].tea'");
 
     var arr = [
         [ 'chai', 'matcha', 'konacha' ]
@@ -562,17 +565,17 @@ describe('expect', function () {
         , { tea: 'matcha' }
         , { tea: 'konacha' } ]
     ];
-    expect(arr).to.have.deep.property('[0][1]', 'matcha');
-    expect(arr).to.have.deep.property('[1][2].tea', 'konacha');
+    expect(arr).to.have.nested.property('[0][1]', 'matcha');
+    expect(arr).to.have.nested.property('[1][2].tea', 'konacha');
     err(function(){
-      expect(arr).to.have.deep.property('[2][1]');
-    }, "expected [ Array(2) ] to have a deep property '[2][1]'");
+      expect(arr).to.have.nested.property('[2][1]');
+    }, "expected [ Array(2) ] to have a nested property '[2][1]'");
     err(function(){
-      expect(arr).to.have.deep.property('[2][1]', 'none');
-    }, "expected [ Array(2) ] to have a deep property '[2][1]'");
+      expect(arr).to.have.nested.property('[2][1]', 'none');
+    }, "expected [ Array(2) ] to have a nested property '[2][1]'");
     err(function(){
-      expect(arr).to.have.deep.property('[0][3]', 'none');
-    }, "expected [ Array(2) ] to have a deep property '[0][3]'");
+      expect(arr).to.have.nested.property('[0][3]', 'none');
+    }, "expected [ Array(2) ] to have a nested property '[0][3]'");
 
     err(function(){
       expect('asd').to.have.property('length', 4, 'blah');
@@ -582,31 +585,68 @@ 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]");
   });
 
-  it('deep.property(name, val)', function(){
+  it('deep.property(name, val)', function () {
+    var obj = {a: {b: 1}};
+    expect(obj).to.have.deep.property('a', {b: 1});
+    expect(obj).to.not.have.deep.property('a', {b: 7});
+    expect(obj).to.not.have.deep.property('a', {z: 1});
+    expect(obj).to.not.have.deep.property('z', {b: 1});
+
+    err(function () {
+      expect(obj).to.have.deep.property('a', {b: 7}, 'blah');
+    }, "blah: expected { a: { b: 1 } } to have a deep property 'a' of { b: 7 }, but got { b: 1 }");
+
+    err(function () {
+      expect(obj).to.have.deep.property('z', {b: 1}, 'blah');
+    }, "blah: expected { a: { b: 1 } } to have a deep property 'z'");
+
+    err(function () {
+      expect(obj).to.not.have.deep.property('a', {b: 1}, 'blah');
+    }, "blah: expected { a: { b: 1 } } to not have a deep property 'a' of { b: 1 }");
+  });
+  
+  it('nested.property(name, val)', function(){
+    expect({ foo: { bar: 'baz' } })
+      .to.have.nested.property('foo.bar', 'baz');
     expect({ foo: { bar: 'baz' } })
-      .to.have.deep.property('foo.bar', 'baz');
+      .to.not.have.nested.property('foo.bar', 'quux');
+    expect({ foo: { bar: 'baz' } })
+      .to.not.have.nested.property('foo.quux', 'baz');
+    expect({a: {b: {c: 1}}}).to.not.have.nested.property('a.b', {c: 1});
 
     err(function(){
       expect({ foo: { bar: 'baz' } })
-        .to.have.deep.property('foo.bar', 'quux', 'blah');
-    }, "blah: expected { foo: { bar: 'baz' } } to have a deep property 'foo.bar' of 'quux', but got 'baz'");
+        .to.have.nested.property('foo.bar', 'quux', 'blah');
+    }, "blah: expected { foo: { bar: 'baz' } } to have a nested property 'foo.bar' of 'quux', but got 'baz'");
     err(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'");
+        .to.not.have.nested.property('foo.bar', 'baz', 'blah');
+    }, "blah: expected { foo: { bar: 'baz' } } to not have a nested property 'foo.bar' of 'baz'");
+  });
+
+  it('deep.nested.property(name, val)', function () {
+    var obj = {a: {b: {c: 1}}};
+    expect(obj).to.have.deep.nested.property('a.b', {c: 1});
+    expect(obj).to.not.have.deep.nested.property('a.b', {c: 7});
+    expect(obj).to.not.have.deep.nested.property('a.b', {z: 1});
+    expect(obj).to.not.have.deep.nested.property('a.z', {c: 1});
+
+    err(function () {
+      expect(obj).to.have.deep.nested.property('a.b', {c: 7}, 'blah');
+    }, "blah: expected { a: { b: { c: 1 } } } to have a deep nested property 'a.b' of { c: 7 }, but got { c: 1 }");
+
+    err(function () {
+      expect(obj).to.have.deep.nested.property('a.z', {c: 1}, 'blah');
+    }, "blah: expected { a: { b: { c: 1 } } } to have a deep nested property 'a.z'");
+
+    err(function () {
+      expect(obj).to.not.have.deep.nested.property('a.b', {c: 1}, 'blah');
+    }, "blah: expected { a: { b: { c: 1 } } } to not have a deep nested property 'a.b' of { c: 1 }");
   });
 
   it('ownProperty(name)', function(){
diff --git a/test/should.js b/test/should.js
index 41cf8b1..9f7dc49 100644
--- a/test/should.js
+++ b/test/should.js
@@ -438,10 +438,26 @@ describe('should', function() {
     }, "expected 'asd' to have a property 'foo'");
   });
 
+  it('nested.property(name)', function(){
+    ({ 'foo.bar': 'baz'}).should.not.have.nested.property('foo.bar');
+    ({ foo: { bar: 'baz' } }).should.have.nested.property('foo.bar');
+
+    ({ 'foo': [1, 2, 3] }).should.have.nested.property('foo[1]');
+
+    ({ 'foo.bar[]': 'baz'}).should.have.nested.property('foo\\.bar\\[\\]');
+
+    err(function(){
+      ({ 'foo.bar': 'baz' }).should.have.nested.property('foo.bar');
+    }, "expected { 'foo.bar': 'baz' } to have a nested 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);
+    ({a: {b: 1}}).should.not.have.property('a', {b: 1});
 
     err(function(){
       'asd'.should.have.property('length', 4, 'blah');
@@ -451,15 +467,65 @@ 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 () {
+    var obj = {a: {b: 1}};
+    obj.should.have.deep.property('a', {b: 1});
+    obj.should.not.have.deep.property('a', {b: 7});
+    obj.should.not.have.deep.property('a', {z: 1});
+    obj.should.not.have.deep.property('z', {b: 1});
+
+    err(function () {
+      obj.should.have.deep.property('a', {b: 7}, 'blah');
+    }, "blah: expected { a: { b: 1 } } to have a deep property 'a' of { b: 7 }, but got { b: 1 }");
+
+    err(function () {
+      obj.should.have.deep.property('z', {b: 1}, 'blah');
+    }, "blah: expected { a: { b: 1 } } to have a deep property 'z'");
+
+    err(function () {
+      obj.should.not.have.deep.property('a', {b: 1}, 'blah');
+    }, "blah: expected { a: { b: 1 } } to not have a deep property 'a' of { b: 1 }");
+  });
+
+  it('nested.property(name, val)', function(){
+    ({ foo: { bar: 'baz' } }).should.have.nested.property('foo.bar', 'baz');
+    ({ foo: { bar: 'baz' } }).should.not.have.nested.property('foo.bar', 'quux');
+    ({ foo: { bar: 'baz' } }).should.not.have.nested.property('foo.quux', 'baz');
+    ({a: {b: {c: 1}}}).should.not.have.nested.property('a.b', {c: 1});
+
+    err(function(){
+      ({ foo: { bar: 'baz' } }).should.have.nested.property('foo.bar', 'quux', 'blah');
+    }, "blah: expected { foo: { bar: 'baz' } } to have a nested property 'foo.bar' of 'quux', but got 'baz'");
+    err(function(){
+      ({ foo: { bar: 'baz' } }).should.not.have.nested.property('foo.bar', 'baz', 'blah');
+    }, "blah: expected { foo: { bar: 'baz' } } to not have a nested property 'foo.bar' of 'baz'");
+  });
+
+  it('deep.nested.property(name, val)', function () {
+    var obj = {a: {b: {c: 1}}};
+    obj.should.have.deep.nested.property('a.b', {c: 1});
+    obj.should.not.have.deep.nested.property('a.b', {c: 7});
+    obj.should.not.have.deep.nested.property('a.b', {z: 1});
+    obj.should.not.have.deep.nested.property('a.z', {c: 1});
+
+    err(function () {
+      obj.should.have.deep.nested.property('a.b', {c: 7}, 'blah');
+    }, "blah: expected { a: { b: { c: 1 } } } to have a deep nested property 'a.b' of { c: 7 }, but got { c: 1 }");
+
+    err(function () {
+      obj.should.have.deep.nested.property('a.z', {c: 1}, 'blah');
+    }, "blah: expected { a: { b: { c: 1 } } } to have a deep nested property 'a.z'");
+
+    err(function () {
+      obj.should.not.have.deep.nested.property('a.b', {c: 1}, 'blah');
+    }, "blah: expected { a: { b: { c: 1 } } } to not have a deep nested property 'a.b' of { c: 1 }");
+  });
+
   it('ownProperty(name)', function(){
     'test'.should.have.ownProperty('length');
     'test'.should.haveOwnProperty('length');