feat(types)!: add better promise support in expects and matchers (#8266)

Co-authored-by: Vladimir Sheremet <[email protected]>
This commit is contained in:
Sam Chung
2026-07-29 15:13:27 +02:00
committed by GitHub
co-authored by Vladimir Sheremet
parent 50689ab3d1
commit 0b07848e4c
23 changed files with 338 additions and 218 deletions
+17 -14
View File
@@ -9,8 +9,11 @@ type Awaitable<T> = T | PromiseLike<T>
`expect` is used to create assertions. In this context `assertions` are functions that can be called to assert a statement. Vitest provides `chai` assertions by default and also `Jest` compatible assertions built on top of `chai`. Since Vitest 4.1, for spy/mock testing, Vitest also provides Chai-style assertions (e.g., [`expect(spy).to.have.been.called()`](#called)) alongside Jest-style assertions (e.g., `expect(spy).toHaveBeenCalled()`). Unlike `Jest`, Vitest supports a message as the second argument - if the assertion fails, the error message will be equal to it.
```ts
export interface ExpectStatic extends Chai.ExpectStatic, AsymmetricMatchersContaining {
<T>(actual: T, message?: string): Assertion<T>
export interface ExpectStatic
extends Chai.ExpectStatic,
Matchers<any>,
AsymmetricMatchersContaining {
<T>(actual: T, message?: string): Assertion<void, T>
extend: (expects: MatchersObject) => void
anything: () => any
any: (constructor: unknown) => any
@@ -2242,12 +2245,11 @@ import { expect, test } from 'vitest'
test('custom matchers', () => {
expect.extend({
toBeFoo: (received, expected) => {
if (received !== 'foo') {
return {
message: () => `expected ${received} to be foo`,
pass: false,
}
toBeFoo(received) {
const { isNot } = this
return {
message: () => `expected ${received} is${isNot ? ' not' : ''} foo`,
pass: received === 'foo',
}
},
})
@@ -2263,19 +2265,20 @@ If you want your matchers to appear in every test, you should call this method i
This function is compatible with Jest's `expect.extend`, so any library that uses it to create custom matchers will work with Vitest.
If you are using TypeScript, since Vitest 0.31.0 you can extend default `Assertion` interface in an ambient declaration file (e.g: `vitest.d.ts`) with the code below:
If you are using TypeScript, you can extend the default `Matchers` interface in an ambient declaration file (e.g: `vitest.d.ts`) with the code below:
```ts
interface CustomMatchers<R = unknown> {
toBeFoo: () => R
}
import 'vitest'
declare module 'vitest' {
interface Assertion<T = any> extends CustomMatchers<T> {}
interface AsymmetricMatchersContaining extends CustomMatchers {}
interface Matchers<R, T> {
toBeFoo: () => R
}
}
```
`R` is the assertion return type, and `T` is the type of the received value.
::: warning
Don't forget to include the ambient declaration file in your `tsconfig.json`.
:::
+1 -2
View File
@@ -239,7 +239,7 @@ Vitest now has a `Matchers` type that you can extend to add type support for all
For example, to have a type-safe `toBeFoo` matcher, you can write something like this:
```ts twoslash
```ts
import { expect } from 'vitest'
interface CustomMatchers<R = unknown> {
@@ -252,7 +252,6 @@ declare module 'vitest' {
expect.extend({
toBeFoo(actual, arg) {
// ^?
// ... implementation
return {
pass: true,
+33 -9
View File
@@ -12,7 +12,7 @@ To extend default matchers, call `expect.extend` with an object containing your
```ts
expect.extend({
toBeFoo(received, expected) {
toBeFoo(received) {
const { isNot } = this
return {
// do not alter your "pass" based on isNot. Vitest does it for you
@@ -29,12 +29,24 @@ If you are using TypeScript, you can extend default `Matchers` interface in an a
import 'vitest'
declare module 'vitest' {
interface Matchers<T = any> {
interface Matchers<R, T> {
toBeFoo: () => R
}
}
```
`R` is the assertion return type, and `T` is the type of the received value.
Return `R` from matchers that run synchronously. This makes the return type `void` for a regular assertion and `Promise<void>` when the assertion is used with `.resolves`, `.rejects`, [`expect.poll`](/api/expect#poll), or [`expect.element`](/api/browser/assertions). You can use `T` when an expected argument should have the same type as the received value:
```ts
declare module 'vitest' {
interface Matchers<R, T> {
toEqualTyped: (expected: T) => R
}
}
```
::: tip
Importing `vitest` makes TypeScript think this is an ES module file, type declaration won't work without it.
:::
@@ -45,30 +57,42 @@ Extending the `Matchers` interface will add a type to `expect.extend`, `expect()
Don't forget to include the ambient declaration file in your `tsconfig.json`.
:::
The return value of a matcher should be compatible with the following interface:
The return value of a matcher should be compatible with the following types:
```ts
interface MatcherResult {
interface SyncMatcherResult {
pass: boolean
message: () => string
// If you pass these, they will automatically appear inside a diff when
// the matcher does not pass, so you don't need to print the diff yourself
actual?: unknown
expected?: unknown
meta?: object
}
type MatcherResult = SyncMatcherResult | Promise<SyncMatcherResult>
```
::: warning
If you create an asynchronous matcher, don't forget to `await` the result (`await expect('foo').toBeFoo()`) in the test itself:
If a matcher implementation is asynchronous, declare its return type as `Promise<void>` instead of `R` and don't forget to `await` it in the test:
```ts
expect.extend({
async toBeAsyncAssertion() {
// ...
async toBeAsyncAssertion(received) {
return {
pass: received === 'foo',
message: () => `expected ${received} to be foo`,
}
}
})
await expect().toBeAsyncAssertion()
declare module 'vitest' {
interface Matchers<R, T> {
toBeAsyncAssertion: () => Promise<void>
}
}
await expect('foo').toBeAsyncAssertion()
```
:::
@@ -155,7 +179,7 @@ The name of the current [`environment`](/config/environment) (for example, `jsdo
Was assertion called as a [`soft`](/api/expect#soft) one. You don't need to respect it, Vitest will always catch the error.
## `assertion` <Advanced /> <Version type="experimental">4.1.4</Version> {#assertion}
## `assertion` <Advanced /> <Version>5.0.0</Version> {#assertion}
The underlying [Chai assertion](https://www.chaijs.com/guide/plugins/) object. This is the same instance that Chai plugins receive, giving you access to Chai's flag system and chainable methods. This can be useful for building custom matchers that need to interact with Chai's internals.
+37
View File
@@ -225,6 +225,43 @@ To assert that a thrown error has an empty message, match the pattern explicitly
expect(() => { throw new Error('boom') }).not.toThrow(/^$/)
```
### Assertion Types Expose Return and Received Types
Assertion interfaces now use two type parameters: `R` is the matcher return type and `T` is the received value type. Synchronous assertions use `void`, while assertions accessed through `.resolves`, `.rejects`, [`expect.poll`](/api/expect#poll), or [`expect.element`](/api/browser/assertions) use `Promise<void>`.
If you declare custom matchers, augment the `Matchers<R, T>` interface. It adds the matcher to instance assertions, asymmetric matchers, and the type accepted by `expect.extend`:
```ts [vitest.d.ts]
import 'vitest'
interface CustomMatchers<R = unknown, T = unknown> {
toBeFoo: () => R
toEqualTyped: (expected: T) => R
}
declare module 'vitest' {
interface Matchers<R, T> extends CustomMatchers<R, T> {}
}
```
This makes custom matcher return types reflect how the matcher is used:
```ts
const syncResult = expect('value').toEqualTyped('other') // void
const asyncResult = expect(Promise.resolve('value')).resolves.toEqualTyped('other') // Promise<void>
await asyncResult
```
Code that refers to assertion types directly must also provide the return type first:
```ts
Assertion<string> // [!code --]
Assertion<void, string> // [!code ++]
Assertion<Promise<void>, string> // asynchronous assertion
```
Vitest no longer reads custom matcher declarations from the global `jest.Matchers` interface. Libraries that support both Jest and Vitest should augment `jest.Matchers` and `vitest.Matchers` separately. This only affects TypeScript declarations; registering matchers with `expect.extend` works as before.
### `expect.poll` Fails When It Times Out
[`expect.poll`](/api/expect#poll) now rejects when its callback, or the polled assertion, does not settle within `timeout`. Previously a callback that resolved after the deadline, or an assertion that only passed on a late attempt, could still succeed. The callback now also receives an `AbortSignal` that aborts when the timeout elapses, so you can cancel in-flight work:
+25 -11
View File
@@ -246,8 +246,8 @@ import { expect, test, Snapshots } from 'vitest'
const { toMatchFileSnapshot, toMatchInlineSnapshot, toMatchSnapshot } = Snapshots
expect.extend({
toMatchTrimmedSnapshot(received: string) {
return toMatchSnapshot.call(this, received.slice(0, 10))
toMatchTrimmedSnapshot(received: string, length: number) {
return toMatchSnapshot.call(this, received.slice(0, length))
},
toMatchTrimmedInlineSnapshot(received: string, inlineSnapshot?: string) {
return toMatchInlineSnapshot.call(this, received.slice(0, 10), inlineSnapshot)
@@ -298,7 +298,7 @@ File snapshot matchers must be `async` — `toMatchFileSnapshot` returns a `Prom
:::
::: warning
When custom inline snapshot matcher is aynchronous, Vitest cannot automatically infer the call location for inline snapshot rewriting. You must capture the call site by setting the `'error'` flag on the chai assertion object:
When custom inline snapshot matcher is asynchronous, Vitest cannot automatically infer the call location for inline snapshot rewriting. You must capture the call site by setting the `'error'` flag on the chai assertion object:
```ts
import { expect, chai, Snapshots } from 'vitest'
@@ -317,16 +317,16 @@ expect.extend({
:::
For TypeScript, extend the `Assertion` interface:
For TypeScript, augment the `Matchers<R, T>` interface:
```ts
import 'vitest'
declare module 'vitest' {
interface Assertion<T = any> {
toMatchTrimmedSnapshot: (length: number) => T
toMatchTrimmedInlineSnapshot: (inlineSnapshot?: string) => T
toMatchTrimmedFileSnapshot: (file: string) => Promise<T>
interface Matchers<R, T> {
toMatchTrimmedSnapshot: (length: number) => R
toMatchTrimmedInlineSnapshot: (inlineSnapshot?: string) => R
toMatchTrimmedFileSnapshot: (file: string) => Promise<void>
}
}
```
@@ -391,14 +391,21 @@ This asymmetry is what makes `--update` work correctly: `match` returns a `resol
Register a custom matcher with `expect.extend(...)` and call the snapshot composables from `vitest`:
```ts [setup.ts]
import { expect, Snaphsots } from 'vitest'
import { expect, Snapshots } from 'vitest'
declare module 'vitest' {
interface Matchers<R, T> {
toMatchMyDomainSnapshot: () => R
toMatchMyDomainInlineSnapshot: (inlineSnapshot?: string) => R
}
}
expect.extend({
toMatchMyDomainSnapshot(received: unknown) {
return Snaphsots.toMatchDomainSnapshot.call(this, myAdapter, received)
return Snapshots.toMatchDomainSnapshot.call(this, myAdapter, received)
},
toMatchMyDomainInlineSnapshot(received: unknown, inlineSnapshot?: string) {
return Snaphsots.toMatchDomainInlineSnapshot.call(
return Snapshots.toMatchDomainInlineSnapshot.call(
this,
myAdapter,
received,
@@ -494,6 +501,13 @@ export const kvAdapter: DomainSnapshotAdapter<KVCaptured, KVExpected> = {
import { expect, Snapshots } from 'vitest'
import { kvAdapter } from './kv-adapter'
declare module 'vitest' {
interface Matchers<R, T> {
toMatchKvSnapshot: () => R
toMatchKvInlineSnapshot: (inlineSnapshot?: string) => R
}
}
expect.extend({
toMatchKvSnapshot(received: unknown) {
return Snapshots.toMatchDomainSnapshot.call(this, kvAdapter, received)
+4 -4
View File
@@ -3,7 +3,7 @@
import { ARIARole } from './aria-role.js'
import { Locator, ScreenshotComparatorRegistry, ScreenshotMatcherOptions } from './context.js'
export interface TestingLibraryMatchers<E, R> {
export interface TestingLibraryMatchers<R extends void | Promise<void>, T = unknown> {
/**
* @description
* Assert whether an element is present in the document or not.
@@ -488,7 +488,7 @@ export interface TestingLibraryMatchers<E, R> {
* await expect.element(page.getByTestId('logo')).toHaveAccessibleDescription('The logo of Our Company')
* @see https://vitest.dev/api/browser/assertions#tohaveaccessibledescription
*/
toHaveAccessibleDescription(text?: string | RegExp | E): R
toHaveAccessibleDescription(text?: string | RegExp | T): R
/**
* @description
@@ -527,7 +527,7 @@ export interface TestingLibraryMatchers<E, R> {
*
* @see https://vitest.dev/api/browser/assertions#tohaveaccessibleerrormessage
*/
toHaveAccessibleErrorMessage(text?: string | RegExp | E): R
toHaveAccessibleErrorMessage(text?: string | RegExp | T): R
/**
* @description
@@ -558,7 +558,7 @@ export interface TestingLibraryMatchers<E, R> {
* await expect.element(page.getByTestId('input-title')).toHaveAccessibleName()
* @see https://vitest.dev/api/browser/assertions#tohaveaccessiblename
*/
toHaveAccessibleName(text?: string | RegExp | E): R
toHaveAccessibleName(text?: string | RegExp | T): R
/**
* @description
* This allows you to assert that an element has the expected
+5 -13
View File
@@ -3,18 +3,7 @@ import type { TestingLibraryMatchers } from './jest-dom.js'
import type { Assertion, ExpectPollOptions } from 'vitest'
declare module 'vitest' {
interface JestAssertion<T = any> extends TestingLibraryMatchers<void, T> {}
interface AsymmetricMatchersContaining extends TestingLibraryMatchers<void, void> {}
type Promisify<O> = {
[K in keyof O]: O[K] extends (...args: infer A) => infer R
? O extends R
? Promisify<O[K]>
: (...args: A) => Promise<R>
: O[K];
}
type PromisifyDomAssertion<T> = Promisify<Assertion<T>>
interface Assertion<R, T> extends TestingLibraryMatchers<R, T> {}
interface ExpectStatic {
/**
@@ -23,7 +12,10 @@ declare module 'vitest' {
* You can set default timeout via `expect.poll.timeout` option in the config.
* @see {@link https://vitest.dev/api/expect#poll}
*/
element: <T extends HTMLElement | SVGElement | null | Locator>(element: T, options?: ExpectPollOptions) => PromisifyDomAssertion<Awaited<HTMLElement | SVGElement | null>>
element: <T extends HTMLElement | SVGElement | null | Locator>(element: T, options?: ExpectPollOptions) => Assertion<
Promise<void>,
HTMLElement | SVGElement | null
>
}
}
@@ -1,4 +1,4 @@
import type { Assertion, ExpectPollOptions, PromisifyDomAssertion } from 'vitest'
import type { Assertion, ExpectPollOptions } from 'vitest'
import type { Locator } from 'vitest/browser'
import type { BrowserTraceEntryStatus } from './trace'
import { chai, expect } from 'vitest'
@@ -11,7 +11,7 @@ import { createBrowserTraceRangeId, recordBrowserTraceEntry } from './trace'
const kLocator = Symbol.for('$$vitest:locator')
function element<T extends HTMLElement | SVGElement | null | Locator>(elementOrLocator: T, options?: ExpectPollOptions): PromisifyDomAssertion<HTMLElement | SVGElement | null> {
function element<T extends HTMLElement | SVGElement | null | Locator>(elementOrLocator: T, options?: ExpectPollOptions): Assertion<Promise<void>, HTMLElement | SVGElement | null> {
if (elementOrLocator != null && !(elementOrLocator instanceof HTMLElement) && !(elementOrLocator instanceof SVGElement) && !(kLocator in elementOrLocator)) {
throw new Error(`Invalid element or locator: ${elementOrLocator}. Expected an instance of HTMLElement, SVGElement or Locator, received ${getType(elementOrLocator)}`)
}
+3 -2
View File
@@ -1,5 +1,6 @@
import type { Test } from '../../vitest/src/runtime/runner/types'
import type {
Assertion,
ChaiPlugin,
ExpectStatic,
MatchersObject,
@@ -20,7 +21,7 @@ import { getState } from './state'
import { wrapAssertion } from './utils'
function getMatcherState(
assertion: Chai.AssertionStatic & Chai.Assertion,
assertion: Chai.AssertionStatic & Assertion,
expect: ExpectStatic,
) {
const obj = assertion._obj
@@ -91,7 +92,7 @@ function JestExtendPlugin(
Object.entries(matchers).forEach(
([expectAssertionName, expectAssertion]) => {
function __VITEST_EXTEND_ASSERTION__(
this: Chai.AssertionStatic & Chai.Assertion,
this: Chai.AssertionStatic & Assertion,
...args: any[]
) {
const { state, isNot, obj, customMessage } = getMatcherState(this, expect)
+95 -101
View File
@@ -106,23 +106,23 @@ export interface RawMatcherFn<T extends MatcherState = MatcherState, E extends A
(this: T, received: any, ...expected: E): ExpectationResult
}
// Allow unused `T` to preserve its name for extensions.
// Allow unused type parameters to preserve their names for extensions.
// Type parameter names must be identical when extending those types.
// eslint-disable-next-line
export interface Matchers<T = any> {}
export interface Matchers<R extends void | Promise<void> = void | Promise<void>, T = unknown> {}
export type MatchersObject<T extends MatcherState = MatcherState> = Record<
string,
RawMatcherFn<T>
> & ThisType<T> & {
[K in keyof Matchers<T>]?: RawMatcherFn<T, Parameters<Matchers<T>[K]>>
[K in keyof Matchers]?: RawMatcherFn<T, Parameters<Matchers[K]>>
}
export interface ExpectStatic
extends Chai.ExpectStatic,
Matchers,
Matchers<any>,
AsymmetricMatchersContaining {
<T>(actual: T, message?: string): Assertion<T>
<T>(actual: T, message?: string): Assertion<void, T>
extend: (expects: MatchersObject) => void
anything: () => any
any: (constructor: unknown) => any
@@ -131,7 +131,7 @@ export interface ExpectStatic
not: AsymmetricMatchersContaining
}
interface CustomMatcher {
interface CustomMatcher<R = any> {
/**
* Checks that a value satisfies a custom matcher function.
*
@@ -142,7 +142,7 @@ interface CustomMatcher {
* expect(age).toSatisfy(val => val >= 18, 'Age must be at least 18');
* expect(age).toEqual(expect.toSatisfy(val => val >= 18, 'Age must be at least 18'));
*/
toSatisfy: (matcher: (value: any) => boolean, message?: string) => any
toSatisfy: (matcher: (value: any) => boolean, message?: string) => R
/**
* Matches if the received value is one of the values in the expected array or set.
@@ -152,10 +152,10 @@ interface CustomMatcher {
* expect('foo').toBeOneOf([expect.any(String)])
* expect({ a: 1 }).toEqual({ a: expect.toBeOneOf(['1', '2', '3']) })
*/
toBeOneOf: <T>(sample: ReadonlyArray<T> | ReadonlySet<T>) => any
toBeOneOf: <T>(sample: ReadonlyArray<T> | ReadonlySet<T>) => R
}
export interface AsymmetricMatchersContaining extends CustomMatcher {
export interface AsymmetricMatchersContaining extends Matchers<any>, CustomMatcher {
/**
* Matches if the received string contains the expected substring.
*
@@ -219,7 +219,8 @@ export type DeeplyAllowMatchers<T> = T extends Array<infer Element>
? WithAsymmetricMatcher<T> | { [K in keyof T]: DeeplyAllowMatchers<T[K]> }
: WithAsymmetricMatcher<T>
export interface JestAssertion<T = any> extends jest.Matchers<void, T>, CustomMatcher {
// eslint-disable-next-line unused-imports/no-unused-vars
export interface JestAssertion<R extends void | Promise<void>, T = unknown> extends CustomMatcher<R> {
/**
* Used when you want to check that two objects have the same value.
* This matcher recursively checks the equality of all fields, rather than checking for object identity.
@@ -227,7 +228,7 @@ export interface JestAssertion<T = any> extends jest.Matchers<void, T>, CustomMa
* @example
* expect(user).toEqual({ name: 'Alice', age: 30 });
*/
toEqual: <E>(expected: E) => void
toEqual: <E>(expected: E) => R
/**
* Use to test that objects have the same types as well as structure.
@@ -235,7 +236,7 @@ export interface JestAssertion<T = any> extends jest.Matchers<void, T>, CustomMa
* @example
* expect(user).toStrictEqual({ name: 'Alice', age: 30 });
*/
toStrictEqual: <E>(expected: E) => void
toStrictEqual: <E>(expected: E) => R
/**
* Checks that a value is what you expect. It calls `Object.is` to compare values.
@@ -245,7 +246,7 @@ export interface JestAssertion<T = any> extends jest.Matchers<void, T>, CustomMa
* expect(result).toBe(42);
* expect(status).toBe(true);
*/
toBe: <E>(expected: E) => void
toBe: <E>(expected: E) => R
/**
* Check that a string matches a regular expression.
@@ -254,7 +255,7 @@ export interface JestAssertion<T = any> extends jest.Matchers<void, T>, CustomMa
* expect(message).toMatch(/hello/);
* expect(greeting).toMatch('world');
*/
toMatch: (expected: string | RegExp) => void
toMatch: (expected: string | RegExp) => R
/**
* Used to check that a JavaScript object matches a subset of the properties of an object
@@ -265,7 +266,7 @@ export interface JestAssertion<T = any> extends jest.Matchers<void, T>, CustomMa
* address: { city: 'Wonderland' }
* });
*/
toMatchObject: <E extends object | any[]>(expected: E) => void
toMatchObject: <E extends object | any[]>(expected: E) => R
/**
* Used when you want to check that an item is in a list.
@@ -275,7 +276,7 @@ export interface JestAssertion<T = any> extends jest.Matchers<void, T>, CustomMa
* expect(items).toContain('apple');
* expect(numbers).toContain(5);
*/
toContain: <E>(item: E) => void
toContain: <E>(item: E) => R
/**
* Used when you want to check that an item is in a list.
@@ -285,7 +286,7 @@ export interface JestAssertion<T = any> extends jest.Matchers<void, T>, CustomMa
* @example
* expect(items).toContainEqual({ name: 'apple', quantity: 1 });
*/
toContainEqual: <E>(item: E) => void
toContainEqual: <E>(item: E) => R
/**
* Use when you don't care what a value is, you just want to ensure a value
@@ -295,7 +296,7 @@ export interface JestAssertion<T = any> extends jest.Matchers<void, T>, CustomMa
* @example
* expect(user.isActive).toBeTruthy();
*/
toBeTruthy: () => void
toBeTruthy: () => R
/**
* When you don't care what a value is, you just want to
@@ -304,7 +305,7 @@ export interface JestAssertion<T = any> extends jest.Matchers<void, T>, CustomMa
* @example
* expect(user.isActive).toBeFalsy();
*/
toBeFalsy: () => void
toBeFalsy: () => R
/**
* For comparing floating point numbers.
@@ -312,7 +313,7 @@ export interface JestAssertion<T = any> extends jest.Matchers<void, T>, CustomMa
* @example
* expect(score).toBeGreaterThan(10);
*/
toBeGreaterThan: (num: number | bigint) => void
toBeGreaterThan: (num: number | bigint) => R
/**
* For comparing floating point numbers.
@@ -320,7 +321,7 @@ export interface JestAssertion<T = any> extends jest.Matchers<void, T>, CustomMa
* @example
* expect(score).toBeGreaterThanOrEqual(10);
*/
toBeGreaterThanOrEqual: (num: number | bigint) => void
toBeGreaterThanOrEqual: (num: number | bigint) => R
/**
* For comparing floating point numbers.
@@ -328,7 +329,7 @@ export interface JestAssertion<T = any> extends jest.Matchers<void, T>, CustomMa
* @example
* expect(score).toBeLessThan(10);
*/
toBeLessThan: (num: number | bigint) => void
toBeLessThan: (num: number | bigint) => R
/**
* For comparing floating point numbers.
@@ -336,7 +337,7 @@ export interface JestAssertion<T = any> extends jest.Matchers<void, T>, CustomMa
* @example
* expect(score).toBeLessThanOrEqual(10);
*/
toBeLessThanOrEqual: (num: number | bigint) => void
toBeLessThanOrEqual: (num: number | bigint) => R
/**
* Used to check that a variable is NaN.
@@ -344,7 +345,7 @@ export interface JestAssertion<T = any> extends jest.Matchers<void, T>, CustomMa
* @example
* expect(value).toBeNaN();
*/
toBeNaN: () => void
toBeNaN: () => R
/**
* Used to check that a variable is undefined.
@@ -352,7 +353,7 @@ export interface JestAssertion<T = any> extends jest.Matchers<void, T>, CustomMa
* @example
* expect(value).toBeUndefined();
*/
toBeUndefined: () => void
toBeUndefined: () => R
/**
* This is the same as `.toBe(null)` but the error messages are a bit nicer.
@@ -361,7 +362,7 @@ export interface JestAssertion<T = any> extends jest.Matchers<void, T>, CustomMa
* @example
* expect(value).toBeNull();
*/
toBeNull: () => void
toBeNull: () => R
/**
* Used to check that a variable is nullable (null or undefined).
@@ -369,7 +370,7 @@ export interface JestAssertion<T = any> extends jest.Matchers<void, T>, CustomMa
* @example
* expect(value).toBeNullable();
*/
toBeNullable: () => void
toBeNullable: () => R
/**
* Ensure that a variable is not undefined.
@@ -377,7 +378,7 @@ export interface JestAssertion<T = any> extends jest.Matchers<void, T>, CustomMa
* @example
* expect(value).toBeDefined();
*/
toBeDefined: () => void
toBeDefined: () => R
/**
* Ensure that an object is an instance of a class.
@@ -386,7 +387,7 @@ export interface JestAssertion<T = any> extends jest.Matchers<void, T>, CustomMa
* @example
* expect(new Date()).toBeInstanceOf(Date);
*/
toBeInstanceOf: <E>(expected: E) => void
toBeInstanceOf: <E>(expected: E) => R
/**
* Used to check that an object has a `.length` property
@@ -396,7 +397,7 @@ export interface JestAssertion<T = any> extends jest.Matchers<void, T>, CustomMa
* expect([1, 2, 3]).toHaveLength(3);
* expect('hello').toHaveLength(5);
*/
toHaveLength: (length: number) => void
toHaveLength: (length: number) => R
/**
* Use to check if a property at the specified path exists on an object.
@@ -414,7 +415,7 @@ export interface JestAssertion<T = any> extends jest.Matchers<void, T>, CustomMa
toHaveProperty: <E>(
property: string | (string | number)[],
value?: E,
) => void
) => R
/**
* Using exact equality with floating point numbers is a bad idea.
@@ -424,7 +425,7 @@ export interface JestAssertion<T = any> extends jest.Matchers<void, T>, CustomMa
* @example
* expect(price).toBeCloseTo(9.99, 2);
*/
toBeCloseTo: (number: number, numDigits?: number) => void
toBeCloseTo: (number: number, numDigits?: number) => R
/**
* Ensures that a mock function is called an exact number of times.
@@ -434,7 +435,7 @@ export interface JestAssertion<T = any> extends jest.Matchers<void, T>, CustomMa
* @example
* expect(mockFunc).toHaveBeenCalledTimes(2);
*/
toHaveBeenCalledTimes: (times: number) => void
toHaveBeenCalledTimes: (times: number) => R
/**
* Ensures that a mock function is called an exact number of times.
@@ -445,7 +446,7 @@ export interface JestAssertion<T = any> extends jest.Matchers<void, T>, CustomMa
* expect(mockFunc).toBeCalledTimes(2);
* @deprecated Use `toHaveBeenCalledTimes` instead
*/
toBeCalledTimes: (times: number) => void
toBeCalledTimes: (times: number) => R
/**
* Ensures that a mock function is called.
@@ -456,7 +457,7 @@ export interface JestAssertion<T = any> extends jest.Matchers<void, T>, CustomMa
* expect(mockFunc).toHaveBeenCalled();
*/
toHaveBeenCalled: () => void
toHaveBeenCalled: () => R
/**
* Ensures that a mock function is called.
@@ -467,7 +468,7 @@ export interface JestAssertion<T = any> extends jest.Matchers<void, T>, CustomMa
* expect(mockFunc).toBeCalled();
* @deprecated Use `toHaveBeenCalled` instead
*/
toBeCalled: () => void
toBeCalled: () => R
/**
* Ensure that a mock function is called with specific arguments.
@@ -477,7 +478,7 @@ export interface JestAssertion<T = any> extends jest.Matchers<void, T>, CustomMa
* @example
* expect(mockFunc).toHaveBeenCalledWith('arg1', 42);
*/
toHaveBeenCalledWith: <E extends any[]>(...args: E) => void
toHaveBeenCalledWith: <E extends any[]>(...args: E) => R
/**
* Ensure that a mock function is called with specific arguments.
@@ -488,7 +489,7 @@ export interface JestAssertion<T = any> extends jest.Matchers<void, T>, CustomMa
* expect(mockFunc).toBeCalledWith('arg1', 42);
* @deprecated Use `toHaveBeenCalledWith` instead
*/
toBeCalledWith: <E extends any[]>(...args: E) => void
toBeCalledWith: <E extends any[]>(...args: E) => R
/**
* Ensure that a mock function is called with specific arguments on an Nth call.
@@ -498,7 +499,7 @@ export interface JestAssertion<T = any> extends jest.Matchers<void, T>, CustomMa
* @example
* expect(mockFunc).toHaveBeenNthCalledWith(2, 'secondArg');
*/
toHaveBeenNthCalledWith: <E extends any[]>(n: number, ...args: E) => void
toHaveBeenNthCalledWith: <E extends any[]>(n: number, ...args: E) => R
/**
* If you have a mock function, you can use `.toHaveBeenLastCalledWith`
@@ -509,7 +510,7 @@ export interface JestAssertion<T = any> extends jest.Matchers<void, T>, CustomMa
* @example
* expect(mockFunc).toHaveBeenLastCalledWith('lastArg');
*/
toHaveBeenLastCalledWith: <E extends any[]>(...args: E) => void
toHaveBeenLastCalledWith: <E extends any[]>(...args: E) => R
/**
* Used to test that a function throws when it is called.
@@ -521,7 +522,7 @@ export interface JestAssertion<T = any> extends jest.Matchers<void, T>, CustomMa
* expect(() => parseJSON('invalid')).toThrow(SyntaxError);
* expect(() => { throw 42 }).toThrow(42);
*/
toThrow: (expected?: any) => void
toThrow: (expected?: any) => R
/**
* Used to test that a function throws when it is called.
@@ -534,7 +535,7 @@ export interface JestAssertion<T = any> extends jest.Matchers<void, T>, CustomMa
* expect(() => { throw 42 }).toThrowError(42);
* @deprecated Use `toThrow` instead
*/
toThrowError: (expected?: any) => void
toThrowError: (expected?: any) => R
/**
* Use to test that the mock function successfully returned (i.e., did not throw an error) at least one time
@@ -545,7 +546,7 @@ export interface JestAssertion<T = any> extends jest.Matchers<void, T>, CustomMa
* expect(mockFunc).toReturn();
* @deprecated Use `toHaveReturned` instead
*/
toReturn: () => void
toReturn: () => R
/**
* Use to test that the mock function successfully returned (i.e., did not throw an error) at least one time
@@ -555,7 +556,7 @@ export interface JestAssertion<T = any> extends jest.Matchers<void, T>, CustomMa
* @example
* expect(mockFunc).toHaveReturned();
*/
toHaveReturned: () => void
toHaveReturned: () => R
/**
* Use to ensure that a mock function returned successfully (i.e., did not throw an error) an exact number of times.
@@ -567,7 +568,7 @@ export interface JestAssertion<T = any> extends jest.Matchers<void, T>, CustomMa
* expect(mockFunc).toReturnTimes(3);
* @deprecated Use `toHaveReturnedTimes` instead
*/
toReturnTimes: (times: number) => void
toReturnTimes: (times: number) => R
/**
* Use to ensure that a mock function returned successfully (i.e., did not throw an error) an exact number of times.
@@ -578,7 +579,7 @@ export interface JestAssertion<T = any> extends jest.Matchers<void, T>, CustomMa
* @example
* expect(mockFunc).toHaveReturnedTimes(3);
*/
toHaveReturnedTimes: (times: number) => void
toHaveReturnedTimes: (times: number) => R
/**
* Use to ensure that a mock function returned a specific value.
@@ -589,7 +590,7 @@ export interface JestAssertion<T = any> extends jest.Matchers<void, T>, CustomMa
* expect(mockFunc).toReturnWith('returnValue');
* @deprecated Use `toHaveReturnedWith` instead
*/
toReturnWith: <E>(value: E) => void
toReturnWith: <E>(value: E) => R
/**
* Use to ensure that a mock function returned a specific value.
@@ -599,7 +600,7 @@ export interface JestAssertion<T = any> extends jest.Matchers<void, T>, CustomMa
* @example
* expect(mockFunc).toHaveReturnedWith('returnValue');
*/
toHaveReturnedWith: <E>(value: E) => void
toHaveReturnedWith: <E>(value: E) => R
/**
* Use to test the specific value that a mock function last returned.
@@ -611,7 +612,7 @@ export interface JestAssertion<T = any> extends jest.Matchers<void, T>, CustomMa
* @example
* expect(mockFunc).toHaveLastReturnedWith('lastValue');
*/
toHaveLastReturnedWith: <E>(value: E) => void
toHaveLastReturnedWith: <E>(value: E) => R
/**
* Use to test the specific value that a mock function returned for the nth call.
@@ -623,30 +624,32 @@ export interface JestAssertion<T = any> extends jest.Matchers<void, T>, CustomMa
* @example
* expect(mockFunc).toHaveNthReturnedWith(2, 'nthValue');
*/
toHaveNthReturnedWith: <E>(nthCall: number, value: E) => void
toHaveNthReturnedWith: <E>(nthCall: number, value: E) => R
}
type VitestAssertion<A, T> = {
type VitestAssertion<A, R extends void | Promise<void>, T = unknown> = {
[K in keyof A]: A[K] extends Chai.Assertion
? Assertion<T>
? Assertion<R, T>
: A[K] extends (...args: any[]) => any
? A[K] // not converting function since they may contain overload
: VitestAssertion<A[K], T>;
} & ((type: string, message?: string) => Assertion)
? R extends Promise<void> ? PromisifyFunction<A[K]> : A[K]
: VitestAssertion<A[K], R, T>;
} & ((type: string, message?: string) => Assertion<R, T>)
type Promisify<O> = {
[K in keyof O]: O[K] extends (...args: infer A) => infer R
? Promisify<O[K]> & ((...args: A) => Promise<R>)
: O[K];
[K in keyof O]: PromisifyFunction<O[K]>
}
export type PromisifyAssertion<T> = Promisify<Assertion<T>>
type PromisifyFunction<T> = T extends (...args: infer A) => infer R
? Promisify<T> & ((...args: A) => R extends Promise<any> ? R : Promise<R>)
: T
export interface Assertion<T = any>
extends VitestAssertion<Chai.Assertion, T>,
JestAssertion<T>,
ChaiMockAssertion,
Matchers<T> {
export type PromisifyAssertion<T> = Assertion<Promise<void>, Awaited<T>>
export interface Assertion<R extends void | Promise<void> = void, T = unknown>
extends VitestAssertion<Chai.Assertion, R, T>,
JestAssertion<R, T>,
ChaiMockAssertion<R, T>,
Matchers<R, T> {
/**
* Ensures a value is of a specific type.
*
@@ -664,7 +667,7 @@ export interface Assertion<T = any>
| 'string'
| 'symbol'
| 'undefined',
) => void
) => R
/**
* Asserts that a mock function was called exactly once.
@@ -672,7 +675,7 @@ export interface Assertion<T = any>
* @example
* expect(mockFunc).toHaveBeenCalledOnce();
*/
toHaveBeenCalledOnce: () => void
toHaveBeenCalledOnce: () => R
/**
* Ensure that a mock function is called with specific arguments and called
@@ -681,7 +684,7 @@ export interface Assertion<T = any>
* @example
* expect(mockFunc).toHaveBeenCalledExactlyOnceWith('arg1', 42);
*/
toHaveBeenCalledExactlyOnceWith: <E extends any[]>(...args: E) => void
toHaveBeenCalledExactlyOnceWith: <E extends any[]>(...args: E) => R
/**
* This assertion checks if a `Mock` was called before another `Mock`.
@@ -697,7 +700,7 @@ export interface Assertion<T = any>
*
* expect(mock1).toHaveBeenCalledBefore(mock2)
*/
toHaveBeenCalledBefore: (mock: MockInstance, failIfNoFirstInvocation?: boolean) => void
toHaveBeenCalledBefore: (mock: MockInstance, failIfNoFirstInvocation?: boolean) => R
/**
* This assertion checks if a `Mock` was called after another `Mock`.
@@ -713,7 +716,7 @@ export interface Assertion<T = any>
*
* expect(mock1).toHaveBeenCalledAfter(mock2)
*/
toHaveBeenCalledAfter: (mock: MockInstance, failIfNoFirstInvocation?: boolean) => void
toHaveBeenCalledAfter: (mock: MockInstance, failIfNoFirstInvocation?: boolean) => R
/**
* Checks that a promise resolves successfully at least once.
@@ -721,7 +724,7 @@ export interface Assertion<T = any>
* @example
* await expect(promise).toHaveResolved();
*/
toHaveResolved: () => void
toHaveResolved: () => R
/**
* Checks that a promise resolves to a specific value.
@@ -729,7 +732,7 @@ export interface Assertion<T = any>
* @example
* await expect(promise).toHaveResolvedWith('success');
*/
toHaveResolvedWith: <E>(value: E) => void
toHaveResolvedWith: <E>(value: E) => R
/**
* Ensures a promise resolves a specific number of times.
@@ -737,7 +740,7 @@ export interface Assertion<T = any>
* @example
* expect(mockAsyncFunc).toHaveResolvedTimes(3);
*/
toHaveResolvedTimes: (times: number) => void
toHaveResolvedTimes: (times: number) => R
/**
* Asserts that the last resolved value of a promise matches an expected value.
@@ -745,7 +748,7 @@ export interface Assertion<T = any>
* @example
* await expect(mockAsyncFunc).toHaveLastResolvedWith('finalResult');
*/
toHaveLastResolvedWith: <E>(value: E) => void
toHaveLastResolvedWith: <E>(value: E) => R
/**
* Ensures a specific value was returned by a promise on the nth resolution.
@@ -753,7 +756,7 @@ export interface Assertion<T = any>
* @example
* await expect(mockAsyncFunc).toHaveNthResolvedWith(2, 'secondResult');
*/
toHaveNthResolvedWith: <E>(nthCall: number, value: E) => void
toHaveNthResolvedWith: <E>(nthCall: number, value: E) => R
/**
* Verifies that a promise resolves.
@@ -769,14 +772,14 @@ export interface Assertion<T = any>
* @example
* await expect(someAsyncFunc).rejects.toThrow('error');
*/
rejects: PromisifyAssertion<T>
rejects: PromisifyAssertion<unknown>
}
/**
* Chai-style assertions for spy/mock testing.
* These provide sinon-chai compatible assertion names that delegate to Jest-style implementations.
*/
export interface ChaiMockAssertion {
export interface ChaiMockAssertion<R extends void | Promise<void>, T = unknown> {
/**
* Checks that a spy was called at least once.
* Chai-style equivalent of `toHaveBeenCalled`.
@@ -784,7 +787,7 @@ export interface ChaiMockAssertion {
* @example
* expect(spy).to.have.been.called
*/
readonly called: Assertion
readonly called: Assertion<R, T>
/**
* Checks that a spy was called a specific number of times.
@@ -793,7 +796,7 @@ export interface ChaiMockAssertion {
* @example
* expect(spy).to.have.callCount(3)
*/
callCount: (count: number) => void
callCount: (count: number) => R
/**
* Checks that a spy was called with specific arguments at least once.
@@ -802,7 +805,7 @@ export interface ChaiMockAssertion {
* @example
* expect(spy).to.have.been.calledWith('arg1', 'arg2')
*/
calledWith: <E extends any[]>(...args: E) => void
calledWith: <E extends any[]>(...args: E) => R
/**
* Checks that a spy was called exactly once.
@@ -811,7 +814,7 @@ export interface ChaiMockAssertion {
* @example
* expect(spy).to.have.been.calledOnce
*/
readonly calledOnce: Assertion
readonly calledOnce: Assertion<R, T>
/**
* Checks that a spy was called exactly once with specific arguments.
@@ -820,7 +823,7 @@ export interface ChaiMockAssertion {
* @example
* expect(spy).to.have.been.calledOnceWith('arg1', 'arg2')
*/
calledOnceWith: <E extends any[]>(...args: E) => void
calledOnceWith: <E extends any[]>(...args: E) => R
/**
* Checks that the last call to a spy was made with specific arguments.
@@ -829,7 +832,7 @@ export interface ChaiMockAssertion {
* @example
* expect(spy).to.have.been.lastCalledWith('arg1', 'arg2')
*/
lastCalledWith: <E extends any[]>(...args: E) => void
lastCalledWith: <E extends any[]>(...args: E) => R
/**
* Checks that the nth call to a spy was made with specific arguments.
@@ -838,7 +841,7 @@ export interface ChaiMockAssertion {
* @example
* expect(spy).to.have.been.nthCalledWith(2, 'arg1', 'arg2')
*/
nthCalledWith: <E extends any[]>(n: number, ...args: E) => void
nthCalledWith: <E extends any[]>(n: number, ...args: E) => R
/**
* Checks that a spy returned a specific value at least once.
@@ -847,7 +850,7 @@ export interface ChaiMockAssertion {
* @example
* expect(spy).to.have.returned('value')
*/
returned: <E>(value: E) => void
returned: <E>(value: E) => R
/**
* Checks that a spy returned a specific value at least once.
@@ -856,7 +859,7 @@ export interface ChaiMockAssertion {
* @example
* expect(spy).to.have.returnedWith('value')
*/
returnedWith: <E>(value: E) => void
returnedWith: <E>(value: E) => R
/**
* Checks that a spy returned successfully a specific number of times.
@@ -865,7 +868,7 @@ export interface ChaiMockAssertion {
* @example
* expect(spy).to.have.returnedTimes(3)
*/
returnedTimes: (count: number) => void
returnedTimes: (count: number) => R
/**
* Checks that the last return value of a spy matches the expected value.
@@ -874,7 +877,7 @@ export interface ChaiMockAssertion {
* @example
* expect(spy).to.have.lastReturnedWith('value')
*/
lastReturnedWith: <E>(value: E) => void
lastReturnedWith: <E>(value: E) => R
/**
* Checks that the nth return value of a spy matches the expected value.
@@ -883,7 +886,7 @@ export interface ChaiMockAssertion {
* @example
* expect(spy).to.have.nthReturnedWith(2, 'value')
*/
nthReturnedWith: <E>(n: number, value: E) => void
nthReturnedWith: <E>(n: number, value: E) => R
/**
* Checks that a spy was called before another spy.
@@ -892,7 +895,7 @@ export interface ChaiMockAssertion {
* @example
* expect(spy1).to.have.been.calledBefore(spy2)
*/
calledBefore: (mock: MockInstance, failIfNoFirstInvocation?: boolean) => void
calledBefore: (mock: MockInstance, failIfNoFirstInvocation?: boolean) => R
/**
* Checks that a spy was called after another spy.
@@ -901,7 +904,7 @@ export interface ChaiMockAssertion {
* @example
* expect(spy1).to.have.been.calledAfter(spy2)
*/
calledAfter: (mock: MockInstance, failIfNoFirstInvocation?: boolean) => void
calledAfter: (mock: MockInstance, failIfNoFirstInvocation?: boolean) => R
/**
* Checks that a spy was called exactly twice.
@@ -910,7 +913,7 @@ export interface ChaiMockAssertion {
* @example
* expect(spy).to.have.been.calledTwice
*/
readonly calledTwice: Assertion
readonly calledTwice: Assertion<R, T>
/**
* Checks that a spy was called exactly three times.
@@ -919,16 +922,7 @@ export interface ChaiMockAssertion {
* @example
* expect(spy).to.have.been.calledThrice
*/
readonly calledThrice: Assertion
}
declare global {
// support augmenting jest.Matchers by other libraries
// eslint-disable-next-line ts/no-namespace
namespace jest {
// eslint-disable-next-line unused-imports/no-unused-vars, ts/no-empty-object-type
interface Matchers<R, T = {}> {}
}
readonly calledThrice: Assertion<R, T>
}
export {}
+16 -16
View File
@@ -4,21 +4,21 @@ import type { SnapshotState } from '@vitest/snapshot'
import type { BenchResult } from '../runtime/benchmark'
import type { Test } from '../runtime/runner/types'
interface SnapshotMatcher<T> {
interface SnapshotMatcher<R extends void | Promise<void>, T = unknown> {
<U extends { [P in keyof T]: any }>(
snapshot: Partial<U>,
hint?: string
): void
(hint?: string): void
): R
(hint?: string): R
}
interface InlineSnapshotMatcher<T> {
interface InlineSnapshotMatcher<R extends void | Promise<void>, T = unknown> {
<U extends { [P in keyof T]: any }>(
properties: Partial<U>,
snapshot?: string,
hint?: string
): void
(hint?: string): void
): R
(hint?: string): R
}
declare module 'vitest' {
@@ -37,7 +37,7 @@ declare module 'vitest' {
interface ExpectStatic {
assert: Chai.AssertStatic
unreachable: (message?: string) => never
soft: <T>(actual: T, message?: string) => Assertion<T>
soft: <T>(actual: T, message?: string) => Assertion<void, T>
poll: <T>(
actual: (options: { signal: AbortSignal }) => T,
options?: ExpectPollOptions,
@@ -48,11 +48,11 @@ declare module 'vitest' {
addSnapshotSerializer: (plugin: PrettyFormatPlugin) => void
}
interface Assertion<T> {
interface Assertion<R, T> {
// Snapshots are extended in @vitest/snapshot and are not part of @vitest/expect
matchSnapshot: SnapshotMatcher<T>
toMatchSnapshot: SnapshotMatcher<T>
toMatchInlineSnapshot: InlineSnapshotMatcher<T>
matchSnapshot: SnapshotMatcher<R, T>
toMatchSnapshot: SnapshotMatcher<R, T>
toMatchInlineSnapshot: InlineSnapshotMatcher<R, T>
/**
* Checks that an error thrown by a function matches a previously recorded snapshot.
@@ -62,7 +62,7 @@ declare module 'vitest' {
* @example
* expect(functionWithError).toThrowErrorMatchingSnapshot();
*/
toThrowErrorMatchingSnapshot: (hint?: string) => void
toThrowErrorMatchingSnapshot: (hint?: string) => R
/**
* Checks that an error thrown by a function matches an inline snapshot within the test file.
@@ -78,7 +78,7 @@ declare module 'vitest' {
toThrowErrorMatchingInlineSnapshot: (
snapshot?: string,
hint?: string,
) => void
) => R
/**
* Compares the received value to a snapshot saved in a specified file.
@@ -107,7 +107,7 @@ declare module 'vitest' {
toBeFasterThan: (
expected: BenchResult,
options?: { delta?: number },
) => void
) => R
/**
* Asserts that a benchmark result is slower than another benchmark result.
@@ -124,7 +124,7 @@ declare module 'vitest' {
toBeSlowerThan: (
expected: BenchResult,
options?: { delta?: number },
) => void
) => R
/**
* Ensures a `vi.when` chain has been exhausted.
@@ -144,6 +144,6 @@ declare module 'vitest' {
*
* expect(w).toHaveBeenExhausted()
*/
toHaveBeenExhausted: () => void
toHaveBeenExhausted: () => R
}
}
@@ -1,7 +1,7 @@
import { expect, test } from 'vitest'
import { page } from 'vitest/browser'
test('.toBeInTheDocument', () => {
test('.toBeInTheDocument', async () => {
const window = document.defaultView
window.customElements.define(
@@ -29,12 +29,14 @@ test('.toBeInTheDocument', () => {
const fakeElement = {thisIsNot: 'an html element'}
const undefinedElement = undefined
const nullElement = null
const promiseValue = Promise.resolve(null)
expect(htmlElement).toBeInTheDocument()
expect(svgElement).toBeInTheDocument()
expect(customElementChild).toBeInTheDocument()
expect(detachedElement).not.toBeInTheDocument()
expect(nullElement).not.toBeInTheDocument()
await (expect(promiseValue).resolves.not.toBeInTheDocument() satisfies Promise<void>)
expect(page.getByTestId('non-existing')).not.toBeInTheDocument()
@@ -62,4 +64,4 @@ test('.toBeInTheDocument', () => {
notFound,
)
expect(() => expect(undefinedElement).not.toBeInTheDocument()).not.toThrow()
})
})
+5 -5
View File
@@ -54,15 +54,15 @@ expect.extend({
})
declare module 'vitest' {
interface Matchers {
interface Matchers<R> {
// eslint-disable-next-line ts/method-signature-style
toReportPassedTest(testName: string, testProject?: string | BrowserInstanceOption[]): void
toReportPassedTest(testName: string, testProject?: string | BrowserInstanceOption[]): R
// eslint-disable-next-line ts/method-signature-style
toReportSummaryTestFiles(options: SummaryOptions): void
toReportSummaryTestFiles(options: SummaryOptions): R
// eslint-disable-next-line ts/method-signature-style
toReportSummaryTests(options: SummaryOptions): void
toReportSummaryTests(options: SummaryOptions): R
// eslint-disable-next-line ts/method-signature-style
toReportNoErrors(): void
toReportNoErrors(): R
}
}
+1
View File
@@ -37,6 +37,7 @@ describe('dom related activity', () => {
// test typing
if (0) {
await (expect.element(div).toHaveClass('x', { exact: true }) satisfies Promise<void>)
await expect.element(div).toHaveClass('x', { exact: true })
await expect.element(div).toHaveClass('x', 'y')
await expect.element(div).toHaveClass('x', /y/)
@@ -1,12 +1,12 @@
import { expect, test } from 'vitest'
interface CustomMatchers<R = unknown> {
toBeAsync: (expected: unknown) => Promise<R>;
toBeAsync: (expected: unknown) => Promise<void>;
toBeDividedBy(divisor: number): R
}
declare module 'vitest' {
interface Assertion<T = any> extends CustomMatchers<T> {}
interface Matchers<R, T> extends CustomMatchers<R> {}
}
expect.extend({
@@ -10,12 +10,12 @@ const {
interface CustomMatchers<R = unknown> {
toMatchCustomSnapshot: (properties?: object) => R
toMatchCustomInlineSnapshot: (snapshot?: string) => R
toMatchCustomFileSnapshot: (filepath: string) => Promise<R>
toMatchCustomAsyncInlineSnapshot: (snapshot?: string) => Promise<R>
toMatchCustomFileSnapshot: (filepath: string) => Promise<void>
toMatchCustomAsyncInlineSnapshot: (snapshot?: string) => Promise<void>
}
declare module 'vitest' {
interface Assertion<T = any> extends CustomMatchers<T> {}
interface Matchers<R> extends CustomMatchers<R> {}
}
function formatCustom(input: string) {
@@ -8,7 +8,7 @@ interface CustomMatchers<R = unknown> {
}
declare module 'vitest' {
interface Assertion<T = any> extends CustomMatchers<T> {}
interface Matchers<R> extends CustomMatchers<R> {}
}
const matchers: MatchersObject = {
@@ -6,8 +6,8 @@ import { toMatchImageSnapshot } from "jest-image-snapshot";
expect.extend({ toMatchImageSnapshot });
declare module 'vitest' {
interface Assertion<T = any> {
toMatchImageSnapshot(): void
interface Matchers<R> {
toMatchImageSnapshot(): R
}
}
+19 -4
View File
@@ -1,15 +1,16 @@
import { expect, expectTypeOf, test } from 'vitest'
interface CustomMatchers<R = unknown> {
interface CustomMatchers<R = unknown, T = unknown> {
toMatchSchema: (schema: { a: string }) => R
toEqualMultiple: (a: string, b: number) => R
toEqualTyped: (expected: T) => R
}
declare module 'vitest' {
interface Matchers<T = any> extends CustomMatchers<T> {}
interface Matchers<R, T> extends CustomMatchers<R, T> {}
}
test('infers matcher declaration type from a custom matcher type', () => {
test('infers matcher declaration type from a custom matcher type', async () => {
expect.extend({
toMatchSchema(received, expected) {
expectTypeOf(received).toBeAny()
@@ -22,12 +23,26 @@ test('infers matcher declaration type from a custom matcher type', () => {
expectTypeOf(a).toBeString()
expectTypeOf(b).toBeNumber()
return { pass: true, message: () => '' }
},
toEqualTyped(received, expected) {
expectTypeOf(received).toBeAny()
expectTypeOf(expected).toBeAny()
return { pass: true, message: () => '' }
},
})
expect({ a: 1, b: '2' }).toMatchSchema({ a: '1' })
expectTypeOf(expect({ a: 1, b: '2' }).toMatchSchema({ a: '1' })).toEqualTypeOf<void>()
await (expect(Promise.resolve({ a: '1' })).resolves.toMatchSchema({ a: '1' }) satisfies Promise<void>)
expect('a').toEqualMultiple('a', 1)
expect('a').toEqualTyped('b')
// @ts-expect-error Expected value must match the received type.
expect('a').toEqualTyped(1)
await (expect(Promise.resolve('a')).resolves.not.toEqualTyped('b') satisfies Promise<void>)
// @ts-expect-error Expected value must match the resolved type.
await expect(Promise.resolve('a')).resolves.toEqualTyped(1)
await (expect(Promise.reject(new Error('error'))).rejects.toEqualTyped(new Error('error')) satisfies Promise<void>)
})
test('automatically extends asymmetric matchers', () => {
+9 -3
View File
@@ -1,5 +1,11 @@
import { expect, test, vi } from 'vitest'
declare module 'vitest' {
interface Matchers<R> {
toBePollCompatible: () => R
}
}
test('simple usage', async () => {
await expect.poll(() => false).toBe(false)
await expect.poll(() => false).not.toBe(true)
@@ -38,7 +44,7 @@ test('timeout', async () => {
await expect.poll(() => false, { timeout: 100, interval: 10 }).toBe(true)
}).rejects.toThrow(expect.objectContaining({
message: 'expected false to be true // Object.is equality',
stack: expect.stringContaining('expect-poll.test.ts:38:68'),
stack: expect.stringContaining('expect-poll.test.ts:44:68'),
cause: expect.objectContaining({
message: 'Matcher did not succeed in time.',
}),
@@ -85,7 +91,7 @@ test('custom matcher works correctly', async () => {
const fn = vi.fn()
let idx = 0
expect.extend({
toBeJestCompatible() {
toBePollCompatible() {
idx++
fn({ poll: this.poll })
return {
@@ -94,7 +100,7 @@ test('custom matcher works correctly', async () => {
}
},
})
await expect.poll(() => 1, { interval: 10 }).toBeJestCompatible()
await expect.poll(() => 1, { interval: 10 }).toBePollCompatible()
expect(fn).toHaveBeenCalledTimes(3)
expect(fn).toHaveBeenCalledWith({ poll: true })
})
+47 -1
View File
@@ -2,35 +2,43 @@
import { expect, test } from 'vitest'
test('expect.* allows asymmetrict mattchers with different types', () => {
test('expect.* allows asymmetrict mattchers with different types', async () => {
// types.ts examples: stringContaining
expect('I have an apple').toEqual(expect.stringContaining('apple'))
expect('I have an apple').toEqual<string>(expect.stringContaining('apple'))
await (expect(Promise.resolve('I have an apple')).resolves.toEqual<string>(expect.stringContaining('apple')) satisfies Promise<void>)
expect({ a: 'test string' }).toEqual({ a: expect.stringContaining('test') })
expect({ a: 'test string' }).toEqual<{ a: string }>({ a: expect.stringContaining('test') })
await (expect(Promise.resolve({ a: 'test string' })).resolves.toEqual<{ a: string }>({ a: expect.stringContaining('test') }) satisfies Promise<void>)
// types.ts examples: objectContaining
expect({ a: '1', b: 2 }).toEqual(expect.objectContaining({ a: '1' }))
expect({ a: '1', b: 2 }).toEqual<{ a: string; b: string }>(expect.objectContaining({ a: '1' }))
await (expect(Promise.resolve({ a: '1', b: 2 })).resolves.toEqual<{ a: string; b: string }>(expect.objectContaining({ a: '1' })) satisfies Promise<void>)
// types.ts examples: arrayContaining
expect(['a', 'b', 'c']).toEqual(expect.arrayContaining(['b', 'a']))
expect(['a', 'b', 'c']).toEqual<string[]>(expect.arrayContaining(['b', 'a']))
await (expect(Promise.resolve(['a', 'b', 'c'])).resolves.toEqual<string[]>(expect.arrayContaining(['b', 'a'])) satisfies Promise<void>)
// types.ts examples: stringMatching
expect('hello world').toEqual(expect.stringMatching(/^hello/))
expect('hello world').toEqual<string>(expect.stringMatching(/^hello/))
await (expect(Promise.resolve('hello world')).resolves.toEqual<string>(expect.stringMatching(/^hello/)) satisfies Promise<void>)
expect('hello world').toEqual(expect.stringMatching('hello'))
expect('hello world').toEqual<string>(expect.stringMatching('hello'))
await (expect(Promise.resolve('hello world')).resolves.toEqual<string>(expect.stringMatching('hello')) satisfies Promise<void>)
// types.ts examples: closeTo
expect(10.45).toEqual(expect.closeTo(10.5, 1))
expect(10.45).toEqual<number>(expect.closeTo(10.5, 1))
await (expect(Promise.resolve(10.45)).resolves.toEqual<number>(expect.closeTo(10.5, 1)) satisfies Promise<void>)
expect(5.11).toEqual(expect.closeTo(5.12))
expect(5.11).toEqual<number>(expect.closeTo(5.12))
await (expect(Promise.resolve(5.11)).resolves.toEqual<number>(expect.closeTo(5.12)) satisfies Promise<void>)
// expect.any(String)
// https://github.com/vitest-dev/vitest/pull/7016#issuecomment-2517674066
@@ -52,6 +60,14 @@ test('expect.* allows asymmetrict mattchers with different types', () => {
id: expect.any(String),
name: 'Amelia',
})
await (expect(Promise.resolve(obj)).resolves.toEqual<{
id: string
name: string
}>({
id: expect.any(String),
name: 'Amelia',
}) satisfies Promise<void>)
}
// expect.any(Date)
@@ -81,6 +97,16 @@ test('expect.* allows asymmetrict mattchers with different types', () => {
createdAt: expect.any(Date),
})
await (expect(Promise.resolve(actual)).resolves.toEqual<{
foo: string
bar: string
createdAt: Date
}>({
foo: 'foo',
bar: 'bar',
createdAt: expect.any(Date),
}) satisfies Promise<void>)
expect(actual).toEqual<{
foo: string
bar: string
@@ -92,6 +118,18 @@ test('expect.* allows asymmetrict mattchers with different types', () => {
createdAt: expect.any(Date),
},
])
await (expect(Promise.resolve(actual)).resolves.toEqual<{
foo: string
bar: string
createdAt: Date
}[]>([
{
foo: 'foo',
bar: 'bar',
createdAt: expect.any(Date),
},
]) satisfies Promise<void>)
}
// expect.arrayContaining
@@ -99,18 +137,22 @@ test('expect.* allows asymmetrict mattchers with different types', () => {
{
expect([1, 2, 3]).toEqual(expect.arrayContaining(['a']))
expect([1, 2, 3]).toEqual<number[]>(expect.arrayContaining(['a']))
await (expect(Promise.resolve([1, 2, 3])).resolves.toEqual<number[]>(expect.arrayContaining(['a'])) satisfies Promise<void>)
expect([1, 2, 3]).toEqual(expect.arrayContaining([expect.any(Number)]))
expect([1, 2, 3]).toEqual<number[]>(expect.arrayContaining([expect.any(Number)]))
await (expect(Promise.resolve([1, 2, 3])).resolves.toEqual<number[]>(expect.arrayContaining([expect.any(Number)])) satisfies Promise<void>)
expect([1, 2, 3]).toEqual(expect.arrayContaining([expect.anything()]))
expect([1, 2, 3]).toEqual<number[]>(expect.arrayContaining([expect.anything()]))
await (expect(Promise.resolve([1, 2, 3])).resolves.toEqual<number[]>(expect.arrayContaining([expect.anything()])) satisfies Promise<void>)
}
// expect.any(Array)
// https://github.com/DefinitelyTyped/DefinitelyTyped/pull/62831/files#diff-ff7b882e4a29e7fe0e348a6bdf8b11774d606eaa221009b166b01389576d921fR1237
expect({ list: [1, 2, 3] }).toMatchObject({ list: expect.any(Array) })
expect({ list: [1, 2, 3] }).toMatchObject<{ list: number[] }>({ list: expect.any(Array) })
await (expect(Promise.resolve({ list: [1, 2, 3] })).resolves.toMatchObject<{ list: number[] }>({ list: expect.any(Array) }) satisfies Promise<void>)
// expect<T>
// https://github.com/vitest-dev/vitest/issues/8081
@@ -140,3 +182,7 @@ test('expect.* allows asymmetrict mattchers with different types', () => {
expect('A').toBeOneOf(new Set(['A', 'B']))
}
})
test('chai assertion', async () => {
await (expect(Promise.resolve('a')).resolves.eq('a') satisfies Promise<(type: string, message?: string) => Chai.Assertion>)
})
+5 -19
View File
@@ -16,17 +16,7 @@ interface CustomMatchers<R = unknown> {
}
declare module 'vitest' {
interface Assertion<T = any> extends CustomMatchers<T> {}
interface AsymmetricMatchersContaining extends CustomMatchers {}
}
declare global {
// eslint-disable-next-line ts/no-namespace
namespace jest {
interface Matchers<R> {
toBeJestCompatible: () => R
}
}
interface Matchers<R> extends CustomMatchers<R> {}
}
describe('jest-expect', () => {
@@ -280,12 +270,6 @@ describe('jest-expect', () => {
message: () => 'toBeTestedPromise',
})
},
toBeJestCompatible() {
return {
pass: true,
message: () => '',
}
},
toBeTestedMatcherContext<T>(received: unknown, expected: T) {
if (typeof this.utils?.stringify !== 'function') {
throw new TypeError('this.utils.stringify is not available.')
@@ -308,8 +292,6 @@ describe('jest-expect', () => {
expect(() => expect(null).toBeTestedSync()).toThrow('toBeTestedSync')
await expect(async () => await expect(null).toBeTestedAsync()).rejects.toThrow('toBeTestedAsync')
await expect(async () => await expect(null).toBeTestedPromise()).rejects.toThrow('toBeTestedPromise')
expect(expect).toBeJestCompatible()
})
it('object', () => {
@@ -815,6 +797,10 @@ describe('toSatisfy()', () => {
snapshotError(() => expect({ value: 2 }).toEqual({ value: expect.toSatisfy(isOdd) }))
snapshotError(() => expect({ value: 2 }).toEqual({ value: expect.toSatisfy(isOdd, 'ODD') }))
})
it('supports a promise return type', async () => {
await (expect(Promise.resolve(1)).resolves.toSatisfy(isOdd) satisfies Promise<void>)
})
})
describe('toHaveBeenCalled', () => {
+2 -2
View File
@@ -8,10 +8,10 @@ function reject() {
}
test('resolved inline', async () => {
await expect(resolve()).resolves.toMatchInlineSnapshot('"foo"')
await (expect(resolve()).resolves.toMatchInlineSnapshot('"foo"') satisfies Promise<void>)
})
test('rejected inline', async () => {
await expect(reject()).rejects.toMatchInlineSnapshot('[Error: foo]')
await (expect(reject()).rejects.toMatchInlineSnapshot('[Error: foo]') satisfies Promise<void>)
await expect(reject()).rejects.toThrowErrorMatchingInlineSnapshot(`[Error: foo]`)
})