Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add .deep.include for deep equality comparisons #761

Merged
merged 3 commits into from
Aug 16, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
126 changes: 101 additions & 25 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 @@ -70,9 +71,15 @@ module.exports = function (chai, _) {
/**
* ### .deep
*
* Sets the `deep` flag, later used by the `equal` assertion.
* Sets the `deep` flag, later used by the `equal`, `include`, `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.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});
*
* @name deep
* @namespace BDD
Expand Down Expand Up @@ -217,6 +224,30 @@ 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}});
*
* 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 @@ -227,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 @@ -237,35 +272,60 @@ 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 expected = false;
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') {
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;
}

if (_.type(obj) === 'array' && _.type(val) === 'object') {
for (var i in obj) {
if (_.eql(obj[i], val)) {
expected = true;
break;
try {
propAssertion.property(prop, val[prop]);
} catch (err) {
if (!_.checkError.compatibleConstructor(err, AssertionError)) throw err;
if (firstErr === null) firstErr = err;
numErrs++;
}
}
} 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);
}, 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
, '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 Expand Up @@ -850,6 +910,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 +928,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 +972,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 +986,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 +1014,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