From c0e6dd7cdbce9bcb78fbd4a2d8fc1c9cc72fdbcc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristj=C3=A1n=20Oddsson?= Date: Sun, 28 Jan 2024 17:02:28 +0100 Subject: [PATCH 1/4] Implement `iterator` assertion --- lib/chai/core/assertions.js | 41 ++++++++++++++++++++++++++++++++++-- lib/chai/interface/assert.js | 18 +++++++++++++++- test/assert.js | 30 ++++++++++++++++++++++++++ test/expect.js | 30 ++++++++++++++++++++++++++ test/should.js | 22 +++++++++++++++++++ 5 files changed, 138 insertions(+), 3 deletions(-) diff --git a/lib/chai/core/assertions.js b/lib/chai/core/assertions.js index 0a55c58e..57b72049 100644 --- a/lib/chai/core/assertions.js +++ b/lib/chai/core/assertions.js @@ -319,6 +319,12 @@ function an (type, msg) { , 'expected #{this} to be ' + article + type , 'expected #{this} not to be ' + article + type ); + } else if (type === 'iterable') { + this.assert( + typeof obj !== 'string' && obj != undefined && obj[Symbol.iterator] + , 'expected #{this} to be ' + article + type + , 'expected #{this} not to be ' + article + type + ); } else { this.assert( type === detectedType @@ -3037,7 +3043,9 @@ Assertion.addMethod('closeTo', closeTo); Assertion.addMethod('approximately', closeTo); // Note: Duplicates are ignored if testing for inclusion instead of sameness. -function isSubsetOf(subset, superset, cmp, contains, ordered) { +function isSubsetOf(_subset, _superset, cmp, contains, ordered) { + let superset = Array.from(_superset); + let subset = Array.from(_subset); if (!contains) { if (subset.length !== superset.length) return false; superset = superset.slice(); @@ -3133,7 +3141,6 @@ function isSubsetOf(subset, superset, cmp, contains, ordered) { * @namespace BDD * @api public */ - Assertion.addMethod('members', function (subset, msg) { if (msg) flag(this, 'message', msg); var obj = flag(this, 'object') @@ -3170,6 +3177,36 @@ Assertion.addMethod('members', function (subset, msg) { ); }); +/** + * ### .iterable + * + * Asserts that the target is an iterable, which means that it has a iterator + * with the exception of `String.` + * + * expect([1, 2]).to.be.iterable; + * + * Add `.not` earlier in the chain to negate `.iterable`. + * + * expect(1).to.not.be.iterable; + * expect("foobar").to.not.be.iterable; + * + * A custom error message can be given as the second argument to `expect`. + * + * expect(1, 'nooo why fail??').to.be.iterable; + * + * @name iterable + * @namespace BDD + * @api public + */ +Assertion.addProperty('iterable', function(msg) { + if (msg) flag(this, 'message', msg); + var obj = flag(this, 'object') + , flagMsg = flag(this, 'message') + , ssfi = flag(this, 'ssfi'); + + new Assertion(obj, flagMsg, ssfi, true).to.be.an('iterable'); +}); + /** * ### .oneOf(list[, msg]) * diff --git a/lib/chai/interface/assert.js b/lib/chai/interface/assert.js index 24ceff36..b59d22fd 100644 --- a/lib/chai/interface/assert.js +++ b/lib/chai/interface/assert.js @@ -8,7 +8,6 @@ import * as chai from '../../../index.js'; import {Assertion} from '../assertion.js'; import {flag, inspect} from '../utils/index.js'; import {AssertionError} from 'assertion-error'; -import {type} from '../utils/type-detect.js'; /** * ### assert(expression, message) @@ -2485,6 +2484,23 @@ assert.oneOf = function (inList, list, msg) { new Assertion(inList, msg, assert.oneOf, true).to.be.oneOf(list); } +/** + * ### isIterable(obj, [message]) + * + * Asserts that the target is an iterable, which means that it has a iterator + * with the exception of `String.` + * + * assert.isIterable([1, 2]); + * + * @param {unknown} obj + * @param {string} [msg] + * @namespace Assert + * @api public + */ +assert.isIterable = function(obj, msg) { + new Assertion(obj, msg, assert.isIterable, true).to.be.an('iterable'); +} + /** * ### .changes(function, object, property, [message]) * diff --git a/test/assert.js b/test/assert.js index 22373a35..7e8fdb0d 100644 --- a/test/assert.js +++ b/test/assert.js @@ -2375,6 +2375,36 @@ describe('assert', function () { }, 'blah: the argument to most must be a number'); }); + it('iterable', function() { + assert.isIterable([1, 2, 3]); + assert.isIterable(new Map([[1, 'one'], [2, 'two'], [3, 'three']])); + assert.isIterable(new Set([1, 2, 3])); + + err(function() { + assert.isIterable(42); + }, 'expected 42 to be an iterable'); + + err(function() { + assert.isIterable('hello'); + }, "expected 'hello' to be an iterable"); + + err(function() { + assert.isIterable(undefined); + }, 'expected undefined to be an iterable'); + + err(function() { + assert.isIterable(null); + }, 'expected null to be an iterable'); + + err(function() { + assert.isIterable(true); + }, 'expected true to be an iterable'); + + err(function() { + assert.isIterable({ key: 'value' }); + }, 'expected { key: \'value\' } to be an iterable'); + }); + it('change', function() { var obj = { value: 10, str: 'foo' }, heroes = ['spiderman', 'superman'], diff --git a/test/expect.js b/test/expect.js index d5247655..34c9fb91 100644 --- a/test/expect.js +++ b/test/expect.js @@ -3641,6 +3641,36 @@ describe('expect', function () { }, 'expected [ { a: 1 }, { b: 2 }, { c: 3 } ] to not be an ordered superset of [ { a: 1 }, { b: 2 } ]'); }); + it('iterable', function() { + expect([1, 2, 3]).to.be.iterable; + expect(new Map([[1, 'one'], [2, 'two'], [3, 'three']])).to.be.iterable; + expect(new Set([1, 2, 3])).to.be.iterable; + + err(function() { + expect(42).to.be.iterable; + }, 'expected 42 to be an iterable'); + + err(function() { + expect('hello').to.be.iterable; + }, "expected 'hello' to be an iterable"); + + err(function() { + expect(undefined).to.be.iterable; + }, 'expected undefined to be an iterable'); + + err(function() { + expect(null).to.be.iterable; + }, 'expected null to be an iterable'); + + err(function() { + expect(true).to.be.iterable; + }, 'expected true to be an iterable'); + + err(function() { + expect({ key: 'value' }).to.be.iterable; + }, 'expected { key: \'value\' } to be an iterable'); + }) + it('change', function() { var obj = { value: 10, str: 'foo' }, heroes = ['spiderman', 'superman'], diff --git a/test/should.js b/test/should.js index adb94d94..5b8c48b1 100644 --- a/test/should.js +++ b/test/should.js @@ -2942,6 +2942,28 @@ describe('should', function() { }, 'expected [ { a: 1 }, { b: 2 }, { c: 3 } ] to not be an ordered superset of [ { a: 1 }, { b: 2 } ]'); }); + it ('iterable', function() { + ([1, 2, 3]).should.be.iterable; + (new Map([[1, 'one'], [2, 'two'], [3, 'three']])).should.be.iterable; + (new Set([1, 2, 3])).should.be.iterable; + + err(function() { + (42).should.be.iterable; + }, 'expected 42 to be an iterable'); + + err(function() { + ('hello').should.be.iterable; + }, "expected 'hello' to be an iterable"); + + err(function() { + (true).should.be.iterable; + }, 'expected true to be an iterable'); + + err(function() { + ({ key: 'value' }).should.be.iterable; + }, 'expected { key: \'value\' } to be an iterable'); + }) + it('change', function() { var obj = { value: 10, str: 'foo' }, heroes = ['spiderman', 'superman'], From 6b8c0cd8886c2e2a76eb9ee19e8ac33cd31dee6a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristj=C3=A1n=20Oddsson?= Date: Sun, 28 Jan 2024 20:37:17 +0100 Subject: [PATCH 2/4] Consider `string` to be iterable --- lib/chai/core/assertions.js | 7 +++---- test/assert.js | 5 +---- test/expect.js | 5 +---- test/should.js | 5 +---- 4 files changed, 6 insertions(+), 16 deletions(-) diff --git a/lib/chai/core/assertions.js b/lib/chai/core/assertions.js index 57b72049..806d5736 100644 --- a/lib/chai/core/assertions.js +++ b/lib/chai/core/assertions.js @@ -321,7 +321,7 @@ function an (type, msg) { ); } else if (type === 'iterable') { this.assert( - typeof obj !== 'string' && obj != undefined && obj[Symbol.iterator] + obj != undefined && obj[Symbol.iterator] , 'expected #{this} to be ' + article + type , 'expected #{this} not to be ' + article + type ); @@ -3180,15 +3180,14 @@ Assertion.addMethod('members', function (subset, msg) { /** * ### .iterable * - * Asserts that the target is an iterable, which means that it has a iterator - * with the exception of `String.` + * Asserts that the target is an iterable, which means that it has a iterator. * * expect([1, 2]).to.be.iterable; * * Add `.not` earlier in the chain to negate `.iterable`. * * expect(1).to.not.be.iterable; - * expect("foobar").to.not.be.iterable; + * expect(true).to.not.be.iterable; * * A custom error message can be given as the second argument to `expect`. * diff --git a/test/assert.js b/test/assert.js index 7e8fdb0d..f2cd322d 100644 --- a/test/assert.js +++ b/test/assert.js @@ -2379,15 +2379,12 @@ describe('assert', function () { assert.isIterable([1, 2, 3]); assert.isIterable(new Map([[1, 'one'], [2, 'two'], [3, 'three']])); assert.isIterable(new Set([1, 2, 3])); + assert.isIterable('hello'); err(function() { assert.isIterable(42); }, 'expected 42 to be an iterable'); - err(function() { - assert.isIterable('hello'); - }, "expected 'hello' to be an iterable"); - err(function() { assert.isIterable(undefined); }, 'expected undefined to be an iterable'); diff --git a/test/expect.js b/test/expect.js index 34c9fb91..bf50c8e5 100644 --- a/test/expect.js +++ b/test/expect.js @@ -3645,15 +3645,12 @@ describe('expect', function () { expect([1, 2, 3]).to.be.iterable; expect(new Map([[1, 'one'], [2, 'two'], [3, 'three']])).to.be.iterable; expect(new Set([1, 2, 3])).to.be.iterable; + expect('hello').to.be.iterable; err(function() { expect(42).to.be.iterable; }, 'expected 42 to be an iterable'); - err(function() { - expect('hello').to.be.iterable; - }, "expected 'hello' to be an iterable"); - err(function() { expect(undefined).to.be.iterable; }, 'expected undefined to be an iterable'); diff --git a/test/should.js b/test/should.js index 5b8c48b1..d8f6cfb1 100644 --- a/test/should.js +++ b/test/should.js @@ -2946,15 +2946,12 @@ describe('should', function() { ([1, 2, 3]).should.be.iterable; (new Map([[1, 'one'], [2, 'two'], [3, 'three']])).should.be.iterable; (new Set([1, 2, 3])).should.be.iterable; + ('hello').should.be.iterable; err(function() { (42).should.be.iterable; }, 'expected 42 to be an iterable'); - err(function() { - ('hello').should.be.iterable; - }, "expected 'hello' to be an iterable"); - err(function() { (true).should.be.iterable; }, 'expected true to be an iterable'); From 0e2db37241ad5367859be7a906ae39a4cc8a466b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristj=C3=A1n=20Oddsson?= Date: Mon, 29 Jan 2024 06:40:27 +0100 Subject: [PATCH 3/4] Revert changes to `isSubsetOf` --- lib/chai/core/assertions.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/chai/core/assertions.js b/lib/chai/core/assertions.js index 806d5736..cb3ea7ac 100644 --- a/lib/chai/core/assertions.js +++ b/lib/chai/core/assertions.js @@ -3043,9 +3043,7 @@ Assertion.addMethod('closeTo', closeTo); Assertion.addMethod('approximately', closeTo); // Note: Duplicates are ignored if testing for inclusion instead of sameness. -function isSubsetOf(_subset, _superset, cmp, contains, ordered) { - let superset = Array.from(_superset); - let subset = Array.from(_subset); +function isSubsetOf(subset, superset, cmp, contains, ordered) { if (!contains) { if (subset.length !== superset.length) return false; superset = superset.slice(); From f19bceb0f5b65bd17f3dbc38c71d8b7f3aac9ce8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristj=C3=A1n=20Oddsson?= Date: Sat, 10 Feb 2024 13:07:44 +0100 Subject: [PATCH 4/4] Move iterable check out of `an()` function --- lib/chai/core/assertions.js | 19 ++++++++----------- lib/chai/interface/assert.js | 12 +++++++++++- 2 files changed, 19 insertions(+), 12 deletions(-) diff --git a/lib/chai/core/assertions.js b/lib/chai/core/assertions.js index cb3ea7ac..28f021cf 100644 --- a/lib/chai/core/assertions.js +++ b/lib/chai/core/assertions.js @@ -319,12 +319,6 @@ function an (type, msg) { , 'expected #{this} to be ' + article + type , 'expected #{this} not to be ' + article + type ); - } else if (type === 'iterable') { - this.assert( - obj != undefined && obj[Symbol.iterator] - , 'expected #{this} to be ' + article + type - , 'expected #{this} not to be ' + article + type - ); } else { this.assert( type === detectedType @@ -3197,11 +3191,14 @@ Assertion.addMethod('members', function (subset, msg) { */ Assertion.addProperty('iterable', function(msg) { if (msg) flag(this, 'message', msg); - var obj = flag(this, 'object') - , flagMsg = flag(this, 'message') - , ssfi = flag(this, 'ssfi'); - - new Assertion(obj, flagMsg, ssfi, true).to.be.an('iterable'); + var obj = flag(this, 'object'); + + this.assert( + obj != undefined && obj[Symbol.iterator] + , 'expected #{this} to be an iterable' + , 'expected #{this} to not be an iterable' + , obj + ); }); /** diff --git a/lib/chai/interface/assert.js b/lib/chai/interface/assert.js index b59d22fd..06e05d38 100644 --- a/lib/chai/interface/assert.js +++ b/lib/chai/interface/assert.js @@ -2498,7 +2498,17 @@ assert.oneOf = function (inList, list, msg) { * @api public */ assert.isIterable = function(obj, msg) { - new Assertion(obj, msg, assert.isIterable, true).to.be.an('iterable'); + if (obj == undefined || !obj[Symbol.iterator]) { + msg = msg ? + `${msg} expected ${inspect(obj)} to be an iterable` : + `expected ${inspect(obj)} to be an iterable`; + + throw new AssertionError( + msg, + undefined, + assert.isIterable + ); + } } /**