Skip to content

Commit

Permalink
feat(expect): add toBeOneOf matcher (#6974)
Browse files Browse the repository at this point in the history
Co-authored-by: Hiroshi Ogawa <[email protected]>
  • Loading branch information
zirkelc and hi-ogawa authored Dec 20, 2024
1 parent 78b62ff commit 3d742b2
Show file tree
Hide file tree
Showing 6 changed files with 340 additions and 35 deletions.
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

0 comments on commit 3d742b2

Please sign in to comment.