From 63d7186ccc17c77f93f782610cd16853c5209bcd Mon Sep 17 00:00:00 2001 From: Andre Wiggins Date: Wed, 30 Nov 2022 17:01:57 -0800 Subject: [PATCH 01/28] Add customEqualityTesters support to toEqual --- .../__tests__/customEqualityTesters.test.ts | 80 +++++++++++++++++++ packages/expect/src/index.ts | 21 ++++- packages/expect/src/jestMatchersObject.ts | 17 ++++ packages/expect/src/matchers.ts | 6 +- packages/expect/src/types.ts | 1 + 5 files changed, 123 insertions(+), 2 deletions(-) create mode 100644 packages/expect/src/__tests__/customEqualityTesters.test.ts diff --git a/packages/expect/src/__tests__/customEqualityTesters.test.ts b/packages/expect/src/__tests__/customEqualityTesters.test.ts new file mode 100644 index 000000000000..4e16599c946a --- /dev/null +++ b/packages/expect/src/__tests__/customEqualityTesters.test.ts @@ -0,0 +1,80 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import type {Tester} from '@jest/expect-utils'; +import jestExpect from '../'; + +const specialObjPropName = '$$special'; +const specialObjSymbol = Symbol('special test object type'); + +interface SpecialObject { + $$special: symbol; + value: number; +} + +function createSpecialObject(value: number) { + return { + [specialObjPropName]: specialObjSymbol, + value, + }; +} + +function isSpecialObject(a: unknown): a is SpecialObject { + return ( + a != null && + typeof a === 'object' && + specialObjPropName in a && + (a as any)[specialObjPropName] === specialObjSymbol + ); +} + +const specialObjTester: Tester = ( + a: unknown, + b: unknown, +): boolean | undefined => { + const isASpecial = isSpecialObject(a); + const isBSpecial = isSpecialObject(b); + + if (isASpecial && isBSpecial) { + return true; + } else if ((isASpecial && !isBSpecial) || (!isASpecial && isBSpecial)) { + return false; + } else { + return undefined; + } +}; + +it('has no custom testers as default', () => { + expect(createSpecialObject(1)).toEqual(createSpecialObject(1)); + expect(createSpecialObject(1)).not.toEqual(createSpecialObject(2)); +}); + +describe('with custom equality testers', () => { + let originalTesters: Array; + + beforeAll(() => { + originalTesters = jestExpect.customEqualityTesters; + jestExpect.customEqualityTesters = [...originalTesters, specialObjTester]; + }); + + afterAll(() => { + jestExpect.customEqualityTesters = originalTesters; + }); + + it('does not apply custom testers to `toBe`', () => { + expect(createSpecialObject(1)).not.toBe(createSpecialObject(1)); + }); + + it('applies custom testers to `toEqual`', () => { + expect(createSpecialObject(1)).toEqual(createSpecialObject(1)); + expect(createSpecialObject(1)).toEqual(createSpecialObject(2)); + }); + + // TODO: Add tests for other matchers + // TODO: Add tests for built-in custom testers (e.g. iterableEquality, subsetObjectEquality) +}); diff --git a/packages/expect/src/index.ts b/packages/expect/src/index.ts index af07fff87287..6e8de8d1a65c 100644 --- a/packages/expect/src/index.ts +++ b/packages/expect/src/index.ts @@ -8,7 +8,12 @@ /* eslint-disable local/prefer-spread-eventually */ -import {equals, iterableEquality, subsetEquality} from '@jest/expect-utils'; +import { + Tester, + equals, + iterableEquality, + subsetEquality, +} from '@jest/expect-utils'; import * as matcherUtils from 'jest-matcher-utils'; import {isPromise} from 'jest-util'; import { @@ -28,8 +33,10 @@ import { import extractExpectedAssertionsErrors from './extractExpectedAssertionsErrors'; import { INTERNAL_MATCHER_FLAG, + getCustomEqualityTesters, getMatchers, getState, + setCustomEqualityTesters, setMatchers, setState, } from './jestMatchersObject'; @@ -383,6 +390,18 @@ const makeThrowingMatcher = ( expect.extend = (matchers: MatchersObject) => setMatchers(matchers, false, expect); +expect.customEqualityTesters = []; // To make TypeScript happy +Object.defineProperty(expect, 'customEqualityTesters', { + configurable: true, + enumerable: true, + get() { + return getCustomEqualityTesters(); + }, + set(newTesters: Array) { + setCustomEqualityTesters(newTesters); + }, +}); + expect.anything = anything; expect.any = any; diff --git a/packages/expect/src/jestMatchersObject.ts b/packages/expect/src/jestMatchersObject.ts index d43aee7df5a1..7776d1c3deae 100644 --- a/packages/expect/src/jestMatchersObject.ts +++ b/packages/expect/src/jestMatchersObject.ts @@ -6,6 +6,7 @@ * */ +import type {Tester} from '@jest/expect-utils'; import {getType} from 'jest-get-type'; import {AsymmetricMatcher} from './asymmetricMatchers'; import type { @@ -32,6 +33,7 @@ if (!Object.prototype.hasOwnProperty.call(globalThis, JEST_MATCHERS_OBJECT)) { }; Object.defineProperty(globalThis, JEST_MATCHERS_OBJECT, { value: { + customEqualityTesters: [], matchers: Object.create(null), state: defaultState, }, @@ -122,3 +124,18 @@ export const setMatchers = ( Object.assign((globalThis as any)[JEST_MATCHERS_OBJECT].matchers, matchers); }; + +export const getCustomEqualityTesters = (): Array => + (globalThis as any)[JEST_MATCHERS_OBJECT].customEqualityTesters; + +export const setCustomEqualityTesters = (newTesters: Array): void => { + if (!Array.isArray(newTesters)) { + throw new TypeError( + `expect.customEqualityTesters: Must be set to an array of Testers. Was given "${getType( + newTesters, + )}"`, + ); + } + + (globalThis as any)[JEST_MATCHERS_OBJECT].customEqualityTesters = newTesters; +}; diff --git a/packages/expect/src/matchers.ts b/packages/expect/src/matchers.ts index 454a5c288efa..b1557ceec530 100644 --- a/packages/expect/src/matchers.ts +++ b/packages/expect/src/matchers.ts @@ -38,6 +38,7 @@ import { printWithType, stringify, } from 'jest-matcher-utils'; +import {getCustomEqualityTesters} from './jestMatchersObject'; import { printCloseTo, printExpectedConstructorName, @@ -605,7 +606,10 @@ const matchers: MatchersObject = { promise: this.promise, }; - const pass = equals(received, expected, [iterableEquality]); + const pass = equals(received, expected, [ + iterableEquality, + ...getCustomEqualityTesters(), + ]); const message = pass ? () => diff --git a/packages/expect/src/types.ts b/packages/expect/src/types.ts index e2fc979165ff..9f3afccd9529 100644 --- a/packages/expect/src/types.ts +++ b/packages/expect/src/types.ts @@ -86,6 +86,7 @@ export type ExpectedAssertionsErrors = Array<{ export interface BaseExpect { assertions(numberOfAssertions: number): void; + customEqualityTesters: Array; extend(matchers: MatchersObject): void; extractExpectedAssertionsErrors(): ExpectedAssertionsErrors; getState(): MatcherState; From 8f1daca994770e3df75d1c088162cd4be2defe51 Mon Sep 17 00:00:00 2001 From: Andre Wiggins Date: Wed, 30 Nov 2022 19:19:08 -0800 Subject: [PATCH 02/28] Add support for custom testers in iterableEquality --- packages/expect-utils/src/jasmineUtils.ts | 2 +- packages/expect-utils/src/types.ts | 6 ++- packages/expect-utils/src/utils.ts | 29 +++++++------- .../__tests__/customEqualityTesters.test.ts | 39 ++++++++++++++++++- 4 files changed, 60 insertions(+), 16 deletions(-) diff --git a/packages/expect-utils/src/jasmineUtils.ts b/packages/expect-utils/src/jasmineUtils.ts index 6aec53aa1127..5bf9d3df4ac1 100644 --- a/packages/expect-utils/src/jasmineUtils.ts +++ b/packages/expect-utils/src/jasmineUtils.ts @@ -76,7 +76,7 @@ function eq( } for (let i = 0; i < customTesters.length; i++) { - const customTesterResult = customTesters[i](a, b); + const customTesterResult = customTesters[i](a, b, customTesters); if (customTesterResult !== undefined) { return customTesterResult; } diff --git a/packages/expect-utils/src/types.ts b/packages/expect-utils/src/types.ts index 361f648f5f56..dbe9b39ac639 100644 --- a/packages/expect-utils/src/types.ts +++ b/packages/expect-utils/src/types.ts @@ -6,4 +6,8 @@ * */ -export type Tester = (a: any, b: any) => boolean | undefined; +export type Tester = ( + a: any, + b: any, + customTesters: Array, +) => boolean | undefined; diff --git a/packages/expect-utils/src/utils.ts b/packages/expect-utils/src/utils.ts index 66394ac071cf..54ef864a90fb 100644 --- a/packages/expect-utils/src/utils.ts +++ b/packages/expect-utils/src/utils.ts @@ -16,6 +16,7 @@ import { isImmutableUnorderedSet, } from './immutableUtils'; import {equals, isA} from './jasmineUtils'; +import type {Tester} from './types'; type GetPath = { hasEndProp?: boolean; @@ -96,6 +97,7 @@ export const getPath = ( }; }; +// TODO: Update with customTesters // Strip properties from object that are not present in the subset. Useful for // printing the diff for toMatchObject() without adding unrelated noise. /* eslint-disable @typescript-eslint/explicit-module-boundary-types */ @@ -147,6 +149,7 @@ const hasIterator = (object: any) => export const iterableEquality = ( a: any, b: any, + customTesters: Array = [], /* eslint-enable @typescript-eslint/explicit-module-boundary-types */ aStack: Array = [], bStack: Array = [], @@ -178,7 +181,12 @@ export const iterableEquality = ( bStack.push(b); const iterableEqualityWithStack = (a: any, b: any) => - iterableEquality(a, b, [...aStack], [...bStack]); + iterableEquality(a, b, [...customTesters], [...aStack], [...bStack]); + + customTesters = [ + ...customTesters.filter(t => t !== iterableEquality), + iterableEqualityWithStack, + ]; if (a.size !== undefined) { if (a.size !== b.size) { @@ -189,7 +197,7 @@ export const iterableEquality = ( if (!b.has(aValue)) { let has = false; for (const bValue of b) { - const isEqual = equals(aValue, bValue, [iterableEqualityWithStack]); + const isEqual = equals(aValue, bValue, customTesters); if (isEqual === true) { has = true; } @@ -213,19 +221,15 @@ export const iterableEquality = ( for (const aEntry of a) { if ( !b.has(aEntry[0]) || - !equals(aEntry[1], b.get(aEntry[0]), [iterableEqualityWithStack]) + !equals(aEntry[1], b.get(aEntry[0]), customTesters) ) { let has = false; for (const bEntry of b) { - const matchedKey = equals(aEntry[0], bEntry[0], [ - iterableEqualityWithStack, - ]); + const matchedKey = equals(aEntry[0], bEntry[0], customTesters); let matchedValue = false; if (matchedKey === true) { - matchedValue = equals(aEntry[1], bEntry[1], [ - iterableEqualityWithStack, - ]); + matchedValue = equals(aEntry[1], bEntry[1], customTesters); } if (matchedValue === true) { has = true; @@ -249,10 +253,7 @@ export const iterableEquality = ( for (const aValue of a) { const nextB = bIterator.next(); - if ( - nextB.done || - !equals(aValue, nextB.value, [iterableEqualityWithStack]) - ) { + if (nextB.done || !equals(aValue, nextB.value, customTesters)) { return false; } } @@ -287,6 +288,7 @@ const isObjectWithKeys = (a: any) => !(a instanceof Array) && !(a instanceof Date); +// TODO: Update with customTesters export const subsetEquality = ( object: unknown, subset: unknown, @@ -363,6 +365,7 @@ export const arrayBufferEquality = ( return true; }; +// TODO: Update with customTesters export const sparseArrayEquality = ( a: unknown, b: unknown, diff --git a/packages/expect/src/__tests__/customEqualityTesters.test.ts b/packages/expect/src/__tests__/customEqualityTesters.test.ts index 4e16599c946a..0b9d0fbf42f0 100644 --- a/packages/expect/src/__tests__/customEqualityTesters.test.ts +++ b/packages/expect/src/__tests__/customEqualityTesters.test.ts @@ -49,9 +49,33 @@ const specialObjTester: Tester = ( } }; +function* toIterator(array: Array): Iterator { + for (const obj of array) { + yield obj; + } +} + it('has no custom testers as default', () => { - expect(createSpecialObject(1)).toEqual(createSpecialObject(1)); + const special1 = createSpecialObject(1); + const special2 = createSpecialObject(2); + + expect(special1).toEqual(special1); + expect([special1, special2]).toEqual([special1, special2]); + expect(new Set([special1])).toEqual(new Set([special1])); + expect(new Map([['key', special1]])).toEqual(new Map([['key', special1]])); + expect(toIterator([special1, special2])).toEqual( + toIterator([special1, special2]), + ); + expect(createSpecialObject(1)).not.toEqual(createSpecialObject(2)); + expect([special1, special2]).not.toEqual([special2, special1]); + expect(new Set([special1])).not.toEqual(new Set([special2])); + expect(new Map([['key', special1]])).not.toEqual( + new Map([['key', special2]]), + ); + expect(toIterator([special1, special2])).not.toEqual( + toIterator([special2, special1]), + ); }); describe('with custom equality testers', () => { @@ -75,6 +99,19 @@ describe('with custom equality testers', () => { expect(createSpecialObject(1)).toEqual(createSpecialObject(2)); }); + it('applies custom testers to iterableEquality', () => { + const special1 = createSpecialObject(1); + const special2 = createSpecialObject(2); + + expect([special1, special2]).toEqual([special2, special1]); + expect(new Map([['key', special1]])).toEqual(new Map([['key', special2]])); + expect(new Set([special1])).toEqual(new Set([special2])); + expect(toIterator([special1, special2])).toEqual( + toIterator([special2, special1]), + ); + }); + // TODO: Add tests for other matchers // TODO: Add tests for built-in custom testers (e.g. iterableEquality, subsetObjectEquality) + // TODO: Add tests for extended expect matchers that use this.equal(); }); From 45243cd7708dcd8240c380245803c67a780f2b48 Mon Sep 17 00:00:00 2001 From: Andre Wiggins Date: Wed, 30 Nov 2022 19:46:25 -0800 Subject: [PATCH 03/28] Add customTester support to toContainEqual and toHaveProperty --- .../__tests__/customEqualityTesters.test.ts | 31 ++++++++++++++----- packages/expect/src/matchers.ts | 7 +++-- 2 files changed, 29 insertions(+), 9 deletions(-) diff --git a/packages/expect/src/__tests__/customEqualityTesters.test.ts b/packages/expect/src/__tests__/customEqualityTesters.test.ts index 0b9d0fbf42f0..e319579eee10 100644 --- a/packages/expect/src/__tests__/customEqualityTesters.test.ts +++ b/packages/expect/src/__tests__/customEqualityTesters.test.ts @@ -59,6 +59,8 @@ it('has no custom testers as default', () => { const special1 = createSpecialObject(1); const special2 = createSpecialObject(2); + // Basic matchers passing + expect(special1).toBe(special1); expect(special1).toEqual(special1); expect([special1, special2]).toEqual([special1, special2]); expect(new Set([special1])).toEqual(new Set([special1])); @@ -67,6 +69,8 @@ it('has no custom testers as default', () => { toIterator([special1, special2]), ); + // Basic matchers not passing + expect(special1).not.toBe(special2); expect(createSpecialObject(1)).not.toEqual(createSpecialObject(2)); expect([special1, special2]).not.toEqual([special2, special1]); expect(new Set([special1])).not.toEqual(new Set([special2])); @@ -90,27 +94,40 @@ describe('with custom equality testers', () => { jestExpect.customEqualityTesters = originalTesters; }); - it('does not apply custom testers to `toBe`', () => { - expect(createSpecialObject(1)).not.toBe(createSpecialObject(1)); - }); + it('basic matchers customTesters do not apply to', () => { + const special1 = createSpecialObject(1); + const special2 = createSpecialObject(2); + + expect(special1).toBe(special1); + expect([special1]).toContain(special1); - it('applies custom testers to `toEqual`', () => { - expect(createSpecialObject(1)).toEqual(createSpecialObject(1)); - expect(createSpecialObject(1)).toEqual(createSpecialObject(2)); + expect(special1).not.toBe(special2); + expect([special1]).not.toContain(special2); }); - it('applies custom testers to iterableEquality', () => { + it('basic matchers customTesters do apply to', () => { const special1 = createSpecialObject(1); const special2 = createSpecialObject(2); + expect(special1).toEqual(special1); + expect(special1).toEqual(special2); expect([special1, special2]).toEqual([special2, special1]); expect(new Map([['key', special1]])).toEqual(new Map([['key', special2]])); expect(new Set([special1])).toEqual(new Set([special2])); expect(toIterator([special1, special2])).toEqual( toIterator([special2, special1]), ); + expect([createSpecialObject(1)]).toContainEqual(createSpecialObject(2)); + expect({a: createSpecialObject(1)}).toHaveProperty( + 'a', + createSpecialObject(2), + ); }); + // it('applies custom testers to toStrictEqual', () => {}); + + // it('applies custom testers to toMatchObject', () => {}); + // TODO: Add tests for other matchers // TODO: Add tests for built-in custom testers (e.g. iterableEquality, subsetObjectEquality) // TODO: Add tests for extended expect matchers that use this.equal(); diff --git a/packages/expect/src/matchers.ts b/packages/expect/src/matchers.ts index b1557ceec530..c1a15cd01f2c 100644 --- a/packages/expect/src/matchers.ts +++ b/packages/expect/src/matchers.ts @@ -571,7 +571,7 @@ const matchers: MatchersObject = { } const index = Array.from(received).findIndex(item => - equals(item, expected, [iterableEquality]), + equals(item, expected, [iterableEquality, ...getCustomEqualityTesters()]), ); const pass = index !== -1; @@ -752,7 +752,10 @@ const matchers: MatchersObject = { const pass = hasValue && endPropIsDefined - ? equals(value, expectedValue, [iterableEquality]) + ? equals(value, expectedValue, [ + iterableEquality, + ...getCustomEqualityTesters(), + ]) : Boolean(hasEndProp); const message = pass From cb73a206e3c4abdae5959894589ab7b90b764f9e Mon Sep 17 00:00:00 2001 From: Andre Wiggins Date: Wed, 30 Nov 2022 20:41:01 -0800 Subject: [PATCH 04/28] Add customTester support toStrictEqual --- packages/expect-utils/src/utils.ts | 9 ++++++-- .../__tests__/customEqualityTesters.test.ts | 21 +++++++++++-------- packages/expect/src/matchers.ts | 7 ++++++- 3 files changed, 25 insertions(+), 12 deletions(-) diff --git a/packages/expect-utils/src/utils.ts b/packages/expect-utils/src/utils.ts index 54ef864a90fb..052b1c6e1511 100644 --- a/packages/expect-utils/src/utils.ts +++ b/packages/expect-utils/src/utils.ts @@ -365,10 +365,10 @@ export const arrayBufferEquality = ( return true; }; -// TODO: Update with customTesters export const sparseArrayEquality = ( a: unknown, b: unknown, + customTesters: Array = [], ): boolean | undefined => { if (!Array.isArray(a) || !Array.isArray(b)) { return undefined; @@ -378,7 +378,12 @@ export const sparseArrayEquality = ( const aKeys = Object.keys(a); const bKeys = Object.keys(b); return ( - equals(a, b, [iterableEquality, typeEquality], true) && equals(aKeys, bKeys) + equals( + a, + b, + customTesters.filter(t => t !== sparseArrayEquality), + true, + ) && equals(aKeys, bKeys) ); }; diff --git a/packages/expect/src/__tests__/customEqualityTesters.test.ts b/packages/expect/src/__tests__/customEqualityTesters.test.ts index e319579eee10..535c0a0fa4b7 100644 --- a/packages/expect/src/__tests__/customEqualityTesters.test.ts +++ b/packages/expect/src/__tests__/customEqualityTesters.test.ts @@ -59,7 +59,7 @@ it('has no custom testers as default', () => { const special1 = createSpecialObject(1); const special2 = createSpecialObject(2); - // Basic matchers passing + // Basic matchers passing with default settings expect(special1).toBe(special1); expect(special1).toEqual(special1); expect([special1, special2]).toEqual([special1, special2]); @@ -69,7 +69,7 @@ it('has no custom testers as default', () => { toIterator([special1, special2]), ); - // Basic matchers not passing + // Basic matchers not passing with default settings expect(special1).not.toBe(special2); expect(createSpecialObject(1)).not.toEqual(createSpecialObject(2)); expect([special1, special2]).not.toEqual([special2, special1]); @@ -80,6 +80,10 @@ it('has no custom testers as default', () => { expect(toIterator([special1, special2])).not.toEqual( toIterator([special2, special1]), ); + expect({a: special1, b: undefined}).not.toStrictEqual({ + a: special2, + b: undefined, + }); }); describe('with custom equality testers', () => { @@ -117,15 +121,14 @@ describe('with custom equality testers', () => { expect(toIterator([special1, special2])).toEqual( toIterator([special2, special1]), ); - expect([createSpecialObject(1)]).toContainEqual(createSpecialObject(2)); - expect({a: createSpecialObject(1)}).toHaveProperty( - 'a', - createSpecialObject(2), - ); + expect([special1]).toContainEqual(special2); + expect({a: special1}).toHaveProperty('a', special2); + expect({a: special1, b: undefined}).toStrictEqual({ + a: special2, + b: undefined, + }); }); - // it('applies custom testers to toStrictEqual', () => {}); - // it('applies custom testers to toMatchObject', () => {}); // TODO: Add tests for other matchers diff --git a/packages/expect/src/matchers.ts b/packages/expect/src/matchers.ts index c1a15cd01f2c..043d52ccd254 100644 --- a/packages/expect/src/matchers.ts +++ b/packages/expect/src/matchers.ts @@ -937,7 +937,12 @@ const matchers: MatchersObject = { promise: this.promise, }; - const pass = equals(received, expected, toStrictEqualTesters, true); + const pass = equals( + received, + expected, + [...toStrictEqualTesters, ...getCustomEqualityTesters()], + true, + ); const message = pass ? () => From ce6aca42c2b64241d7deb013a66e560642367e5d Mon Sep 17 00:00:00 2001 From: Andre Wiggins Date: Wed, 30 Nov 2022 21:07:56 -0800 Subject: [PATCH 05/28] Add customTester support to toMatchObject --- packages/expect-utils/src/utils.ts | 8 +++++--- .../expect/src/__tests__/customEqualityTesters.test.ts | 7 +++++-- packages/expect/src/matchers.ts | 6 +++++- 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/packages/expect-utils/src/utils.ts b/packages/expect-utils/src/utils.ts index 052b1c6e1511..2cce1ae9d715 100644 --- a/packages/expect-utils/src/utils.ts +++ b/packages/expect-utils/src/utils.ts @@ -288,11 +288,13 @@ const isObjectWithKeys = (a: any) => !(a instanceof Array) && !(a instanceof Date); -// TODO: Update with customTesters export const subsetEquality = ( object: unknown, subset: unknown, + customTesters: Array = [], ): boolean | undefined => { + const filteredCustomTesters = customTesters.filter(t => t !== subsetEquality); + // subsetEquality needs to keep track of the references // it has already visited to avoid infinite loops in case // there are circular references in the subset passed to it. @@ -306,7 +308,7 @@ export const subsetEquality = ( return Object.keys(subset).every(key => { if (isObjectWithKeys(subset[key])) { if (seenReferences.has(subset[key])) { - return equals(object[key], subset[key], [iterableEquality]); + return equals(object[key], subset[key], filteredCustomTesters); } seenReferences.set(subset[key], true); } @@ -314,7 +316,7 @@ export const subsetEquality = ( object != null && hasPropertyInObject(object, key) && equals(object[key], subset[key], [ - iterableEquality, + ...filteredCustomTesters, subsetEqualityWithContext(seenReferences), ]); // The main goal of using seenReference is to avoid circular node on tree. diff --git a/packages/expect/src/__tests__/customEqualityTesters.test.ts b/packages/expect/src/__tests__/customEqualityTesters.test.ts index 535c0a0fa4b7..ac4d193af200 100644 --- a/packages/expect/src/__tests__/customEqualityTesters.test.ts +++ b/packages/expect/src/__tests__/customEqualityTesters.test.ts @@ -84,6 +84,7 @@ it('has no custom testers as default', () => { a: special2, b: undefined, }); + expect({a: 1, b: {c: special1}}).not.toMatchObject({a: 1, b: {c: special2}}); }); describe('with custom equality testers', () => { @@ -127,10 +128,12 @@ describe('with custom equality testers', () => { a: special2, b: undefined, }); + expect({a: 1, b: {c: special1}}).toMatchObject({ + a: 1, + b: {c: special2}, + }); }); - // it('applies custom testers to toMatchObject', () => {}); - // TODO: Add tests for other matchers // TODO: Add tests for built-in custom testers (e.g. iterableEquality, subsetObjectEquality) // TODO: Add tests for extended expect matchers that use this.equal(); diff --git a/packages/expect/src/matchers.ts b/packages/expect/src/matchers.ts index 043d52ccd254..0eab4e9e8efa 100644 --- a/packages/expect/src/matchers.ts +++ b/packages/expect/src/matchers.ts @@ -903,7 +903,11 @@ const matchers: MatchersObject = { ); } - const pass = equals(received, expected, [iterableEquality, subsetEquality]); + const pass = equals(received, expected, [ + iterableEquality, + subsetEquality, + ...getCustomEqualityTesters(), + ]); const message = pass ? () => From b5684ce1867779c5f9e997f8b3d93b7fde09e6a4 Mon Sep 17 00:00:00 2001 From: Andre Wiggins Date: Wed, 30 Nov 2022 21:40:51 -0800 Subject: [PATCH 06/28] Add customTesters to asymmetric matchers --- .../src/__tests__/customEqualityTesters.test.ts | 16 ++++++++++++++++ packages/expect/src/asymmetricMatchers.ts | 12 +++++++++--- 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/packages/expect/src/__tests__/customEqualityTesters.test.ts b/packages/expect/src/__tests__/customEqualityTesters.test.ts index ac4d193af200..2bf9b4427a05 100644 --- a/packages/expect/src/__tests__/customEqualityTesters.test.ts +++ b/packages/expect/src/__tests__/customEqualityTesters.test.ts @@ -85,6 +85,12 @@ it('has no custom testers as default', () => { b: undefined, }); expect({a: 1, b: {c: special1}}).not.toMatchObject({a: 1, b: {c: special2}}); + + // Asymmetric matchers + expect([special1]).not.toEqual(expect.arrayContaining([special2])); + expect({a: 1, b: {c: special1}}).not.toEqual( + expect.objectContaining({b: {c: special2}}), + ); }); describe('with custom equality testers', () => { @@ -134,6 +140,16 @@ describe('with custom equality testers', () => { }); }); + it('asymmetric matchers with custom testers', () => { + const special1 = createSpecialObject(1); + const special2 = createSpecialObject(2); + + expect([special1]).toEqual(expect.arrayContaining([special2])); + expect({a: 1, b: {c: special1}}).toEqual( + expect.objectContaining({b: {c: special2}}), + ); + }); + // TODO: Add tests for other matchers // TODO: Add tests for built-in custom testers (e.g. iterableEquality, subsetObjectEquality) // TODO: Add tests for extended expect matchers that use this.equal(); diff --git a/packages/expect/src/asymmetricMatchers.ts b/packages/expect/src/asymmetricMatchers.ts index 5c51bd921d8b..5915a645eafd 100644 --- a/packages/expect/src/asymmetricMatchers.ts +++ b/packages/expect/src/asymmetricMatchers.ts @@ -14,7 +14,7 @@ import { } from '@jest/expect-utils'; import * as matcherUtils from 'jest-matcher-utils'; import {pluralize} from 'jest-util'; -import {getState} from './jestMatchersObject'; +import {getCustomEqualityTesters, getState} from './jestMatchersObject'; import type { AsymmetricMatcher as AsymmetricMatcherInterface, MatcherContext, @@ -197,7 +197,9 @@ class ArrayContaining extends AsymmetricMatcher> { this.sample.length === 0 || (Array.isArray(other) && this.sample.every(item => - other.some(another => equals(item, another)), + other.some(another => + equals(item, another, getCustomEqualityTesters()), + ), )); return this.inverse ? !result : result; @@ -230,7 +232,11 @@ class ObjectContaining extends AsymmetricMatcher> { for (const property in this.sample) { if ( !hasProperty(other, property) || - !equals(this.sample[property], other[property]) + !equals( + this.sample[property], + other[property], + getCustomEqualityTesters(), + ) ) { result = false; break; From 2fa13360eef508fa8f9f89696f0834b8b77fa008 Mon Sep 17 00:00:00 2001 From: Andre Wiggins Date: Wed, 30 Nov 2022 22:36:10 -0800 Subject: [PATCH 07/28] Add customTesters to spy matchers --- .../__tests__/customEqualityTesters.test.ts | 42 ++++++++++++++++--- packages/expect/src/spyMatchers.ts | 3 +- 2 files changed, 39 insertions(+), 6 deletions(-) diff --git a/packages/expect/src/__tests__/customEqualityTesters.test.ts b/packages/expect/src/__tests__/customEqualityTesters.test.ts index 2bf9b4427a05..aa0afc6dacb7 100644 --- a/packages/expect/src/__tests__/customEqualityTesters.test.ts +++ b/packages/expect/src/__tests__/customEqualityTesters.test.ts @@ -14,10 +14,10 @@ const specialObjSymbol = Symbol('special test object type'); interface SpecialObject { $$special: symbol; - value: number; + value: number | string; } -function createSpecialObject(value: number) { +function createSpecialObject(value: number | string) { return { [specialObjPropName]: specialObjSymbol, value, @@ -55,6 +55,17 @@ function* toIterator(array: Array): Iterator { } } +const specialArg1 = createSpecialObject('arg1'); +const specialArg2 = createSpecialObject('arg2'); +const specialArg3 = createSpecialObject('arg3'); +const specialArg4 = createSpecialObject('arg4'); +const specialReturn1 = createSpecialObject('return1'); +const specialReturn2 = createSpecialObject('return2'); + +const testArgs = [specialArg1, specialArg2, [specialArg3, specialArg4]]; +// Swap the order of args to assert customer tester does not affect test +const expectedArgs = [specialArg2, specialArg1, [specialArg4, specialArg3]]; + it('has no custom testers as default', () => { const special1 = createSpecialObject(1); const special2 = createSpecialObject(2); @@ -91,6 +102,18 @@ it('has no custom testers as default', () => { expect({a: 1, b: {c: special1}}).not.toEqual( expect.objectContaining({b: {c: special2}}), ); + + // Spy matchers + const mockFn = jest.fn(() => specialReturn1); + mockFn(...testArgs); + + expect(mockFn).not.toHaveBeenCalledWith(...expectedArgs); + expect(mockFn).not.toHaveBeenLastCalledWith(...expectedArgs); + expect(mockFn).not.toHaveBeenNthCalledWith(1, ...expectedArgs); + + expect(mockFn).not.toHaveReturnedWith(specialReturn2); + expect(mockFn).not.toHaveLastReturnedWith(specialReturn2); + expect(mockFn).not.toHaveNthReturnedWith(1, specialReturn2); }); describe('with custom equality testers', () => { @@ -150,7 +173,16 @@ describe('with custom equality testers', () => { ); }); - // TODO: Add tests for other matchers - // TODO: Add tests for built-in custom testers (e.g. iterableEquality, subsetObjectEquality) - // TODO: Add tests for extended expect matchers that use this.equal(); + it('spy matchers with custom testers', () => { + const mockFn = jest.fn(() => specialReturn1); + mockFn(...testArgs); + + expect(mockFn).toHaveBeenCalledWith(...expectedArgs); + expect(mockFn).toHaveBeenLastCalledWith(...expectedArgs); + expect(mockFn).toHaveBeenNthCalledWith(1, ...expectedArgs); + + expect(mockFn).toHaveReturnedWith(specialReturn2); + expect(mockFn).toHaveLastReturnedWith(specialReturn2); + expect(mockFn).toHaveNthReturnedWith(1, specialReturn2); + }); }); diff --git a/packages/expect/src/spyMatchers.ts b/packages/expect/src/spyMatchers.ts index bb7b9e1e8702..95b9a196d08d 100644 --- a/packages/expect/src/spyMatchers.ts +++ b/packages/expect/src/spyMatchers.ts @@ -22,6 +22,7 @@ import { printWithType, stringify, } from 'jest-matcher-utils'; +import {getCustomEqualityTesters} from './jestMatchersObject'; import type { MatcherFunction, MatchersObject, @@ -59,7 +60,7 @@ const printReceivedArgs = ( const printCommon = (val: unknown) => DIM_COLOR(stringify(val)); const isEqualValue = (expected: unknown, received: unknown): boolean => - equals(expected, received, [iterableEquality]); + equals(expected, received, [iterableEquality, ...getCustomEqualityTesters()]); const isEqualCall = ( expected: Array, From e414bcc7b45c166ca1f03e31631acbf4136db7cf Mon Sep 17 00:00:00 2001 From: Andre Wiggins Date: Wed, 30 Nov 2022 22:43:12 -0800 Subject: [PATCH 08/28] Add test for custom matcher --- .../__tests__/customEqualityTesters.test.ts | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/packages/expect/src/__tests__/customEqualityTesters.test.ts b/packages/expect/src/__tests__/customEqualityTesters.test.ts index aa0afc6dacb7..9f07678d2cf3 100644 --- a/packages/expect/src/__tests__/customEqualityTesters.test.ts +++ b/packages/expect/src/__tests__/customEqualityTesters.test.ts @@ -66,6 +66,28 @@ const testArgs = [specialArg1, specialArg2, [specialArg3, specialArg4]]; // Swap the order of args to assert customer tester does not affect test const expectedArgs = [specialArg2, specialArg1, [specialArg4, specialArg3]]; +declare module '../types' { + interface Matchers { + toSpecialObjectEqual(expected: SpecialObject): R; + } +} + +jestExpect.extend({ + toSpecialObjectEqual(expected: SpecialObject, actual: SpecialObject) { + const result = this.equals( + expected, + actual, + jestExpect.customEqualityTesters, + ); + + return { + message: () => + `Expected special object: ${expected.value}. Actual special object: ${actual.value}`, + pass: result, + }; + }, +}); + it('has no custom testers as default', () => { const special1 = createSpecialObject(1); const special2 = createSpecialObject(2); @@ -114,6 +136,9 @@ it('has no custom testers as default', () => { expect(mockFn).not.toHaveReturnedWith(specialReturn2); expect(mockFn).not.toHaveLastReturnedWith(specialReturn2); expect(mockFn).not.toHaveNthReturnedWith(1, specialReturn2); + + // Custom matchers + expect(special1).not.toSpecialObjectEqual(special2); }); describe('with custom equality testers', () => { @@ -185,4 +210,8 @@ describe('with custom equality testers', () => { expect(mockFn).toHaveLastReturnedWith(specialReturn2); expect(mockFn).toHaveNthReturnedWith(1, specialReturn2); }); + + it('custom matchers with custom testers', () => { + expect(createSpecialObject(1)).toSpecialObjectEqual(createSpecialObject(2)); + }); }); From 944934fb394a5bf943db97658bf7ceceb951c908 Mon Sep 17 00:00:00 2001 From: Andre Wiggins Date: Wed, 30 Nov 2022 23:54:46 -0800 Subject: [PATCH 09/28] Clean up new tests a bit --- .../__tests__/customEqualityTesters.test.ts | 152 +++++++++--------- 1 file changed, 77 insertions(+), 75 deletions(-) diff --git a/packages/expect/src/__tests__/customEqualityTesters.test.ts b/packages/expect/src/__tests__/customEqualityTesters.test.ts index 9f07678d2cf3..cbf9412bbc43 100644 --- a/packages/expect/src/__tests__/customEqualityTesters.test.ts +++ b/packages/expect/src/__tests__/customEqualityTesters.test.ts @@ -14,10 +14,10 @@ const specialObjSymbol = Symbol('special test object type'); interface SpecialObject { $$special: symbol; - value: number | string; + value: string; } -function createSpecialObject(value: number | string) { +function createSpecialObject(value: string) { return { [specialObjPropName]: specialObjSymbol, value, @@ -55,17 +55,6 @@ function* toIterator(array: Array): Iterator { } } -const specialArg1 = createSpecialObject('arg1'); -const specialArg2 = createSpecialObject('arg2'); -const specialArg3 = createSpecialObject('arg3'); -const specialArg4 = createSpecialObject('arg4'); -const specialReturn1 = createSpecialObject('return1'); -const specialReturn2 = createSpecialObject('return2'); - -const testArgs = [specialArg1, specialArg2, [specialArg3, specialArg4]]; -// Swap the order of args to assert customer tester does not affect test -const expectedArgs = [specialArg2, specialArg1, [specialArg4, specialArg3]]; - declare module '../types' { interface Matchers { toSpecialObjectEqual(expected: SpecialObject): R; @@ -88,57 +77,82 @@ jestExpect.extend({ }, }); -it('has no custom testers as default', () => { - const special1 = createSpecialObject(1); - const special2 = createSpecialObject(2); - - // Basic matchers passing with default settings - expect(special1).toBe(special1); - expect(special1).toEqual(special1); - expect([special1, special2]).toEqual([special1, special2]); - expect(new Set([special1])).toEqual(new Set([special1])); - expect(new Map([['key', special1]])).toEqual(new Map([['key', special1]])); - expect(toIterator([special1, special2])).toEqual( - toIterator([special1, special2]), - ); +const special1 = createSpecialObject('1'); +const special2 = createSpecialObject('2'); - // Basic matchers not passing with default settings - expect(special1).not.toBe(special2); - expect(createSpecialObject(1)).not.toEqual(createSpecialObject(2)); - expect([special1, special2]).not.toEqual([special2, special1]); - expect(new Set([special1])).not.toEqual(new Set([special2])); - expect(new Map([['key', special1]])).not.toEqual( - new Map([['key', special2]]), - ); - expect(toIterator([special1, special2])).not.toEqual( - toIterator([special2, special1]), - ); - expect({a: special1, b: undefined}).not.toStrictEqual({ - a: special2, - b: undefined, +const specialArg1 = createSpecialObject('arg1'); +const specialArg2 = createSpecialObject('arg2'); +const specialArg3 = createSpecialObject('arg3'); +const specialArg4 = createSpecialObject('arg4'); +const specialReturn1 = createSpecialObject('return1'); +const specialReturn2 = createSpecialObject('return2'); + +const testArgs = [specialArg1, specialArg2, [specialArg3, specialArg4]]; +// Swap the order of args to assert customer tester does not affect test +const expectedArgs = [specialArg2, specialArg1, [specialArg4, specialArg3]]; + +describe('without custom equality testers', () => { + it('basic matchers correctly match the same special objects', () => { + // Basic matchers passing with default settings + expect(special1).toBe(special1); + expect(special1).toEqual(special1); + expect([special1, special2]).toEqual([special1, special2]); + expect(new Set([special1])).toEqual(new Set([special1])); + expect(new Map([['key', special1]])).toEqual(new Map([['key', special1]])); + expect(toIterator([special1, special2])).toEqual( + toIterator([special1, special2]), + ); + expect({a: special1, b: undefined}).toStrictEqual({ + a: special1, + b: undefined, + }); + expect({a: 1, b: {c: special1}}).toMatchObject({a: 1, b: {c: special1}}); }); - expect({a: 1, b: {c: special1}}).not.toMatchObject({a: 1, b: {c: special2}}); - // Asymmetric matchers - expect([special1]).not.toEqual(expect.arrayContaining([special2])); - expect({a: 1, b: {c: special1}}).not.toEqual( - expect.objectContaining({b: {c: special2}}), - ); + it('basic matchers do not pass different special objects', () => { + expect(special1).not.toBe(special2); + expect(special1).not.toEqual(special2); + expect([special1, special2]).not.toEqual([special2, special1]); + expect(new Set([special1])).not.toEqual(new Set([special2])); + expect(new Map([['key', special1]])).not.toEqual( + new Map([['key', special2]]), + ); + expect(toIterator([special1, special2])).not.toEqual( + toIterator([special2, special1]), + ); + expect({a: special1, b: undefined}).not.toStrictEqual({ + a: special2, + b: undefined, + }); + expect({a: 1, b: {c: special1}}).not.toMatchObject({ + a: 1, + b: {c: special2}, + }); + }); - // Spy matchers - const mockFn = jest.fn(() => specialReturn1); - mockFn(...testArgs); + it('asymmetric matchers do not pass different special objects', () => { + expect([special1]).not.toEqual(expect.arrayContaining([special2])); + expect({a: 1, b: {c: special1}}).not.toEqual( + expect.objectContaining({b: {c: special2}}), + ); + }); + + it('spy matchers do not pass different special objects', () => { + const mockFn = jest.fn(() => specialReturn1); + mockFn(...testArgs); - expect(mockFn).not.toHaveBeenCalledWith(...expectedArgs); - expect(mockFn).not.toHaveBeenLastCalledWith(...expectedArgs); - expect(mockFn).not.toHaveBeenNthCalledWith(1, ...expectedArgs); + expect(mockFn).not.toHaveBeenCalledWith(...expectedArgs); + expect(mockFn).not.toHaveBeenLastCalledWith(...expectedArgs); + expect(mockFn).not.toHaveBeenNthCalledWith(1, ...expectedArgs); - expect(mockFn).not.toHaveReturnedWith(specialReturn2); - expect(mockFn).not.toHaveLastReturnedWith(specialReturn2); - expect(mockFn).not.toHaveNthReturnedWith(1, specialReturn2); + expect(mockFn).not.toHaveReturnedWith(specialReturn2); + expect(mockFn).not.toHaveLastReturnedWith(specialReturn2); + expect(mockFn).not.toHaveNthReturnedWith(1, specialReturn2); + }); - // Custom matchers - expect(special1).not.toSpecialObjectEqual(special2); + it('custom matcher does not pass different special objects', () => { + expect(special1).not.toSpecialObjectEqual(special2); + }); }); describe('with custom equality testers', () => { @@ -153,21 +167,12 @@ describe('with custom equality testers', () => { jestExpect.customEqualityTesters = originalTesters; }); - it('basic matchers customTesters do not apply to', () => { - const special1 = createSpecialObject(1); - const special2 = createSpecialObject(2); - - expect(special1).toBe(special1); - expect([special1]).toContain(special1); - + it('basic matchers customTesters do not apply to still do not pass different special objects', () => { expect(special1).not.toBe(special2); expect([special1]).not.toContain(special2); }); - it('basic matchers customTesters do apply to', () => { - const special1 = createSpecialObject(1); - const special2 = createSpecialObject(2); - + it('basic matchers pass different special objects', () => { expect(special1).toEqual(special1); expect(special1).toEqual(special2); expect([special1, special2]).toEqual([special2, special1]); @@ -188,17 +193,14 @@ describe('with custom equality testers', () => { }); }); - it('asymmetric matchers with custom testers', () => { - const special1 = createSpecialObject(1); - const special2 = createSpecialObject(2); - + it('asymmetric matchers pass different special objects', () => { expect([special1]).toEqual(expect.arrayContaining([special2])); expect({a: 1, b: {c: special1}}).toEqual( expect.objectContaining({b: {c: special2}}), ); }); - it('spy matchers with custom testers', () => { + it('spy matchers pass different special objects', () => { const mockFn = jest.fn(() => specialReturn1); mockFn(...testArgs); @@ -211,7 +213,7 @@ describe('with custom equality testers', () => { expect(mockFn).toHaveNthReturnedWith(1, specialReturn2); }); - it('custom matchers with custom testers', () => { - expect(createSpecialObject(1)).toSpecialObjectEqual(createSpecialObject(2)); + it('custom matchers pass different special objects', () => { + expect(special1).toSpecialObjectEqual(special2); }); }); From 8a8bc17861cb3c83df14685e0aff097535f857a2 Mon Sep 17 00:00:00 2001 From: Andre Wiggins Date: Thu, 1 Dec 2022 00:18:41 -0800 Subject: [PATCH 10/28] Add support for customTesters to matcher recommendations in errors --- .../__tests__/customEqualityTesters.test.ts | 16 ++++++++++++++ packages/expect/src/matchers.ts | 21 ++++++++++++++++--- 2 files changed, 34 insertions(+), 3 deletions(-) diff --git a/packages/expect/src/__tests__/customEqualityTesters.test.ts b/packages/expect/src/__tests__/customEqualityTesters.test.ts index cbf9412bbc43..a89ee889539f 100644 --- a/packages/expect/src/__tests__/customEqualityTesters.test.ts +++ b/packages/expect/src/__tests__/customEqualityTesters.test.ts @@ -216,4 +216,20 @@ describe('with custom equality testers', () => { it('custom matchers pass different special objects', () => { expect(special1).toSpecialObjectEqual(special2); }); + + it('toBe recommends toStrictEqual', () => { + expect(() => expect(special1).toBe(special2)).toThrow('toStrictEqual'); + }); + + it('toBe recommends toEqual', () => { + expect(() => + expect({a: undefined, b: special1}).toBe({b: special2}), + ).toThrow('toEqual'); + }); + + it('toContains recommends toContainEquals', () => { + expect(() => expect([special1]).toContain(special2)).toThrow( + 'toContainEqual', + ); + }); }); diff --git a/packages/expect/src/matchers.ts b/packages/expect/src/matchers.ts index 0eab4e9e8efa..4c7283de73a2 100644 --- a/packages/expect/src/matchers.ts +++ b/packages/expect/src/matchers.ts @@ -98,9 +98,21 @@ const matchers: MatchersObject = { if (expectedType !== 'map' && expectedType !== 'set') { // If deep equality passes when referential identity fails, // but exclude map and set until review of their equality logic. - if (equals(received, expected, toStrictEqualTesters, true)) { + if ( + equals( + received, + expected, + [...toStrictEqualTesters, ...getCustomEqualityTesters()], + true, + ) + ) { deepEqualityName = 'toStrictEqual'; - } else if (equals(received, expected, [iterableEquality])) { + } else if ( + equals(received, expected, [ + iterableEquality, + ...getCustomEqualityTesters(), + ]) + ) { deepEqualityName = 'toEqual'; } } @@ -541,7 +553,10 @@ const matchers: MatchersObject = { }` + (!isNot && indexable.findIndex(item => - equals(item, expected, [iterableEquality]), + equals(item, expected, [ + iterableEquality, + ...getCustomEqualityTesters(), + ]), ) !== -1 ? `\n\n${SUGGEST_TO_CONTAIN_EQUAL}` : '') From 1ee862f6985f3267d49596adf16e80039114e174 Mon Sep 17 00:00:00 2001 From: Andre Wiggins Date: Thu, 1 Dec 2022 00:58:57 -0800 Subject: [PATCH 11/28] Give custom testers higher priority over built-in testers --- packages/expect/src/matchers.ts | 16 ++++++++-------- packages/expect/src/spyMatchers.ts | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/expect/src/matchers.ts b/packages/expect/src/matchers.ts index 4c7283de73a2..375128ae2c1a 100644 --- a/packages/expect/src/matchers.ts +++ b/packages/expect/src/matchers.ts @@ -102,15 +102,15 @@ const matchers: MatchersObject = { equals( received, expected, - [...toStrictEqualTesters, ...getCustomEqualityTesters()], + [...getCustomEqualityTesters(), ...toStrictEqualTesters], true, ) ) { deepEqualityName = 'toStrictEqual'; } else if ( equals(received, expected, [ - iterableEquality, ...getCustomEqualityTesters(), + iterableEquality, ]) ) { deepEqualityName = 'toEqual'; @@ -554,8 +554,8 @@ const matchers: MatchersObject = { (!isNot && indexable.findIndex(item => equals(item, expected, [ - iterableEquality, ...getCustomEqualityTesters(), + iterableEquality, ]), ) !== -1 ? `\n\n${SUGGEST_TO_CONTAIN_EQUAL}` @@ -586,7 +586,7 @@ const matchers: MatchersObject = { } const index = Array.from(received).findIndex(item => - equals(item, expected, [iterableEquality, ...getCustomEqualityTesters()]), + equals(item, expected, [...getCustomEqualityTesters(), iterableEquality]), ); const pass = index !== -1; @@ -622,8 +622,8 @@ const matchers: MatchersObject = { }; const pass = equals(received, expected, [ - iterableEquality, ...getCustomEqualityTesters(), + iterableEquality, ]); const message = pass @@ -768,8 +768,8 @@ const matchers: MatchersObject = { const pass = hasValue && endPropIsDefined ? equals(value, expectedValue, [ - iterableEquality, ...getCustomEqualityTesters(), + iterableEquality, ]) : Boolean(hasEndProp); @@ -919,9 +919,9 @@ const matchers: MatchersObject = { } const pass = equals(received, expected, [ + ...getCustomEqualityTesters(), iterableEquality, subsetEquality, - ...getCustomEqualityTesters(), ]); const message = pass @@ -959,7 +959,7 @@ const matchers: MatchersObject = { const pass = equals( received, expected, - [...toStrictEqualTesters, ...getCustomEqualityTesters()], + [...getCustomEqualityTesters(), ...toStrictEqualTesters], true, ); diff --git a/packages/expect/src/spyMatchers.ts b/packages/expect/src/spyMatchers.ts index 95b9a196d08d..3c7c1e1decb4 100644 --- a/packages/expect/src/spyMatchers.ts +++ b/packages/expect/src/spyMatchers.ts @@ -60,7 +60,7 @@ const printReceivedArgs = ( const printCommon = (val: unknown) => DIM_COLOR(stringify(val)); const isEqualValue = (expected: unknown, received: unknown): boolean => - equals(expected, received, [iterableEquality, ...getCustomEqualityTesters()]); + equals(expected, received, [...getCustomEqualityTesters(), iterableEquality]); const isEqualCall = ( expected: Array, From 3961c375d404c9986233df63a608c03a2c796474 Mon Sep 17 00:00:00 2001 From: Andre Wiggins Date: Thu, 1 Dec 2022 01:08:25 -0800 Subject: [PATCH 12/28] Add custom testers to getObjectSubset --- packages/expect-utils/src/utils.ts | 19 +++++++++++++++---- .../customEqualityTesters.test.ts.snap | 17 +++++++++++++++++ .../__tests__/customEqualityTesters.test.ts | 12 +++++++++--- packages/expect/src/matchers.ts | 2 +- 4 files changed, 42 insertions(+), 8 deletions(-) create mode 100644 packages/expect/src/__tests__/__snapshots__/customEqualityTesters.test.ts.snap diff --git a/packages/expect-utils/src/utils.ts b/packages/expect-utils/src/utils.ts index 2cce1ae9d715..cffd4319d1c2 100644 --- a/packages/expect-utils/src/utils.ts +++ b/packages/expect-utils/src/utils.ts @@ -97,13 +97,13 @@ export const getPath = ( }; }; -// TODO: Update with customTesters // Strip properties from object that are not present in the subset. Useful for // printing the diff for toMatchObject() without adding unrelated noise. /* eslint-disable @typescript-eslint/explicit-module-boundary-types */ export const getObjectSubset = ( object: any, subset: any, + customTesters: Array = [], seenReferences: WeakMap = new WeakMap(), ): any => { /* eslint-enable @typescript-eslint/explicit-module-boundary-types */ @@ -111,13 +111,19 @@ export const getObjectSubset = ( if (Array.isArray(subset) && subset.length === object.length) { // The map method returns correct subclass of subset. return subset.map((sub: any, i: number) => - getObjectSubset(object[i], sub), + getObjectSubset(object[i], sub, customTesters), ); } } else if (object instanceof Date) { return object; } else if (isObject(object) && isObject(subset)) { - if (equals(object, subset, [iterableEquality, subsetEquality])) { + if ( + equals(object, subset, [ + ...customTesters, + iterableEquality, + subsetEquality, + ]) + ) { // Avoid unnecessary copy which might return Object instead of subclass. return subset; } @@ -130,7 +136,12 @@ export const getObjectSubset = ( .forEach(key => { trimmed[key] = seenReferences.has(object[key]) ? seenReferences.get(object[key]) - : getObjectSubset(object[key], subset[key], seenReferences); + : getObjectSubset( + object[key], + subset[key], + customTesters, + seenReferences, + ); }); if (Object.keys(trimmed).length > 0) { diff --git a/packages/expect/src/__tests__/__snapshots__/customEqualityTesters.test.ts.snap b/packages/expect/src/__tests__/__snapshots__/customEqualityTesters.test.ts.snap new file mode 100644 index 000000000000..a4cbfe46985f --- /dev/null +++ b/packages/expect/src/__tests__/__snapshots__/customEqualityTesters.test.ts.snap @@ -0,0 +1,17 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`with custom equality testers toMatchObject error shows special objects as equal 1`] = ` +"expect(received).toMatchObject(expected) + +- Expected - 1 ++ Received + 1 + + Object { +- "a": 2, ++ "a": 1, + "b": Object { + "$$special": Symbol(special test object type), + "value": "2", + }, + }" +`; diff --git a/packages/expect/src/__tests__/customEqualityTesters.test.ts b/packages/expect/src/__tests__/customEqualityTesters.test.ts index a89ee889539f..4e11c021ca0f 100644 --- a/packages/expect/src/__tests__/customEqualityTesters.test.ts +++ b/packages/expect/src/__tests__/customEqualityTesters.test.ts @@ -217,19 +217,25 @@ describe('with custom equality testers', () => { expect(special1).toSpecialObjectEqual(special2); }); - it('toBe recommends toStrictEqual', () => { + it('toBe recommends toStrictEqual even with different special objects', () => { expect(() => expect(special1).toBe(special2)).toThrow('toStrictEqual'); }); - it('toBe recommends toEqual', () => { + it('toBe recommends toEqual even with different special objects', () => { expect(() => expect({a: undefined, b: special1}).toBe({b: special2}), ).toThrow('toEqual'); }); - it('toContains recommends toContainEquals', () => { + it('toContains recommends toContainEquals even with different special objects', () => { expect(() => expect([special1]).toContain(special2)).toThrow( 'toContainEqual', ); }); + + it('toMatchObject error shows special objects as equal', () => { + expect(() => + expect({a: 1, b: special1}).toMatchObject({a: 2, b: special2}), + ).toThrowErrorMatchingSnapshot(); + }); }); diff --git a/packages/expect/src/matchers.ts b/packages/expect/src/matchers.ts index 375128ae2c1a..3c9c0dc66040 100644 --- a/packages/expect/src/matchers.ts +++ b/packages/expect/src/matchers.ts @@ -939,7 +939,7 @@ const matchers: MatchersObject = { '\n\n' + printDiffOrStringify( expected, - getObjectSubset(received, expected), + getObjectSubset(received, expected, getCustomEqualityTesters()), EXPECTED_LABEL, RECEIVED_LABEL, isExpand(this.expand), From 7bdcded3fc7f27b4d9b5513e3a8b36b0e7bfcfd9 Mon Sep 17 00:00:00 2001 From: Andre Wiggins Date: Thu, 1 Dec 2022 12:50:11 -0800 Subject: [PATCH 13/28] Add CHANGELOG entry --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 413fb973a41b..623369438946 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ### Features +- `[expect, @jest/expect-utils]` Support custom equality testers + ### Fixes ### Chore & Maintenance From 144f35105bb2e3f8943b4772df09767d2a24c3b1 Mon Sep 17 00:00:00 2001 From: Andre Wiggins Date: Thu, 1 Dec 2022 14:29:15 -0800 Subject: [PATCH 14/28] Fix customEqualityTesters TS errors --- packages/expect/src/__tests__/customEqualityTesters.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/expect/src/__tests__/customEqualityTesters.test.ts b/packages/expect/src/__tests__/customEqualityTesters.test.ts index 4e11c021ca0f..48d331d79bd4 100644 --- a/packages/expect/src/__tests__/customEqualityTesters.test.ts +++ b/packages/expect/src/__tests__/customEqualityTesters.test.ts @@ -138,7 +138,7 @@ describe('without custom equality testers', () => { }); it('spy matchers do not pass different special objects', () => { - const mockFn = jest.fn(() => specialReturn1); + const mockFn: (...args: Array) => any = jest.fn(() => specialReturn1); mockFn(...testArgs); expect(mockFn).not.toHaveBeenCalledWith(...expectedArgs); @@ -201,7 +201,7 @@ describe('with custom equality testers', () => { }); it('spy matchers pass different special objects', () => { - const mockFn = jest.fn(() => specialReturn1); + const mockFn: (...args: Array) => any = jest.fn(() => specialReturn1); mockFn(...testArgs); expect(mockFn).toHaveBeenCalledWith(...expectedArgs); From 1c9b629a495fc5e24d18c23fb7953ffe3fdee8d8 Mon Sep 17 00:00:00 2001 From: Andre Wiggins <459878+andrewiggins@users.noreply.github.com> Date: Fri, 2 Dec 2022 03:34:48 -0800 Subject: [PATCH 15/28] Update packages/expect/src/__tests__/customEqualityTesters.test.ts Co-authored-by: Tom Mrazauskas --- .../expect/src/__tests__/customEqualityTesters.test.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/expect/src/__tests__/customEqualityTesters.test.ts b/packages/expect/src/__tests__/customEqualityTesters.test.ts index 48d331d79bd4..97e5eb7074fb 100644 --- a/packages/expect/src/__tests__/customEqualityTesters.test.ts +++ b/packages/expect/src/__tests__/customEqualityTesters.test.ts @@ -138,7 +138,9 @@ describe('without custom equality testers', () => { }); it('spy matchers do not pass different special objects', () => { - const mockFn: (...args: Array) => any = jest.fn(() => specialReturn1); + const mockFn = jest.fn<(...args: Array) => unknown>( + () => specialReturn1, + ); mockFn(...testArgs); expect(mockFn).not.toHaveBeenCalledWith(...expectedArgs); @@ -201,7 +203,9 @@ describe('with custom equality testers', () => { }); it('spy matchers pass different special objects', () => { - const mockFn: (...args: Array) => any = jest.fn(() => specialReturn1); + const mockFn = jest.fn<(...args: Array) => unknown>( + () => specialReturn1, + ); mockFn(...testArgs); expect(mockFn).toHaveBeenCalledWith(...expectedArgs); From 999827950074f9aed2fcc08a3bb235830f68a3f5 Mon Sep 17 00:00:00 2001 From: Andre Wiggins Date: Thu, 15 Dec 2022 21:26:00 -0800 Subject: [PATCH 16/28] Change API to addEqualityTesters --- .../__tests__/customEqualityTesters.test.ts | 83 +------------------ packages/expect/src/asymmetricMatchers.ts | 1 + packages/expect/src/index.ts | 23 ++--- packages/expect/src/jestMatchersObject.ts | 6 +- packages/expect/src/types.ts | 3 +- 5 files changed, 14 insertions(+), 102 deletions(-) diff --git a/packages/expect/src/__tests__/customEqualityTesters.test.ts b/packages/expect/src/__tests__/customEqualityTesters.test.ts index 97e5eb7074fb..6cc725eed10e 100644 --- a/packages/expect/src/__tests__/customEqualityTesters.test.ts +++ b/packages/expect/src/__tests__/customEqualityTesters.test.ts @@ -63,11 +63,7 @@ declare module '../types' { jestExpect.extend({ toSpecialObjectEqual(expected: SpecialObject, actual: SpecialObject) { - const result = this.equals( - expected, - actual, - jestExpect.customEqualityTesters, - ); + const result = this.equals(expected, actual, this.customTesters); return { message: () => @@ -91,84 +87,9 @@ const testArgs = [specialArg1, specialArg2, [specialArg3, specialArg4]]; // Swap the order of args to assert customer tester does not affect test const expectedArgs = [specialArg2, specialArg1, [specialArg4, specialArg3]]; -describe('without custom equality testers', () => { - it('basic matchers correctly match the same special objects', () => { - // Basic matchers passing with default settings - expect(special1).toBe(special1); - expect(special1).toEqual(special1); - expect([special1, special2]).toEqual([special1, special2]); - expect(new Set([special1])).toEqual(new Set([special1])); - expect(new Map([['key', special1]])).toEqual(new Map([['key', special1]])); - expect(toIterator([special1, special2])).toEqual( - toIterator([special1, special2]), - ); - expect({a: special1, b: undefined}).toStrictEqual({ - a: special1, - b: undefined, - }); - expect({a: 1, b: {c: special1}}).toMatchObject({a: 1, b: {c: special1}}); - }); - - it('basic matchers do not pass different special objects', () => { - expect(special1).not.toBe(special2); - expect(special1).not.toEqual(special2); - expect([special1, special2]).not.toEqual([special2, special1]); - expect(new Set([special1])).not.toEqual(new Set([special2])); - expect(new Map([['key', special1]])).not.toEqual( - new Map([['key', special2]]), - ); - expect(toIterator([special1, special2])).not.toEqual( - toIterator([special2, special1]), - ); - expect({a: special1, b: undefined}).not.toStrictEqual({ - a: special2, - b: undefined, - }); - expect({a: 1, b: {c: special1}}).not.toMatchObject({ - a: 1, - b: {c: special2}, - }); - }); - - it('asymmetric matchers do not pass different special objects', () => { - expect([special1]).not.toEqual(expect.arrayContaining([special2])); - expect({a: 1, b: {c: special1}}).not.toEqual( - expect.objectContaining({b: {c: special2}}), - ); - }); - - it('spy matchers do not pass different special objects', () => { - const mockFn = jest.fn<(...args: Array) => unknown>( - () => specialReturn1, - ); - mockFn(...testArgs); - - expect(mockFn).not.toHaveBeenCalledWith(...expectedArgs); - expect(mockFn).not.toHaveBeenLastCalledWith(...expectedArgs); - expect(mockFn).not.toHaveBeenNthCalledWith(1, ...expectedArgs); - - expect(mockFn).not.toHaveReturnedWith(specialReturn2); - expect(mockFn).not.toHaveLastReturnedWith(specialReturn2); - expect(mockFn).not.toHaveNthReturnedWith(1, specialReturn2); - }); - - it('custom matcher does not pass different special objects', () => { - expect(special1).not.toSpecialObjectEqual(special2); - }); -}); +expect.addEqualityTesters([specialObjTester]); describe('with custom equality testers', () => { - let originalTesters: Array; - - beforeAll(() => { - originalTesters = jestExpect.customEqualityTesters; - jestExpect.customEqualityTesters = [...originalTesters, specialObjTester]; - }); - - afterAll(() => { - jestExpect.customEqualityTesters = originalTesters; - }); - it('basic matchers customTesters do not apply to still do not pass different special objects', () => { expect(special1).not.toBe(special2); expect([special1]).not.toContain(special2); diff --git a/packages/expect/src/asymmetricMatchers.ts b/packages/expect/src/asymmetricMatchers.ts index 5915a645eafd..26541c624714 100644 --- a/packages/expect/src/asymmetricMatchers.ts +++ b/packages/expect/src/asymmetricMatchers.ts @@ -73,6 +73,7 @@ export abstract class AsymmetricMatcher protected getMatcherContext(): MatcherContext { return { + customTesters: getCustomEqualityTesters(), // eslint-disable-next-line @typescript-eslint/no-empty-function dontThrow: () => {}, ...getState(), diff --git a/packages/expect/src/index.ts b/packages/expect/src/index.ts index 6e8de8d1a65c..a553320a5c8b 100644 --- a/packages/expect/src/index.ts +++ b/packages/expect/src/index.ts @@ -8,12 +8,7 @@ /* eslint-disable local/prefer-spread-eventually */ -import { - Tester, - equals, - iterableEquality, - subsetEquality, -} from '@jest/expect-utils'; +import {equals, iterableEquality, subsetEquality} from '@jest/expect-utils'; import * as matcherUtils from 'jest-matcher-utils'; import {isPromise} from 'jest-util'; import { @@ -33,10 +28,10 @@ import { import extractExpectedAssertionsErrors from './extractExpectedAssertionsErrors'; import { INTERNAL_MATCHER_FLAG, + addCustomEqualityTesters, getCustomEqualityTesters, getMatchers, getState, - setCustomEqualityTesters, setMatchers, setState, } from './jestMatchersObject'; @@ -284,6 +279,7 @@ const makeThrowingMatcher = ( }; const matcherUtilsThing: MatcherUtils = { + customTesters: getCustomEqualityTesters(), // When throws is disabled, the matcher will not throw errors during test // execution but instead add them to the global matcher state. If a // matcher throws, test execution is normally stopped immediately. The @@ -390,17 +386,8 @@ const makeThrowingMatcher = ( expect.extend = (matchers: MatchersObject) => setMatchers(matchers, false, expect); -expect.customEqualityTesters = []; // To make TypeScript happy -Object.defineProperty(expect, 'customEqualityTesters', { - configurable: true, - enumerable: true, - get() { - return getCustomEqualityTesters(); - }, - set(newTesters: Array) { - setCustomEqualityTesters(newTesters); - }, -}); +expect.addEqualityTesters = customTesters => + addCustomEqualityTesters(customTesters); expect.anything = anything; expect.any = any; diff --git a/packages/expect/src/jestMatchersObject.ts b/packages/expect/src/jestMatchersObject.ts index 7776d1c3deae..2cb600be6772 100644 --- a/packages/expect/src/jestMatchersObject.ts +++ b/packages/expect/src/jestMatchersObject.ts @@ -128,7 +128,7 @@ export const setMatchers = ( export const getCustomEqualityTesters = (): Array => (globalThis as any)[JEST_MATCHERS_OBJECT].customEqualityTesters; -export const setCustomEqualityTesters = (newTesters: Array): void => { +export const addCustomEqualityTesters = (newTesters: Array): void => { if (!Array.isArray(newTesters)) { throw new TypeError( `expect.customEqualityTesters: Must be set to an array of Testers. Was given "${getType( @@ -137,5 +137,7 @@ export const setCustomEqualityTesters = (newTesters: Array): void => { ); } - (globalThis as any)[JEST_MATCHERS_OBJECT].customEqualityTesters = newTesters; + (globalThis as any)[JEST_MATCHERS_OBJECT].customEqualityTesters.push( + ...newTesters, + ); }; diff --git a/packages/expect/src/types.ts b/packages/expect/src/types.ts index 9f3afccd9529..2f449b124d94 100644 --- a/packages/expect/src/types.ts +++ b/packages/expect/src/types.ts @@ -46,6 +46,7 @@ export type ThrowingMatcherFn = (actual: any) => void; export type PromiseMatcherFn = (actual: any) => Promise; export interface MatcherUtils { + customTesters: Array; dontThrow(): void; equals: EqualsFunction; utils: typeof jestMatcherUtils & { @@ -86,7 +87,7 @@ export type ExpectedAssertionsErrors = Array<{ export interface BaseExpect { assertions(numberOfAssertions: number): void; - customEqualityTesters: Array; + addEqualityTesters(testers: Array): void; extend(matchers: MatchersObject): void; extractExpectedAssertionsErrors(): ExpectedAssertionsErrors; getState(): MatcherState; From 9fcf9b5a736b515c23c9b6597cd8903575577b39 Mon Sep 17 00:00:00 2001 From: Andre Wiggins Date: Thu, 15 Dec 2022 21:36:27 -0800 Subject: [PATCH 17/28] Get customTesters from matcherContext --- packages/expect/src/asymmetricMatchers.ts | 6 ++++-- packages/expect/src/matchers.ts | 22 +++++++++------------- 2 files changed, 13 insertions(+), 15 deletions(-) diff --git a/packages/expect/src/asymmetricMatchers.ts b/packages/expect/src/asymmetricMatchers.ts index 26541c624714..d4ab8f8f374f 100644 --- a/packages/expect/src/asymmetricMatchers.ts +++ b/packages/expect/src/asymmetricMatchers.ts @@ -194,12 +194,13 @@ class ArrayContaining extends AsymmetricMatcher> { ); } + const matcherContext = this.getMatcherContext(); const result = this.sample.length === 0 || (Array.isArray(other) && this.sample.every(item => other.some(another => - equals(item, another, getCustomEqualityTesters()), + equals(item, another, matcherContext.customTesters), ), )); @@ -230,13 +231,14 @@ class ObjectContaining extends AsymmetricMatcher> { let result = true; + const matcherContext = this.getMatcherContext(); for (const property in this.sample) { if ( !hasProperty(other, property) || !equals( this.sample[property], other[property], - getCustomEqualityTesters(), + matcherContext.customTesters, ) ) { result = false; diff --git a/packages/expect/src/matchers.ts b/packages/expect/src/matchers.ts index 3c9c0dc66040..49cda23321fb 100644 --- a/packages/expect/src/matchers.ts +++ b/packages/expect/src/matchers.ts @@ -38,7 +38,6 @@ import { printWithType, stringify, } from 'jest-matcher-utils'; -import {getCustomEqualityTesters} from './jestMatchersObject'; import { printCloseTo, printExpectedConstructorName, @@ -102,14 +101,14 @@ const matchers: MatchersObject = { equals( received, expected, - [...getCustomEqualityTesters(), ...toStrictEqualTesters], + [...this.customTesters, ...toStrictEqualTesters], true, ) ) { deepEqualityName = 'toStrictEqual'; } else if ( equals(received, expected, [ - ...getCustomEqualityTesters(), + ...this.customTesters, iterableEquality, ]) ) { @@ -553,10 +552,7 @@ const matchers: MatchersObject = { }` + (!isNot && indexable.findIndex(item => - equals(item, expected, [ - ...getCustomEqualityTesters(), - iterableEquality, - ]), + equals(item, expected, [...this.customTesters, iterableEquality]), ) !== -1 ? `\n\n${SUGGEST_TO_CONTAIN_EQUAL}` : '') @@ -586,7 +582,7 @@ const matchers: MatchersObject = { } const index = Array.from(received).findIndex(item => - equals(item, expected, [...getCustomEqualityTesters(), iterableEquality]), + equals(item, expected, [...this.customTesters, iterableEquality]), ); const pass = index !== -1; @@ -622,7 +618,7 @@ const matchers: MatchersObject = { }; const pass = equals(received, expected, [ - ...getCustomEqualityTesters(), + ...this.customTesters, iterableEquality, ]); @@ -768,7 +764,7 @@ const matchers: MatchersObject = { const pass = hasValue && endPropIsDefined ? equals(value, expectedValue, [ - ...getCustomEqualityTesters(), + ...this.customTesters, iterableEquality, ]) : Boolean(hasEndProp); @@ -919,7 +915,7 @@ const matchers: MatchersObject = { } const pass = equals(received, expected, [ - ...getCustomEqualityTesters(), + ...this.customTesters, iterableEquality, subsetEquality, ]); @@ -939,7 +935,7 @@ const matchers: MatchersObject = { '\n\n' + printDiffOrStringify( expected, - getObjectSubset(received, expected, getCustomEqualityTesters()), + getObjectSubset(received, expected, this.customTesters), EXPECTED_LABEL, RECEIVED_LABEL, isExpand(this.expand), @@ -959,7 +955,7 @@ const matchers: MatchersObject = { const pass = equals( received, expected, - [...getCustomEqualityTesters(), ...toStrictEqualTesters], + [...this.customTesters, ...toStrictEqualTesters], true, ); From a791dcb505fa6dd535da99a8d4643d3ce90f8af0 Mon Sep 17 00:00:00 2001 From: Andre Wiggins Date: Thu, 15 Dec 2022 21:57:11 -0800 Subject: [PATCH 18/28] Rename customTesters to filteredCustomTesters --- packages/expect-utils/src/utils.ts | 34 ++++++++++++++----- .../__tests__/customEqualityTesters.test.ts | 12 +++++++ 2 files changed, 38 insertions(+), 8 deletions(-) diff --git a/packages/expect-utils/src/utils.ts b/packages/expect-utils/src/utils.ts index cffd4319d1c2..3f751e211a8a 100644 --- a/packages/expect-utils/src/utils.ts +++ b/packages/expect-utils/src/utils.ts @@ -191,10 +191,20 @@ export const iterableEquality = ( aStack.push(a); bStack.push(b); + // eslint-disable-next-line prefer-const + let filteredCustomTesters: Array; const iterableEqualityWithStack = (a: any, b: any) => - iterableEquality(a, b, [...customTesters], [...aStack], [...bStack]); - - customTesters = [ + iterableEquality( + a, + b, + [...filteredCustomTesters], + [...aStack], + [...bStack], + ); + + // Replace any instance of iterableEquality with the new + // iterableEqualityWithStack so we can do circular detection + filteredCustomTesters = [ ...customTesters.filter(t => t !== iterableEquality), iterableEqualityWithStack, ]; @@ -208,7 +218,7 @@ export const iterableEquality = ( if (!b.has(aValue)) { let has = false; for (const bValue of b) { - const isEqual = equals(aValue, bValue, customTesters); + const isEqual = equals(aValue, bValue, filteredCustomTesters); if (isEqual === true) { has = true; } @@ -232,15 +242,23 @@ export const iterableEquality = ( for (const aEntry of a) { if ( !b.has(aEntry[0]) || - !equals(aEntry[1], b.get(aEntry[0]), customTesters) + !equals(aEntry[1], b.get(aEntry[0]), filteredCustomTesters) ) { let has = false; for (const bEntry of b) { - const matchedKey = equals(aEntry[0], bEntry[0], customTesters); + const matchedKey = equals( + aEntry[0], + bEntry[0], + filteredCustomTesters, + ); let matchedValue = false; if (matchedKey === true) { - matchedValue = equals(aEntry[1], bEntry[1], customTesters); + matchedValue = equals( + aEntry[1], + bEntry[1], + filteredCustomTesters, + ); } if (matchedValue === true) { has = true; @@ -264,7 +282,7 @@ export const iterableEquality = ( for (const aValue of a) { const nextB = bIterator.next(); - if (nextB.done || !equals(aValue, nextB.value, customTesters)) { + if (nextB.done || !equals(aValue, nextB.value, filteredCustomTesters)) { return false; } } diff --git a/packages/expect/src/__tests__/customEqualityTesters.test.ts b/packages/expect/src/__tests__/customEqualityTesters.test.ts index 6cc725eed10e..44bb669982e6 100644 --- a/packages/expect/src/__tests__/customEqualityTesters.test.ts +++ b/packages/expect/src/__tests__/customEqualityTesters.test.ts @@ -163,4 +163,16 @@ describe('with custom equality testers', () => { expect({a: 1, b: special1}).toMatchObject({a: 2, b: special2}), ).toThrowErrorMatchingSnapshot(); }); + + it('iterableEquality still properly detects cycles', () => { + const a = new Set(); + a.add(special1); + a.add(a); + + const b = new Set(); + b.add(special2); + b.add(b); + + expect(a).toEqual(b); + }); }); From 32d0e389b6a0b5ab15570687ecf800960c774292 Mon Sep 17 00:00:00 2001 From: Andre Wiggins Date: Thu, 15 Dec 2022 22:09:40 -0800 Subject: [PATCH 19/28] Add type tests for new API --- packages/expect/__typetests__/expect.test.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/packages/expect/__typetests__/expect.test.ts b/packages/expect/__typetests__/expect.test.ts index 9bc1ccae24cb..f8a950d56d0e 100644 --- a/packages/expect/__typetests__/expect.test.ts +++ b/packages/expect/__typetests__/expect.test.ts @@ -48,6 +48,7 @@ expectType( expectType>(this.suppressedErrors); expectType(this.testPath); expectType(this.utils); + expectType>(this.customTesters); const pass = actual >= floor && actual <= ceiling; if (pass) { @@ -79,6 +80,17 @@ declare module 'expect' { expectType(expect(100).toBeWithinRange(90, 110)); expectType(expect(101).not.toBeWithinRange(0, 100)); +expectType( + expect.addEqualityTesters([ + (a, b, customTesters) => { + expectType(a); + expectType(b); + expectType>(customTesters); + return true; + }, + ]), +); + expectType( expect({apples: 6, bananas: 3}).toEqual({ apples: expect.toBeWithinRange(1, 10), From c9dc27c1bf57a3ab020ebaf35c41152839f4f0e4 Mon Sep 17 00:00:00 2001 From: Andre Wiggins Date: Sun, 1 Jan 2023 16:56:56 -0800 Subject: [PATCH 20/28] Add docs, pt. 1 --- docs/ExpectAPI.md | 145 +++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 143 insertions(+), 2 deletions(-) diff --git a/docs/ExpectAPI.md b/docs/ExpectAPI.md index 54d2d16a9d48..b195e0008878 100644 --- a/docs/ExpectAPI.md +++ b/docs/ExpectAPI.md @@ -300,9 +300,9 @@ A string allowing you to display a clear and correct matcher hint: - `'resolves'` if matcher was called with the promise `.resolves` modifier - `''` if matcher was not called with a promise modifier -#### `this.equals(a, b)` +#### `this.equals(a, b, customTesters?)` -This is a deep-equality function that will return `true` if two objects have the same values (recursively). +This is a deep-equality function that will return `true` if two objects have the same values (recursively). It optionally takes a list of custom equality testers to apply to the deep equality checks (see `this.customTesters` below). #### `this.expand` @@ -366,6 +366,10 @@ This will print something like this: When an assertion fails, the error message should give as much signal as necessary to the user so they can resolve their issue quickly. You should craft a precise failure message to make sure users of your custom assertions have a good developer experience. +#### `this.customTesters` + +If your matcher does a deep equality check using `this.equals`, you may want to pass user provided custom testers to `this.equals`. The custom equality testers that the user has provided using the `addEqualityTesters` API are available on this property. The built-in Jest matchers pass `this.customTesters` (along with other built-in testers) to `this.equals` to do deep equality, and your custom matchers may want to do the same. + #### Custom snapshot matchers To use snapshot testing inside of your custom matcher you can import `jest-snapshot` and use it from within your matcher. @@ -495,6 +499,143 @@ it('transitions as expected', () => { }); ``` +### `expect.addEqualityTesters(testers)` + +You can use `expect.addEqualityTesters` to add your own methods to test if two objects are equal. For example, let's say you have a class in your code that represents volume and it supports determining if two volumes using different units are equal or not. You may want `toEqual` (and other equality matchers) to use this custom equality method when comparing to Volume classes. You can add a custom equality tester to have `toEqual` detect and apply custom logic when comparing Volume classes: + +```js title="Volume.js" +// For simplicity in this example, we'll just support the units 'L' and 'mL' +export class Volume { + constructor(amount, unit) { + this.amount = amount; + this.unit = unit; + } + + toString() { + return `[Volume ${this.amount}${this.unit}]`; + } + + equals(other) { + if (this.unit === other.unit) { + return this.amount === other.amount; + } else if (this.unit === 'L' && other.unit === 'mL') { + return this.amount * 1000 === other.unit; + } else { + return this.amount === other.unit * 1000; + } + } +} +``` + +```js title="areVolumesEqual.js" +import {expect} from '@jest/globals'; +import {Volume} from './Volume.js'; + +function areVolumesEqual(a, b) { + const isAVolume = a instanceof Volume; + const isBVolume = b instanceof Volume; + + if (isAVolume && isBVolume) { + return a.equals(b); + } else if (isAVolume !== isBVolume) { + return false; + } else { + return undefined; + } +} + +expect.addEqualityTesters([areVolumesEqual]); +``` + +```js title="__tests__/Volume.test.js" +import {expect, test} from '@jest/globals'; +import {Volume} from '../Volume.js'; +import '../areVolumesEqual.js'; + +test('are equal with different units', () => { + expect(new Volume(1, 'L')).toEqual(new Volume(1000, 'mL')); +}); +``` + +```ts title="Volume.ts" +// For simplicity in this example, we'll just support the units 'L' and 'mL' +export class Volume { + public amount: number; + public unit: 'L' | 'mL'; + + constructor(amount: number, unit: 'L' | 'mL') { + this.amount = amount; + this.unit = unit; + } + + toString(): string { + return `[Volume ${this.amount}${this.unit}]`; + } + + equals(other: Volume): boolean { + if (this.unit === other.unit) { + return this.amount === other.amount; + } else if (this.unit === 'L' && other.unit === 'mL') { + return this.amount * 1000 === other.amount; + } else { + return this.amount === other.amount * 1000; + } + } +} +``` + +```ts title="areVolumesEqual.ts" +import {expect} from '@jest/globals'; +import {Volume} from './Volume.js'; + +function areVolumesEqual(a: unknown, b: unknown): boolean | undefined { + const isAVolume = a instanceof Volume; + const isBVolume = b instanceof Volume; + + if (isAVolume && isBVolume) { + return a.equals(b); + } else if (isAVolume !== isBVolume) { + return false; + } else { + return undefined; + } +} + +expect.addEqualityTesters([areVolumesEqual]); +``` + +```ts title="__tests__/Volume.test.ts" +import {expect, test} from '@jest/globals'; +import {Volume} from '../Volume.js'; +import '../areVolumesEqual.js'; + +test('are equal with different units', () => { + expect(new Volume(1, 'L')).toEqual(new Volume(1000, 'mL')); +}); +``` + +#### Custom equality testers API + +Custom testers are functions that return either the result (`true` or `false`) of comparing the equality of the two given arguments or `undefined` if tester does not handle the given the objects and wants to delegate equality to other testers (for example, the built in equality testers). + +Custom testers are called with 3 arguments: the two objects to compare and the array of custom testers (used for recursive testers, see section below). + +#### Matchers vs Testers + +Matchers are methods available on `expect`, for example `expect().toEqual()`. `toEqual` is a matcher. A tester is a method used by matchers that do equality checks to determine if objects are the same. + +Custom matchers are good to use when you want to provide a custom assertion that test authors can use in their tests. For example, the `toBeWithinRange` example in the `expect.extend` section is a good example of a custom matcher. Sometimes a test author may want to assert two numbers are exactly equal and should use `toBe`. Other times however, a test author may want to allow for some flexibility in their test and `toBeWithinRange` may be a more appropriate assertion. + +Custom equality testers are good to use for globally extending Jest matchers to apply custom equality logic for all equality comparisons. Test authors can't turn on custom testers for certain assertions and turn off for others (a custom matcher should be used instead if that behavior is desired). For example, defining how to check if two `Volume` objects are equal for all matchers would be a good custom equality tester. + +#### Using custom testers in custom matchers + +TODO + +#### Recursive custom testers + +TODO + ### `expect.anything()` `expect.anything()` matches anything but `null` or `undefined`. You can use it inside `toEqual` or `toBeCalledWith` instead of a literal value. For example, if you want to check that a mock function is called with a non-null argument: From d22295b3664f1293be36fb8bee760b2eeb6860b1 Mon Sep 17 00:00:00 2001 From: Andre Wiggins Date: Sun, 1 Jan 2023 17:29:32 -0800 Subject: [PATCH 21/28] Reorganize code to obsolete eslint ignore --- packages/expect-utils/src/utils.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/expect-utils/src/utils.ts b/packages/expect-utils/src/utils.ts index 17ab6d8f92f6..32a403ba4874 100644 --- a/packages/expect-utils/src/utils.ts +++ b/packages/expect-utils/src/utils.ts @@ -191,8 +191,6 @@ export const iterableEquality = ( aStack.push(a); bStack.push(b); - // eslint-disable-next-line prefer-const - let filteredCustomTesters: Array; const iterableEqualityWithStack = (a: any, b: any) => iterableEquality( a, @@ -204,7 +202,7 @@ export const iterableEquality = ( // Replace any instance of iterableEquality with the new // iterableEqualityWithStack so we can do circular detection - filteredCustomTesters = [ + const filteredCustomTesters: Array = [ ...customTesters.filter(t => t !== iterableEquality), iterableEqualityWithStack, ]; From 8591181a1172acd4a7df6116d260bbc694b4ddb0 Mon Sep 17 00:00:00 2001 From: Andre Wiggins Date: Sun, 1 Jan 2023 17:37:12 -0800 Subject: [PATCH 22/28] Convert custom equal testers tests to use Volume object matching docs --- .../customEqualityTesters.test.ts.snap | 8 +- .../__tests__/customEqualityTesters.test.ts | 165 +++++++++--------- 2 files changed, 91 insertions(+), 82 deletions(-) diff --git a/packages/expect/src/__tests__/__snapshots__/customEqualityTesters.test.ts.snap b/packages/expect/src/__tests__/__snapshots__/customEqualityTesters.test.ts.snap index a4cbfe46985f..7f36dfe58990 100644 --- a/packages/expect/src/__tests__/__snapshots__/customEqualityTesters.test.ts.snap +++ b/packages/expect/src/__tests__/__snapshots__/customEqualityTesters.test.ts.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`with custom equality testers toMatchObject error shows special objects as equal 1`] = ` +exports[`with custom equality testers toMatchObject error shows Volume objects as equal 1`] = ` "expect(received).toMatchObject(expected) - Expected - 1 @@ -9,9 +9,9 @@ exports[`with custom equality testers toMatchObject error shows special objects Object { - "a": 2, + "a": 1, - "b": Object { - "$$special": Symbol(special test object type), - "value": "2", + "b": Volume { + "amount": 1000, + "unit": "mL", }, }" `; diff --git a/packages/expect/src/__tests__/customEqualityTesters.test.ts b/packages/expect/src/__tests__/customEqualityTesters.test.ts index 44bb669982e6..3620220609c9 100644 --- a/packages/expect/src/__tests__/customEqualityTesters.test.ts +++ b/packages/expect/src/__tests__/customEqualityTesters.test.ts @@ -9,40 +9,48 @@ import type {Tester} from '@jest/expect-utils'; import jestExpect from '../'; -const specialObjPropName = '$$special'; -const specialObjSymbol = Symbol('special test object type'); +class Volume { + public amount: number; + public unit: 'L' | 'mL'; -interface SpecialObject { - $$special: symbol; - value: string; + constructor(amount: number, unit: 'L' | 'mL') { + this.amount = amount; + this.unit = unit; + } + + toString(): string { + return `[Volume ${this.amount}${this.unit}]`; + } + + equals(other: Volume): boolean { + if (this.unit === other.unit) { + return this.amount === other.amount; + } else if (this.unit === 'L' && other.unit === 'mL') { + return this.amount * 1000 === other.amount; + } else { + return this.amount === other.amount * 1000; + } + } } -function createSpecialObject(value: string) { - return { - [specialObjPropName]: specialObjSymbol, - value, - }; +function createVolume(amount: number, unit: 'L' | 'mL' = 'L') { + return new Volume(amount, unit); } -function isSpecialObject(a: unknown): a is SpecialObject { - return ( - a != null && - typeof a === 'object' && - specialObjPropName in a && - (a as any)[specialObjPropName] === specialObjSymbol - ); +function isVolume(a: unknown): a is Volume { + return a instanceof Volume; } -const specialObjTester: Tester = ( +const areVolumesEqual: Tester = ( a: unknown, b: unknown, ): boolean | undefined => { - const isASpecial = isSpecialObject(a); - const isBSpecial = isSpecialObject(b); + const isAVolume = isVolume(a); + const isBVolume = isVolume(b); - if (isASpecial && isBSpecial) { - return true; - } else if ((isASpecial && !isBSpecial) || (!isASpecial && isBSpecial)) { + if (isAVolume && isBVolume) { + return a.equals(b); + } else if (isAVolume !== isBVolume) { return false; } else { return undefined; @@ -57,75 +65,76 @@ function* toIterator(array: Array): Iterator { declare module '../types' { interface Matchers { - toSpecialObjectEqual(expected: SpecialObject): R; + toEqualVolume(expected: Volume): R; } } jestExpect.extend({ - toSpecialObjectEqual(expected: SpecialObject, actual: SpecialObject) { + toEqualVolume(expected: Volume, actual: Volume) { const result = this.equals(expected, actual, this.customTesters); return { message: () => - `Expected special object: ${expected.value}. Actual special object: ${actual.value}`, + `Expected Volume object: ${expected.toString()}. Actual Volume object: ${actual.toString()}`, pass: result, }; }, }); -const special1 = createSpecialObject('1'); -const special2 = createSpecialObject('2'); +const volume1 = createVolume(1, 'L'); +const volume2 = createVolume(1000, 'mL'); + +const volumeArg1 = createVolume(1, 'L'); +const volumeArg2 = createVolume(1000, 'mL'); +const volumeArg3 = createVolume(2, 'L'); +const volumeArg4 = createVolume(2000, 'mL'); -const specialArg1 = createSpecialObject('arg1'); -const specialArg2 = createSpecialObject('arg2'); -const specialArg3 = createSpecialObject('arg3'); -const specialArg4 = createSpecialObject('arg4'); -const specialReturn1 = createSpecialObject('return1'); -const specialReturn2 = createSpecialObject('return2'); +const volumeReturn1 = createVolume(2, 'L'); +const volumeReturn2 = createVolume(2000, 'mL'); -const testArgs = [specialArg1, specialArg2, [specialArg3, specialArg4]]; -// Swap the order of args to assert customer tester does not affect test -const expectedArgs = [specialArg2, specialArg1, [specialArg4, specialArg3]]; +const testArgs = [volumeArg1, volumeArg2, [volumeArg3, volumeArg4]]; +// Swap the order of args to assert custom tester does not affect test +const expectedArgs = [volumeArg2, volumeArg1, [volumeArg4, volumeArg3]]; -expect.addEqualityTesters([specialObjTester]); +expect.addEqualityTesters([areVolumesEqual]); describe('with custom equality testers', () => { - it('basic matchers customTesters do not apply to still do not pass different special objects', () => { - expect(special1).not.toBe(special2); - expect([special1]).not.toContain(special2); + it('basic matchers customTesters do not apply to still do not pass different Volume objects', () => { + expect(volume1).not.toBe(volume2); + expect([volume1]).not.toContain(volume2); }); - it('basic matchers pass different special objects', () => { - expect(special1).toEqual(special1); - expect(special1).toEqual(special2); - expect([special1, special2]).toEqual([special2, special1]); - expect(new Map([['key', special1]])).toEqual(new Map([['key', special2]])); - expect(new Set([special1])).toEqual(new Set([special2])); - expect(toIterator([special1, special2])).toEqual( - toIterator([special2, special1]), + it('basic matchers pass different Volume objects', () => { + expect(volume1).toEqual(volume1); + expect(volume1).toEqual(volume2); + expect([volume1, volume2]).toEqual([volume2, volume1]); + expect(new Map([['key', volume1]])).toEqual(new Map([['key', volume2]])); + expect(new Set([volume1])).toEqual(new Set([volume2])); + expect(toIterator([volume1, volume2])).toEqual( + toIterator([volume2, volume1]), ); - expect([special1]).toContainEqual(special2); - expect({a: special1}).toHaveProperty('a', special2); - expect({a: special1, b: undefined}).toStrictEqual({ - a: special2, + expect([volume1]).toContainEqual(volume2); + expect({a: volume1}).toHaveProperty('a', volume2); + expect({a: volume1, b: undefined}).toStrictEqual({ + a: volume2, b: undefined, }); - expect({a: 1, b: {c: special1}}).toMatchObject({ + expect({a: 1, b: {c: volume1}}).toMatchObject({ a: 1, - b: {c: special2}, + b: {c: volume2}, }); }); - it('asymmetric matchers pass different special objects', () => { - expect([special1]).toEqual(expect.arrayContaining([special2])); - expect({a: 1, b: {c: special1}}).toEqual( - expect.objectContaining({b: {c: special2}}), + it('asymmetric matchers pass different Volume objects', () => { + expect([volume1]).toEqual(expect.arrayContaining([volume2])); + expect({a: 1, b: {c: volume1}}).toEqual( + expect.objectContaining({b: {c: volume2}}), ); }); - it('spy matchers pass different special objects', () => { + it('spy matchers pass different Volume objects', () => { const mockFn = jest.fn<(...args: Array) => unknown>( - () => specialReturn1, + () => volumeReturn1, ); mockFn(...testArgs); @@ -133,44 +142,44 @@ describe('with custom equality testers', () => { expect(mockFn).toHaveBeenLastCalledWith(...expectedArgs); expect(mockFn).toHaveBeenNthCalledWith(1, ...expectedArgs); - expect(mockFn).toHaveReturnedWith(specialReturn2); - expect(mockFn).toHaveLastReturnedWith(specialReturn2); - expect(mockFn).toHaveNthReturnedWith(1, specialReturn2); + expect(mockFn).toHaveReturnedWith(volumeReturn2); + expect(mockFn).toHaveLastReturnedWith(volumeReturn2); + expect(mockFn).toHaveNthReturnedWith(1, volumeReturn2); }); - it('custom matchers pass different special objects', () => { - expect(special1).toSpecialObjectEqual(special2); + it('custom matchers pass different Volume objects', () => { + expect(volume1).toEqualVolume(volume2); }); - it('toBe recommends toStrictEqual even with different special objects', () => { - expect(() => expect(special1).toBe(special2)).toThrow('toStrictEqual'); + it('toBe recommends toStrictEqual even with different Volume objects', () => { + expect(() => expect(volume1).toBe(volume2)).toThrow('toStrictEqual'); }); - it('toBe recommends toEqual even with different special objects', () => { - expect(() => - expect({a: undefined, b: special1}).toBe({b: special2}), - ).toThrow('toEqual'); + it('toBe recommends toEqual even with different Volume objects', () => { + expect(() => expect({a: undefined, b: volume1}).toBe({b: volume2})).toThrow( + 'toEqual', + ); }); - it('toContains recommends toContainEquals even with different special objects', () => { - expect(() => expect([special1]).toContain(special2)).toThrow( + it('toContains recommends toContainEquals even with different Volume objects', () => { + expect(() => expect([volume1]).toContain(volume2)).toThrow( 'toContainEqual', ); }); - it('toMatchObject error shows special objects as equal', () => { + it('toMatchObject error shows Volume objects as equal', () => { expect(() => - expect({a: 1, b: special1}).toMatchObject({a: 2, b: special2}), + expect({a: 1, b: volume1}).toMatchObject({a: 2, b: volume2}), ).toThrowErrorMatchingSnapshot(); }); it('iterableEquality still properly detects cycles', () => { const a = new Set(); - a.add(special1); + a.add(volume1); a.add(a); const b = new Set(); - b.add(special2); + b.add(volume2); b.add(b); expect(a).toEqual(b); From e88fff23c03d5a1e5e2f9619e5ecfdd5ba11b88e Mon Sep 17 00:00:00 2001 From: Andre Wiggins Date: Sun, 1 Jan 2023 20:11:41 -0800 Subject: [PATCH 23/28] Finish out ExpectAPI docs and add recursive equality tester test --- docs/ExpectAPI.md | 42 +++- ...ustomEqualityTestersRecursive.test.ts.snap | 18 ++ .../__tests__/customEqualityTesters.test.ts | 6 +- .../customEqualityTestersRecursive.test.ts | 223 ++++++++++++++++++ 4 files changed, 284 insertions(+), 5 deletions(-) create mode 100644 packages/expect/src/__tests__/__snapshots__/customEqualityTestersRecursive.test.ts.snap create mode 100644 packages/expect/src/__tests__/customEqualityTestersRecursive.test.ts diff --git a/docs/ExpectAPI.md b/docs/ExpectAPI.md index b195e0008878..2fb97f40e73b 100644 --- a/docs/ExpectAPI.md +++ b/docs/ExpectAPI.md @@ -628,13 +628,47 @@ Custom matchers are good to use when you want to provide a custom assertion that Custom equality testers are good to use for globally extending Jest matchers to apply custom equality logic for all equality comparisons. Test authors can't turn on custom testers for certain assertions and turn off for others (a custom matcher should be used instead if that behavior is desired). For example, defining how to check if two `Volume` objects are equal for all matchers would be a good custom equality tester. -#### Using custom testers in custom matchers +#### Recursive custom equality testers -TODO +If you custom equality testers is testing objects with properties you'd like to do deep equality with, you should use the `equals` helper from the `@jest/expect-utils` package. This `equals` method is the same deep equals method Jest uses internally for all of its deep equality comparisons. Its the method that invokes your custom equality tester. It accepts an array of custom equality testers as a third argument. Custom equality testers are also given an array of custom testers as their third argument. Pass this argument into the third argument of `equals` so that any further equality checks deeper in your object can also take advantage of custom equality testers. -#### Recursive custom testers +For example, let's say you have a `Book` class that contains an array of `Author` classes and both of these classes have custom testers. The `Book` custom tester would want to do a deep equality check on the array of `Author`s and pass in the custom testers so the `Author`s custom equality tester is applied: -TODO +```js title="customEqualityTesters.js" +import {equals} from '@jest/expect-utils'; + +const areAuthorsEqual = (a, b) => { + const isAAuthor = a instanceof Author; + const isBAuthor = b instanceof Author; + + if (isAAuthor && isBAuthor) { + // Authors are equal if they have the same name + return a.name === b.name; + } else if (isAAuthor !== isBAuthor) { + return false; + } else { + return undefined; + } +}; + +const areBooksEqual = (a, b, customTesters) => { + const isABook = a instanceof Book; + const isBBook = b instanceof Book; + + if (isABook && isBBook) { + // Books are the same if they have the same name and author array. We need + // to pass customTesters to equals here so the Author custom tester will be + // used when comparing Authors + return a.name === b.name && equals(a.authors, b.authors, customTesters); + } else if (isABook !== isBBook) { + return false; + } else { + return undefined; + } +}; + +expect.addEqualityTesters([areAuthorsEqual, areBooksEqual]); +``` ### `expect.anything()` diff --git a/packages/expect/src/__tests__/__snapshots__/customEqualityTestersRecursive.test.ts.snap b/packages/expect/src/__tests__/__snapshots__/customEqualityTestersRecursive.test.ts.snap new file mode 100644 index 000000000000..500263eded6b --- /dev/null +++ b/packages/expect/src/__tests__/__snapshots__/customEqualityTestersRecursive.test.ts.snap @@ -0,0 +1,18 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`with custom equality testers toMatchObject error shows Book objects as equal 1`] = ` +"expect(received).toMatchObject(expected) + +- Expected - 1 ++ Received + 1 + +@@ -1,7 +1,7 @@ + Object { +- "a": 2, ++ "a": 1, + "b": Book { + "__connection": 5, + "authors": Array [ + Author { + "__connection": 3," +`; diff --git a/packages/expect/src/__tests__/customEqualityTesters.test.ts b/packages/expect/src/__tests__/customEqualityTesters.test.ts index 3620220609c9..6a6f4cf63c17 100644 --- a/packages/expect/src/__tests__/customEqualityTesters.test.ts +++ b/packages/expect/src/__tests__/customEqualityTesters.test.ts @@ -81,6 +81,10 @@ jestExpect.extend({ }, }); +// Create Volumes with different specifications but the same value for use in +// tests. Without the custom tester, these volumes would not be equal because +// their properties have different values. However, with our custom tester they +// are equal. const volume1 = createVolume(1, 'L'); const volume2 = createVolume(1000, 'mL'); @@ -93,7 +97,7 @@ const volumeReturn1 = createVolume(2, 'L'); const volumeReturn2 = createVolume(2000, 'mL'); const testArgs = [volumeArg1, volumeArg2, [volumeArg3, volumeArg4]]; -// Swap the order of args to assert custom tester does not affect test +// Swap the order of args to assert custom tester sees these volumes as equal const expectedArgs = [volumeArg2, volumeArg1, [volumeArg4, volumeArg3]]; expect.addEqualityTesters([areVolumesEqual]); diff --git a/packages/expect/src/__tests__/customEqualityTestersRecursive.test.ts b/packages/expect/src/__tests__/customEqualityTestersRecursive.test.ts new file mode 100644 index 000000000000..22dba000ac07 --- /dev/null +++ b/packages/expect/src/__tests__/customEqualityTestersRecursive.test.ts @@ -0,0 +1,223 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import {Tester, equals} from '@jest/expect-utils'; +import jestExpect from '../'; + +// Test test file demonstrates and tests the capability of recursive custom +// testers that call `equals` within their tester logic. These testers should +// receive the array of custom testers and be able to pass it into equals + +const CONNECTION_PROP = '__connection'; +type DbConnection = number; +let DbConnectionId = 0; + +class Author { + public name: string; + public [CONNECTION_PROP]: DbConnection; + + constructor(name: string) { + this.name = name; + this[CONNECTION_PROP] = DbConnectionId++; + } +} + +class Book { + public name: string; + public authors: Array; + public [CONNECTION_PROP]: DbConnection; + + constructor(name: string, authors: Array) { + this.name = name; + this.authors = authors; + this[CONNECTION_PROP] = DbConnectionId++; + } +} + +const areAuthorsEqual: Tester = (a: unknown, b: unknown) => { + const isAAuthor = a instanceof Author; + const isBAuthor = b instanceof Author; + + if (isAAuthor && isBAuthor) { + return a.name === b.name; + } else if (isAAuthor !== isBAuthor) { + return false; + } else { + return undefined; + } +}; + +const areBooksEqual: Tester = ( + a: unknown, + b: unknown, + customTesters: Array, +) => { + const isABook = a instanceof Book; + const isBBook = b instanceof Book; + + if (isABook && isBBook) { + return a.name === b.name && equals(a.authors, b.authors, customTesters); + } else if (isABook !== isBBook) { + return false; + } else { + return undefined; + } +}; + +function* toIterator(array: Array): Iterator { + for (const obj of array) { + yield obj; + } +} + +declare module '../types' { + interface Matchers { + toEqualBook(expected: Book): R; + } +} + +jestExpect.extend({ + toEqualBook(expected: Book, actual: Book) { + const result = this.equals(expected, actual, this.customTesters); + + return { + message: () => + `Expected Book object: ${expected.name}. Actual Book object: ${actual.name}`, + pass: result, + }; + }, +}); + +// Create books with the same name and authors for use in tests. Without the +// custom tester, these books would not be equal because their DbConnections +// would have different values. However, with our custom tester they are equal. +const book1 = new Book('Book 1', [ + new Author('Author 1'), + new Author('Author 2'), +]); +const book1b = new Book('Book 1', [ + new Author('Author 1'), + new Author('Author 2'), +]); + +const bookArg1a = new Book('Book Arg 1', [ + new Author('Author Arg 1'), + new Author('Author Arg 2'), +]); +const bookArg1b = new Book('Book Arg 1', [ + new Author('Author Arg 1'), + new Author('Author Arg 2'), +]); +const bookArg2a = new Book('Book Arg 2', [ + new Author('Author Arg 3'), + new Author('Author Arg 4'), +]); +const bookArg2b = new Book('Book Arg 2', [ + new Author('Author Arg 3'), + new Author('Author Arg 4'), +]); + +const bookReturn1a = new Book('Book Return 1', [ + new Author('Author Return 1'), + new Author('Author Return 2'), +]); +const bookReturn1b = new Book('Book Return 1', [ + new Author('Author Return 1'), + new Author('Author Return 2'), +]); + +const testArgs = [bookArg1a, bookArg1b, [bookArg2a, bookArg2b]]; +// Swap the order of args to assert custom tester works correctly and ignores +// DbConnection differences +const expectedArgs = [bookArg1b, bookArg1a, [bookArg2b, bookArg2a]]; + +expect.addEqualityTesters([areAuthorsEqual, areBooksEqual]); + +describe('with custom equality testers', () => { + it('basic matchers customTesters do not apply to still do not pass different Book objects', () => { + expect(book1).not.toBe(book1b); + expect([book1]).not.toContain(book1b); + }); + + it('basic matchers pass different Book objects', () => { + expect(book1).toEqual(book1); + expect(book1).toEqual(book1b); + expect([book1, book1b]).toEqual([book1b, book1]); + expect(new Map([['key', book1]])).toEqual(new Map([['key', book1b]])); + expect(new Set([book1])).toEqual(new Set([book1b])); + expect(toIterator([book1, book1b])).toEqual(toIterator([book1b, book1])); + expect([book1]).toContainEqual(book1b); + expect({a: book1}).toHaveProperty('a', book1b); + expect({a: book1, b: undefined}).toStrictEqual({ + a: book1b, + b: undefined, + }); + expect({a: 1, b: {c: book1}}).toMatchObject({ + a: 1, + b: {c: book1b}, + }); + }); + + it('asymmetric matchers pass different Book objects', () => { + expect([book1]).toEqual(expect.arrayContaining([book1b])); + expect({a: 1, b: {c: book1}}).toEqual( + expect.objectContaining({b: {c: book1b}}), + ); + }); + + it('spy matchers pass different Book objects', () => { + const mockFn = jest.fn<(...args: Array) => unknown>( + () => bookReturn1a, + ); + mockFn(...testArgs); + + expect(mockFn).toHaveBeenCalledWith(...expectedArgs); + expect(mockFn).toHaveBeenLastCalledWith(...expectedArgs); + expect(mockFn).toHaveBeenNthCalledWith(1, ...expectedArgs); + + expect(mockFn).toHaveReturnedWith(bookReturn1b); + expect(mockFn).toHaveLastReturnedWith(bookReturn1b); + expect(mockFn).toHaveNthReturnedWith(1, bookReturn1b); + }); + + it('custom matchers pass different Book objects', () => { + expect(book1).toEqualBook(book1b); + }); + + it('toBe recommends toStrictEqual even with different Book objects', () => { + expect(() => expect(book1).toBe(book1b)).toThrow('toStrictEqual'); + }); + + it('toBe recommends toEqual even with different Book objects', () => { + expect(() => expect({a: undefined, b: book1}).toBe({b: book1b})).toThrow( + 'toEqual', + ); + }); + + it('toContains recommends toContainEquals even with different Book objects', () => { + expect(() => expect([book1]).toContain(book1b)).toThrow('toContainEqual'); + }); + + it('toMatchObject error shows Book objects as equal', () => { + expect(() => + expect({a: 1, b: book1}).toMatchObject({a: 2, b: book1b}), + ).toThrowErrorMatchingSnapshot(); + }); + + it('iterableEquality still properly detects cycles', () => { + const a = new Set(); + a.add(book1); + a.add(a); + + const b = new Set(); + b.add(book1b); + b.add(b); + + expect(a).toEqual(b); + }); +}); From bed9a1eae1219a1a401854c35e80eb1fa6b63fd2 Mon Sep 17 00:00:00 2001 From: Simen Bekkhus Date: Mon, 2 Jan 2023 15:17:40 +0100 Subject: [PATCH 24/28] reorganize --- packages/expect/__typetests__/expect.test.ts | 24 ++++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/packages/expect/__typetests__/expect.test.ts b/packages/expect/__typetests__/expect.test.ts index f8a950d56d0e..69776d153681 100644 --- a/packages/expect/__typetests__/expect.test.ts +++ b/packages/expect/__typetests__/expect.test.ts @@ -22,6 +22,17 @@ expectError(() => { type E = Matchers; }); +expectType( + expect.addEqualityTesters([ + (a, b, customTesters) => { + expectType(a); + expectType(b); + expectType>(customTesters); + return true; + }, + ]), +); + // extend type MatcherUtils = typeof jestMatcherUtils & { @@ -35,6 +46,7 @@ expectType( toBeWithinRange(actual: number, floor: number, ceiling: number) { expectType(this.assertionCalls); expectType(this.currentTestName); + expectType>(this.customTesters); expectType<() => void>(this.dontThrow); expectType(this.error); expectType(this.equals); @@ -48,7 +60,6 @@ expectType( expectType>(this.suppressedErrors); expectType(this.testPath); expectType(this.utils); - expectType>(this.customTesters); const pass = actual >= floor && actual <= ceiling; if (pass) { @@ -80,17 +91,6 @@ declare module 'expect' { expectType(expect(100).toBeWithinRange(90, 110)); expectType(expect(101).not.toBeWithinRange(0, 100)); -expectType( - expect.addEqualityTesters([ - (a, b, customTesters) => { - expectType(a); - expectType(b); - expectType>(customTesters); - return true; - }, - ]), -); - expectType( expect({apples: 6, bananas: 3}).toEqual({ apples: expect.toBeWithinRange(1, 10), From 15e509cc464a4f388bc3ab7876916b505b09ae40 Mon Sep 17 00:00:00 2001 From: Simen Bekkhus Date: Mon, 2 Jan 2023 15:18:29 +0100 Subject: [PATCH 25/28] link in changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bea1c16d8ecd..2fb895c3062e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ ### Features -- `[expect, @jest/expect-utils]` Support custom equality testers +- `[expect, @jest/expect-utils]` Support custom equality testers ([#13654](https://github.com/facebook/jest/pull/13654)) - `[jest-haste-map]` ignore Sapling vcs directories (`.sl/`) ([#13674](https://github.com/facebook/jest/pull/13674)) - `[jest-resolve]` Support subpath imports ([#13705](https://github.com/facebook/jest/pull/13705)) - `[jest-runtime]` Add `jest.isolateModulesAsync` for scoped module initialization of asynchronous functions ([#13680](https://github.com/facebook/jest/pull/13680)) From 96de10d961674578ea29463115ccff5522374ea3 Mon Sep 17 00:00:00 2001 From: Simen Bekkhus Date: Mon, 2 Jan 2023 15:23:44 +0100 Subject: [PATCH 26/28] link section --- docs/ExpectAPI.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/ExpectAPI.md b/docs/ExpectAPI.md index 2fb97f40e73b..8b7d05de058f 100644 --- a/docs/ExpectAPI.md +++ b/docs/ExpectAPI.md @@ -624,7 +624,7 @@ Custom testers are called with 3 arguments: the two objects to compare and the a Matchers are methods available on `expect`, for example `expect().toEqual()`. `toEqual` is a matcher. A tester is a method used by matchers that do equality checks to determine if objects are the same. -Custom matchers are good to use when you want to provide a custom assertion that test authors can use in their tests. For example, the `toBeWithinRange` example in the `expect.extend` section is a good example of a custom matcher. Sometimes a test author may want to assert two numbers are exactly equal and should use `toBe`. Other times however, a test author may want to allow for some flexibility in their test and `toBeWithinRange` may be a more appropriate assertion. +Custom matchers are good to use when you want to provide a custom assertion that test authors can use in their tests. For example, the `toBeWithinRange` example in the [`expect.extend`](#expectextendmatchers) section is a good example of a custom matcher. Sometimes a test author may want to assert two numbers are exactly equal and should use `toBe`. Other times however, a test author may want to allow for some flexibility in their test and `toBeWithinRange` may be a more appropriate assertion. Custom equality testers are good to use for globally extending Jest matchers to apply custom equality logic for all equality comparisons. Test authors can't turn on custom testers for certain assertions and turn off for others (a custom matcher should be used instead if that behavior is desired). For example, defining how to check if two `Volume` objects are equal for all matchers would be a good custom equality tester. From dea49495baa2cadc741f8e8f3fd8384cc50b7a5c Mon Sep 17 00:00:00 2001 From: Andre Wiggins Date: Tue, 3 Jan 2023 00:42:19 -0800 Subject: [PATCH 27/28] Expose equals function on tester context --- docs/ExpectAPI.md | 30 +++++++++++++------ packages/expect-utils/src/index.ts | 2 +- packages/expect-utils/src/jasmineUtils.ts | 10 +++++-- packages/expect-utils/src/types.ts | 7 +++++ packages/expect/__typetests__/expect.test.ts | 11 ++++++- .../customEqualityTestersRecursive.test.ts | 30 +++++++++++++++++-- packages/expect/src/index.ts | 2 ++ 7 files changed, 76 insertions(+), 16 deletions(-) diff --git a/docs/ExpectAPI.md b/docs/ExpectAPI.md index 8b7d05de058f..4c19635d9f28 100644 --- a/docs/ExpectAPI.md +++ b/docs/ExpectAPI.md @@ -620,6 +620,12 @@ Custom testers are functions that return either the result (`true` or `false`) o Custom testers are called with 3 arguments: the two objects to compare and the array of custom testers (used for recursive testers, see section below). +These helper functions and properties can be found on `this` inside a custom tester: + +#### `this.equals(a, b, customTesters?)` + +This is a deep-equality function that will return `true` if two objects have the same values (recursively). It optionally takes a list of custom equality testers to apply to the deep equality checks. If you use this function, pass through the custom testers your tester is given so further equality checks `equals` applies can also use custom testers the test author may have configured. See the example in the [Recursive custom equality testers][#recursivecustomequalitytesters] section for more details. + #### Matchers vs Testers Matchers are methods available on `expect`, for example `expect().toEqual()`. `toEqual` is a matcher. A tester is a method used by matchers that do equality checks to determine if objects are the same. @@ -630,14 +636,12 @@ Custom equality testers are good to use for globally extending Jest matchers to #### Recursive custom equality testers -If you custom equality testers is testing objects with properties you'd like to do deep equality with, you should use the `equals` helper from the `@jest/expect-utils` package. This `equals` method is the same deep equals method Jest uses internally for all of its deep equality comparisons. Its the method that invokes your custom equality tester. It accepts an array of custom equality testers as a third argument. Custom equality testers are also given an array of custom testers as their third argument. Pass this argument into the third argument of `equals` so that any further equality checks deeper in your object can also take advantage of custom equality testers. +If you custom equality testers is testing objects with properties you'd like to do deep equality with, you should use the `this.equals` helper available to equality testers. This `equals` method is the same deep equals method Jest uses internally for all of its deep equality comparisons. Its the method that invokes your custom equality tester. It accepts an array of custom equality testers as a third argument. Custom equality testers are also given an array of custom testers as their third argument. Pass this argument into the third argument of `equals` so that any further equality checks deeper in your object can also take advantage of custom equality testers. -For example, let's say you have a `Book` class that contains an array of `Author` classes and both of these classes have custom testers. The `Book` custom tester would want to do a deep equality check on the array of `Author`s and pass in the custom testers so the `Author`s custom equality tester is applied: +For example, let's say you have a `Book` class that contains an array of `Author` classes and both of these classes have custom testers. The `Book` custom tester would want to do a deep equality check on the array of `Author`s and pass in the custom testers given to it so the `Author`s custom equality tester is applied: ```js title="customEqualityTesters.js" -import {equals} from '@jest/expect-utils'; - -const areAuthorsEqual = (a, b) => { +function areAuthorEqual(a, b) { const isAAuthor = a instanceof Author; const isBAuthor = b instanceof Author; @@ -649,9 +653,9 @@ const areAuthorsEqual = (a, b) => { } else { return undefined; } -}; +} -const areBooksEqual = (a, b, customTesters) => { +function areBooksEqual(a, b, customTesters) { const isABook = a instanceof Book; const isBBook = b instanceof Book; @@ -659,17 +663,25 @@ const areBooksEqual = (a, b, customTesters) => { // Books are the same if they have the same name and author array. We need // to pass customTesters to equals here so the Author custom tester will be // used when comparing Authors - return a.name === b.name && equals(a.authors, b.authors, customTesters); + return ( + a.name === b.name && this.equals(a.authors, b.authors, customTesters) + ); } else if (isABook !== isBBook) { return false; } else { return undefined; } -}; +} expect.addEqualityTesters([areAuthorsEqual, areBooksEqual]); ``` +:::note + +Remember to define your equality testers as regular functions and **not** arrow functions in order to access the tester context helpers (e.g. `this.equals`). + +::: + ### `expect.anything()` `expect.anything()` matches anything but `null` or `undefined`. You can use it inside `toEqual` or `toBeCalledWith` instead of a literal value. For example, if you want to check that a mock function is called with a non-null argument: diff --git a/packages/expect-utils/src/index.ts b/packages/expect-utils/src/index.ts index 6eb5e82d3d91..1cb442e73c51 100644 --- a/packages/expect-utils/src/index.ts +++ b/packages/expect-utils/src/index.ts @@ -10,4 +10,4 @@ export {equals, isA} from './jasmineUtils'; export type {EqualsFunction} from './jasmineUtils'; export * from './utils'; -export type {Tester} from './types'; +export type {Tester, TesterContext} from './types'; diff --git a/packages/expect-utils/src/jasmineUtils.ts b/packages/expect-utils/src/jasmineUtils.ts index 5bf9d3df4ac1..dcaebae99698 100644 --- a/packages/expect-utils/src/jasmineUtils.ts +++ b/packages/expect-utils/src/jasmineUtils.ts @@ -22,7 +22,7 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -import type {Tester} from './types'; +import type {Tester, TesterContext} from './types'; export type EqualsFunction = ( a: unknown, @@ -75,8 +75,14 @@ function eq( return asymmetricResult; } + const testerContext: TesterContext = {equals}; for (let i = 0; i < customTesters.length; i++) { - const customTesterResult = customTesters[i](a, b, customTesters); + const customTesterResult = customTesters[i].call( + testerContext, + a, + b, + customTesters, + ); if (customTesterResult !== undefined) { return customTesterResult; } diff --git a/packages/expect-utils/src/types.ts b/packages/expect-utils/src/types.ts index dbe9b39ac639..66c8644d3868 100644 --- a/packages/expect-utils/src/types.ts +++ b/packages/expect-utils/src/types.ts @@ -6,8 +6,15 @@ * */ +import type {EqualsFunction} from './jasmineUtils'; + export type Tester = ( + this: TesterContext, a: any, b: any, customTesters: Array, ) => boolean | undefined; + +export interface TesterContext { + equals: EqualsFunction; +} diff --git a/packages/expect/__typetests__/expect.test.ts b/packages/expect/__typetests__/expect.test.ts index 69776d153681..1eb27b7bbf4a 100644 --- a/packages/expect/__typetests__/expect.test.ts +++ b/packages/expect/__typetests__/expect.test.ts @@ -6,7 +6,7 @@ */ import {expectAssignable, expectError, expectType} from 'tsd-lite'; -import type {EqualsFunction, Tester} from '@jest/expect-utils'; +import type {EqualsFunction, Tester, TesterContext} from '@jest/expect-utils'; import { MatcherContext, MatcherFunction, @@ -28,8 +28,17 @@ expectType( expectType(a); expectType(b); expectType>(customTesters); + expectType(this); return true; }, + function anotherTester(a, b, customTesters) { + expectType(a); + expectType(b); + expectType>(customTesters); + expectType(this); + expectType(this.equals); + return undefined; + }, ]), ); diff --git a/packages/expect/src/__tests__/customEqualityTestersRecursive.test.ts b/packages/expect/src/__tests__/customEqualityTestersRecursive.test.ts index 22dba000ac07..51f1b54acfa7 100644 --- a/packages/expect/src/__tests__/customEqualityTestersRecursive.test.ts +++ b/packages/expect/src/__tests__/customEqualityTestersRecursive.test.ts @@ -52,16 +52,18 @@ const areAuthorsEqual: Tester = (a: unknown, b: unknown) => { } }; -const areBooksEqual: Tester = ( +const areBooksEqual: Tester = function ( a: unknown, b: unknown, customTesters: Array, -) => { +) { const isABook = a instanceof Book; const isBBook = b instanceof Book; if (isABook && isBBook) { - return a.name === b.name && equals(a.authors, b.authors, customTesters); + return ( + a.name === b.name && this.equals(a.authors, b.authors, customTesters) + ); } else if (isABook !== isBBook) { return false; } else { @@ -139,6 +141,28 @@ const expectedArgs = [bookArg1b, bookArg1a, [bookArg2b, bookArg2a]]; expect.addEqualityTesters([areAuthorsEqual, areBooksEqual]); describe('with custom equality testers', () => { + it('exposes an equality function to custom testers', () => { + const runTestSymbol = Symbol('run this test'); + + // jestExpect and expect share the same global state + expect.assertions(3); + jestExpect.addEqualityTesters([ + function dummyTester(a) { + // Equality testers are globally added. Only run this assertion for this test + if (a === runTestSymbol) { + expect(this.equals).toBe(equals); + return true; + } + + return undefined; + }, + ]); + + expect(() => + jestExpect(runTestSymbol).toEqual(runTestSymbol), + ).not.toThrow(); + }); + it('basic matchers customTesters do not apply to still do not pass different Book objects', () => { expect(book1).not.toBe(book1b); expect([book1]).not.toContain(book1b); diff --git a/packages/expect/src/index.ts b/packages/expect/src/index.ts index a553320a5c8b..62bcbdf72785 100644 --- a/packages/expect/src/index.ts +++ b/packages/expect/src/index.ts @@ -69,6 +69,8 @@ export type { SyncExpectationResult, } from './types'; +// TODO: Re-export Tester and TestContext? + export class JestAssertionError extends Error { matcherResult?: Omit & {message: string}; } From 90afadbfbb0d6029dc741af50967418fda00c9d8 Mon Sep 17 00:00:00 2001 From: Andre Wiggins Date: Tue, 3 Jan 2023 00:46:25 -0800 Subject: [PATCH 28/28] Re-export Tester and TesterContext in expect package --- packages/expect/__typetests__/expect.test.ts | 14 +++++++++++++- packages/expect/src/index.ts | 3 +-- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/packages/expect/__typetests__/expect.test.ts b/packages/expect/__typetests__/expect.test.ts index 1eb27b7bbf4a..a34f1c28f4ee 100644 --- a/packages/expect/__typetests__/expect.test.ts +++ b/packages/expect/__typetests__/expect.test.ts @@ -6,12 +6,14 @@ */ import {expectAssignable, expectError, expectType} from 'tsd-lite'; -import type {EqualsFunction, Tester, TesterContext} from '@jest/expect-utils'; +import type {EqualsFunction} from '@jest/expect-utils'; import { MatcherContext, MatcherFunction, MatcherFunctionWithContext, Matchers, + Tester, + TesterContext, expect, } from 'expect'; import type * as jestMatcherUtils from 'jest-matcher-utils'; @@ -22,8 +24,18 @@ expectError(() => { type E = Matchers; }); +const tester1: Tester = function (a, b, customTesters) { + expectType(a); + expectType(b); + expectType>(customTesters); + expectType(this); + expectType(this.equals); + return undefined; +}; + expectType( expect.addEqualityTesters([ + tester1, (a, b, customTesters) => { expectType(a); expectType(b); diff --git a/packages/expect/src/index.ts b/packages/expect/src/index.ts index 62bcbdf72785..88b913e8f181 100644 --- a/packages/expect/src/index.ts +++ b/packages/expect/src/index.ts @@ -53,6 +53,7 @@ import type { ThrowingMatcherFn, } from './types'; +export type {Tester, TesterContext} from '@jest/expect-utils'; export {AsymmetricMatcher} from './asymmetricMatchers'; export type { AsyncExpectationResult, @@ -69,8 +70,6 @@ export type { SyncExpectationResult, } from './types'; -// TODO: Re-export Tester and TestContext? - export class JestAssertionError extends Error { matcherResult?: Omit & {message: string}; }