Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions docs/api/expect.md
Original file line number Diff line number Diff line change
Expand Up @@ -387,7 +387,7 @@ test('getApplesCount has some unusual side effects...', () => {

- **Type:** `(sample: Array<any> | Set<any>) => 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.
Expand Down Expand Up @@ -1298,7 +1298,7 @@ test('spy function returns a value two times', () => {

- **Type:** `(returnValue: any) => Awaitable<void>`

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'
Expand All @@ -1316,7 +1316,7 @@ test('spy function returns a product', () => {

- **Type:** `(returnValue: any) => Awaitable<void>`

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'
Expand All @@ -1335,7 +1335,7 @@ test('spy function returns bananas on a last call', () => {

- **Type:** `(time: number, returnValue: any) => Awaitable<void>`

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, ...)`.

Expand Down Expand Up @@ -1403,7 +1403,7 @@ test('spy function resolved a value two times', async () => {

- **Type:** `(returnValue: any) => Awaitable<void>`

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.

Expand All @@ -1423,7 +1423,7 @@ test('spy function resolved a product', async () => {

- **Type:** `(returnValue: any) => Awaitable<void>`

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.

Expand All @@ -1444,7 +1444,7 @@ test('spy function resolves bananas on a last call', async () => {

- **Type:** `(time: number, returnValue: any) => Awaitable<void>`

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.

Expand Down
2 changes: 1 addition & 1 deletion docs/api/vi.md
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
7 changes: 4 additions & 3 deletions packages/expect/src/custom-matchers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,17 +29,18 @@ ${printReceived(actual)}`,

toBeOneOf(actual: unknown, expected: Array<unknown> | Set<unknown>) {
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}'.`,
Expand Down
39 changes: 30 additions & 9 deletions packages/expect/src/jest-expect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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 })}`
Expand Down Expand Up @@ -741,7 +743,7 @@ export const JestChaiExpect: ChaiPlugin = (chai, utils) => {
) {
const matcher = expected as any as AsymmetricMatcher<any>
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,
Expand Down Expand Up @@ -833,15 +835,18 @@ 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',
},
{
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',
},
Expand Down Expand Up @@ -872,15 +877,23 @@ 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',
},
{
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',
},
Expand All @@ -906,15 +919,23 @@ 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',
},
{
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',
},
Expand Down
28 changes: 19 additions & 9 deletions test/unit/test/expect.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'))

Expand All @@ -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,
Expand All @@ -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', () => {
Expand All @@ -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])
Comment on lines +285 to +292

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Drive-by tweak. This wasn't testing custom equality since person1 === person1 and person2 === person2 would hold trivially.


const asyncFn = vi.fn(async () => person1)
await asyncFn()

expect(asyncFn).toHaveResolvedWith(person2)
expect(asyncFn).toHaveLastResolvedWith(person2)
expect(asyncFn).toHaveNthResolvedWith(1, person2)
})
})

Expand Down
69 changes: 66 additions & 3 deletions test/unit/test/jest-expect.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down
Loading