From a5a90cbd9ff5746cdb23638c3391b1cdc35bbbee Mon Sep 17 00:00:00 2001 From: Grant Snodgrass Date: Mon, 25 Jul 2016 20:44:58 -0400 Subject: [PATCH] Add `.deep.property` for deep equality comparisons --- lib/chai/core/assertions.js | 28 +++++++++-- lib/chai/interface/assert.js | 90 ++++++++++++++++++++++++++++++++++++ test/assert.js | 41 ++++++++++++++++ test/expect.js | 42 +++++++++++++++++ test/should.js | 42 +++++++++++++++++ 5 files changed, 239 insertions(+), 4 deletions(-) diff --git a/lib/chai/core/assertions.js b/lib/chai/core/assertions.js index f7b5dbaed..dfafa8bcb 100644 --- a/lib/chai/core/assertions.js +++ b/lib/chai/core/assertions.js @@ -70,9 +70,13 @@ module.exports = function (chai, _) { /** * ### .deep * - * Sets the `deep` flag, later used by the `equal` assertion. + * Sets the `deep` flag, later used by the `equal`, `members`, and `property` + * assertions. * - * expect(foo).to.deep.equal({ bar: 'baz' }); + * 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 @@ -850,6 +854,13 @@ module.exports = function (chai, _) { * 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. * @@ -861,6 +872,11 @@ module.exports = function (chai, _) { * 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({ 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 `nested.property` * assertion, or traverse nested arrays. * @@ -900,6 +916,7 @@ module.exports = function (chai, _) { * expect(deepCss).to.have.nested.property('\\.link.\\[target\\]', 42); * * @name property + * @alias deep.property * @alias nested.property * @param {String} name * @param {Mixed} value (optional) @@ -913,7 +930,10 @@ module.exports = function (chai, _) { if (msg) flag(this, 'message', msg); var isNested = !!flag(this, 'nested') - , descriptor = isNested ? 'nested property ' : 'property ' + , isDeep = !!flag(this, 'deep') + , descriptor = (isDeep ? 'deep ' : '') + + (isNested ? 'nested ' : '') + + 'property ' , negate = flag(this, 'negate') , obj = flag(this, 'object') , pathInfo = isNested ? _.getPathInfo(name, obj) : null @@ -938,7 +958,7 @@ module.exports = function (chai, _) { if (arguments.length > 1) { this.assert( - hasProperty && 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 1e2951448..a9d96e6c5 100644 --- a/lib/chai/interface/assert.js +++ b/lib/chai/interface/assert.js @@ -1024,6 +1024,50 @@ module.exports = function (chai, util) { 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 a value given + * by `value`. Uses a deep equality check. + * + * assert.deepPropertyVal({ tea: { green: 'matcha' } }, 'tea', { green: 'matcha' }); + * + * @name deepPropertyVal + * @param {Object} object + * @param {String} property + * @param {Mixed} value + * @param {String} message + * @namespace Assert + * @api public + */ + + assert.deepPropertyVal = function (obj, prop, val, msg) { + new Assertion(obj, msg).to.have.deep.property(prop, val); + }; + + /** + * ### .notDeepPropertyVal(object, property, value, [message]) + * + * Asserts that `object` does _not_ have a property named by `property` with + * value given by `value`. Uses a deep equality check. + * + * 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 notDeepPropertyVal + * @param {Object} object + * @param {String} property + * @param {Mixed} value + * @param {String} message + * @namespace Assert + * @api public + */ + + assert.notDeepPropertyVal = function (obj, prop, val, msg) { + new Assertion(obj, msg).to.not.have.deep.property(prop, val); + }; + /** * ### .nestedPropertyVal(object, property, value, [message]) * @@ -1069,6 +1113,52 @@ module.exports = function (chai, util) { 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]) * diff --git a/test/assert.js b/test/assert.js index a9c74645c..defd4b0b9 100644 --- a/test/assert.js +++ b/test/assert.js @@ -947,6 +947,7 @@ describe('assert', function () { assert.notProperty(obj, 'foo.bar'); 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'); @@ -989,6 +990,46 @@ describe('assert', function () { }, "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() { ['throws', 'throw', 'Throw'].forEach(function (throws) { assert[throws](function() { throw new Error('foo'); }); diff --git a/test/expect.js b/test/expect.js index 4f0105334..4298879e1 100644 --- a/test/expect.js +++ b/test/expect.js @@ -534,6 +534,7 @@ describe('expect', function () { 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' } @@ -589,6 +590,26 @@ describe('expect', function () { }, "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}}; + 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'); @@ -596,6 +617,7 @@ describe('expect', function () { .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' } }) @@ -607,6 +629,26 @@ describe('expect', function () { }, "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(){ expect('test').to.have.ownProperty('length'); expect('test').to.haveOwnProperty('length'); diff --git a/test/should.js b/test/should.js index e369b29ae..9f7dc4985 100644 --- a/test/should.js +++ b/test/should.js @@ -457,6 +457,7 @@ describe('should', function() { ({ 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'); @@ -471,10 +472,31 @@ describe('should', function() { }, "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'); @@ -484,6 +506,26 @@ describe('should', function() { }, "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');