diff --git a/lib/chai/core/assertions.js b/lib/chai/core/assertions.js index dfafa8bcb..531220c9e 100644 --- a/lib/chai/core/assertions.js +++ b/lib/chai/core/assertions.js @@ -7,6 +7,7 @@ module.exports = function (chai, _) { var Assertion = chai.Assertion + , AssertionError = chai.AssertionError , toString = Object.prototype.toString , flag = _.flag; @@ -221,6 +222,22 @@ module.exports = function (chai, _) { * expect([1,2,3]).to.include(2); * expect('foobar').to.contain('foo'); * expect({ foo: 'bar', hello: 'universe' }).to.include({ foo: 'bar' }); + * + * By default, strict equality (===) is used. When asserting the inclusion of + * a value in an array, the array is searched for an element that's strictly + * equal to the given value. When asserting a subset of properties in an + * object, the object is searched for the given property keys, checking that + * each one is present and stricty equal to the given property value. For + * instance: + * + * var obj1 = {a: 1} + * , obj2 = {b: 2}; + * expect([obj1, obj2]).to.include(obj1); + * expect([obj1, obj2]).to.not.include({a: 1}); + * expect({foo: obj1, bar: obj2}).to.include({foo: obj1}); + * expect({foo: obj1, bar: obj2}).to.include({foo: obj1, bar: obj2}); + * expect({foo: obj1, bar: obj2}).to.not.include({foo: {a: 1}}); + * expect({foo: obj1, bar: obj2}).to.not.include({foo: obj1, bar: {b: 2}}); * * These assertions can also be used as property based language chains, * enabling the `contains` flag for the `keys` assertion. For instance: @@ -246,28 +263,44 @@ module.exports = function (chai, _) { if (msg) flag(this, 'message', msg); var obj = flag(this, 'object'); - var expected = false; - if (_.type(obj) === 'array' && _.type(val) === 'object') { - for (var i in obj) { - if (_.eql(obj[i], val)) { - expected = true; - break; + // This block is for asserting a subset of properties in an object. + if (_.type(obj) === 'object') { + var props = Object.keys(val) + , negate = flag(this, 'negate') + , firstErr = null + , numErrs = 0; + + props.forEach(function (prop) { + var propAssertion = new Assertion(obj); + _.transferFlags(this, propAssertion, false); + + if (!negate || props.length === 1) { + propAssertion.property(prop, val[prop]); + return; } - } - } else if (_.type(val) === 'object') { - if (!flag(this, 'negate')) { - for (var k in val) new Assertion(obj).property(k, val[k]); - return; - } - var subset = {}; - for (var k in val) subset[k] = obj[k]; - expected = _.eql(subset, val); - } else { - expected = (obj != undefined) && ~obj.indexOf(val); + + try { + propAssertion.property(prop, val[prop]); + } catch (err) { + if (!_.checkError.compatibleConstructor(err, AssertionError)) throw err; + if (firstErr === null) firstErr = err; + numErrs++; + } + }, this); + + // When validating .not.include with multiple properties, we only want + // to throw an assertion error if all of the properties are included, + // in which case we throw the first property assertion error that we + // encountered. + if (negate && props.length > 1 && numErrs === props.length) throw firstErr; + + return; } + + // Assert inclusion in an array or substring in a string. this.assert( - expected + typeof obj !== "undefined" && typeof obj !== "null" && ~obj.indexOf(val) , 'expected #{this} to include ' + _.inspect(val) , 'expected #{this} to not include ' + _.inspect(val)); } diff --git a/lib/chai/interface/assert.js b/lib/chai/interface/assert.js index a9d96e6c5..3f780501a 100644 --- a/lib/chai/interface/assert.js +++ b/lib/chai/interface/assert.js @@ -826,11 +826,25 @@ module.exports = function (chai, util) { /** * ### .include(haystack, needle, [message]) * - * Asserts that `haystack` includes `needle`. Works - * for strings and arrays. - * - * assert.include('foobar', 'bar', 'foobar contains string "bar"'); - * assert.include([ 1, 2, 3 ], 3, 'array contains value'); + * Asserts that `haystack` includes `needle`. Can be used to assert the + * inclusion of a value in an array, a substring in a string, or a subset of + * properties in an object. + * + * assert.include([1,2,3], 2, 'array contains value'); + * assert.include('foobar', 'foo', 'string contains substring'); + * assert.include({ foo: 'bar', hello: 'universe' }, { foo: 'bar' }, 'object contains property'); + * + * Strict equality (===) is used. When asserting the inclusion of a value in + * an array, the array is searched for an element that's strictly equal to the + * given value. When asserting a subset of properties in an object, the object + * is searched for the given property keys, checking that each one is present + * and stricty equal to the given property value. For instance: + * + * var obj1 = {a: 1} + * , obj2 = {b: 2}; + * assert.include([obj1, obj2], obj1); + * assert.include({foo: obj1, bar: obj2}, {foo: obj1}); + * assert.include({foo: obj1, bar: obj2}, {foo: obj1, bar: obj2}); * * @name include * @param {Array|String} haystack @@ -847,11 +861,26 @@ module.exports = function (chai, util) { /** * ### .notInclude(haystack, needle, [message]) * - * Asserts that `haystack` does not include `needle`. Works - * for strings and arrays. - * - * assert.notInclude('foobar', 'baz', 'string not include substring'); - * assert.notInclude([ 1, 2, 3 ], 4, 'array not include contain value'); + * Asserts that `haystack` does not include `needle`. Can be used to assert + * the absence of a value in an array, a substring in a string, or a subset of + * properties in an object. + * + * assert.notInclude([1,2,3], 4, 'array doesn't contain value'); + * assert.notInclude('foobar', 'baz', 'string doesn't contain substring'); + * assert.notInclude({ foo: 'bar', hello: 'universe' }, { foo: 'baz' }, 'object doesn't contain property'); + * + * Strict equality (===) is used. When asserting the absence of a value in an + * array, the array is searched to confirm the absence of an element that's + * strictly equal to the given value. When asserting a subset of properties in + * an object, the object is searched to confirm that at least one of the given + * property keys is either not present or not strictly equal to the given + * property value. For instance: + * + * var obj1 = {a: 1} + * , obj2 = {b: 2}; + * assert.notInclude([obj1, obj2], {a: 1}); + * assert.notInclude({foo: obj1, bar: obj2}, {foo: {a: 1}}); + * assert.notInclude({foo: obj1, bar: obj2}, {foo: obj1, bar: {b: 2}}); * * @name notInclude * @param {Array|String} haystack diff --git a/test/assert.js b/test/assert.js index defd4b0b9..a9688931e 100644 --- a/test/assert.js +++ b/test/assert.js @@ -459,7 +459,12 @@ describe('assert', function () { assert.include('foobar', 'bar'); assert.include('', ''); assert.include([ 1, 2, 3], 3); - assert.include({a:1, b:2}, {b:2}); + + var obj1 = {a: 1} + , obj2 = {b: 2}; + assert.include([obj1, obj2], obj1); + assert.include({foo: obj1, bar: obj2}, {foo: obj1}); + assert.include({foo: obj1, bar: obj2}, {foo: obj1, bar: obj2}); if (typeof Symbol === 'function') { var sym1 = Symbol() @@ -471,6 +476,14 @@ describe('assert', function () { assert.include('foobar', 'baz'); }, "expected \'foobar\' to include \'baz\'"); + err(function () { + assert.include([{a: 1}, {b: 2}], {a: 1}); + }, "expected [ { a: 1 }, { b: 2 } ] to include { a: 1 }"); + + err(function () { + assert.include({foo: {a: 1}, bar: {b: 2}}, {foo: {a: 1}}); + }, "expected { foo: { a: 1 }, bar: { b: 2 } } to have a property 'foo' of { a: 1 }, but got { a: 1 }"); + err(function(){ assert.include(true, true); }, "object tested must be an array, an object, or a string, but boolean given"); @@ -492,6 +505,12 @@ describe('assert', function () { assert.notInclude('foobar', 'baz'); assert.notInclude([ 1, 2, 3 ], 4); + var obj1 = {a: 1} + , obj2 = {b: 2}; + assert.notInclude([obj1, obj2], {a: 1}); + assert.notInclude({foo: obj1, bar: obj2}, {foo: {a: 1}}); + assert.notInclude({foo: obj1, bar: obj2}, {foo: obj1, bar: {b: 2}}); + if (typeof Symbol === 'function') { var sym1 = Symbol() , sym2 = Symbol() @@ -499,6 +518,18 @@ describe('assert', function () { assert.notInclude([sym1, sym2], sym3); } + err(function () { + var obj1 = {a: 1} + , obj2 = {b: 2}; + assert.notInclude([obj1, obj2], obj1); + }, "expected [ { a: 1 }, { b: 2 } ] to not include { a: 1 }"); + + err(function () { + var obj1 = {a: 1} + , obj2 = {b: 2}; + assert.notInclude({foo: obj1, bar: obj2}, {foo: obj1, bar: obj2}); + }, "expected { foo: { a: 1 }, bar: { b: 2 } } to not have a property 'foo' of { a: 1 }"); + err(function(){ assert.notInclude(true, true); }, "object tested must be an array, an object, or a string, but boolean given"); diff --git a/test/expect.js b/test/expect.js index 4298879e1..29d91f2e7 100644 --- a/test/expect.js +++ b/test/expect.js @@ -722,14 +722,15 @@ describe('expect', function () { expect([1,2]).to.include(1); expect(['foo', 'bar']).to.not.include('baz'); expect(['foo', 'bar']).to.not.include(1); - expect({a:1,b:2}).to.include({b:2}); - expect({a:1,b:2}).to.not.include({b:3}); - expect({a:1,b:2}).to.include({a:1,b:2}); - expect({a:1,b:2}).to.not.include({a:1,c:2}); - expect([{a:1},{b:2}]).to.include({a:1}); - expect([{a:1}]).to.include({a:1}); - expect([{a:1}]).to.not.include({b:1}); + var obj1 = {a: 1} + , obj2 = {b: 2}; + expect([obj1, obj2]).to.include(obj1); + expect([obj1, obj2]).to.not.include({a: 1}); + expect({foo: obj1, bar: obj2}).to.include({foo: obj1}); + expect({foo: obj1, bar: obj2}).to.include({foo: obj1, bar: obj2}); + 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 (typeof Symbol === 'function') { var sym1 = Symbol() @@ -753,11 +754,27 @@ describe('expect', function () { err(function(){ expect({a:1,b:2}).to.not.include({b:2}); - }, "expected { a: 1, b: 2 } to not include { b: 2 }"); + }, "expected { a: 1, b: 2 } to not have a property 'b' of 2"); - err(function(){ - expect([{a:1},{b:2}]).to.not.include({b:2}); - }, "expected [ { a: 1 }, { b: 2 } ] to not include { b: 2 }"); + err(function () { + expect([{a: 1}, {b: 2}]).to.include({a: 1}); + }, "expected [ { a: 1 }, { b: 2 } ] to include { a: 1 }"); + + err(function () { + var obj1 = {a: 1} + , obj2 = {b: 2}; + expect([obj1, obj2]).to.not.include(obj1); + }, "expected [ { a: 1 }, { b: 2 } ] to not include { a: 1 }"); + + err(function () { + expect({foo: {a: 1}, bar: {b: 2}}).to.include({foo: {a: 1}}); + }, "expected { foo: { a: 1 }, bar: { b: 2 } } to have a property 'foo' of { a: 1 }, but got { a: 1 }"); + + err(function () { + var obj1 = {a: 1} + , obj2 = {b: 2}; + expect({foo: obj1, bar: obj2}).to.not.include({foo: obj1, bar: obj2}); + }, "expected { foo: { a: 1 }, bar: { b: 2 } } to not have a property 'foo' of { a: 1 }"); err(function(){ expect(true).to.include(true); diff --git a/test/should.js b/test/should.js index 9f7dc4985..345af8417 100644 --- a/test/should.js +++ b/test/should.js @@ -614,8 +614,15 @@ describe('should', function() { [1,2].should.include(1); ['foo', 'bar'].should.not.include('baz'); ['foo', 'bar'].should.not.include(1); - ({a:1,b:2}).should.include({b:2}); - ({a:1,b:2}).should.not.include({b:3}); + + var obj1 = {a: 1} + , obj2 = {b: 2}; + [obj1, obj2].should.include(obj1); + [obj1, obj2].should.not.include({a: 1}); + ({foo: obj1, bar: obj2}).should.include({foo: obj1}); + ({foo: obj1, bar: obj2}).should.include({foo: obj1, bar: obj2}); + ({foo: obj1, bar: obj2}).should.not.include({foo: {a: 1}}); + ({foo: obj1, bar: obj2}).should.not.include({foo: obj1, bar: {b: 2}}); if (typeof Symbol === 'function') { var sym1 = Symbol() @@ -637,6 +644,26 @@ describe('should', function() { ({a:1}).should.include({b:2}); }, "expected { a: 1 } to have a property 'b'"); + err(function () { + [{a: 1}, {b: 2}].should.include({a: 1}); + }, "expected [ { a: 1 }, { b: 2 } ] to include { a: 1 }"); + + err(function () { + var obj1 = {a: 1} + , obj2 = {b: 2}; + [obj1, obj2].should.not.include(obj1); + }, "expected [ { a: 1 }, { b: 2 } ] to not include { a: 1 }"); + + err(function () { + ({foo: {a: 1}, bar: {b: 2}}).should.include({foo: {a: 1}}); + }, "expected { foo: { a: 1 }, bar: { b: 2 } } to have a property 'foo' of { a: 1 }, but got { a: 1 }"); + + err(function () { + var obj1 = {a: 1} + , obj2 = {b: 2}; + ({foo: obj1, bar: obj2}).should.not.include({foo: obj1, bar: obj2}); + }, "expected { foo: { a: 1 }, bar: { b: 2 } } to not have a property 'foo' of { a: 1 }"); + err(function(){ (true).should.include(true); }, "object tested must be an array, an object, or a string, but boolean given");