| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -25,7 +25,19 @@ expect.extend({ | |||
| 25 | 25 | ||
| 26 | 26 | 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: | |
| 27 | 27 | ||
| 28 | - ```ts | ||
| 28 | + ::: code-group | ||
| 29 | + ```ts [<Version>3.2.0</Version>] | ||
| 30 | + import 'vitest' | ||
| 31 | + | ||
| 32 | + interface CustomMatchers<R = unknown> { | ||
| 33 | + toBeFoo: () => R | ||
| 34 | + } | ||
| 35 | + | ||
| 36 | + declare module 'vitest' { | ||
| 37 | + interface Matchers<T = any> extends CustomMatchers<T> {} | ||
| 38 | + } | ||
| 39 | + ``` | ||
| 40 | + ```ts [<Version>3.0.0</Version>] | ||
| 29 | 41 | import 'vitest' | |
| 30 | 42 | ||
| 31 | 43 | interface CustomMatchers<R = unknown> { | |
@@ -37,6 +49,11 @@ declare module 'vitest' { | |||
| 37 | 49 | interface AsymmetricMatchersContaining extends CustomMatchers {} | |
| 38 | 50 | } | |
| 39 | 51 | ``` | |
| 52 | + ::: | ||
| 53 | + | ||
| 54 | + ::: tip | ||
| 55 | + 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. | ||
| 56 | + ::: | ||
| 40 | 57 | ||
| 41 | 58 | ::: warning | |
| 42 | 59 | Don't forget to include the ambient declaration file in your `tsconfig.json`. | |
@@ -56,35 +73,45 @@ interface ExpectationResult { | |||
| 56 | 73 | ``` | |
| 57 | 74 | ||
| 58 | 75 | ::: warning | |
| 59 | - If you create an asynchronous matcher, don't forget to `await` the result (`await expect('foo').toBeFoo()`) in the test itself. | ||
| 76 | + If you create an asynchronous matcher, don't forget to `await` the result (`await expect('foo').toBeFoo()`) in the test itself:: | ||
| 77 | + | ||
| 78 | + ```ts | ||
| 79 | + expect.extend({ | ||
| 80 | + async toBeAsyncAssertion() { | ||
| 81 | + // ... | ||
| 82 | + } | ||
| 83 | + }) | ||
| 84 | + | ||
| 85 | + await expect().toBeAsyncAssertion() | ||
| 86 | + ``` | ||
| 60 | 87 | ::: | |
| 61 | 88 | ||
| 62 | 89 | 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. | |
| 63 | 90 | ||
| 64 | - Matcher function have access to `this` context with the following properties: | ||
| 91 | + Matcher function has access to `this` context with the following properties: | ||
| 65 | 92 | ||
| 66 | - - `isNot` | ||
| 93 | + ### `isNot` | ||
| 67 | 94 | ||
| 68 | - Returns true, if matcher was called on `not` (`expect(received).not.toBeFoo()`). | ||
| 95 | + Returns true, if matcher was called on `not` (`expect(received).not.toBeFoo()`). | ||
| 69 | 96 | ||
| 70 | - - `promise` | ||
| 97 | + ### `promise` | ||
| 71 | 98 | ||
| 72 | - If matcher was called on `resolved/rejected`, this value will contain the name of modifier. Otherwise, it will be an empty string. | ||
| 99 | + If matcher was called on `resolved/rejected`, this value will contain the name of modifier. Otherwise, it will be an empty string. | ||
| 73 | 100 | ||
| 74 | - - `equals` | ||
| 101 | + ### `equals` | ||
| 75 | 102 | ||
| 76 | - 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. | ||
| 103 | + 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. | ||
| 77 | 104 | ||
| 78 | - - `utils` | ||
| 105 | + ### `utils` | ||
| 79 | 106 | ||
| 80 | - This contains a set of utility functions that you can use to display messages. | ||
| 107 | + This contains a set of utility functions that you can use to display messages. | ||
| 81 | 108 | ||
| 82 | 109 | `this` context also contains information about the current test. You can also get it by calling `expect.getState()`. The most useful properties are: | |
| 83 | 110 | ||
| 84 | - - `currentTestName` | ||
| 111 | + ### `currentTestName` | ||
| 85 | 112 | ||
| 86 | - Full name of the current test (including describe block). | ||
| 113 | + Full name of the current test (including describe block). | ||
| 87 | 114 | ||
| 88 | - - `testPath` | ||
| 115 | + ### `testPath` | ||
| 89 | 116 | ||
| 90 | - Path to the current test. | ||
| 117 | + Path to the current test. | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -86,17 +86,25 @@ export type AsyncExpectationResult = Promise<SyncExpectationResult> | |||
| 86 | 86 | ||
| 87 | 87 | export type ExpectationResult = SyncExpectationResult | AsyncExpectationResult | |
| 88 | 88 | ||
| 89 | - export interface RawMatcherFn<T extends MatcherState = MatcherState> { | ||
| 90 | - (this: T, received: any, ...expected: Array<any>): ExpectationResult | ||
| 89 | + export interface RawMatcherFn<T extends MatcherState = MatcherState, E extends Array<any> = Array<any>> { | ||
| 90 | + (this: T, received: any, ...expected: E): ExpectationResult | ||
| 91 | 91 | } | |
| 92 | 92 | ||
| 93 | + // Allow unused `T` to preserve its name for extensions. | ||
| 94 | + // Type parameter names must be identical when extending those types. | ||
| 95 | + // eslint-disable-next-line | ||
| 96 | + export interface Matchers<T = any> {} | ||
| 97 | + | ||
| 93 | 98 | export type MatchersObject<T extends MatcherState = MatcherState> = Record< | |
| 94 | 99 | string, | |
| 95 | 100 | RawMatcherFn<T> | |
| 96 | - > & ThisType<T> | ||
| 101 | + > & ThisType<T> & { | ||
| 102 | + [K in keyof Matchers<T>]?: RawMatcherFn<T, Parameters<Matchers<T>[K]>> | ||
| 103 | + } | ||
| 97 | 104 | ||
| 98 | 105 | export interface ExpectStatic | |
| 99 | 106 | extends Chai.ExpectStatic, | |
| 107 | + Matchers, | ||
| 100 | 108 | AsymmetricMatchersContaining { | |
| 101 | 109 | <T>(actual: T, message?: string): Assertion<T> | |
| 102 | 110 | extend: (expects: MatchersObject) => void | |
@@ -639,7 +647,8 @@ export type PromisifyAssertion<T> = Promisify<Assertion<T>> | |||
| 639 | 647 | ||
| 640 | 648 | export interface Assertion<T = any> | |
| 641 | 649 | extends VitestAssertion<Chai.Assertion, T>, | |
| 642 | - JestAssertion<T> { | ||
| 650 | + JestAssertion<T>, | ||
| 651 | + Matchers<T> { | ||
| 643 | 652 | /** | |
| 644 | 653 | * Ensures a value is of a specific type. | |
| 645 | 654 | * | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -252,6 +252,7 @@ export type { | |||
| 252 | 252 | ExpectPollOptions, | |
| 253 | 253 | ExpectStatic, | |
| 254 | 254 | JestAssertion, | |
| 255 | + Matchers, | ||
| 255 | 256 | } from '@vitest/expect' | |
| 256 | 257 | export { | |
| 257 | 258 | afterAll, | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -0,0 +1,52 @@ | |||
| 1 | + import { expect, expectTypeOf, test } from 'vitest' | ||
| 2 | + | ||
| 3 | + interface CustomMatchers<R = unknown> { | ||
| 4 | + toMatchSchema: (schema: { a: string }) => R | ||
| 5 | + toEqualMultiple: (a: string, b: number) => R | ||
| 6 | + } | ||
| 7 | + | ||
| 8 | + declare module 'vitest' { | ||
| 9 | + interface Matchers<T = any> extends CustomMatchers<T> {} | ||
| 10 | + } | ||
| 11 | + | ||
| 12 | + test('infers matcher declaration type from a custom matcher type', () => { | ||
| 13 | + expect.extend({ | ||
| 14 | + toMatchSchema(received, expected) { | ||
| 15 | + expectTypeOf(received).toBeAny() | ||
| 16 | + expectTypeOf(expected).toEqualTypeOf<{ a: string }>() | ||
| 17 | + | ||
| 18 | + return { pass: true, message: () => '' } | ||
| 19 | + }, | ||
| 20 | + toEqualMultiple(received, a, b) { | ||
| 21 | + expectTypeOf(received).toBeAny() | ||
| 22 | + expectTypeOf(a).toBeString() | ||
| 23 | + expectTypeOf(b).toBeNumber() | ||
| 24 | + | ||
| 25 | + return { pass: true, message: () => '' } | ||
| 26 | + }, | ||
| 27 | + }) | ||
| 28 | + | ||
| 29 | + expect({ a: 1, b: '2' }).toMatchSchema({ a: '1' }) | ||
| 30 | + expect('a').toEqualMultiple('a', 1) | ||
| 31 | + }) | ||
| 32 | + | ||
| 33 | + test('automatically extends asymmetric matchers', () => { | ||
| 34 | + expect({}).toEqual({ | ||
| 35 | + nestedSchema: expect.toMatchSchema({ | ||
| 36 | + a: '1', | ||
| 37 | + // @ts-expect-error Unknown property. | ||
| 38 | + b: 2, | ||
| 39 | + }), | ||
| 40 | + }) | ||
| 41 | + }) | ||
| 42 | + | ||
| 43 | + test('treats matcher declarations as optional', () => { | ||
| 44 | + expect.extend( | ||
| 45 | + /** | ||
| 46 | + * @note Although annotated, you don't have to declare matchers. | ||
| 47 | + * You can call `expect.extend()` multiple times or get the matcher | ||
| 48 | + * declarations from a third-party library. | ||
| 49 | + */ | ||
| 50 | + {}, | ||
| 51 | + ) | ||
| 52 | + }) | ||
| Back | FazBrowse Home | New Git URL |
0 commit comments