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

Breaking: Fix .include to always use strict equality #760

Merged
merged 2 commits into from
Aug 15, 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
97 changes: 75 additions & 22 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,13 @@ module.exports = function (chai, _) {
/**
* ### .deep
*
* Sets the `deep` flag, later used by the `equal` assertion.
* Sets the `deep` flag, later used by the `equal`, `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.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 +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:
Expand All @@ -242,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));
}
Expand Down Expand Up @@ -850,6 +887,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 +905,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 +949,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 +963,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 +991,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
137 changes: 128 additions & 9 deletions lib/chai/interface/assert.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.
* 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:
*
* 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 Expand Up @@ -1024,6 +1053,50 @@ module.exports = function (chai, util) {
new Assertion(obj, msg).to.not.have.property(prop, val);
};

/**
* ### .deepPropertyVal(object, property, value, [message])
*
* Asserts that `object` has a property named by `property` with a value given
* by `value`. Uses a deep equality check.
*
* assert.deepPropertyVal({ tea: { green: 'matcha' } }, 'tea', { green: 'matcha' });
*
* @name deepPropertyVal
* @param {Object} object
* @param {String} property
* @param {Mixed} value
* @param {String} message
* @namespace Assert
* @api public
*/

assert.deepPropertyVal = function (obj, prop, val, msg) {
new Assertion(obj, msg).to.have.deep.property(prop, val);
};

/**
* ### .notDeepPropertyVal(object, property, value, [message])
*
* Asserts that `object` does _not_ have a property named by `property` with
* value given by `value`. Uses a deep equality check.
*
* assert.notDeepPropertyVal({ tea: { green: 'matcha' } }, 'tea', { black: 'matcha' });
* assert.notDeepPropertyVal({ tea: { green: 'matcha' } }, 'tea', { green: 'oolong' });
* assert.notDeepPropertyVal({ tea: { green: 'matcha' } }, 'coffee', { green: 'matcha' });
*
* @name notDeepPropertyVal
* @param {Object} object
* @param {String} property
* @param {Mixed} value
* @param {String} message
* @namespace Assert
* @api public
*/

assert.notDeepPropertyVal = function (obj, prop, val, msg) {
new Assertion(obj, msg).to.not.have.deep.property(prop, val);
};

/**
* ### .nestedPropertyVal(object, property, value, [message])
*
Expand Down Expand Up @@ -1069,6 +1142,52 @@ module.exports = function (chai, util) {
new Assertion(obj, msg).to.not.have.nested.property(prop, val);
};

/**
* ### .deepNestedPropertyVal(object, property, value, [message])
*
* Asserts that `object` has a property named by `property` with a value given
* by `value`. `property` can use dot- and bracket-notation for nested
* reference. Uses a deep equality check.
*
* assert.deepNestedPropertyVal({ tea: { green: { matcha: 'yum' } } }, 'tea.green', { matcha: 'yum' });
*
* @name deepNestedPropertyVal
* @param {Object} object
* @param {String} property
* @param {Mixed} value
* @param {String} message
* @namespace Assert
* @api public
*/

assert.deepNestedPropertyVal = function (obj, prop, val, msg) {
new Assertion(obj, msg).to.have.deep.nested.property(prop, val);
};

/**
* ### .notDeepNestedPropertyVal(object, property, value, [message])
*
* Asserts that `object` does _not_ have a property named by `property` with
* value given by `value`. `property` can use dot- and bracket-notation for
* nested reference. Uses a deep equality check.
*
* assert.notDeepNestedPropertyVal({ tea: { green: { matcha: 'yum' } } }, 'tea.green', { oolong: 'yum' });
* assert.notDeepNestedPropertyVal({ tea: { green: { matcha: 'yum' } } }, 'tea.green', { matcha: 'yuck' });
* assert.notDeepNestedPropertyVal({ tea: { green: { matcha: 'yum' } } }, 'tea.black', { matcha: 'yum' });
*
* @name notDeepNestedPropertyVal
* @param {Object} object
* @param {String} property
* @param {Mixed} value
* @param {String} message
* @namespace Assert
* @api public
*/

assert.notDeepNestedPropertyVal = function (obj, prop, val, msg) {
new Assertion(obj, msg).to.not.have.deep.nested.property(prop, val);
}

/**
* ### .lengthOf(object, length, [message])
*
Expand Down