Skip to content

Commit

Permalink
Deep flag support for keys assertion
Browse files Browse the repository at this point in the history
  • Loading branch information
lucasfcosta committed Sep 15, 2016
1 parent dfbcf8f commit 2f4a242
Show file tree
Hide file tree
Showing 5 changed files with 344 additions and 6 deletions.
24 changes: 21 additions & 3 deletions lib/chai/core/assertions.js
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ module.exports = function (chai, _) {
/**
* ### .deep
*
* Sets the `deep` flag, later used by the `equal`, `include`, `members`, and
* Sets the `deep` flag, later used by the `equal`, `include`, `members`, `keys` and
* `property` assertions.
*
* const obj = {a: 1};
Expand Down Expand Up @@ -1328,6 +1328,10 @@ module.exports = function (chai, _) {
* can have objects as keys, so, in this case, you can pass multiple objects as arguments if
* you want to.
*
* The default behavior when it comes to Maps and Sets is to use strict equality
* (===) to compare values. You can also use the `deep` flag if you want a deep comparison
* instead of a strict comparison.
*
* expect({ foo: 1, bar: 2 }).to.have.any.keys('foo', 'baz');
* expect({ foo: 1, bar: 2 }).to.have.any.keys('foo');
* expect({ foo: 1, bar: 2 }).to.contain.any.keys('bar', 'baz');
Expand All @@ -1338,9 +1342,13 @@ module.exports = function (chai, _) {
* expect({ foo: 1, bar: 2, baz: 3 }).to.contain.all.keys(['bar', 'foo']);
* expect({ foo: 1, bar: 2, baz: 3 }).to.contain.all.keys({'bar': 6});
* expect(new Map([[{objKey: 'value'}, 'value'], [1, 2]])).to.contain.key({objKey: 'value'});
* expect(new Map([[{objKey: 'value'}, 'value'], [1, 2]])).to.contain.deep.key({objKey: 'value'});
* expect(new Map([[{objKey: 'value'}, 'value'], [1, 2]])).to.contain.any.keys([{objKey: 'value'}, {anotherKey: 'anotherValue'}]);
* expect(new Map([[{objKey: 'value'}, 'value'], [1, 2]])).to.contain.any.deep.keys([{objKey: 'value'}, {anotherKey: 'anotherValue'}]);
* expect(new Map([['firstKey', 'firstValue'], [1, 2]])).to.contain.all.keys('firstKey', 1);
* expect(new Map([['firstKey', 'firstValue'], [1, 2]])).to.contain.all.deep.keys('firstKey', 1);
* expect(new Set([['foo', 'bar'], ['example', 1]])).to.have.any.keys('foo');
* expect(new Set([['foo', 'bar'], ['example', 1]])).to.have.any.deep.keys('foo');
*
* @name keys
* @alias key
Expand All @@ -1351,11 +1359,13 @@ module.exports = function (chai, _) {

function assertKeys (keys) {
var obj = flag(this, 'object')
, isDeep = flag(this, 'deep')
, str
, ok = true
, mixedArgsMsg = 'when testing keys against an object or an array you must give a single Array|Object|String argument or multiple String arguments';

if (_.type(obj) === 'map' || _.type(obj) === 'set') {
str = isDeep ? 'deeply ' : '';
actual = [];

// Map and Set '.keys' aren't supported in IE 11. Therefore, use .forEach.
Expand Down Expand Up @@ -1402,7 +1412,11 @@ module.exports = function (chai, _) {
if (any) {
ok = expected.some(function(expectedKey) {
return actual.some(function(actualKey) {
return expectedKey === actualKey;
if (isDeep) {
return _.eql(expectedKey, actualKey);
} else {
return expectedKey === actualKey;
}
});
});
}
Expand All @@ -1411,7 +1425,11 @@ module.exports = function (chai, _) {
if (all) {
ok = expected.every(function(expectedKey) {
return actual.some(function(actualKey) {
return expectedKey === actualKey;
if (isDeep) {
return _.eql(expectedKey, actualKey);
} else {
return expectedKey === actualKey;
}
});
});

Expand Down
152 changes: 152 additions & 0 deletions lib/chai/interface/assert.js
Original file line number Diff line number Diff line change
Expand Up @@ -1544,6 +1544,158 @@ module.exports = function (chai, util) {
}

/**
* ### .hasAnyDeepKeys(object, [keys], [message])
*
* Asserts that `object` has at least one of the `keys` provided.
* Since Sets and Maps can have objects as keys you can use this assertion to perform
* a deep comparison.
* You can also provide a single object instead of a `keys` array and its keys
* will be used as the expected set of keys.
*
* assert.hasAnyDeepKeys(new Map([[{one: 'one'}, 'valueOne'], [1, 2]]), {one: 'one'});
* assert.hasAnyDeepKeys(new Map([[{one: 'one'}, 'valueOne'], [1, 2]]), [{one: 'one'}, {two: 'two'}]);
* assert.hasAnyDeepKeys(new Map([[{one: 'one'}, 'valueOne'], [{two: 'two'}, 'valueTwo']]), [{one: 'one'}, {two: 'two'}]);
* assert.hasAnyDeepKeys(new Set([{one: 'one'}, {two: 'two'}]), {one: 'one'});
* assert.hasAnyDeepKeys(new Set([{one: 'one'}, {two: 'two'}]), [{one: 'one'}, {three: 'three'}]);
* assert.hasAnyDeepKeys(new Set([{one: 'one'}, {two: 'two'}]), [{one: 'one'}, {two: 'two'}]);
*
* @name doesNotHaveAllKeys
* @param {Mixed} object
* @param {...String|Array|Object} keys
* @param {String} message
* @namespace Assert
* @api public
*/

assert.hasAnyDeepKeys = function (obj, keys, msg) {
if (keys === undefined) {
new Assertion(obj, msg).to.have.any.deep.keys();
}

new Assertion(obj, msg).to.have.any.deep.keys(keys);
}

/**
* ### .hasAllDeepKeys(object, [keys], [message])
*
* Asserts that `object` has all and only all of the `keys` provided.
* Since Sets and Maps can have objects as keys you can use this assertion to perform
* a deep comparison.
* You can also provide a single object instead of a `keys` array and its keys
* will be used as the expected set of keys.
*
* assert.hasAllDeepKeys(new Map([[{one: 'one'}, 'valueOne']]), {one: 'one'});
* assert.hasAllDeepKeys(new Map([[{one: 'one'}, 'valueOne'], [{two: 'two'}, 'valueTwo']]), [{one: 'one'}, {two: 'two'}]);
* assert.hasAllDeepKeys(new Set([{one: 'one'}]), {one: 'one'});
* assert.hasAllDeepKeys(new Set([{one: 'one'}, {two: 'two'}]), [{one: 'one'}, {two: 'two'}]);
*
* @name hasAllDeepKeys
* @param {Mixed} object
* @param {...String|Array|Object} keys
* @param {String} message
* @namespace Assert
* @api public
*/

assert.hasAllDeepKeys = function (obj, keys, msg) {
if (keys === undefined) {
new Assertion(obj, msg).to.have.all.deep.keys();
}

new Assertion(obj, msg).to.have.all.deep.keys(keys);
}

/**
* ### .containsAllDeepKeys(object, [keys], [message])
*
* Asserts that `object` contains all of the `keys` provided.
* Since Sets and Maps can have objects as keys you can use this assertion to perform
* a deep comparison.
* You can also provide a single object instead of a `keys` array and its keys
* will be used as the expected set of keys.
*
* assert.containsAllDeepKeys(new Map([[{one: 'one'}, 'valueOne'], [1, 2]]), {one: 'one'});
* assert.containsAllDeepKeys(new Map([[{one: 'one'}, 'valueOne'], [{two: 'two'}, 'valueTwo']]), [{one: 'one'}, {two: 'two'}]);
* assert.containsAllDeepKeys(new Set([{one: 'one'}, {two: 'two'}]), {one: 'one'});
* assert.containsAllDeepKeys(new Set([{one: 'one'}, {two: 'two'}]), [{one: 'one'}, {two: 'two'}]);
*
* @name containsAllDeepKeys
* @param {Mixed} object
* @param {...String|Array|Object} keys
* @param {String} message
* @namespace Assert
* @api public
*/

assert.containsAllDeepKeys = function (obj, keys, msg) {
if (keys === undefined) {
new Assertion(obj, msg).to.contain.all.deep.keys();
}

new Assertion(obj, msg).to.contain.all.deep.keys(keys);
}

/**
* ### .doesNotHaveAnyDeepKeys(object, [keys], [message])
*
* Asserts that `object` has none of the `keys` provided.
* Since Sets and Maps can have objects as keys you can use this assertion to perform
* a deep comparison.
* You can also provide a single object instead of a `keys` array and its keys
* will be used as the expected set of keys.
*
* assert.doesNotHaveAnyDeepKeys(new Map([[{one: 'one'}, 'valueOne'], [1, 2]]), {thisDoesNot: 'exist'});
* assert.doesNotHaveAnyDeepKeys(new Map([[{one: 'one'}, 'valueOne'], [{two: 'two'}, 'valueTwo']]), [{twenty: 'twenty'}, {fifty: 'fifty'}]);
* assert.doesNotHaveAnyDeepKeys(new Set([{one: 'one'}, {two: 'two'}]), {twenty: 'twenty'});
* assert.doesNotHaveAnyDeepKeys(new Set([{one: 'one'}, {two: 'two'}]), [{twenty: 'twenty'}, {fifty: 'fifty'}]);
*
* @name doesNotHaveAnyDeepKeys
* @param {Mixed} object
* @param {...String|Array|Object} keys
* @param {String} message
* @namespace Assert
* @api public
*/

assert.doesNotHaveAnyDeepKeys = function (obj, keys, msg) {
if (keys === undefined) {
new Assertion(obj, msg).to.not.have.any.deep.keys();
}

new Assertion(obj, msg).to.not.have.any.deep.keys(keys);
}

/**
* ### .doesNotHaveAllDeepKeys(object, [keys], [message])
*
* Asserts that `object` does not have at least one of the `keys` provided.
* Since Sets and Maps can have objects as keys you can use this assertion to perform
* a deep comparison.
* You can also provide a single object instead of a `keys` array and its keys
* will be used as the expected set of keys.
*
* assert.doesNotHaveAllDeepKeys(new Map([[{one: 'one'}, 'valueOne'], [1, 2]]), {thisDoesNot: 'exist'});
* assert.doesNotHaveAllDeepKeys(new Map([[{one: 'one'}, 'valueOne'], [{two: 'two'}, 'valueTwo']]), [{twenty: 'twenty'}, {one: 'one'}]);
* assert.doesNotHaveAllDeepKeys(new Set([{one: 'one'}, {two: 'two'}]), {twenty: 'twenty'});
* assert.doesNotHaveAllDeepKeys(new Set([{one: 'one'}, {two: 'two'}]), [{one: 'one'}, {fifty: 'fifty'}]);
*
* @name doesNotHaveAllDeepKeys
* @param {Mixed} object
* @param {...String|Array|Object} keys
* @param {String} message
* @namespace Assert
* @api public
*/

assert.doesNotHaveAllDeepKeys = function (obj, keys, msg) {
if (keys === undefined) {
new Assertion(obj, msg).to.not.have.all.deep.keys();
}

new Assertion(obj, msg).to.not.have.all.deep.keys(keys);
}

/**
* ### .throws(fn, [errorLike/string/regexp], [string/regexp], [message])
*
* If `errorLike` is an `Error` constructor, asserts that `fn` will throw an error that is an
Expand Down
56 changes: 53 additions & 3 deletions test/assert.js
Original file line number Diff line number Diff line change
Expand Up @@ -747,6 +747,22 @@ describe('assert', function () {
assert.doesNotHaveAnyKeys(testMap, [ 'thisDoesNotExist', 'thisToo', {iDoNot: 'exist'} ]);
assert.doesNotHaveAllKeys(testMap, [ aKey, {iDoNot: 'exist'} ]);

// Tests for the deep variations of the keys assertion
assert.hasAnyDeepKeys(testMap, {thisIs: 'anExampleObject'});
assert.hasAnyDeepKeys(testMap, [{thisIs: 'anExampleObject'}, {three: 'three'}]);
assert.hasAnyDeepKeys(testMap, [{thisIs: 'anExampleObject'}, {doingThisBecauseOf: 'referential equality'}]);

assert.hasAllDeepKeys(testMap, [{thisIs: 'anExampleObject'}, {doingThisBecauseOf: 'referential equality'}]);

assert.containsAllDeepKeys(testMap, {thisIs: 'anExampleObject'});
assert.containsAllDeepKeys(testMap, [{thisIs: 'anExampleObject'}, {doingThisBecauseOf: 'referential equality'}]);

assert.doesNotHaveAnyDeepKeys(testMap, {thisDoesNot: 'exist'});
assert.doesNotHaveAnyDeepKeys(testMap, [{twenty: 'twenty'}, {fifty: 'fifty'}]);

assert.doesNotHaveAllDeepKeys(testMap, {thisDoesNot: 'exist'});
assert.doesNotHaveAllDeepKeys(testMap, [{twenty: 'twenty'}, {thisIs: 'anExampleObject'}]);

var weirdMapKey1 = Object.create(null)
, weirdMapKey2 = {toString: NaN}
, weirdMapKey3 = []
Expand Down Expand Up @@ -778,7 +794,7 @@ describe('assert', function () {
var errMap = new Map();

errMap.set({1: 20}, 'number');

err(function(){
assert.hasAllKeys(errMap);
}, "keys required");
Expand Down Expand Up @@ -818,6 +834,16 @@ describe('assert', function () {
err(function(){
assert.doesNotHaveAnyKeys(errMap, []);
}, "keys required");

// Uncomment this after solving https://github.com/chaijs/chai/issues/662
// This should fail because of referential equality (this is a strict comparison)
// err(function(){
// assert.containsAllKeys(new Map([[{foo: 1}, 'bar']]), { foo: 1 });
// }, 'expected [ [ { foo: 1 }, 'bar' ] ] to contain key { foo: 1 }');

// err(function(){
// assert.containsAllDeepKeys(new Map([[{foo: 1}, 'bar']]), { iDoNotExist: 0 })
// }, 'expected [ { foo: 1 } ] to deeply contain key { iDoNotExist: 0 }');
}

if (typeof Set !== 'undefined') {
Expand Down Expand Up @@ -877,6 +903,22 @@ describe('assert', function () {
errSet.add({1: 20});
errSet.add('number');

// Tests for the deep variations of the keys assertion
assert.hasAnyDeepKeys(testSet, {thisIs: 'anExampleObject'});
assert.hasAnyDeepKeys(testSet, [{thisIs: 'anExampleObject'}, {three: 'three'}]);
assert.hasAnyDeepKeys(testSet, [{thisIs: 'anExampleObject'}, {doingThisBecauseOf: 'referential equality'}]);

assert.hasAllDeepKeys(testSet, [{thisIs: 'anExampleObject'}, {doingThisBecauseOf: 'referential equality'}]);

assert.containsAllDeepKeys(testSet, {thisIs: 'anExampleObject'});
assert.containsAllDeepKeys(testSet, [{thisIs: 'anExampleObject'}, {doingThisBecauseOf: 'referential equality'}]);

assert.doesNotHaveAnyDeepKeys(testSet, {twenty: 'twenty'});
assert.doesNotHaveAnyDeepKeys(testSet, [{twenty: 'twenty'}, {fifty: 'fifty'}]);

assert.doesNotHaveAllDeepKeys(testSet, {twenty: 'twenty'});
assert.doesNotHaveAllDeepKeys(testSet, [{thisIs: 'anExampleObject'}, {fifty: 'fifty'}]);

err(function(){
assert.hasAllKeys(errSet);
}, "keys required");
Expand Down Expand Up @@ -916,6 +958,16 @@ describe('assert', function () {
err(function(){
assert.doesNotHaveAnyKeys(errSet, []);
}, "keys required");

// Uncomment this after solving https://github.com/chaijs/chai/issues/662
// This should fail because of referential equality (this is a strict comparison)
// err(function(){
// assert.containsAllKeys(new Set([{foo: 1}]), { foo: 1 });
// }, 'expected [ [ { foo: 1 }, 'bar' ] ] to contain key { foo: 1 }');

// err(function(){
// assert.containsAllDeepKeys(new Set([{foo: 1}]), { iDoNotExist: 0 })
// }, 'expected [ { foo: 1 } ] to deeply contain key { iDoNotExist: 0 }');
}

err(function(){
Expand Down Expand Up @@ -1023,7 +1075,6 @@ describe('assert', function () {
assert.doesNotHaveAllKeys({ foo: 1, bar: 2 }, { 'foo': 1, 'bar': 1});
}, "expected { foo: 1, bar: 2 } to not have keys 'foo', and 'bar'");


err(function() {
assert.hasAnyKeys({ foo: 1 }, 'baz');
}, "expected { foo: 1 } to have key 'baz'");
Expand All @@ -1035,7 +1086,6 @@ describe('assert', function () {
err(function(){
assert.doesNotHaveAnyKeys({ foo: 1, bar: 2 }, { 'foo': 1, 'baz': 1});
}, "expected { foo: 1, bar: 2 } to not have keys 'foo', or 'baz'");

});

it('lengthOf', function() {
Expand Down

0 comments on commit 2f4a242

Please sign in to comment.