Skip to content
Merged
48 changes: 47 additions & 1 deletion docs/api/expect.md
Original file line number Diff line number Diff line change
Expand Up @@ -1075,7 +1075,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 value with certain parameters on a specific invokation. Requires a spy function to be passed to `expect`.

```ts
import { expect, test, vi } from 'vitest'
Expand Down Expand Up @@ -1427,6 +1427,52 @@ test('"id" is a number', () => {
})
```

## expect.oneOf

- **Type:** `(sample: Array<any>) => any`

When used with an equality check, this asymmetric matcher will return `true` if the value matches any of the values in the provided array.

```ts
import { expect, test } from 'vitest'

test('fruit is one of the allowed types', () => {
const fruit = {
name: 'apple',
count: 1
}

expect(fruit).toEqual({
name: expect.oneOf(['apple', 'banana', 'orange']),
count: 1
})
})
```

This is particularly useful when testing optional properties that could be either `null` or `undefined`:

```ts
test('optional properties can be null or undefined', () => {
const user = {
id: 1,
firstName: 'John',
middleName: undefined,
lastName: 'Doe'
}

expect(user).toEqual({
id: expect.any(Number),
firstName: expect.any(String),
middleName: expect.oneOf([expect.any(String), undefined]),
lastName: expect.any(String),
})
})
```

:::tip
You can use `expect.not` with this matcher to ensure a value does NOT match any of the provided options.
:::

## expect.closeTo {#expect-closeto}

- **Type:** `(expected: any, precision?: number) => any`
Expand Down
41 changes: 41 additions & 0 deletions packages/expect/src/jest-asymmetric-matchers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -377,11 +377,50 @@
}
}


Check failure on line 380 in packages/expect/src/jest-asymmetric-matchers.ts

View workflow job for this annotation

GitHub Actions / Lint: node-latest, ubuntu-latest

More than 1 blank line not allowed
class OneOf<T = unknown> extends AsymmetricMatcher<Array<T>> {
constructor(sample: Array<T>, inverse = false) {
super(sample, inverse)
}

asymmetricMatch(other: unknown) {
if (!Array.isArray(this.sample)) {
throw new TypeError(
`You must provide an array to ${this.toString()}, not '${typeof this
.sample}'.`,
)
}

const matcherContext = this.getMatcherContext()
const result
= this.sample.length === 0
|| this.sample.some(item =>
equals(item, other, matcherContext.customTesters),
)

return this.inverse ? !result : result
}

toString() {
return `${this.inverse ? 'Not' : ''}OneOf`
}

getExpectedType() {
return this.sample.map(item => stringify(item)).join(' | ')
}

toAsymmetricMatcher() {
return `${this.toString()}<${this.getExpectedType()}>`
}
}

export const JestAsymmetricMatchers: ChaiPlugin = (chai, utils) => {
utils.addMethod(chai.expect, 'anything', () => new Anything())

utils.addMethod(chai.expect, 'any', (expected: unknown) => new Any(expected))

utils.addMethod(chai.expect, 'oneOf', (expected: Array<unknown>) => new OneOf(expected))

utils.addMethod(
chai.expect,
'stringContaining',
Expand Down Expand Up @@ -423,5 +462,7 @@
new StringMatching(expected, true),
closeTo: (expected: any, precision?: number) =>
new CloseTo(expected, precision, true),
oneOf: <T = unknown>(expected: Array<T>) =>
new OneOf<T>(expected, true),
}
}
10 changes: 10 additions & 0 deletions packages/expect/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,16 @@ export interface AsymmetricMatchersContaining {
* expect(5.11).toEqual(expect.closeTo(5.12)); // with default precision
*/
closeTo: (expected: number, precision?: number) => any

/**
* Matches if the received value is one of the values in the expected array.
*
* @example
* expect(1).toEqual(expect.oneOf([1, 2, 3]))
* expect('foo').toEqual(expect.oneOf([expect.any(String), undefined]))
* expect({ a: 1 }).toEqual(expect.oneOf([expect.objectContaining({ a: '1' }), null]))
*/
oneOf: <T>(sample: Array<T>) => any
}

export interface JestAssertion<T = any> extends jest.Matchers<void, T> {
Expand Down
20 changes: 20 additions & 0 deletions test/core/test/jest-expect.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -194,13 +194,33 @@
sum: expect.closeTo(0.4),
})
}).toThrowErrorMatchingInlineSnapshot(`[AssertionError: expected { sum: 0.30000000000000004 } to deeply equal { sum: NumberCloseTo 0.4 (2 digits) }]`)

expect(0).toEqual(expect.oneOf([0, 1, 2]))
expect(0).toEqual(expect.oneOf([expect.any(Number), undefined]))
expect('string').toEqual(expect.oneOf([expect.any(String), undefined]))
expect({ a: 0 }).toEqual(expect.oneOf([expect.objectContaining({ a: 0 }), null]))
expect({
name: 'apple',
count: 1

Check failure on line 204 in test/core/test/jest-expect.test.ts

View workflow job for this annotation

GitHub Actions / Lint: node-latest, ubuntu-latest

Missing trailing comma
}).toEqual({
name: expect.oneOf(['apple', 'banana', 'orange']),
count: 1,
})
expect(null).toEqual(expect.oneOf([expect.any(Object)]))
expect(null).toEqual(expect.oneOf([null]))
expect(undefined).toEqual(expect.oneOf([undefined]))
})

it('asymmetric matchers negate', () => {
expect('bar').toEqual(expect.not.stringContaining('zoo'))
expect('bar').toEqual(expect.not.stringMatching(/zoo/))
expect({ bar: 'zoo' }).toEqual(expect.not.objectContaining({ zoo: 'bar' }))
expect(['Bob', 'Eve']).toEqual(expect.not.arrayContaining(['Steve']))
expect(0).toEqual(expect.not.oneOf([1, 2, 3]))
expect('foo').toEqual(expect.not.oneOf([expect.any(Number), undefined]))
expect({ a: 0 }).toEqual(expect.not.oneOf([expect.objectContaining({ b: 0 }), null]))
expect(null).toEqual(expect.not.oneOf([expect.any(String)]))
expect(undefined).toEqual(expect.not.oneOf([expect.any(Object)]))
})

it('expect.extend', async () => {
Expand Down
Loading