mirror of
https://github.com/vitest-dev/vitest.git
synced 2025-12-08 18:26:03 +00:00
fix(expect): support type-safe declaration of custom matchers (#7656)
Co-authored-by: Vladimir Sheremet <sleuths.slews0s@icloud.com>
This commit is contained in:
parent
2854ad663f
commit
e996b4103b
@ -25,7 +25,19 @@ expect.extend({
|
||||
|
||||
If you are using TypeScript, you can extend default `Assertion` interface in an ambient declaration file (e.g: `vitest.d.ts`) with the code below:
|
||||
|
||||
```ts
|
||||
::: code-group
|
||||
```ts [<Version>3.2.0</Version>]
|
||||
import 'vitest'
|
||||
|
||||
interface CustomMatchers<R = unknown> {
|
||||
toBeFoo: () => R
|
||||
}
|
||||
|
||||
declare module 'vitest' {
|
||||
interface Matchers<T = any> extends CustomMatchers<T> {}
|
||||
}
|
||||
```
|
||||
```ts [<Version>3.0.0</Version>]
|
||||
import 'vitest'
|
||||
|
||||
interface CustomMatchers<R = unknown> {
|
||||
@ -37,6 +49,11 @@ declare module 'vitest' {
|
||||
interface AsymmetricMatchersContaining extends CustomMatchers {}
|
||||
}
|
||||
```
|
||||
:::
|
||||
|
||||
::: tip
|
||||
Since Vitest 3.2, you can extend the `Matchers` interface to have type-safe assertions in `expect.extend`, `expect().*`, and `expect.*` methods at the same time. Previously, you had to define separate interfaces for each of them.
|
||||
:::
|
||||
|
||||
::: warning
|
||||
Don't forget to include the ambient declaration file in your `tsconfig.json`.
|
||||
@ -56,35 +73,45 @@ interface ExpectationResult {
|
||||
```
|
||||
|
||||
::: warning
|
||||
If you create an asynchronous matcher, don't forget to `await` the result (`await expect('foo').toBeFoo()`) in the test itself.
|
||||
If you create an asynchronous matcher, don't forget to `await` the result (`await expect('foo').toBeFoo()`) in the test itself::
|
||||
|
||||
```ts
|
||||
expect.extend({
|
||||
async toBeAsyncAssertion() {
|
||||
// ...
|
||||
}
|
||||
})
|
||||
|
||||
await expect().toBeAsyncAssertion()
|
||||
```
|
||||
:::
|
||||
|
||||
The first argument inside a matcher's function is the received value (the one inside `expect(received)`). The rest are arguments passed directly to the matcher.
|
||||
|
||||
Matcher function have access to `this` context with the following properties:
|
||||
Matcher function has access to `this` context with the following properties:
|
||||
|
||||
- `isNot`
|
||||
### `isNot`
|
||||
|
||||
Returns true, if matcher was called on `not` (`expect(received).not.toBeFoo()`).
|
||||
Returns true, if matcher was called on `not` (`expect(received).not.toBeFoo()`).
|
||||
|
||||
- `promise`
|
||||
### `promise`
|
||||
|
||||
If matcher was called on `resolved/rejected`, this value will contain the name of modifier. Otherwise, it will be an empty string.
|
||||
If matcher was called on `resolved/rejected`, this value will contain the name of modifier. Otherwise, it will be an empty string.
|
||||
|
||||
- `equals`
|
||||
### `equals`
|
||||
|
||||
This is a utility function that allows you to compare two values. It will return `true` if values are equal, `false` otherwise. This function is used internally for almost every matcher. It supports objects with asymmetric matchers by default.
|
||||
This is a utility function that allows you to compare two values. It will return `true` if values are equal, `false` otherwise. This function is used internally for almost every matcher. It supports objects with asymmetric matchers by default.
|
||||
|
||||
- `utils`
|
||||
### `utils`
|
||||
|
||||
This contains a set of utility functions that you can use to display messages.
|
||||
This contains a set of utility functions that you can use to display messages.
|
||||
|
||||
`this` context also contains information about the current test. You can also get it by calling `expect.getState()`. The most useful properties are:
|
||||
|
||||
- `currentTestName`
|
||||
### `currentTestName`
|
||||
|
||||
Full name of the current test (including describe block).
|
||||
Full name of the current test (including describe block).
|
||||
|
||||
- `testPath`
|
||||
### `testPath`
|
||||
|
||||
Path to the current test.
|
||||
Path to the current test.
|
||||
|
||||
@ -86,17 +86,25 @@ export type AsyncExpectationResult = Promise<SyncExpectationResult>
|
||||
|
||||
export type ExpectationResult = SyncExpectationResult | AsyncExpectationResult
|
||||
|
||||
export interface RawMatcherFn<T extends MatcherState = MatcherState> {
|
||||
(this: T, received: any, ...expected: Array<any>): ExpectationResult
|
||||
export interface RawMatcherFn<T extends MatcherState = MatcherState, E extends Array<any> = Array<any>> {
|
||||
(this: T, received: any, ...expected: E): ExpectationResult
|
||||
}
|
||||
|
||||
// Allow unused `T` to preserve its name for extensions.
|
||||
// Type parameter names must be identical when extending those types.
|
||||
// eslint-disable-next-line
|
||||
export interface Matchers<T = any> {}
|
||||
|
||||
export type MatchersObject<T extends MatcherState = MatcherState> = Record<
|
||||
string,
|
||||
RawMatcherFn<T>
|
||||
> & ThisType<T>
|
||||
> & ThisType<T> & {
|
||||
[K in keyof Matchers<T>]?: RawMatcherFn<T, Parameters<Matchers<T>[K]>>
|
||||
}
|
||||
|
||||
export interface ExpectStatic
|
||||
extends Chai.ExpectStatic,
|
||||
Matchers,
|
||||
AsymmetricMatchersContaining {
|
||||
<T>(actual: T, message?: string): Assertion<T>
|
||||
extend: (expects: MatchersObject) => void
|
||||
@ -639,7 +647,8 @@ export type PromisifyAssertion<T> = Promisify<Assertion<T>>
|
||||
|
||||
export interface Assertion<T = any>
|
||||
extends VitestAssertion<Chai.Assertion, T>,
|
||||
JestAssertion<T> {
|
||||
JestAssertion<T>,
|
||||
Matchers<T> {
|
||||
/**
|
||||
* Ensures a value is of a specific type.
|
||||
*
|
||||
|
||||
@ -252,6 +252,7 @@ export type {
|
||||
ExpectPollOptions,
|
||||
ExpectStatic,
|
||||
JestAssertion,
|
||||
Matchers,
|
||||
} from '@vitest/expect'
|
||||
export {
|
||||
afterAll,
|
||||
|
||||
52
test/typescript/test-d/expect-extend.test-d.ts
Normal file
52
test/typescript/test-d/expect-extend.test-d.ts
Normal file
@ -0,0 +1,52 @@
|
||||
import { expect, expectTypeOf, test } from 'vitest'
|
||||
|
||||
interface CustomMatchers<R = unknown> {
|
||||
toMatchSchema: (schema: { a: string }) => R
|
||||
toEqualMultiple: (a: string, b: number) => R
|
||||
}
|
||||
|
||||
declare module 'vitest' {
|
||||
interface Matchers<T = any> extends CustomMatchers<T> {}
|
||||
}
|
||||
|
||||
test('infers matcher declaration type from a custom matcher type', () => {
|
||||
expect.extend({
|
||||
toMatchSchema(received, expected) {
|
||||
expectTypeOf(received).toBeAny()
|
||||
expectTypeOf(expected).toEqualTypeOf<{ a: string }>()
|
||||
|
||||
return { pass: true, message: () => '' }
|
||||
},
|
||||
toEqualMultiple(received, a, b) {
|
||||
expectTypeOf(received).toBeAny()
|
||||
expectTypeOf(a).toBeString()
|
||||
expectTypeOf(b).toBeNumber()
|
||||
|
||||
return { pass: true, message: () => '' }
|
||||
},
|
||||
})
|
||||
|
||||
expect({ a: 1, b: '2' }).toMatchSchema({ a: '1' })
|
||||
expect('a').toEqualMultiple('a', 1)
|
||||
})
|
||||
|
||||
test('automatically extends asymmetric matchers', () => {
|
||||
expect({}).toEqual({
|
||||
nestedSchema: expect.toMatchSchema({
|
||||
a: '1',
|
||||
// @ts-expect-error Unknown property.
|
||||
b: 2,
|
||||
}),
|
||||
})
|
||||
})
|
||||
|
||||
test('treats matcher declarations as optional', () => {
|
||||
expect.extend(
|
||||
/**
|
||||
* @note Although annotated, you don't have to declare matchers.
|
||||
* You can call `expect.extend()` multiple times or get the matcher
|
||||
* declarations from a third-party library.
|
||||
*/
|
||||
{},
|
||||
)
|
||||
})
|
||||
Loading…
x
Reference in New Issue
Block a user