Skip to content

Commit 3d742b2

Browse files
zirkelchi-ogawa
andauthored
feat(expect): add toBeOneOf matcher (#6974)
Co-authored-by: Hiroshi Ogawa <hi.ogawa.zz@gmail.com>
1 parent 78b62ff commit 3d742b2

File tree

6 files changed

+340
-35
lines changed

6 files changed

+340
-35
lines changed

docs/api/expect.md

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -309,6 +309,42 @@ test('getApplesCount has some unusual side effects...', () => {
309309
})
310310
```
311311

312+
## toBeOneOf
313+
314+
- **Type:** `(sample: Array<any>) => any`
315+
316+
`toBeOneOf` asserts if a value matches any of the values in the provided array.
317+
318+
```ts
319+
import { expect, test } from 'vitest'
320+
321+
test('fruit is one of the allowed values', () => {
322+
expect(fruit).toBeOneOf(['apple', 'banana', 'orange'])
323+
})
324+
```
325+
326+
The asymmetric matcher is particularly useful when testing optional properties that could be either `null` or `undefined`:
327+
328+
```ts
329+
test('optional properties can be null or undefined', () => {
330+
const user = {
331+
firstName: 'John',
332+
middleName: undefined,
333+
lastName: 'Doe'
334+
}
335+
336+
expect(user).toEqual({
337+
firstName: expect.any(String),
338+
middleName: expect.toBeOneOf([expect.any(String), undefined]),
339+
lastName: expect.any(String),
340+
})
341+
})
342+
```
343+
344+
:::tip
345+
You can use `expect.not` with this matcher to ensure a value does NOT match any of the provided options.
346+
:::
347+
312348
## toBeTypeOf
313349

314350
- **Type:** `(c: 'bigint' | 'boolean' | 'function' | 'number' | 'object' | 'string' | 'symbol' | 'undefined') => Awaitable<void>`

packages/expect/src/custom-matchers.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,43 @@ ${matcherHint('.toSatisfy', 'received', '')}
2222
Expected value to satisfy:
2323
${message || printExpected(expected)}
2424
25+
Received:
26+
${printReceived(actual)}`,
27+
}
28+
},
29+
30+
toBeOneOf(actual: unknown, expected: Array<unknown>) {
31+
const { equals, customTesters } = this
32+
const { printReceived, printExpected, matcherHint } = this.utils
33+
34+
if (!Array.isArray(expected)) {
35+
throw new TypeError(
36+
`You must provide an array to ${matcherHint('.toBeOneOf')}, not '${typeof expected}'.`,
37+
)
38+
}
39+
40+
const pass = expected.length === 0
41+
|| expected.some(item =>
42+
equals(item, actual, customTesters),
43+
)
44+
45+
return {
46+
pass,
47+
message: () =>
48+
pass
49+
? `\
50+
${matcherHint('.not.toBeOneOf', 'received', '')}
51+
52+
Expected value to not be one of:
53+
${printExpected(expected)}
54+
Received:
55+
${printReceived(actual)}`
56+
: `\
57+
${matcherHint('.toBeOneOf', 'received', '')}
58+
59+
Expected value to be one of:
60+
${printExpected(expected)}
61+
2562
Received:
2663
${printReceived(actual)}`,
2764
}

packages/expect/src/jest-extend.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ function JestExtendPlugin(
132132
}
133133

134134
toAsymmetricMatcher() {
135-
return `${this.toString()}<${this.sample.map(String).join(', ')}>`
135+
return `${this.toString()}<${this.sample.map(item => stringify(item)).join(', ')}>`
136136
}
137137
}
138138

packages/expect/src/types.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,16 @@ interface CustomMatcher {
118118
* expect(age).toEqual(expect.toSatisfy(val => val >= 18, 'Age must be at least 18'));
119119
*/
120120
toSatisfy: (matcher: (value: any) => boolean, message?: string) => any
121+
122+
/**
123+
* Matches if the received value is one of the values in the expected array.
124+
*
125+
* @example
126+
* expect(1).toBeOneOf([1, 2, 3])
127+
* expect('foo').toBeOneOf([expect.any(String)])
128+
* expect({ a: 1 }).toEqual({ a: expect.toBeOneOf(['1', '2', '3']) })
129+
*/
130+
toBeOneOf: <T>(sample: Array<T>) => any
121131
}
122132

123133
export interface AsymmetricMatchersContaining extends CustomMatcher {

0 commit comments

Comments
 (0)