diff --git a/docs/api/expect.md b/docs/api/expect.md index 0d312ab45..91971aedd 100644 --- a/docs/api/expect.md +++ b/docs/api/expect.md @@ -387,7 +387,7 @@ test('getApplesCount has some unusual side effects...', () => { - **Type:** `(sample: Array | Set) => any` -`toBeOneOf` asserts if a value matches any of the values in the provided array or set. +`toBeOneOf` asserts if a value matches any of the values in the provided array or set. Values are compared with the same deep equality as [`toEqual`](#toequal). ::: warning EXPERIMENTAL Providing a `Set` is an experimental feature and may change in a future release. @@ -1298,7 +1298,7 @@ test('spy function returns a value two times', () => { - **Type:** `(returnValue: any) => Awaitable` -You can call this assertion to check if a function has successfully returned a value with certain parameters at least once. Requires a spy function to be passed to `expect`. +You can call this assertion to check if a function has successfully returned a certain value at least once. Requires a spy function to be passed to `expect`. Values are compared with the same deep equality as [`toEqual`](#toequal). ```ts import { expect, test, vi } from 'vitest' @@ -1316,7 +1316,7 @@ test('spy function returns a product', () => { - **Type:** `(returnValue: any) => Awaitable` -You can call this assertion to check if a function has successfully returned a certain value when it was last invoked. Requires a spy function to be passed to `expect`. +You can call this assertion to check if a function has successfully returned a certain value when it was last invoked. Requires a spy function to be passed to `expect`. Values are compared with the same deep equality as [`toEqual`](#toequal). ```ts import { expect, test, vi } from 'vitest' @@ -1335,7 +1335,7 @@ test('spy function returns bananas on a last call', () => { - **Type:** `(time: number, returnValue: any) => Awaitable` -You can call this assertion to check if a function has successfully returned a value with certain parameters on a certain call. Requires a spy function to be passed to `expect`. +You can call this assertion to check if a function has successfully returned a certain value on a certain call. Requires a spy function to be passed to `expect`. Values are compared with the same deep equality as [`toEqual`](#toequal). The count starts at 1. So, to check the second entry, you would write `.toHaveNthReturnedWith(2, ...)`. @@ -1403,7 +1403,7 @@ test('spy function resolved a value two times', async () => { - **Type:** `(returnValue: any) => Awaitable` -You can call this assertion to check if a function has successfully resolved a certain value at least once. Requires a spy function to be passed to `expect`. +You can call this assertion to check if a function has successfully resolved a certain value at least once. Requires a spy function to be passed to `expect`. Values are compared with the same deep equality as [`toEqual`](#toequal). If the function returned a promise, but it was not resolved yet, this will fail. @@ -1423,7 +1423,7 @@ test('spy function resolved a product', async () => { - **Type:** `(returnValue: any) => Awaitable` -You can call this assertion to check if a function has successfully resolved a certain value when it was last invoked. Requires a spy function to be passed to `expect`. +You can call this assertion to check if a function has successfully resolved a certain value when it was last invoked. Requires a spy function to be passed to `expect`. Values are compared with the same deep equality as [`toEqual`](#toequal). If the function returned a promise, but it was not resolved yet, this will fail. @@ -1444,7 +1444,7 @@ test('spy function resolves bananas on a last call', async () => { - **Type:** `(time: number, returnValue: any) => Awaitable` -You can call this assertion to check if a function has successfully resolved a certain value on a specific invocation. Requires a spy function to be passed to `expect`. +You can call this assertion to check if a function has successfully resolved a certain value on a specific invocation. Requires a spy function to be passed to `expect`. Values are compared with the same deep equality as [`toEqual`](#toequal). If the function returned a promise, but it was not resolved yet, this will fail. diff --git a/docs/api/vi.md b/docs/api/vi.md index c8b3c2b80..43760c14f 100644 --- a/docs/api/vi.md +++ b/docs/api/vi.md @@ -828,7 +828,7 @@ function when(spy: Mock, options?: WhenOptions): When Defines per-argument behaviors on a spy, replacing its implementation for the duration of the `when` chain. -Call `.calledWith(...args)` on the returned object to specify which call arguments to match, then chain one or more `then*` methods to declare what the spy should return, throw, or resolve when invoked with those arguments. Arguments are matched with deep equality and support asymmetric matchers such as `expect.any()`. +Call `.calledWith(...args)` on the returned object to specify which call arguments to match, then chain one or more `then*` methods to declare what the spy should return, throw, or resolve when invoked with those arguments. Arguments are matched with the same deep equality as [`toEqual`](/api/expect#toequal) and support asymmetric matchers such as `expect.any()`. ```ts const spy = vi.fn() diff --git a/packages/expect/src/custom-matchers.ts b/packages/expect/src/custom-matchers.ts index 9923bdc62..904e36709 100644 --- a/packages/expect/src/custom-matchers.ts +++ b/packages/expect/src/custom-matchers.ts @@ -29,17 +29,18 @@ ${printReceived(actual)}`, toBeOneOf(actual: unknown, expected: Array | Set) { const { equals, customTesters } = this - const { printReceived, printExpected, matcherHint } = this.utils + const { printReceived, printExpected, matcherHint, iterableEquality } = this.utils + const testers = [...customTesters, iterableEquality] let pass: boolean if (Array.isArray(expected)) { - pass = expected.length === 0 || expected.some((item) => equals(item, actual, customTesters)) + pass = expected.length === 0 || expected.some((item) => equals(item, actual, testers)) } else if (expected instanceof Set) { pass = expected.size === 0 || expected.has(actual) || - [...expected].some((item) => equals(item, actual, customTesters)) + [...expected].some((item) => equals(item, actual, testers)) } else { throw new TypeError( `You must provide an array or set to ${matcherHint('.toBeOneOf')}, not '${typeof expected}'.`, diff --git a/packages/expect/src/jest-expect.ts b/packages/expect/src/jest-expect.ts index 05ab9e8c8..33d0858c1 100644 --- a/packages/expect/src/jest-expect.ts +++ b/packages/expect/src/jest-expect.ts @@ -252,7 +252,7 @@ export const JestChaiExpect: ChaiPlugin = (chai, utils) => { def('toContainEqual', function (expected) { const obj = utils.flag(this, 'object') const index = Array.from(obj).findIndex((item) => { - return jestEquals(item, expected, customTesters) + return jestEquals(item, expected, [...customTesters, iterableEquality]) }) this.assert( @@ -432,7 +432,9 @@ export const JestChaiExpect: ChaiPlugin = (chai, utils) => { return utils.getPathInfo(actual, propertyName) } const { value, exists } = getValue() - const pass = exists && (args.length === 1 || jestEquals(expected, value, customTesters)) + const pass = + exists && + (args.length === 1 || jestEquals(expected, value, [...customTesters, iterableEquality])) const valueString = args.length === 1 ? '' : ` with value ${inspect(expected, { truncate: 40 })}` @@ -741,7 +743,7 @@ export const JestChaiExpect: ChaiPlugin = (chai, utils) => { ) { const matcher = expected as any as AsymmetricMatcher return this.assert( - thrown && matcher.asymmetricMatch(thrown), + thrown && matcher.asymmetricMatch(thrown, [...customTesters, iterableEquality]), 'expected error to match asymmetric matcher', 'expected error not to match asymmetric matcher', matcher, @@ -833,7 +835,9 @@ export const JestChaiExpect: ChaiPlugin = (chai, utils) => { name: 'toHaveResolvedWith', condition: (spy, value) => spy.mock.settledResults.some( - ({ type, value: result }) => type === 'fulfilled' && jestEquals(value, result), + ({ type, value: result }) => + type === 'fulfilled' && + jestEquals(value, result, [...customTesters, iterableEquality]), ), action: 'resolve', }, @@ -841,7 +845,8 @@ export const JestChaiExpect: ChaiPlugin = (chai, utils) => { name: ['toHaveReturnedWith', 'toReturnWith'], condition: (spy, value) => spy.mock.results.some( - ({ type, value: result }) => type === 'return' && jestEquals(value, result), + ({ type, value: result }) => + type === 'return' && jestEquals(value, result, [...customTesters, iterableEquality]), ), action: 'return', }, @@ -872,7 +877,11 @@ export const JestChaiExpect: ChaiPlugin = (chai, utils) => { name: 'toHaveLastResolvedWith', condition: (spy, value) => { const result = spy.mock.settledResults.at(-1) - return Boolean(result && result.type === 'fulfilled' && jestEquals(result.value, value)) + return Boolean( + result && + result.type === 'fulfilled' && + jestEquals(result.value, value, [...customTesters, iterableEquality]), + ) }, action: 'resolve', }, @@ -880,7 +889,11 @@ export const JestChaiExpect: ChaiPlugin = (chai, utils) => { name: 'toHaveLastReturnedWith', condition: (spy, value) => { const result = spy.mock.results.at(-1) - return Boolean(result && result.type === 'return' && jestEquals(result.value, value)) + return Boolean( + result && + result.type === 'return' && + jestEquals(result.value, value, [...customTesters, iterableEquality]), + ) }, action: 'return', }, @@ -906,7 +919,11 @@ export const JestChaiExpect: ChaiPlugin = (chai, utils) => { name: 'toHaveNthResolvedWith', condition: (spy, index, value) => { const result = spy.mock.settledResults[index - 1] - return result && result.type === 'fulfilled' && jestEquals(result.value, value) + return ( + result && + result.type === 'fulfilled' && + jestEquals(result.value, value, [...customTesters, iterableEquality]) + ) }, action: 'resolve', }, @@ -914,7 +931,11 @@ export const JestChaiExpect: ChaiPlugin = (chai, utils) => { name: 'toHaveNthReturnedWith', condition: (spy, index, value) => { const result = spy.mock.results[index - 1] - return result && result.type === 'return' && jestEquals(result.value, value) + return ( + result && + result.type === 'return' && + jestEquals(result.value, value, [...customTesters, iterableEquality]) + ) }, action: 'return', }, diff --git a/test/unit/test/expect.test.ts b/test/unit/test/expect.test.ts index 876df99d2..d21003f7b 100644 --- a/test/unit/test/expect.test.ts +++ b/test/unit/test/expect.test.ts @@ -224,6 +224,7 @@ describe('recursive custom equality tester', () => { } } + // different `personId`, so they are equal only through `arePersonsEqual` const person1 = new Person('Luke Skywalker', new Address('Tatooine')) const person2 = new Person('Luke Skywalker', new Address('Tatooine')) @@ -239,6 +240,8 @@ describe('recursive custom equality tester', () => { expect(new Set([person1])).toEqual(new Set([person2])) expect([person1]).toContainEqual(person2) expect({ a: person1 }).toHaveProperty('a', person2) + expect(person1).toBeOneOf([person2]) + expect(person1).toBeOneOf(new Set([person2])) expect({ a: person1, b: undefined }).toStrictEqual({ a: person2, b: undefined, @@ -252,6 +255,7 @@ describe('recursive custom equality tester', () => { test('asymmetric matchers pass different Address objects', () => { expect([person1]).toEqual(expect.arrayContaining([person2])) expect({ a: 1, b: { c: person1 } }).toEqual(expect.objectContaining({ b: { c: person2 } })) + expect(person1).toEqual(expect.toBeOneOf([person2])) }) test('toBe recommends toStrictEqual even with different Address objects', () => { @@ -274,19 +278,25 @@ describe('recursive custom equality tester', () => { expect(a).toEqual(b) }) - test('spy matchers pass different Person objects', () => { + test('spy matchers pass different Person objects', async () => { const mockFn = vi.fn((person: Person) => [person, person2]) mockFn(person1) - expect(mockFn).toHaveBeenCalledWith(person1) - expect(mockFn).toHaveBeenCalledWith(person1) - expect(mockFn).toHaveBeenLastCalledWith(person1) - expect(mockFn).toHaveBeenNthCalledWith(1, person1) + expect(mockFn).toHaveBeenCalledWith(person2) + expect(mockFn).toHaveBeenLastCalledWith(person2) + expect(mockFn).toHaveBeenNthCalledWith(1, person2) - expect(mockFn).toHaveReturnedWith([person1, person2]) - expect(mockFn).toHaveLastReturnedWith([person1, person2]) - expect(mockFn).to.have.lastReturnedWith([person1, person2]) - expect(mockFn).toHaveNthReturnedWith(1, [person1, person2]) + expect(mockFn).toHaveReturnedWith([person2, person1]) + expect(mockFn).toHaveLastReturnedWith([person2, person1]) + expect(mockFn).to.have.lastReturnedWith([person2, person1]) + expect(mockFn).toHaveNthReturnedWith(1, [person2, person1]) + + const asyncFn = vi.fn(async () => person1) + await asyncFn() + + expect(asyncFn).toHaveResolvedWith(person2) + expect(asyncFn).toHaveLastResolvedWith(person2) + expect(asyncFn).toHaveNthResolvedWith(1, person2) }) }) diff --git a/test/unit/test/jest-expect.test.ts b/test/unit/test/jest-expect.test.ts index 9b404ed6d..36151bc24 100644 --- a/test/unit/test/jest-expect.test.ts +++ b/test/unit/test/jest-expect.test.ts @@ -239,10 +239,9 @@ describe('jest-expect', () => { expect(new Stock('x')).not.toStrictEqual(expect.toBeOneOf([{ type: 'x' }])) expect(new Stock('x')).toStrictEqual(expect.toBeOneOf([new Stock('x')])) - // non-asymmetric usage only sees registered testers - // TODO: https://github.com/vitest-dev/vitest/issues/11296 + // non-asymmetric usage expect(new Set(['x'])).toBeOneOf([new Set(['x'])]) - expect(new Set()).toBeOneOf([new Set(['x'])]) + expect(new Set()).not.toBeOneOf([new Set(['x'])]) }) // https://github.com/vitest-dev/vitest/issues/11071 @@ -798,6 +797,70 @@ describe('toBeOneOf()', () => { }) }) +// https://github.com/vitest-dev/vitest/issues/11296 +describe('Map and Set equality', () => { + it('toContainEqual', () => { + expect([new Map([['x', 1]])]).toContainEqual(new Map([['x', 1]])) + expect([new Map([['y', 1]])]).not.toContainEqual(new Map([['x', 1]])) + expect([new Set(['x'])]).toContainEqual(new Set(['x'])) + expect([new Set(['y'])]).not.toContainEqual(new Set(['x'])) + }) + + it('toHaveProperty', () => { + expect({ a: new Map([['x', 1]]) }).toHaveProperty('a', new Map([['x', 1]])) + expect({ a: new Map([['y', 1]]) }).not.toHaveProperty('a', new Map([['x', 1]])) + expect({ a: new Set(['x']) }).toHaveProperty('a', new Set(['x'])) + expect({ a: new Set(['y']) }).not.toHaveProperty('a', new Set(['x'])) + }) + + it('toBeOneOf', () => { + expect(new Map([['x', 1]])).toBeOneOf([new Map([['x', 1]])]) + expect(new Map([['y', 1]])).not.toBeOneOf([new Map([['x', 1]])]) + expect(new Set(['x'])).toBeOneOf(new Set([new Set(['x'])])) + expect(new Set(['y'])).not.toBeOneOf(new Set([new Set(['x'])])) + expect(new Set(['x'])).toEqual(expect.toBeOneOf([new Set(['x'])])) + expect(new Set(['y'])).not.toEqual(expect.toBeOneOf([new Set(['x'])])) + }) + + it('toThrow with asymmetric matcher', () => { + class DataError extends Error { + constructor(public data: unknown) { + super('data') + } + } + const fn = () => { + throw new DataError(new Set(['x'])) + } + + expect(fn).toThrow(expect.objectContaining({ data: new Set(['x']) })) + expect(fn).not.toThrow(expect.objectContaining({ data: new Set(['y']) })) + }) + + it('return matchers', () => { + const fn = vi.fn(() => new Set(['x'])) + fn() + + expect(fn).toHaveReturnedWith(new Set(['x'])) + expect(fn).not.toHaveReturnedWith(new Set(['y'])) + expect(fn).toHaveLastReturnedWith(new Set(['x'])) + expect(fn).not.toHaveLastReturnedWith(new Set(['y'])) + expect(fn).toHaveNthReturnedWith(1, new Set(['x'])) + expect(fn).not.toHaveNthReturnedWith(1, new Set(['y'])) + }) + + it('resolve matchers', async () => { + const fn = vi.fn(async () => new Map([['x', 1]])) + await fn() + + expect(fn).toHaveResolvedWith(new Map([['x', 1]])) + expect(fn).not.toHaveResolvedWith(new Map([['y', 1]])) + expect(fn).toHaveLastResolvedWith(new Map([['x', 1]])) + expect(fn).not.toHaveLastResolvedWith(new Map([['y', 1]])) + expect(fn).toHaveNthResolvedWith(1, new Map([['x', 1]])) + expect(fn).not.toHaveNthResolvedWith(1, new Map([['y', 1]])) + }) +}) + describe('toSatisfy()', () => { const isOdd = (value: number) => value % 2 !== 0