Skip to content

Commit

Permalink
Breaking: Fix .include to always use strict equality
Browse files Browse the repository at this point in the history
- Previously, `.include` was using strict equality for non-negated property
inclusion, but deep equality for negated property inclusion and array
inclusion. This fix causes `.include` to always use strict equality.
  • Loading branch information
meeber committed Jul 27, 2016
1 parent a5a90cb commit 10d0a4d
Show file tree
Hide file tree
Showing 5 changed files with 163 additions and 40 deletions.
64 changes: 46 additions & 18 deletions lib/chai/core/assertions.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

module.exports = function (chai, _) {
var Assertion = chai.Assertion
, AssertionError = chai.AssertionError
, toString = Object.prototype.toString
, flag = _.flag;

Expand Down Expand Up @@ -221,6 +222,17 @@ 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.
*
* 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:
Expand All @@ -246,28 +258,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));
}
Expand Down
36 changes: 28 additions & 8 deletions lib/chai/interface/assert.js
Original file line number Diff line number Diff line change
Expand Up @@ -826,11 +826,21 @@ module.exports = function (chai, util) {
/**
* ### .include(haystack, needle, [message])
*
* Asserts that `haystack` includes `needle`. Works
* for strings and arrays.
* 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('foobar', 'bar', 'foobar contains string "bar"');
* assert.include([ 1, 2, 3 ], 3, 'array contains value');
* 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.
*
* 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
Expand All @@ -847,11 +857,21 @@ module.exports = function (chai, util) {
/**
* ### .notInclude(haystack, needle, [message])
*
* Asserts that `haystack` does not include `needle`. Works
* for strings and arrays.
* 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.
*
* assert.notInclude('foobar', 'baz', 'string not include substring');
* assert.notInclude([ 1, 2, 3 ], 4, 'array not include contain value');
* 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
Expand Down
33 changes: 32 additions & 1 deletion test/assert.js
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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");
Expand All @@ -492,13 +505,31 @@ 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()
, sym3 = Symbol();
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");
Expand Down
39 changes: 28 additions & 11 deletions test/expect.js
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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);
Expand Down
31 changes: 29 additions & 2 deletions test/should.js
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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");
Expand Down

0 comments on commit 10d0a4d

Please sign in to comment.