diff --git a/lib/chai/core/assertions.js b/lib/chai/core/assertions.js index 055f6b697..42d88d273 100644 --- a/lib/chai/core/assertions.js +++ b/lib/chai/core/assertions.js @@ -82,6 +82,41 @@ module.exports = function (chai, _) { flag(this, 'deep', true); }); + /** + * ### .any + * + * Sets the `any` flag, (opposite of the `all` flag) + * later used in the `keys` assertion. + * + * expect(foo).to.have.any.keys('bar', 'baz'); + * + * @name any + * @api public + */ + + Assertion.addProperty('any', function () { + flag(this, 'any', true); + flag(this, 'all', false) + }); + + + /** + * ### .all + * + * Sets the `all` flag (opposite of the `any` flag) + * later used by the `keys` assertion. + * + * expect(foo).to.have.all.keys('bar', 'baz'); + * + * @name all + * @api public + */ + + Assertion.addProperty('all', function () { + flag(this, 'all', true); + flag(this, 'any', false); + }); + /** * ### .a(type) * @@ -930,12 +965,30 @@ module.exports = function (chai, _) { /** * ### .keys(key1, [key2], [...]) * - * Asserts that the target has exactly the given keys, or - * asserts the inclusion of some keys when using the - * `include` or `contain` modifiers. - * - * expect({ foo: 1, bar: 2 }).to.have.keys(['foo', 'bar']); - * expect({ foo: 1, bar: 2, baz: 3 }).to.contain.keys('foo', 'bar'); + * Asserts that the target contains any or all of the passed-in keys. + * Use in combination with `any`, `all`, `contains`, or `have` will affect + * what will pass. + * + * When used in conjunction with `any`, at least one key that is passed + * in must exist in the target object. This is regardless whether or not + * the `have` or `contain` qualifiers are used. Note, either `any` or `all` + * should be used in the assertion. If neither are used, the assertion is + * defaulted to `all`. + * + * When both `all` and `contain` are used, the target object must have at + * least all of the passed-in keys but may have more keys not listed. + * + * When both `all` and `have` are used, the target object must both contain + * all of the passed-in keys AND the number of keys in the target object must + * match the number of keys passed in (in other words, a target object must + * have all and only all of the passed-in keys). + * + * 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'); + * expect({ foo: 1, bar: 2 }).to.contain.any.keys(['foo']); + * expect({ foo: 1, bar: 2 }).to.have.all.keys(['bar', 'foo']); + * expect({ foo: 1, bar: 2, baz: 3 }).to.contain.all.keys(['bar', 'foo']); * * @name keys * @alias key @@ -956,16 +1009,30 @@ module.exports = function (chai, _) { var actual = Object.keys(obj) , expected = keys - , len = keys.length; + , len = keys.length + , any = flag(this, 'any') + , all = flag(this, 'all'); - // Inclusion - ok = keys.every(function(key){ - return ~actual.indexOf(key); - }); + if (!any && !all) { + all = true; + } - // Strict - if (!flag(this, 'negate') && !flag(this, 'contains')) { - ok = ok && keys.length == actual.length; + // Has any + if (any) { + var intersection = expected.filter(function(key) { + return ~actual.indexOf(key); + }); + ok = intersection.length > 0; + } + + // Has all + if (all) { + ok = keys.every(function(key){ + return ~actual.indexOf(key); + }); + if (!flag(this, 'negate') && !flag(this, 'contains')) { + ok = ok && keys.length == actual.length; + } } // Key string @@ -974,7 +1041,12 @@ module.exports = function (chai, _) { return _.inspect(key); }); var last = keys.pop(); - str = keys.join(', ') + ', and ' + last; + if (all) { + str = keys.join(', ') + ', and ' + last; + } + if (any) { + str = keys.join(', ') + ', or ' + last; + } } else { str = _.inspect(keys[0]); } diff --git a/test/expect.js b/test/expect.js index 6282027c4..810088387 100644 --- a/test/expect.js +++ b/test/expect.js @@ -581,6 +581,7 @@ describe('expect', function () { expect({ foo: 1, bar: 2 }).to.contain.keys(['foo']); expect({ foo: 1, bar: 2 }).to.contain.keys(['bar']); expect({ foo: 1, bar: 2 }).to.contain.keys(['bar', 'foo']); + expect({ foo: 1, bar: 2, baz: 3 }).to.contain.all.keys(['bar', 'foo']); expect({ foo: 1, bar: 2 }).to.not.have.keys('baz'); expect({ foo: 1, bar: 2 }).to.not.have.keys('foo', 'baz'); @@ -588,6 +589,19 @@ describe('expect', function () { expect({ foo: 1, bar: 2 }).to.not.contain.keys('foo', 'baz'); expect({ foo: 1, bar: 2 }).to.not.contain.keys('baz', 'foo'); + 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'); + expect({ foo: 1, bar: 2 }).to.contain.any.keys(['foo']); + expect({ foo: 1, bar: 2 }).to.have.all.keys(['bar', 'foo']); + expect({ foo: 1, bar: 2 }).to.contain.all.keys(['bar', 'foo']); + + expect({ foo: 1, bar: 2 }).to.not.have.any.keys('baz', 'abc', 'def'); + expect({ foo: 1, bar: 2 }).to.not.have.any.keys('baz'); + expect({ foo: 1, bar: 2 }).to.not.contain.any.keys('baz'); + expect({ foo: 1, bar: 2 }).to.not.have.all.keys(['baz', 'foo']); + expect({ foo: 1, bar: 2 }).to.not.contain.all.keys(['baz', 'foo']); + err(function(){ expect({ foo: 1 }).to.have.keys(); }, "keys required"); @@ -627,6 +641,10 @@ describe('expect', function () { err(function(){ expect({ foo: 1, bar: 2 }).to.not.have.keys(['foo', 'bar']); }, "expected { foo: 1, bar: 2 } to not have keys 'foo', and 'bar'"); + + err(function(){ + expect({ foo: 1, bar: 2 }).to.have.all.keys('foo'); + }, "expected { foo: 1, bar: 2 } to have key 'foo'"); err(function(){ expect({ foo: 1 }).to.not.contain.keys(['foo']); @@ -635,6 +653,19 @@ describe('expect', function () { err(function(){ expect({ foo: 1 }).to.contain.keys('foo', 'bar'); }, "expected { foo: 1 } to contain keys 'foo', and 'bar'"); + + err(function() { + expect({ foo: 1 }).to.have.any.keys('baz'); + }, "expected { foo: 1 } to have key 'baz'"); + + err(function(){ + expect({ foo: 1, bar: 2 }).to.not.have.all.keys(['foo', 'bar']); + }, "expected { foo: 1, bar: 2 } to not have keys 'foo', and 'bar'"); + + err(function(){ + expect({ foo: 1, bar: 2 }).to.not.have.any.keys(['foo', 'baz']); + }, "expected { foo: 1, bar: 2 } to not have keys 'foo', or 'baz'"); + }); it('chaining', function(){ diff --git a/test/should.js b/test/should.js index 5b3d24e06..1ba704dde 100644 --- a/test/should.js +++ b/test/should.js @@ -451,6 +451,19 @@ describe('should', function() { ({ foo: 1, bar: 2 }).should.not.contain.keys('foo', 'baz'); ({ foo: 1, bar: 2 }).should.not.contain.keys('baz', 'foo'); + ({ foo: 1, bar: 2 }).should.have.any.keys('foo', 'baz'); + ({ foo: 1, bar: 2 }).should.have.any.keys('foo'); + ({ foo: 1, bar: 2 }).should.contain.any.keys('bar', 'baz'); + ({ foo: 1, bar: 2 }).should.contain.any.keys(['foo']); + ({ foo: 1, bar: 2 }).should.have.all.keys(['bar', 'foo']); + ({ foo: 1, bar: 2 }).should.contain.all.keys(['bar', 'foo']); + + ({ foo: 1, bar: 2 }).should.not.have.any.keys('baz', 'abc', 'def'); + ({ foo: 1, bar: 2 }).should.not.have.any.keys('baz'); + ({ foo: 1, bar: 2 }).should.not.contain.any.keys('baz'); + ({ foo: 1, bar: 2 }).should.not.have.all.keys(['baz', 'foo']); + ({ foo: 1, bar: 2 }).should.not.contain.all.keys(['baz', 'foo']); + err(function(){ ({ foo: 1 }).should.have.keys(); }, "keys required"); @@ -498,6 +511,18 @@ describe('should', function() { err(function(){ ({ foo: 1 }).should.contain.keys('foo', 'bar'); }, "expected { foo: 1 } to contain keys 'foo', and 'bar'"); + + err(function() { + ({ foo: 1 }).should.have.any.keys('baz'); + }, "expected { foo: 1 } to have key 'baz'"); + + err(function(){ + ({ foo: 1, bar: 2 }).should.not.have.all.keys(['foo', 'bar']); + }, "expected { foo: 1, bar: 2 } to not have keys 'foo', and 'bar'"); + + err(function(){ + ({ foo: 1, bar: 2 }).should.not.have.any.keys(['foo', 'baz']); + }, "expected { foo: 1, bar: 2 } to not have keys 'foo', or 'baz'"); }); it('throw', function () {