From 6373c78b568c3632fb247ebc0389fe91d96822fc Mon Sep 17 00:00:00 2001 From: Nahuel Garbezza Date: Wed, 28 Oct 2026 16:02:45 -0300 Subject: [PATCH] feat(assertions): support substrings in includes/isIncludedIn on Strings includes, doesNotInclude, isIncludedIn and isNotIncludedIn compared a String character by character, so only single-character searches worked. When both the collection and the searched object are Strings (and no custom equality criteria is given) they now check for a substring. The shared lookup also uses some() instead of find(), which fixes includes() failing when the matching element is falsy (e.g. 0). Closes #449 --- README.md | 2 + lib/core/assertion.js | 28 +++++---- .../assertions/collection_assertions_test.js | 61 +++++++++++++++++++ 3 files changed, 79 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index f01561c5..89336695 100644 --- a/README.md +++ b/README.md @@ -193,6 +193,8 @@ There must be at least one assertion on the test to be valid. These are all the - `assert.that(collection).includes(object)` - `assert.that(collection).doesNotInclude(object)` - `assert.that(collection).includesExactly(...objects)` + - when the collection is a `String`, `includes`, `doesNotInclude`, `isIncludedIn` and `isNotIncludedIn` work with + substrings, e.g. `assert.that('hello').includes('ell')` - Emptiness - `assert.that(collection).isEmpty()` or `assert.isEmpty(collection)` - `assert.that(collection).isNotEmpty()` or `assert.isNotEmpty(collection)` diff --git a/lib/core/assertion.js b/lib/core/assertion.js index da69214f..a88263d0 100644 --- a/lib/core/assertion.js +++ b/lib/core/assertion.js @@ -265,7 +265,7 @@ export class Assertion extends TestResultReporter { * assert.that(new Set([1, 2, 3])).includes(3) * * @example string - * assert.that('42').includes('4') + * assert.that('hello').includes('ell') * * @param {*} expectedObject the object that you are expecting to be included. * @param {Function} [equalityCriteria] a two-argument function to be used to compare elements from the `actual` collection and `expectedObject`. Optional. @@ -273,8 +273,7 @@ export class Assertion extends TestResultReporter { * @returns {void} */ includes(expectedObject, equalityCriteria) { - const resultIsSuccessful = convertToArray(this.#actual).find(element => - this.#areConsideredEqual(element, expectedObject, equalityCriteria)); + const resultIsSuccessful = this.#collectionIncludes(this.#actual, expectedObject, equalityCriteria); const failureMessage = () => I18nMessage.of('expectation_include', this.#actualResultAsString(), prettyPrint(expectedObject)); this.#reportAssertionResult(resultIsSuccessful, failureMessage); } @@ -290,7 +289,7 @@ export class Assertion extends TestResultReporter { * assert.that(3).isIncludedIn(new Set([1, 2, 3])) * * @example string - * assert.that('l').isIncludedIn('hello') + * assert.that('ell').isIncludedIn('hello') * * @param {Array|Set|Map|String} expectedCollection the collection that you are expecting the `actual` to be included in. * @param {Function} [equalityCriteria] a two-argument function to be used to compare elements from the @@ -299,8 +298,7 @@ export class Assertion extends TestResultReporter { * @returns {void} */ isIncludedIn(expectedCollection, equalityCriteria) { - const resultIsSuccessful = convertToArray(expectedCollection).find(element => - this.#areConsideredEqual(element, this.#actual, equalityCriteria)); + const resultIsSuccessful = this.#collectionIncludes(expectedCollection, this.#actual, equalityCriteria); const failureMessage = () => I18nMessage.of('expectation_be_included_in', this.#actualResultAsString(), prettyPrint(expectedCollection)); this.#reportAssertionResult(resultIsSuccessful, failureMessage); } @@ -316,7 +314,7 @@ export class Assertion extends TestResultReporter { * assert.that(new Set([1, 2, 3])).doesNotInclude(4) * * @example string - * assert.that('42').doesNotInclude('5') + * assert.that('hello').doesNotInclude('xyz') * * @param {*} expectedObject the object that you are expecting to not be included. * @param {Function} [equalityCriteria] a two-argument function to be used to compare elements from the `actual` @@ -325,8 +323,7 @@ export class Assertion extends TestResultReporter { * @returns {void} */ doesNotInclude(expectedObject, equalityCriteria) { - const resultIsSuccessful = !convertToArray(this.#actual).find(element => - this.#areConsideredEqual(element, expectedObject, equalityCriteria)); + const resultIsSuccessful = !this.#collectionIncludes(this.#actual, expectedObject, equalityCriteria); const failureMessage = () => I18nMessage.of('expectation_not_include', this.#actualResultAsString(), prettyPrint(expectedObject)); this.#reportAssertionResult(resultIsSuccessful, failureMessage); } @@ -342,7 +339,7 @@ export class Assertion extends TestResultReporter { * assert.that(4).isNotIncludedIn(new Set([1, 2, 3])) * * @example string - * assert.that('x').isNotIncludedIn('hello') + * assert.that('xyz').isNotIncludedIn('hello') * * @param {*} expectedCollection the collection that you are expecting the `actual` to not be included in. * @param {Function} [equalityCriteria] a two-argument function to be used to compare elements from the @@ -351,8 +348,7 @@ export class Assertion extends TestResultReporter { * @returns {void} */ isNotIncludedIn(expectedCollection, equalityCriteria) { - const resultIsSuccessful = !convertToArray(expectedCollection).find(element => - this.#areConsideredEqual(element, this.#actual, equalityCriteria)); + const resultIsSuccessful = !this.#collectionIncludes(expectedCollection, this.#actual, equalityCriteria); const failureMessage = () => I18nMessage.of('expectation_be_not_included_in', this.#actualResultAsString(), prettyPrint(expectedCollection)); this.#reportAssertionResult(resultIsSuccessful, failureMessage); } @@ -635,6 +631,14 @@ export class Assertion extends TestResultReporter { return EqualityAssertionStrategy.evaluate(objectOne, objectTwo, equalityCriteria).comparisonResult; } + #collectionIncludes(collection, object, equalityCriteria) { + if (isString(collection) && isString(object) && isUndefined(equalityCriteria)) { + return collection.includes(object); + } + return convertToArray(collection).some(element => + this.#areConsideredEqual(element, object, equalityCriteria)); + } + #checkIfErrorMatchesExpectation(errorExpectation, actualError) { if (isRegex(errorExpectation)) { return errorExpectation.test(actualError); diff --git a/tests/core/assertions/collection_assertions_test.js b/tests/core/assertions/collection_assertions_test.js index 8ae8a738..beabf4d1 100644 --- a/tests/core/assertions/collection_assertions_test.js +++ b/tests/core/assertions/collection_assertions_test.js @@ -53,6 +53,31 @@ suite('collection assertions', () => { expectSuccess(result); }); + test('includes works with substrings of Strings', async() => { + const result = await resultOfATestWith(assert => assert.that('hello').includes('ell')); + + expectSuccess(result); + }); + + test('includes works when the matching element is falsy', async() => { + const result = await resultOfATestWith(assert => assert.that([0, 1]).includes(0)); + + expectSuccess(result); + }); + + test('includes fails when a String does not contain the substring', async() => { + const result = await resultOfATestWith(assert => assert.that('hello').includes('xyz')); + + expectFailureOn(result, I18nMessage.of('expectation_include', '\'hello\'', '\'xyz\'')); + }); + + test('includes on Strings uses the equality criteria when given, comparing character by character', async() => { + const result = await resultOfATestWith(assert => + assert.that('hello').includes('L', (character, expected) => character.toLowerCase() === expected.toLowerCase())); + + expectSuccess(result); + }); + test('includes using the equality criteria for a positive case', async() => { const result = await resultOfATestWith(assert => { assert.that([{ hello: 'world' }]).includes( @@ -110,6 +135,18 @@ suite('collection assertions', () => { expectSuccess(result); }); + test('doesNotInclude is successful when a String does not contain the substring', async() => { + const result = await resultOfATestWith(assert => assert.that('hello').doesNotInclude('xyz')); + + expectSuccess(result); + }); + + test('doesNotInclude fails when a String contains the substring', async() => { + const result = await resultOfATestWith(assert => assert.that('hello').doesNotInclude('ell')); + + expectFailureOn(result, I18nMessage.of('expectation_not_include', '\'hello\'', '\'ell\'')); + }); + // includesExactly() assertion tests test('includesExactly passes with a single object included', async() => { @@ -280,6 +317,18 @@ suite('collection assertions', () => { expectSuccess(result); }); + test('isIncludedIn works with substrings of Strings', async() => { + const result = await resultOfATestWith(assert => assert.that('ell').isIncludedIn('hello')); + + expectSuccess(result); + }); + + test('isIncludedIn fails when the String does not contain the substring', async() => { + const result = await resultOfATestWith(assert => assert.that('xyz').isIncludedIn('hello')); + + expectFailureOn(result, I18nMessage.of('expectation_be_included_in', '\'xyz\'', '\'hello\'')); + }); + // isNotIncludedIn() assertion tests test('isNotIncludedIn is successful when the actual object is not part of the expected list', async() => { @@ -311,4 +360,16 @@ suite('collection assertions', () => { expectSuccess(result); }); + + test('isNotIncludedIn is successful when the String does not contain the substring', async() => { + const result = await resultOfATestWith(assert => assert.that('xyz').isNotIncludedIn('hello')); + + expectSuccess(result); + }); + + test('isNotIncludedIn fails when the String contains the substring', async() => { + const result = await resultOfATestWith(assert => assert.that('ell').isNotIncludedIn('hello')); + + expectFailureOn(result, I18nMessage.of('expectation_be_not_included_in', '\'ell\'', '\'hello\'')); + }); });