Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(expect): add toBeOneOf matcher #6974

Merged
merged 11 commits into from
Dec 20, 2024
36 changes: 36 additions & 0 deletions docs/api/expect.md
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,42 @@ test('getApplesCount has some unusual side effects...', () => {
})
```

## toBeOneOf

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

`toBeOneOf` asserts if a value matches any of the values in the provided array.

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

test('fruit is one of the allowed values', () => {
expect(fruit).toBeOneOf(['apple', 'banana', 'orange'])
})
```

The asymmetric matcher 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 = {
firstName: 'John',
middleName: undefined,
lastName: 'Doe'
}

expect(user).toEqual({
firstName: expect.any(String),
middleName: expect.toBeOneOf([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.
:::

## toBeTypeOf

- **Type:** `(c: 'bigint' | 'boolean' | 'function' | 'number' | 'object' | 'string' | 'symbol' | 'undefined') => Awaitable<void>`
Expand Down
37 changes: 37 additions & 0 deletions packages/expect/src/custom-matchers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,43 @@ ${matcherHint('.toSatisfy', 'received', '')}
Expected value to satisfy:
${message || printExpected(expected)}

Received:
${printReceived(actual)}`,
}
},

toBeOneOf(actual: unknown, expected: Array<unknown>) {
const { equals, customTesters } = this
const { printReceived, printExpected, matcherHint } = this.utils

if (!Array.isArray(expected)) {
throw new TypeError(
`You must provide an array to ${matcherHint('.toBeOneOf')}, not '${typeof expected}'.`,
)
}

const pass = expected.length === 0
|| expected.some(item =>
equals(item, actual, customTesters),
)

return {
pass,
message: () =>
pass
? `\
${matcherHint('.not.toBeOneOf', 'received', '')}

Expected value to not be one of:
${printExpected(expected)}
Received:
${printReceived(actual)}`
: `\
${matcherHint('.toBeOneOf', 'received', '')}

Expected value to be one of:
${printExpected(expected)}

Received:
${printReceived(actual)}`,
}
Expand Down
2 changes: 1 addition & 1 deletion packages/expect/src/jest-extend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ function JestExtendPlugin(
}

toAsymmetricMatcher() {
return `${this.toString()}<${this.sample.map(String).join(', ')}>`
return `${this.toString()}<${this.sample.map(item => stringify(item)).join(', ')}>`
}
}

Expand Down
10 changes: 10 additions & 0 deletions packages/expect/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,16 @@ interface CustomMatcher {
* expect(age).toEqual(expect.toSatisfy(val => val >= 18, 'Age must be at least 18'));
*/
toSatisfy: (matcher: (value: any) => boolean, message?: string) => any

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

export interface AsymmetricMatchersContaining extends CustomMatcher {
Expand Down
Loading
Loading