Skip to content

Commit

Permalink
Merge pull request #758 from meeber/deep-property
Browse files Browse the repository at this point in the history
Add `.deep.property` for deep equality comparisons
  • Loading branch information
lucasfcosta authored Aug 15, 2016
2 parents 0662b7b + a5a90cb commit dcd5524
Show file tree
Hide file tree
Showing 5 changed files with 239 additions and 4 deletions.
28 changes: 24 additions & 4 deletions lib/chai/core/assertions.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
*
Expand All @@ -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.
*
Expand Down Expand Up @@ -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)
Expand All @@ -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
Expand All @@ -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
Expand Down
90 changes: 90 additions & 0 deletions lib/chai/interface/assert.js
Original file line number Diff line number Diff line change
Expand Up @@ -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])
*
Expand Down Expand Up @@ -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])
*
Expand Down
41 changes: 41 additions & 0 deletions test/assert.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down Expand Up @@ -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'); });
Expand Down
42 changes: 42 additions & 0 deletions test/expect.js
Original file line number Diff line number Diff line change
Expand Up @@ -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' }
Expand Down Expand Up @@ -589,13 +590,34 @@ 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');
expect({ 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' } })
Expand All @@ -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');
Expand Down
42 changes: 42 additions & 0 deletions test/should.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand All @@ -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');
Expand All @@ -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');
Expand Down

0 comments on commit dcd5524

Please sign in to comment.