From 2eddd790026ac152fe18ff2de4ff2c06d41f178c Mon Sep 17 00:00:00 2001 From: Aleksey Shvayka Date: Sat, 24 Jun 2017 01:35:07 +0300 Subject: [PATCH] Add ES6 collection support to include() (#994) * fix error messages tests * add tests * add implementation * performance tweaks * add tests for SameValueZero * drop weakmap support * update docs --- lib/chai/core/assertions.js | 91 +++++++++++++++++++++++++------ test/assert.js | 104 +++++++++++++++++++++++++++++++++--- test/expect.js | 80 +++++++++++++++++++++++---- test/should.js | 70 ++++++++++++++++++++++-- 4 files changed, 309 insertions(+), 36 deletions(-) diff --git a/lib/chai/core/assertions.js b/lib/chai/core/assertions.js index 1f6adfe72..09d0a6f28 100644 --- a/lib/chai/core/assertions.js +++ b/lib/chai/core/assertions.js @@ -330,6 +330,16 @@ module.exports = function (chai, _) { * * expect({a: 1, b: 2, c: 3}).to.include({a: 1, b: 2}); * + * When the target is a Set or WeakSet, `.include` asserts that the given `val` is a + * member of the target. SameValueZero equality algorithm is used. + * + * expect(new Set([1, 2])).to.include(2); + * + * When the target is a Map, `.include` asserts that the given `val` is one of + * the values of the target. SameValueZero equality algorithm is used. + * + * expect(new Map([['a', 1], ['b', 2]])).to.include(2); + * * Because `.include` does different things based on the target's type, it's * important to check the target's type before using `.include`. See the `.a` * doc for info on testing a target's type. @@ -338,8 +348,8 @@ module.exports = function (chai, _) { * * By default, strict (`===`) equality is used to compare array members and * object properties. Add `.deep` earlier in the chain to use deep equality - * instead. See the `deep-eql` project page for info on the deep equality - * algorithm: https://github.com/chaijs/deep-eql. + * instead (WeakSet targets are not supported). See the `deep-eql` project + * page for info on the deep equality algorithm: https://github.com/chaijs/deep-eql. * * // Target array deeply (but not strictly) includes `{a: 1}` * expect([{a: 1}]).to.deep.include({a: 1}); @@ -449,25 +459,24 @@ module.exports = function (chai, _) { * @api public */ - function includeChainingBehavior () { - flag(this, 'contains', true); + function SameValueZero(a, b) { + return (_.isNaN(a) && _.isNaN(b)) || a === b; } - function isDeepIncluded (arr, val) { - return arr.some(function (arrVal) { - return _.eql(arrVal, val); - }); + function includeChainingBehavior () { + flag(this, 'contains', true); } function include (val, msg) { if (msg) flag(this, 'message', msg); - _.expectTypes(this, ['array', 'object', 'string']); + _.expectTypes(this, [ + 'array', 'object', 'string', + 'map', 'set', 'weakset', + ]); var obj = flag(this, 'object') - , objType = _.type(obj).toLowerCase() - , isDeep = flag(this, 'deep') - , descriptor = isDeep ? 'deep ' : ''; + , objType = _.type(obj).toLowerCase(); // This block is for asserting a subset of properties in an object. if (objType === 'object') { @@ -504,10 +513,62 @@ module.exports = function (chai, _) { return; } - // Assert inclusion in an array or substring in a string. + var isDeep = flag(this, 'deep') + , descriptor = isDeep ? 'deep ' : '' + , included = false; + + switch (objType) { + case 'string': + included = obj.indexOf(val) !== -1; + break; + + case 'weakset': + if (isDeep) { + var flagMsg = flag(this, 'message') + , ssfi = flag(this, 'ssfi'); + flagMsg = flagMsg ? flagMsg + ': ' : ''; + + throw new AssertionError( + flagMsg + 'unable to use .deep.include with WeakSet', + undefined, + ssfi + ); + } + + included = obj.has(val); + break; + + case 'map': + var isEql = isDeep ? _.eql : SameValueZero; + obj.forEach(function (item) { + included = included || isEql(item, val); + }); + break; + + case 'set': + if (isDeep) { + obj.forEach(function (item) { + included = included || _.eql(item, val); + }); + } else { + included = obj.has(val); + } + break; + + case 'array': + if (isDeep) { + included = obj.some(function (item) { + return _.eql(item, val); + }) + } else { + included = obj.indexOf(val) !== -1; + } + break; + } + + // Assert inclusion in collection or substring in a string. this.assert( - objType === 'string' || !isDeep ? ~obj.indexOf(val) - : isDeepIncluded(obj, val) + included , 'expected #{this} to ' + descriptor + 'include ' + _.inspect(val) , 'expected #{this} to not ' + descriptor + 'include ' + _.inspect(val)); } diff --git a/test/assert.js b/test/assert.js index 87f7588a9..3e3b51f1f 100644 --- a/test/assert.js +++ b/test/assert.js @@ -633,6 +633,42 @@ describe('assert', function () { assert.include({foo: obj1, bar: obj2}, {foo: obj1}); assert.include({foo: obj1, bar: obj2}, {foo: obj1, bar: obj2}); + if (typeof Map === 'function') { + var map = new Map(); + var val = [{a: 1}]; + map.set('a', val); + map.set('b', 2); + map.set('c', -0); + map.set('d', NaN); + + assert.include(map, val); + assert.include(map, 2); + assert.include(map, 0); + assert.include(map, NaN); + } + + if (typeof Set === 'function') { + var set = new Set(); + var val = [{a: 1}]; + set.add(val); + set.add(2); + set.add(-0); + set.add(NaN); + + assert.include(set, val); + assert.include(set, 2); + assert.include(set, 0); + assert.include(set, NaN); + } + + if (typeof WeakSet === 'function') { + var ws = new WeakSet(); + var val = [{a: 1}]; + ws.add(val); + + assert.include(ws, val); + } + if (typeof Symbol === 'function') { var sym1 = Symbol() , sym2 = Symbol(); @@ -653,19 +689,19 @@ describe('assert', function () { err(function(){ assert.include(true, true, 'blah'); - }, "blah: object tested must be an array, an object, or a string, but boolean given"); + }, "blah: object tested must be an array, a map, an object, a set, a string, or a weakset, but boolean given"); err(function () { assert.include(42, 'bar'); - }, "object tested must be an array, an object, or a string, but number given"); + }, "object tested must be an array, a map, an object, a set, a string, or a weakset, but number given"); err(function(){ assert.include(null, 42); - }, "object tested must be an array, an object, or a string, but null given"); + }, "object tested must be an array, a map, an object, a set, a string, or a weakset, but null given"); err(function () { assert.include(undefined, 'bar'); - }, "object tested must be an array, an object, or a string, but undefined given"); + }, "object tested must be an array, a map, an object, a set, a string, or a weakset, but undefined given"); }); it('notInclude', function () { @@ -678,6 +714,38 @@ describe('assert', function () { assert.notInclude({foo: obj1, bar: obj2}, {foo: {a: 1}}); assert.notInclude({foo: obj1, bar: obj2}, {foo: obj1, bar: {b: 2}}); + if (typeof Map === 'function') { + var map = new Map(); + var val = [{a: 1}]; + map.set('a', val); + map.set('b', 2); + + assert.notInclude(map, [{a: 1}]); + assert.notInclude(map, 3); + } + + if (typeof Set === 'function') { + var set = new Set(); + var val = [{a: 1}]; + set.add(val); + set.add(2); + + assert.include(set, val); + assert.include(set, 2); + + assert.notInclude(set, [{a: 1}]); + assert.notInclude(set, 3); + } + + if (typeof WeakSet === 'function') { + var ws = new WeakSet(); + var val = [{a: 1}]; + ws.add(val); + + assert.notInclude(ws, [{a: 1}]); + assert.notInclude(ws, {}); + } + if (typeof Symbol === 'function') { var sym1 = Symbol() , sym2 = Symbol() @@ -699,19 +767,19 @@ describe('assert', function () { err(function(){ assert.notInclude(true, true, 'blah'); - }, "blah: object tested must be an array, an object, or a string, but boolean given"); + }, "blah: object tested must be an array, a map, an object, a set, a string, or a weakset, but boolean given"); err(function () { assert.notInclude(42, 'bar'); - }, "object tested must be an array, an object, or a string, but number given"); + }, "object tested must be an array, a map, an object, a set, a string, or a weakset, but number given"); err(function(){ assert.notInclude(null, 42); - }, "object tested must be an array, an object, or a string, but null given"); + }, "object tested must be an array, a map, an object, a set, a string, or a weakset, but null given"); err(function () { assert.notInclude(undefined, 'bar'); - }, "object tested must be an array, an object, or a string, but undefined given"); + }, "object tested must be an array, a map, an object, a set, a string, or a weakset, but undefined given"); err(function () { assert.notInclude('foobar', 'bar'); @@ -731,6 +799,26 @@ describe('assert', function () { assert.notDeepInclude({foo: obj1, bar: obj2}, {baz: {a: 1}}); assert.notDeepInclude({foo: obj1, bar: obj2}, {foo: {a: 1}, bar: {b: 9}}); + if (typeof Map === 'function') { + var map = new Map(); + map.set(1, [{a: 1}]); + + assert.deepInclude(map, [{a: 1}]); + } + + if (typeof Set === 'function') { + var set = new Set(); + set.add([{a: 1}]); + + assert.deepInclude(set, [{a: 1}]); + } + + if (typeof WeakSet === 'function') { + err(function() { + assert.deepInclude(new WeakSet(), {}, 'foo'); + }, 'foo: unable to use .deep.include with WeakSet'); + } + err(function () { assert.deepInclude([obj1, obj2], {a: 9}, 'blah'); }, "blah: expected [ { a: 1 }, { b: 2 } ] to deep include { a: 9 }"); diff --git a/test/expect.js b/test/expect.js index dddb27a3d..44e098f2d 100644 --- a/test/expect.js +++ b/test/expect.js @@ -1882,6 +1882,48 @@ describe('expect', function () { 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 Map === 'function') { + var map = new Map(); + var val = [{a: 1}]; + map.set('a', val); + map.set('b', 2); + map.set('c', -0); + map.set('d', NaN); + + expect(map).to.include(val); + expect(map).to.not.include([{a: 1}]); + expect(map).to.include(2); + expect(map).to.not.include(3); + expect(map).to.include(0); + expect(map).to.include(NaN); + } + + if (typeof Set === 'function') { + var set = new Set(); + var val = [{a: 1}]; + set.add(val); + set.add(2); + set.add(-0); + set.add(NaN); + + expect(set).to.include(val); + expect(set).to.not.include([{a: 1}]); + expect(set).to.include(2); + expect(set).to.not.include(3); + expect(set).to.include(0); + expect(set).to.include(NaN); + } + + if (typeof WeakSet === 'function') { + var ws = new WeakSet(); + var val = [{a: 1}]; + ws.add(val); + + expect(ws).to.include(val); + expect(ws).to.not.include([{a: 1}]); + expect(ws).to.not.include({}); + } + if (typeof Symbol === 'function') { var sym1 = Symbol() , sym2 = Symbol() @@ -1936,39 +1978,39 @@ describe('expect', function () { err(function(){ expect(true).to.include(true, 'blah'); - }, "blah: object tested must be an array, an object, or a string, but boolean given"); + }, "blah: object tested must be an array, a map, an object, a set, a string, or a weakset, but boolean given"); err(function(){ expect(true, 'blah').to.include(true); - }, "blah: object tested must be an array, an object, or a string, but boolean given"); + }, "blah: object tested must be an array, a map, an object, a set, a string, or a weakset, but boolean given"); err(function(){ expect(42.0).to.include(42); - }, "object tested must be an array, an object, or a string, but number given"); + }, "object tested must be an array, a map, an object, a set, a string, or a weakset, but number given"); err(function(){ expect(null).to.include(42); - }, "object tested must be an array, an object, or a string, but null given"); + }, "object tested must be an array, a map, an object, a set, a string, or a weakset, but null given"); err(function(){ expect(undefined).to.include(42); - }, "object tested must be an array, an object, or a string, but undefined given"); + }, "object tested must be an array, a map, an object, a set, a string, or a weakset, but undefined given"); err(function(){ expect(true).to.not.include(true); - }, "object tested must be an array, an object, or a string, but boolean given"); + }, "object tested must be an array, a map, an object, a set, a string, or a weakset, but boolean given"); err(function(){ expect(42.0).to.not.include(42); - }, "object tested must be an array, an object, or a string, but number given"); + }, "object tested must be an array, a map, an object, a set, a string, or a weakset, but number given"); err(function(){ expect(null).to.not.include(42); - }, "object tested must be an array, an object, or a string, but null given"); + }, "object tested must be an array, a map, an object, a set, a string, or a weakset, but null given"); err(function(){ expect(undefined).to.not.include(42); - }, "object tested must be an array, an object, or a string, but undefined given"); + }, "object tested must be an array, a map, an object, a set, a string, or a weakset, but undefined given"); }); it('deep.include()', function () { @@ -1984,6 +2026,26 @@ describe('expect', function () { expect({foo: obj1, bar: obj2}).to.not.deep.include({baz: {a: 1}}); expect({foo: obj1, bar: obj2}).to.not.deep.include({foo: {a: 1}, bar: {b: 9}}); + if (typeof Map === 'function') { + var map = new Map(); + map.set(1, [{a: 1}]); + + expect(map).to.deep.include([{a: 1}]); + } + + if (typeof Set === 'function') { + var set = new Set(); + set.add([{a: 1}]); + + expect(set).to.deep.include([{a: 1}]); + } + + if (typeof WeakSet === 'function') { + err(function() { + expect(new WeakSet()).to.deep.include({}, 'foo'); + }, 'foo: unable to use .deep.include with WeakSet'); + } + err(function () { expect([obj1, obj2]).to.deep.include({a: 9}, 'blah'); }, "blah: expected [ { a: 1 }, { b: 2 } ] to deep include { a: 9 }"); diff --git a/test/should.js b/test/should.js index e26a642d2..080bf5bf7 100644 --- a/test/should.js +++ b/test/should.js @@ -1540,6 +1540,48 @@ describe('should', function() { ({foo: obj1, bar: obj2}).should.not.include({foo: {a: 1}}); ({foo: obj1, bar: obj2}).should.not.include({foo: obj1, bar: {b: 2}}); + if (typeof Map === 'function') { + var map = new Map(); + var val = [{a: 1}]; + map.set('a', val); + map.set('b', 2); + map.set('c', -0); + map.set('d', NaN); + + map.should.include(val); + map.should.not.include([{a: 1}]); + map.should.include(2); + map.should.not.include(3); + map.should.include(0); + map.should.include(NaN); + } + + if (typeof Set === 'function') { + var set = new Set(); + var val = [{a: 1}]; + set.add(val); + set.add(2); + set.add(-0); + set.add(NaN); + + set.should.include(val); + set.should.not.include([{a: 1}]); + set.should.include(2); + set.should.not.include(3); + set.should.include(0); + set.should.include(NaN); + } + + if (typeof WeakSet === 'function') { + var ws = new WeakSet(); + var val = [{a: 1}]; + ws.add(val); + + ws.should.include(val); + ws.should.not.include([{a: 1}]); + ws.should.not.include({}); + } + if (typeof Symbol === 'function') { var sym1 = Symbol() , sym2 = Symbol() @@ -1582,19 +1624,19 @@ describe('should', function() { err(function(){ (true).should.include(true, 'blah'); - }, "blah: object tested must be an array, an object, or a string, but boolean given"); + }, "blah: object tested must be an array, a map, an object, a set, a string, or a weakset, but boolean given"); err(function(){ (42).should.include(4); - }, "object tested must be an array, an object, or a string, but number given"); + }, "object tested must be an array, a map, an object, a set, a string, or a weakset, but number given"); err(function(){ (true).should.not.include(true); - }, "object tested must be an array, an object, or a string, but boolean given"); + }, "object tested must be an array, a map, an object, a set, a string, or a weakset, but boolean given"); err(function(){ (42).should.not.include(4); - }, "object tested must be an array, an object, or a string, but number given"); + }, "object tested must be an array, a map, an object, a set, a string, or a weakset, but number given"); }); it('deep.include()', function () { @@ -1610,6 +1652,26 @@ describe('should', function() { ({foo: obj1, bar: obj2}).should.not.deep.include({baz: {a: 1}}); ({foo: obj1, bar: obj2}).should.not.deep.include({foo: {a: 1}, bar: {b: 9}}); + if (typeof Map === 'function') { + var map = new Map(); + + map.set(1, [{a: 1}]); + map.should.deep.include([{a: 1}]); + } + + if (typeof Set === 'function') { + var set = new Set(); + + set.add([{a: 1}]); + set.should.deep.include([{a: 1}]); + } + + if (typeof WeakSet === 'function') { + err(function() { + new WeakSet().should.deep.include({}, 'foo'); + }, 'foo: unable to use .deep.include with WeakSet'); + } + err(function () { [obj1, obj2].should.deep.include({a: 9}, 'blah'); }, "blah: expected [ { a: 1 }, { b: 2 } ] to deep include { a: 9 }");