From f7a5f60d16eab507ab63289cd5aaea9618ec379c Mon Sep 17 00:00:00 2001 From: Raz Luvaton <16746759+rluvaton@users.noreply.github.com> Date: Thu, 3 Aug 2023 20:21:12 +0300 Subject: [PATCH 1/9] improve `includeSameMembers` error message --- src/matchers/toIncludeSameMembers.js | 113 +++++++++++++++++++++++---- 1 file changed, 96 insertions(+), 17 deletions(-) diff --git a/src/matchers/toIncludeSameMembers.js b/src/matchers/toIncludeSameMembers.js index 264c647a..702aff94 100644 --- a/src/matchers/toIncludeSameMembers.js +++ b/src/matchers/toIncludeSameMembers.js @@ -1,43 +1,122 @@ +const kEmpty = Symbol('kEmpty'); + export function toIncludeSameMembers(actual, expected) { - const { printReceived, printExpected, matcherHint } = this.utils; + const { printReceived, printExpected, matcherHint, printDiffOrStringify } = this.utils; - const pass = predicate(this.equals, actual, expected); + const { pass, newActual, useDiffOutput } = predicate(this.equals, actual, expected); return { pass, - message: () => - pass - ? matcherHint('.not.toIncludeSameMembers') + + message: () => { + if (pass) { + return ( + matcherHint('.not.toIncludeSameMembers') + '\n\n' + 'Expected list to not exactly match the members of:\n' + ` ${printExpected(expected)}\n` + 'Received:\n' + ` ${printReceived(actual)}` - : matcherHint('.toIncludeSameMembers') + + ); + } + + if (useDiffOutput) { + return ( + matcherHint('.toIncludeSameMembers') + '\n\n' + - 'Expected list to have the following members and no more:\n' + - ` ${printExpected(expected)}\n` + - 'Received:\n' + - ` ${printReceived(actual)}`, + printDiffOrStringify(expected, newActual, 'Expected', 'Received', this.expand !== false) + ); + } + + // Fallback to the original hard-to-read for large data output + return ( + matcherHint('.toIncludeSameMembers') + + '\n\n' + + 'Expected list to have the following members and no more:\n' + + ` ${printExpected(expected)}\n` + + 'Received:\n' + + ` ${printReceived(actual)}` + ); + }, }; } const predicate = (equals, actual, expected) => { if (!Array.isArray(actual) || !Array.isArray(expected) || actual.length !== expected.length) { - return false; + return { + pass: false, + newActual: actual, + useDiffOutput: false, + }; } - const remaining = expected.reduce((remaining, secondValue) => { - if (remaining === null) return remaining; + let pass = true; - const index = remaining.findIndex(firstValue => equals(secondValue, firstValue)); + let newActual = Array(expected.length).fill(kEmpty); + + const added = expected.reduce((actualItemsRemaining, expectedItem, expectedIndex) => { + const index = actualItemsRemaining.findIndex(actualItem => equals(expectedItem, actualItem)); if (index === -1) { - return null; + pass = false; + return actualItemsRemaining; } - return remaining.slice(0, index).concat(remaining.slice(index + 1)); + newActual[expectedIndex] = actualItemsRemaining[index]; + return actualItemsRemaining.slice(0, index).concat(actualItemsRemaining.slice(index + 1)); }, actual); - return !!remaining && remaining.length === 0; + pass = pass && added.length === 0; + let firstEmptyIndex = added.length ? newActual.findIndex(item => item === kEmpty) : -1; + let checkIfArrayHaveGaps = true; + + for (const item of added) { + while (firstEmptyIndex < expected.length && newActual[firstEmptyIndex] !== kEmpty) { + firstEmptyIndex++; + } + + if (firstEmptyIndex >= expected.length) { + checkIfArrayHaveGaps = false; + newActual.push(item); + } else { + newActual[firstEmptyIndex] = item; + firstEmptyIndex++; + } + } + + let useDiffOutput; + + // If the have gaps the output would be confusing and element will be displayed as removed + if (checkIfArrayHaveGaps && doesArrayHaveGaps(newActual)) { + // Fallback to the original array + newActual = actual; + useDiffOutput = false; + } else { + // Compact the array + newActual = newActual.filter(item => item !== kEmpty); + useDiffOutput = true; + } + + return { + pass, + newActual, + useDiffOutput, + }; }; + +function doesArrayHaveGaps(array) { + const lastEmptyIndex = array.lastIndexOf(kEmpty); + + if (lastEmptyIndex === -1) { + return false; + } + + const firstEmptyIndex = array.indexOf(kEmpty); + + for (let i = firstEmptyIndex; i <= lastEmptyIndex; i++) { + if (array[i] !== kEmpty) { + return true; + } + } + + return false; +} From f4bed2f2118901c9d600408f8996a95b4e2106c1 Mon Sep 17 00:00:00 2001 From: Raz Luvaton <16746759+rluvaton@users.noreply.github.com> Date: Thu, 3 Aug 2023 20:53:17 +0300 Subject: [PATCH 2/9] show detailed error even when arrays does not match --- src/matchers/toIncludeSameMembers.js | 8 ++++++-- .../__snapshots__/toIncludeSameMembers.test.js.snap | 11 +++++++---- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/src/matchers/toIncludeSameMembers.js b/src/matchers/toIncludeSameMembers.js index 702aff94..3571b4b9 100644 --- a/src/matchers/toIncludeSameMembers.js +++ b/src/matchers/toIncludeSameMembers.js @@ -41,7 +41,7 @@ export function toIncludeSameMembers(actual, expected) { } const predicate = (equals, actual, expected) => { - if (!Array.isArray(actual) || !Array.isArray(expected) || actual.length !== expected.length) { + if (!Array.isArray(actual) || !Array.isArray(expected)) { return { pass: false, newActual: actual, @@ -49,7 +49,7 @@ const predicate = (equals, actual, expected) => { }; } - let pass = true; + let pass = actual.length === expected.length; let newActual = Array(expected.length).fill(kEmpty); @@ -110,6 +110,10 @@ function doesArrayHaveGaps(array) { return false; } + if (lastEmptyIndex !== array.length - 1) { + return true; + } + const firstEmptyIndex = array.indexOf(kEmpty); for (let i = firstEmptyIndex; i <= lastEmptyIndex; i++) { diff --git a/test/matchers/__snapshots__/toIncludeSameMembers.test.js.snap b/test/matchers/__snapshots__/toIncludeSameMembers.test.js.snap index db442d73..5f58a0ef 100644 --- a/test/matchers/__snapshots__/toIncludeSameMembers.test.js.snap +++ b/test/matchers/__snapshots__/toIncludeSameMembers.test.js.snap @@ -12,8 +12,11 @@ Received: exports[`.toIncludeSameMembers fails when the arrays are not equal in length 1`] = ` "expect(received).toIncludeSameMembers(expected) -Expected list to have the following members and no more: - [1] -Received: - [1, 2]" +- Expected - 0 ++ Received + 1 + + Array [ + 1, ++ 2, + ]" `; From a25e6f74fce8b71a75d86253c8845008c61ccbca Mon Sep 17 00:00:00 2001 From: Raz Luvaton <16746759+rluvaton@users.noreply.github.com> Date: Thu, 3 Aug 2023 22:36:36 +0300 Subject: [PATCH 3/9] add tests and support for `fnOrKey` --- src/matchers/toIncludeSameMembers.js | 160 ++++++++++++--- .../toIncludeSameMembers.test.js.snap | 182 ++++++++++++++++++ test/matchers/toIncludeSameMembers.test.js | 100 ++++++++++ 3 files changed, 420 insertions(+), 22 deletions(-) diff --git a/src/matchers/toIncludeSameMembers.js b/src/matchers/toIncludeSameMembers.js index 3571b4b9..9d87f6fe 100644 --- a/src/matchers/toIncludeSameMembers.js +++ b/src/matchers/toIncludeSameMembers.js @@ -1,9 +1,13 @@ const kEmpty = Symbol('kEmpty'); -export function toIncludeSameMembers(actual, expected) { +export function toIncludeSameMembers(actual, expected, keyOrFn) { const { printReceived, printExpected, matcherHint, printDiffOrStringify } = this.utils; - const { pass, newActual, useDiffOutput } = predicate(this.equals, actual, expected); + if (keyOrFn !== undefined && typeof keyOrFn !== 'string' && typeof keyOrFn !== 'function') { + throw new Error('toIncludeSameMembers: keyOrFn must be a undefined or string or a function'); + } + + const pass = predicate(this.equals, actual, expected); return { pass, @@ -19,6 +23,12 @@ export function toIncludeSameMembers(actual, expected) { ); } + let { pass: newPass, newActual, useDiffOutput } = getBetterDiff(this.equals, actual, expected, keyOrFn); + + if (newPass !== pass) { + useDiffOutput = false; + } + if (useDiffOutput) { return ( matcherHint('.toIncludeSameMembers') + @@ -41,42 +51,59 @@ export function toIncludeSameMembers(actual, expected) { } const predicate = (equals, actual, expected) => { - if (!Array.isArray(actual) || !Array.isArray(expected)) { - return { - pass: false, - newActual: actual, - useDiffOutput: false, - }; + if (!Array.isArray(actual) || !Array.isArray(expected) || actual.length !== expected.length) { + return false; } - let pass = actual.length === expected.length; + const remaining = expected.reduce((remaining, secondValue) => { + if (remaining === null) return remaining; - let newActual = Array(expected.length).fill(kEmpty); - - const added = expected.reduce((actualItemsRemaining, expectedItem, expectedIndex) => { - const index = actualItemsRemaining.findIndex(actualItem => equals(expectedItem, actualItem)); + const index = remaining.findIndex(firstValue => equals(secondValue, firstValue)); if (index === -1) { - pass = false; - return actualItemsRemaining; + return null; } - newActual[expectedIndex] = actualItemsRemaining[index]; - return actualItemsRemaining.slice(0, index).concat(actualItemsRemaining.slice(index + 1)); + return remaining.slice(0, index).concat(remaining.slice(index + 1)); }, actual); - pass = pass && added.length === 0; - let firstEmptyIndex = added.length ? newActual.findIndex(item => item === kEmpty) : -1; + return !!remaining && remaining.length === 0; +}; + +const getBetterDiff = (equals, actual, expected, fnOrKey) => { + let { invalid, added, missing, partialNewActual: newActual } = getChanged(equals, actual, expected); + + const pass = !invalid && added.length === 0 && missing.length === 0; + + // If we have gaps the output would be confusing and element will be displayed as removed and added for the wrong place when having partial match + if (invalid || !canFillTheGapsIfHave(newActual, added)) { + return { + pass, + newActual: actual, + useDiffOutput: false, + }; + } + + const key = fnOrKey; + fnOrKey = typeof fnOrKey === 'string' ? (itemA, itemB) => itemA?.[key] === itemB?.[key] : fnOrKey; + + // Fill the gaps with matching items + if (added.length && fnOrKey) { + fillWithMatchingItems({ added, missing, newActual, fn: fnOrKey }); + } + let checkIfArrayHaveGaps = true; + let firstEmptyIndex = added.length ? newActual.findIndex(item => item === kEmpty) : -1; + // Fill with the rest that don't match or user didn't provide a matching function for (const item of added) { while (firstEmptyIndex < expected.length && newActual[firstEmptyIndex] !== kEmpty) { firstEmptyIndex++; } if (firstEmptyIndex >= expected.length) { - checkIfArrayHaveGaps = false; newActual.push(item); + checkIfArrayHaveGaps = false; } else { newActual[firstEmptyIndex] = item; firstEmptyIndex++; @@ -85,9 +112,8 @@ const predicate = (equals, actual, expected) => { let useDiffOutput; - // If the have gaps the output would be confusing and element will be displayed as removed + // If Still have gaps fallback to the original array (the output would be confusing) if (checkIfArrayHaveGaps && doesArrayHaveGaps(newActual)) { - // Fallback to the original array newActual = actual; useDiffOutput = false; } else { @@ -103,6 +129,58 @@ const predicate = (equals, actual, expected) => { }; }; +function getChanged(equals, actual, expected) { + if (!Array.isArray(actual) || !Array.isArray(expected)) { + return { invalid: true }; + } + + const missing = []; + const newActual = Array(expected.length).fill(kEmpty); + + const added = expected.reduce((actualItemsRemaining, expectedItem, expectedIndex) => { + const index = actualItemsRemaining.findIndex(actualItem => equals(expectedItem, actualItem)); + + if (index === -1) { + missing.push({ index: expectedIndex, value: expectedItem }); + return actualItemsRemaining; + } + + newActual[expectedIndex] = actualItemsRemaining[index]; + return actualItemsRemaining.slice(0, index).concat(actualItemsRemaining.slice(index + 1)); + }, actual); + + return { + added, + missing, + partialNewActual: newActual, + }; +} + +function fillWithMatchingItems({ added, missing, newActual, fn }) { + let addedIndex = 0; + while (added.length > addedIndex) { + const item = added[addedIndex]; + let matched = false; + + for (let i = 0; i < missing.length; i++) { + const { index, value: removedItem } = missing[i]; + if (fn(removedItem, item)) { + newActual[index] = item; + + missing.splice(i, 1); + matched = true; + break; + } + } + + if (matched) { + added.splice(addedIndex, 1); + } else { + addedIndex++; + } + } +} + function doesArrayHaveGaps(array) { const lastEmptyIndex = array.lastIndexOf(kEmpty); @@ -124,3 +202,41 @@ function doesArrayHaveGaps(array) { return false; } + +function canFillTheGapsIfHave(arrayWithPossibleGaps, itemsToAdd) { + const lastEmptyIndex = arrayWithPossibleGaps.lastIndexOf(kEmpty); + + if (lastEmptyIndex === -1) { + return true; + } + + if (lastEmptyIndex !== arrayWithPossibleGaps.length - 1) { + // Have gaps + return arrayWithPossibleGaps.filter(item => item === kEmpty).length <= itemsToAdd.length; + } + + let startPaddingIndex; + + // The array ends with empty items, so we need to find the first non-empty item from the end + // so we would only be left with gaps + for (let i = lastEmptyIndex; i >= 0; i--) { + if (arrayWithPossibleGaps[i] !== kEmpty) { + startPaddingIndex = i; + } + } + + // Array full of empty items + if (startPaddingIndex === undefined) { + return arrayWithPossibleGaps.length <= itemsToAdd.length; + } + + let accumulatedGapSize = 0; + + for (let i = startPaddingIndex; i >= 0; i--) { + if (arrayWithPossibleGaps[i] === kEmpty) { + accumulatedGapSize++; + } + } + + return accumulatedGapSize <= itemsToAdd.length; +} diff --git a/test/matchers/__snapshots__/toIncludeSameMembers.test.js.snap b/test/matchers/__snapshots__/toIncludeSameMembers.test.js.snap index 5f58a0ef..45140afe 100644 --- a/test/matchers/__snapshots__/toIncludeSameMembers.test.js.snap +++ b/test/matchers/__snapshots__/toIncludeSameMembers.test.js.snap @@ -9,6 +9,112 @@ Received: [1]" `; +exports[`.toIncludeSameMembers fails when actual has less items than expected (when the ones exists match) objects 1`] = ` +"expect(received).toIncludeSameMembers(expected) + +- Expected - 3 ++ Received + 0 + +@@ -6,9 +6,6 @@ + "id": 2, + }, + Object { + "id": 3, + }, +- Object { +- "id": 4, +- }, + ]" +`; + +exports[`.toIncludeSameMembers fails when actual has less items than expected (when the ones exists match) simple items 1`] = ` +"expect(received).toIncludeSameMembers(expected) + +- Expected - 1 ++ Received + 0 + + Array [ + 1, + 2, + 3, +- 4, + ]" +`; + +exports[`.toIncludeSameMembers fails when actual has more items than expected (when the ones exists match) objects 1`] = ` +"expect(received).toIncludeSameMembers(expected) + +- Expected - 0 ++ Received + 3 + +@@ -6,6 +6,9 @@ + "id": 2, + }, + Object { + "id": 3, + }, ++ Object { ++ "id": 4, ++ }, + ]" +`; + +exports[`.toIncludeSameMembers fails when actual has more items than expected (when the ones exists match) simple items 1`] = ` +"expect(received).toIncludeSameMembers(expected) + +- Expected - 0 ++ Received + 1 + + Array [ + 1, + 2, + 3, ++ 4, + ]" +`; + +exports[`.toIncludeSameMembers fails when actual has more items than expected (when the ones exists not all match) objects 1`] = ` +"expect(received).toIncludeSameMembers(expected) + +- Expected - 1 ++ Received + 7 + + Array [ + Object { + "id": 1, + }, + Object { +- "id": 2, ++ "id": 8, + }, + Object { + "id": 3, ++ }, ++ Object { ++ "id": 5, ++ }, ++ Object { ++ "id": 6, + }, + ]" +`; + +exports[`.toIncludeSameMembers fails when actual has more items than expected (when the ones exists not all match) simple items 1`] = ` +"expect(received).toIncludeSameMembers(expected) + +- Expected - 1 ++ Received + 3 + + Array [ + 1, +- 2, ++ 8, + 3, ++ 5, ++ 6, + ]" +`; + exports[`.toIncludeSameMembers fails when the arrays are not equal in length 1`] = ` "expect(received).toIncludeSameMembers(expected) @@ -20,3 +126,79 @@ exports[`.toIncludeSameMembers fails when the arrays are not equal in length 1`] + 2, ]" `; + +exports[`.toIncludeSameMembers have gaps objects 1`] = ` +"expect(received).toIncludeSameMembers(expected) + +Expected list to have the following members and no more: + [{"a": 1, "value": "hello"}, {"b": 2, "value": "world"}, {"c": 3, "value": "how are you"}, {"d": 4, "value": "im good"}, {"e": 5, "value": "thanks, you"}] +Received: + [{"e": 5, "value": "thanks, you"}, {"f": 6, "value": "?"}, {"a": 1, "value": "no"}]" +`; + +exports[`.toIncludeSameMembers have gaps simple items 1`] = ` +"expect(received).toIncludeSameMembers(expected) + +Expected list to have the following members and no more: + [1, 2, 3, 4, 5] +Received: + [5, 6, 1]" +`; + +exports[`.toIncludeSameMembers keyOrFn passed function 1`] = ` +"expect(received).toIncludeSameMembers(expected) + +- Expected - 3 ++ Received + 7 + + Array [ + Object { + "id": 1, +- "name": "Tony", ++ "name": "Bruce", + }, + Object { + "id": 2, +- "name": "Bruce", ++ "name": "Steve", + }, + Object { + "id": 3, +- "name": "Steve", ++ "name": "Tony", ++ }, ++ Object { ++ "id": 4, ++ "name": "Bucky", + }, + ]" +`; + +exports[`.toIncludeSameMembers keyOrFn passed property of the items as key 1`] = ` +"expect(received).toIncludeSameMembers(expected) + +- Expected - 3 ++ Received + 7 + + Array [ + Object { + "id": 1, +- "name": "Tony", ++ "name": "Bruce", + }, + Object { + "id": 2, +- "name": "Bruce", ++ "name": "Steve", + }, + Object { + "id": 3, +- "name": "Steve", ++ "name": "Tony", ++ }, ++ Object { ++ "id": 4, ++ "name": "Bucky", + }, + ]" +`; diff --git a/test/matchers/toIncludeSameMembers.test.js b/test/matchers/toIncludeSameMembers.test.js index 020ab2ac..b4eb1922 100644 --- a/test/matchers/toIncludeSameMembers.test.js +++ b/test/matchers/toIncludeSameMembers.test.js @@ -20,6 +20,106 @@ describe('.toIncludeSameMembers', () => { test('fails when the arrays are not equal in length', () => { expect(() => expect([1, 2]).toIncludeSameMembers([1])).toThrowErrorMatchingSnapshot(); }); + + describe('fails when actual has more items than expected (when the ones exists match)', () => { + test('simple items', () => { + expect(() => expect([2, 4, 3, 1]).toIncludeSameMembers([1, 2, 3])).toThrowErrorMatchingSnapshot(); + }); + + test('objects', () => { + expect(() => + expect([{ id: 2 }, { id: 4 }, { id: 3 }, { id: 1 }]).toIncludeSameMembers([{ id: 1 }, { id: 2 }, { id: 3 }]), + ).toThrowErrorMatchingSnapshot(); + }); + }); + + describe('fails when actual has less items than expected (when the ones exists match)', () => { + test('simple items', () => { + expect(() => expect([2, 3, 1]).toIncludeSameMembers([1, 2, 3, 4])).toThrowErrorMatchingSnapshot(); + }); + + test('objects', () => { + expect(() => + expect([{ id: 2 }, { id: 3 }, { id: 1 }]).toIncludeSameMembers([{ id: 1 }, { id: 2 }, { id: 3 }, { id: 4 }]), + ).toThrowErrorMatchingSnapshot(); + }); + }); + + describe('fails when actual has more items than expected (when the ones exists not all match)', () => { + test('simple items', () => { + expect(() => expect([3, 1, 8, 5, 6]).toIncludeSameMembers([1, 2, 3])).toThrowErrorMatchingSnapshot(); + }); + + test('objects', () => { + expect(() => + expect([{ id: 3 }, { id: 1 }, { id: 8 }, { id: 5 }, { id: 6 }]).toIncludeSameMembers([ + { id: 1 }, + { id: 2 }, + { id: 3 }, + ]), + ).toThrowErrorMatchingSnapshot(); + }); + }); + + describe('have gaps', () => { + test('simple items', () => { + expect(() => expect([5, 6, 1]).toIncludeSameMembers([1, 2, 3, 4, 5])).toThrowErrorMatchingSnapshot(); + }); + + test('objects', () => { + expect(() => + expect([ + { e: 5, value: 'thanks, you' }, + { f: 6, value: '?' }, + { a: 1, value: 'no' }, + ]).toIncludeSameMembers([ + { a: 1, value: 'hello' }, + { b: 2, value: 'world' }, + { c: 3, value: 'how are you' }, + { d: 4, value: 'im good' }, + { e: 5, value: 'thanks, you' }, + ]), + ).toThrowErrorMatchingSnapshot(); + }); + }); + + describe('keyOrFn', () => { + test('passed property of the items as key', () => { + expect(() => + expect([ + { id: 2, name: 'Steve' }, + { id: 4, name: 'Bucky' }, + { id: 3, name: 'Tony' }, + { id: 1, name: 'Bruce' }, + ]).toIncludeSameMembers( + [ + { id: 1, name: 'Tony' }, + { id: 2, name: 'Bruce' }, + { id: 3, name: 'Steve' }, + ], + 'id', + ), + ).toThrowErrorMatchingSnapshot(); + }); + + test('passed function', () => { + expect(() => + expect([ + { id: 2, name: 'Steve' }, + { id: 4, name: 'Bucky' }, + { id: 3, name: 'Tony' }, + { id: 1, name: 'Bruce' }, + ]).toIncludeSameMembers( + [ + { id: 1, name: 'Tony' }, + { id: 2, name: 'Bruce' }, + { id: 3, name: 'Steve' }, + ], + (itemA, itemB) => itemA.id === itemB.id, + ), + ).toThrowErrorMatchingSnapshot(); + }); + }); }); describe('.not.toIncludeSameMembers', () => { From 3a34eb2fc87c6c90fa5fb6fdd5dd69ab9590b296 Mon Sep 17 00:00:00 2001 From: Raz Luvaton <16746759+rluvaton@users.noreply.github.com> Date: Thu, 3 Aug 2023 22:41:34 +0300 Subject: [PATCH 4/9] update types and docs --- types/index.d.ts | 8 ++++++-- website/docs/matchers/Array.mdx | 3 ++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/types/index.d.ts b/types/index.d.ts index a0cef966..5d1b0308 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -76,9 +76,11 @@ interface CustomMatchers extends Record { /** * Use `.toIncludeSameMembers` when checking if two arrays contain equal values, in any order. + * for better error message use the optional `fnOrKey` argument to specify how to determine two items similarity (e.g. the id property) * @param {Array.<*>} members + * @param fnOrKey */ - toIncludeSameMembers(members: readonly E[]): R; + toIncludeSameMembers(members: readonly E[], fnOrKey?: string | ((itemA: E, itemB: E) => boolean)): R; /** * Use `.toPartiallyContain` when checking if any array value matches the partial member. @@ -510,9 +512,11 @@ declare namespace jest { /** * Use `.toIncludeSameMembers` when checking if two arrays contain equal values, in any order. + * for better error message use the optional `fnOrKey` argument to specify how to determine two items similarity (e.g. the id property) * @param {Array.<*>} members + * @param fnOrKey */ - toIncludeSameMembers(members: readonly E[]): R; + toIncludeSameMembers(members: readonly E[], fnOrKey?: string | ((itemA: E, itemB: E) => boolean)): R; /** * Use `.toPartiallyContain` when checking if any array value matches the partial member. diff --git a/website/docs/matchers/Array.mdx b/website/docs/matchers/Array.mdx index 7903a5e6..1980f3ee 100644 --- a/website/docs/matchers/Array.mdx +++ b/website/docs/matchers/Array.mdx @@ -59,9 +59,10 @@ Use `.toIncludeAnyMembers` when checking if an `Array` contains any of the membe });`} -### .toIncludeSameMembers([members]) +### .toIncludeSameMembers([members], fnOrKey) Use `.toIncludeSameMembers` when checking if two arrays contain equal values, in any order. +for better error message use the optional `fnOrKey` argument to specify how to determine two items similarity (e.g. the id property) {`test('passes when arrays match in a different order', () => { From 5edc7e45443d5ae2a53a20c331f52437c82e2351 Mon Sep 17 00:00:00 2001 From: Raz Luvaton <16746759+rluvaton@users.noreply.github.com> Date: Thu, 3 Aug 2023 22:47:25 +0300 Subject: [PATCH 5/9] only fallback when have gaps and complex data type --- src/matchers/toIncludeSameMembers.js | 6 ++++-- .../toIncludeSameMembers.test.js.snap | 15 +++++++++++---- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/src/matchers/toIncludeSameMembers.js b/src/matchers/toIncludeSameMembers.js index 9d87f6fe..7412d1b0 100644 --- a/src/matchers/toIncludeSameMembers.js +++ b/src/matchers/toIncludeSameMembers.js @@ -75,8 +75,10 @@ const getBetterDiff = (equals, actual, expected, fnOrKey) => { const pass = !invalid && added.length === 0 && missing.length === 0; + const containComplexDiffData = actual.concat(expected).some(item => typeof item === 'object' && item !== null); + // If we have gaps the output would be confusing and element will be displayed as removed and added for the wrong place when having partial match - if (invalid || !canFillTheGapsIfHave(newActual, added)) { + if (invalid || (containComplexDiffData && !canFillTheGapsIfHave(newActual, added))) { return { pass, newActual: actual, @@ -113,7 +115,7 @@ const getBetterDiff = (equals, actual, expected, fnOrKey) => { let useDiffOutput; // If Still have gaps fallback to the original array (the output would be confusing) - if (checkIfArrayHaveGaps && doesArrayHaveGaps(newActual)) { + if (checkIfArrayHaveGaps && containComplexDiffData && doesArrayHaveGaps(newActual)) { newActual = actual; useDiffOutput = false; } else { diff --git a/test/matchers/__snapshots__/toIncludeSameMembers.test.js.snap b/test/matchers/__snapshots__/toIncludeSameMembers.test.js.snap index 45140afe..97958ca9 100644 --- a/test/matchers/__snapshots__/toIncludeSameMembers.test.js.snap +++ b/test/matchers/__snapshots__/toIncludeSameMembers.test.js.snap @@ -139,10 +139,17 @@ Received: exports[`.toIncludeSameMembers have gaps simple items 1`] = ` "expect(received).toIncludeSameMembers(expected) -Expected list to have the following members and no more: - [1, 2, 3, 4, 5] -Received: - [5, 6, 1]" +- Expected - 3 ++ Received + 1 + + Array [ + 1, +- 2, +- 3, +- 4, ++ 6, + 5, + ]" `; exports[`.toIncludeSameMembers keyOrFn passed function 1`] = ` From 6ac2c26bf9fa2c77348ff413f698434cedb59790 Mon Sep 17 00:00:00 2001 From: Raz Luvaton <16746759+rluvaton@users.noreply.github.com> Date: Thu, 3 Aug 2023 22:49:52 +0300 Subject: [PATCH 6/9] add changeset --- .changeset/tasty-points-nail.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/tasty-points-nail.md diff --git a/.changeset/tasty-points-nail.md b/.changeset/tasty-points-nail.md new file mode 100644 index 00000000..3aff9ce7 --- /dev/null +++ b/.changeset/tasty-points-nail.md @@ -0,0 +1,5 @@ +--- +'jest-extended': minor +--- + +improve error message for `.toIncludeSameMembers` and add optional `keyOrFn` argument for matching changedItems From 34b1a5cb5dfdf3dfc20678a09aa2eadba6cd52e3 Mon Sep 17 00:00:00 2001 From: Raz Luvaton <16746759+rluvaton@users.noreply.github.com> Date: Thu, 3 Aug 2023 23:24:54 +0300 Subject: [PATCH 7/9] add more tests --- src/matchers/toIncludeSameMembers.js | 4 ++-- test/matchers/toIncludeSameMembers.test.js | 21 +++++++++++++++++++++ 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/src/matchers/toIncludeSameMembers.js b/src/matchers/toIncludeSameMembers.js index 7412d1b0..4336096f 100644 --- a/src/matchers/toIncludeSameMembers.js +++ b/src/matchers/toIncludeSameMembers.js @@ -70,7 +70,7 @@ const predicate = (equals, actual, expected) => { return !!remaining && remaining.length === 0; }; -const getBetterDiff = (equals, actual, expected, fnOrKey) => { +function getBetterDiff(equals, actual, expected, fnOrKey) { let { invalid, added, missing, partialNewActual: newActual } = getChanged(equals, actual, expected); const pass = !invalid && added.length === 0 && missing.length === 0; @@ -129,7 +129,7 @@ const getBetterDiff = (equals, actual, expected, fnOrKey) => { newActual, useDiffOutput, }; -}; +} function getChanged(equals, actual, expected) { if (!Array.isArray(actual) || !Array.isArray(expected)) { diff --git a/test/matchers/toIncludeSameMembers.test.js b/test/matchers/toIncludeSameMembers.test.js index b4eb1922..c5e43adf 100644 --- a/test/matchers/toIncludeSameMembers.test.js +++ b/test/matchers/toIncludeSameMembers.test.js @@ -17,10 +17,31 @@ describe('.toIncludeSameMembers', () => { expect([{ foo: 'bar' }, { baz: 'qux' }]).toIncludeSameMembers([{ baz: 'qux' }, { foo: 'bar' }]); }); + test('fail with fallback output when result of the matcher changed', () => { + expect(() => + expect([ + { + get id() { + const stack = new Error().stack; + if (!stack.includes('getBetterDiff')) { + // Fail + return 5; + } + return 1; + }, + }, + ]).toIncludeSameMembers([{ id: 1 }]), + ).toThrowErrorMatchingSnapshot(); + }); + test('fails when the arrays are not equal in length', () => { expect(() => expect([1, 2]).toIncludeSameMembers([1])).toThrowErrorMatchingSnapshot(); }); + test('fails when not passed array', () => { + expect(() => expect(2).toIncludeSameMembers([1])).toThrowErrorMatchingSnapshot(); + }); + describe('fails when actual has more items than expected (when the ones exists match)', () => { test('simple items', () => { expect(() => expect([2, 4, 3, 1]).toIncludeSameMembers([1, 2, 3])).toThrowErrorMatchingSnapshot(); From df277feed56dd79af5cd0e9b55c5b261c58229c8 Mon Sep 17 00:00:00 2001 From: Raz Luvaton <16746759+rluvaton@users.noreply.github.com> Date: Thu, 3 Aug 2023 23:39:23 +0300 Subject: [PATCH 8/9] fix --- src/matchers/toIncludeSameMembers.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/matchers/toIncludeSameMembers.js b/src/matchers/toIncludeSameMembers.js index 4336096f..0a42ecc5 100644 --- a/src/matchers/toIncludeSameMembers.js +++ b/src/matchers/toIncludeSameMembers.js @@ -75,7 +75,8 @@ function getBetterDiff(equals, actual, expected, fnOrKey) { const pass = !invalid && added.length === 0 && missing.length === 0; - const containComplexDiffData = actual.concat(expected).some(item => typeof item === 'object' && item !== null); + const containComplexDiffData = + !invalid && actual.concat(expected).some(item => typeof item === 'object' && item !== null); // If we have gaps the output would be confusing and element will be displayed as removed and added for the wrong place when having partial match if (invalid || (containComplexDiffData && !canFillTheGapsIfHave(newActual, added))) { From eb9994b00d2bf7fc9f8bc3e1fcd4e9c6c72294ff Mon Sep 17 00:00:00 2001 From: Raz Luvaton <16746759+rluvaton@users.noreply.github.com> Date: Thu, 3 Aug 2023 23:46:05 +0300 Subject: [PATCH 9/9] fix snapshot --- .../toIncludeSameMembers.test.js.snap | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/test/matchers/__snapshots__/toIncludeSameMembers.test.js.snap b/test/matchers/__snapshots__/toIncludeSameMembers.test.js.snap index 97958ca9..7692e887 100644 --- a/test/matchers/__snapshots__/toIncludeSameMembers.test.js.snap +++ b/test/matchers/__snapshots__/toIncludeSameMembers.test.js.snap @@ -9,6 +9,15 @@ Received: [1]" `; +exports[`.toIncludeSameMembers fail with fallback output when result of the matcher changed 1`] = ` +"expect(received).toIncludeSameMembers(expected) + +Expected list to have the following members and no more: + [{"id": 1}] +Received: + [{"id": 5}]" +`; + exports[`.toIncludeSameMembers fails when actual has less items than expected (when the ones exists match) objects 1`] = ` "expect(received).toIncludeSameMembers(expected) @@ -115,6 +124,15 @@ exports[`.toIncludeSameMembers fails when actual has more items than expected (w ]" `; +exports[`.toIncludeSameMembers fails when not passed array 1`] = ` +"expect(received).toIncludeSameMembers(expected) + +Expected list to have the following members and no more: + [1] +Received: + 2" +`; + exports[`.toIncludeSameMembers fails when the arrays are not equal in length 1`] = ` "expect(received).toIncludeSameMembers(expected)