Skip to content

Commit

Permalink
Add .deep.include for deep equality comparisons
Browse files Browse the repository at this point in the history
  • Loading branch information
meeber committed Aug 14, 2016
1 parent c7413af commit 96866ac
Show file tree
Hide file tree
Showing 5 changed files with 169 additions and 6 deletions.
35 changes: 29 additions & 6 deletions lib/chai/core/assertions.js
Original file line number Diff line number Diff line change
Expand Up @@ -71,11 +71,13 @@ module.exports = function (chai, _) {
/**
* ### .deep
*
* Sets the `deep` flag, later used by the `equal`, `members`, and `property`
* assertions.
* Sets the `deep` flag, later used by the `equal`, `include`, `members`, and
* `property` assertions.
*
* const obj = {a: 1};
* expect(obj).to.deep.equal({a: 1});
* expect([obj]).to.deep.include({a:1});
* expect({foo: obj}).to.deep.include({foo: {a:1}});
* expect([obj]).to.have.deep.members([{a: 1}]);
* expect({foo: obj}).to.have.deep.property('foo', {a: 1});
*
Expand Down Expand Up @@ -239,6 +241,14 @@ module.exports = function (chai, _) {
* expect({foo: obj1, bar: obj2}).to.not.include({foo: {a: 1}});
* expect({foo: obj1, bar: obj2}).to.not.include({foo: obj1, bar: {b: 2}});
*
* If the `deep` flag is set, deep equality is used instead. For instance:
*
* var obj1 = {a: 1}
* , obj2 = {b: 2};
* expect([obj1, obj2]).to.deep.include({a: 1});
* expect({foo: obj1, bar: obj2}).to.deep.include({foo: {a: 1}});
* expect({foo: obj1, bar: obj2}).to.deep.include({foo: {a: 1}, bar: {b: 2}});
*
* These assertions can also be used as property based language chains,
* enabling the `contains` flag for the `keys` assertion. For instance:
*
Expand All @@ -248,6 +258,10 @@ module.exports = function (chai, _) {
* @alias contain
* @alias includes
* @alias contains
* @alias deep.include
* @alias deep.contain
* @alias deep.includes
* @alias deep.contains
* @param {Object|String|Number} obj
* @param {String} message _optional_
* @namespace BDD
Expand All @@ -258,11 +272,19 @@ module.exports = function (chai, _) {
flag(this, 'contains', true);
}

function isDeepIncluded (arr, val) {
return arr.some(function (arrVal) {
return _.eql(arrVal, val);
});
}

function include (val, msg) {
_.expectTypes(this, ['array', 'object', 'string']);

if (msg) flag(this, 'message', msg);
var obj = flag(this, 'object');
var obj = flag(this, 'object')
, isDeep = flag(this, 'deep')
, descriptor = isDeep ? 'deep ' : '';

// This block is for asserting a subset of properties in an object.
if (_.type(obj) === 'object') {
Expand Down Expand Up @@ -300,9 +322,10 @@ module.exports = function (chai, _) {

// Assert inclusion in an array or substring in a string.
this.assert(
typeof obj !== "undefined" && typeof obj !== "null" && ~obj.indexOf(val)
, 'expected #{this} to include ' + _.inspect(val)
, 'expected #{this} to not include ' + _.inspect(val));
typeof obj === 'string' || !isDeep ? ~obj.indexOf(val)
: isDeepIncluded(obj, val)
, 'expected #{this} to ' + descriptor + 'include ' + _.inspect(val)
, 'expected #{this} to not ' + descriptor + 'include ' + _.inspect(val));
}

Assertion.addChainableMethod('include', include, includeChainingBehavior);
Expand Down
50 changes: 50 additions & 0 deletions lib/chai/interface/assert.js
Original file line number Diff line number Diff line change
Expand Up @@ -894,6 +894,56 @@ module.exports = function (chai, util) {
new Assertion(exp, msg, assert.notInclude).not.include(inc);
};

/**
* ### .deepInclude(haystack, needle, [message])
*
* Asserts that `haystack` includes `needle`. Can be used to assert the
* inclusion of a value in an array or a subset of properties in an object.
* Deep equality is used.
*
* var obj1 = {a: 1}
* , obj2 = {b: 2};
* assert.deepInclude([obj1, obj2], {a: 1});
* assert.deepInclude({foo: obj1, bar: obj2}, {foo: {a: 1}});
* assert.deepInclude({foo: obj1, bar: obj2}, {foo: {a: 1}, bar: {b: 2}});
*
* @name deepInclude
* @param {Array|String} haystack
* @param {Mixed} needle
* @param {String} message
* @namespace Assert
* @api public
*/

assert.deepInclude = function (exp, inc, msg) {
new Assertion(exp, msg, assert.include).deep.include(inc);
};

/**
* ### .notDeepInclude(haystack, needle, [message])
*
* Asserts that `haystack` does not include `needle`. Can be used to assert
* the absence of a value in an array or a subset of properties in an object.
* Deep equality is used.
*
* var obj1 = {a: 1}
* , obj2 = {b: 2};
* assert.notDeepInclude([obj1, obj2], {a: 9});
* assert.notDeepInclude({foo: obj1, bar: obj2}, {foo: {a: 9}});
* assert.notDeepInclude({foo: obj1, bar: obj2}, {foo: {a: 1}, bar: {b: 9}});
*
* @name notDeepInclude
* @param {Array|String} haystack
* @param {Mixed} needle
* @param {String} message
* @namespace Assert
* @api public
*/

assert.notDeepInclude = function (exp, inc, msg) {
new Assertion(exp, msg, assert.notInclude).not.deep.include(inc);
};

/**
* ### .match(value, regexp, [message])
*
Expand Down
30 changes: 30 additions & 0 deletions test/assert.js
Original file line number Diff line number Diff line change
Expand Up @@ -551,6 +551,36 @@ describe('assert', function () {
}, "expected \'foobar\' to not include \'bar\'");
});

it('deepInclude and notDeepInclude', function () {
var obj1 = {a: 1}
, obj2 = {b: 2};
assert.deepInclude([obj1, obj2], {a: 1});
assert.notDeepInclude([obj1, obj2], {a: 9});
assert.notDeepInclude([obj1, obj2], {z: 1});
assert.deepInclude({foo: obj1, bar: obj2}, {foo: {a: 1}});
assert.deepInclude({foo: obj1, bar: obj2}, {foo: {a: 1}, bar: {b: 2}});
assert.notDeepInclude({foo: obj1, bar: obj2}, {foo: {a: 9}});
assert.notDeepInclude({foo: obj1, bar: obj2}, {foo: {z: 1}});
assert.notDeepInclude({foo: obj1, bar: obj2}, {baz: {a: 1}});
assert.notDeepInclude({foo: obj1, bar: obj2}, {foo: {a: 1}, bar: {b: 9}});

err(function () {
assert.deepInclude([obj1, obj2], {a: 9});
}, "expected [ { a: 1 }, { b: 2 } ] to deep include { a: 9 }");

err(function () {
assert.notDeepInclude([obj1, obj2], {a: 1});
}, "expected [ { a: 1 }, { b: 2 } ] to not deep include { a: 1 }");

err(function () {
assert.deepInclude({foo: obj1, bar: obj2}, {foo: {a: 1}, bar: {b: 9}});
}, "expected { foo: { a: 1 }, bar: { b: 2 } } to have a deep property 'bar' of { b: 9 }, but got { b: 2 }");

err(function () {
assert.notDeepInclude({foo: obj1, bar: obj2}, {foo: {a: 1}, bar: {b: 2}});
}, "expected { foo: { a: 1 }, bar: { b: 2 } } to not have a deep property 'foo' of { a: 1 }");
});

it('keys(array|Object|arguments)', function(){
assert.hasAllKeys({ foo: 1 }, [ 'foo' ]);
assert.hasAllKeys({ foo: 1, bar: 2 }, [ 'foo', 'bar' ]);
Expand Down
30 changes: 30 additions & 0 deletions test/expect.js
Original file line number Diff line number Diff line change
Expand Up @@ -809,6 +809,36 @@ describe('expect', function () {
}, "object tested must be an array, an object, or a string, but undefined given");
});

it('deep.include()', function () {
var obj1 = {a: 1}
, obj2 = {b: 2};
expect([obj1, obj2]).to.deep.include({a: 1});
expect([obj1, obj2]).to.not.deep.include({a: 9});
expect([obj1, obj2]).to.not.deep.include({z: 1});
expect({foo: obj1, bar: obj2}).to.deep.include({foo: {a: 1}});
expect({foo: obj1, bar: obj2}).to.deep.include({foo: {a: 1}, bar: {b: 2}});
expect({foo: obj1, bar: obj2}).to.not.deep.include({foo: {a: 9}});
expect({foo: obj1, bar: obj2}).to.not.deep.include({foo: {z: 1}});
expect({foo: obj1, bar: obj2}).to.not.deep.include({baz: {a: 1}});
expect({foo: obj1, bar: obj2}).to.not.deep.include({foo: {a: 1}, bar: {b: 9}});

err(function () {
expect([obj1, obj2]).to.deep.include({a: 9});
}, "expected [ { a: 1 }, { b: 2 } ] to deep include { a: 9 }");

err(function () {
expect([obj1, obj2]).to.not.deep.include({a: 1});
}, "expected [ { a: 1 }, { b: 2 } ] to not deep include { a: 1 }");

err(function () {
expect({foo: obj1, bar: obj2}).to.deep.include({foo: {a: 1}, bar: {b: 9}});
}, "expected { foo: { a: 1 }, bar: { b: 2 } } to have a deep property 'bar' of { b: 9 }, but got { b: 2 }");

err(function () {
expect({foo: obj1, bar: obj2}).to.not.deep.include({foo: {a: 1}, bar: {b: 2}});
}, "expected { foo: { a: 1 }, bar: { b: 2 } } to not have a deep property 'foo' of { a: 1 }");
});

it('keys(array|Object|arguments)', function(){
expect({ foo: 1 }).to.have.keys(['foo']);
expect({ foo: 1 }).have.keys({ 'foo': 6 });
Expand Down
30 changes: 30 additions & 0 deletions test/should.js
Original file line number Diff line number Diff line change
Expand Up @@ -681,6 +681,36 @@ describe('should', function() {
}, "object tested must be an array, an object, or a string, but number given");
});

it('deep.include()', function () {
var obj1 = {a: 1}
, obj2 = {b: 2};
[obj1, obj2].should.deep.include({a: 1});
[obj1, obj2].should.not.deep.include({a: 9});
[obj1, obj2].should.not.deep.include({z: 1});
({foo: obj1, bar: obj2}).should.deep.include({foo: {a: 1}});
({foo: obj1, bar: obj2}).should.deep.include({foo: {a: 1}, bar: {b: 2}});
({foo: obj1, bar: obj2}).should.not.deep.include({foo: {a: 9}});
({foo: obj1, bar: obj2}).should.not.deep.include({foo: {z: 1}});
({foo: obj1, bar: obj2}).should.not.deep.include({baz: {a: 1}});
({foo: obj1, bar: obj2}).should.not.deep.include({foo: {a: 1}, bar: {b: 9}});

err(function () {
[obj1, obj2].should.deep.include({a: 9});
}, "expected [ { a: 1 }, { b: 2 } ] to deep include { a: 9 }");

err(function () {
[obj1, obj2].should.not.deep.include({a: 1});
}, "expected [ { a: 1 }, { b: 2 } ] to not deep include { a: 1 }");

err(function () {
({foo: obj1, bar: obj2}).should.deep.include({foo: {a: 1}, bar: {b: 9}});
}, "expected { foo: { a: 1 }, bar: { b: 2 } } to have a deep property 'bar' of { b: 9 }, but got { b: 2 }");

err(function () {
({foo: obj1, bar: obj2}).should.not.deep.include({foo: {a: 1}, bar: {b: 2}});
}, "expected { foo: { a: 1 }, bar: { b: 2 } } to not have a deep property 'foo' of { a: 1 }");
});

it('keys(array|Object|arguments)', function(){
({ foo: 1 }).should.have.keys(['foo']);
({ foo: 1 }).should.have.keys({ 'foo': 6 });
Expand Down

0 comments on commit 96866ac

Please sign in to comment.