diff --git a/lib/chai/core/assertions.js b/lib/chai/core/assertions.js index 4806e2118..a8991827a 100644 --- a/lib/chai/core/assertions.js +++ b/lib/chai/core/assertions.js @@ -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}); * @@ -234,6 +236,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. + * + * 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: * @@ -243,6 +253,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 @@ -253,11 +267,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') { @@ -295,9 +317,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); diff --git a/lib/chai/interface/assert.js b/lib/chai/interface/assert.js index 25d882dd5..f983251b1 100644 --- a/lib/chai/interface/assert.js +++ b/lib/chai/interface/assert.js @@ -885,6 +885,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]) * diff --git a/test/assert.js b/test/assert.js index a9688931e..a569ef81a 100644 --- a/test/assert.js +++ b/test/assert.js @@ -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' ]); diff --git a/test/expect.js b/test/expect.js index 29d91f2e7..4c34b0ee0 100644 --- a/test/expect.js +++ b/test/expect.js @@ -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 }); diff --git a/test/should.js b/test/should.js index 345af8417..68579c52b 100644 --- a/test/should.js +++ b/test/should.js @@ -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 });