diff --git a/.eslintrc b/.eslintrc index 018a2f1c078c..50ecd0645a6e 100644 --- a/.eslintrc +++ b/.eslintrc @@ -4,6 +4,7 @@ "no-only-tests/no-only-tests": "off", // prefer global Buffer to not initialize the whole module "n/prefer-global/buffer": "off", + "@typescript-eslint/no-invalid-this": "off", "no-restricted-imports": [ "error", { diff --git a/docs/api/expect.md b/docs/api/expect.md index 68527034bed0..9be9c272c57a 100644 --- a/docs/api/expect.md +++ b/docs/api/expect.md @@ -1290,18 +1290,16 @@ If the value in the error message is too truncated, you can increase [chaiConfig 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, you can extend default `Matchers` interface in an ambient declaration file (e.g: `vitest.d.ts`) with the code below: + 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: ```ts interface CustomMatchers { toBeFoo(): R } - declare namespace Vi { - interface Assertion extends CustomMatchers {} + declare module '@vitest/expect' { + interface Assertion extends CustomMatchers {} interface AsymmetricMatchersContaining extends CustomMatchers {} - - // Note: augmenting jest.Matchers interface will also work. } ``` diff --git a/docs/guide/extending-matchers.md b/docs/guide/extending-matchers.md index 15bd3d24813a..0e8790f80b5d 100644 --- a/docs/guide/extending-matchers.md +++ b/docs/guide/extending-matchers.md @@ -23,18 +23,16 @@ expect.extend({ }) ``` -If you are using TypeScript, you can extend default Matchers interface in an ambient declaration file (e.g: `vitest.d.ts`) with the code below: +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: ```ts interface CustomMatchers { toBeFoo(): R } -declare namespace Vi { - interface Assertion extends CustomMatchers {} +declare module '@vitest/expect' { + interface Assertion extends CustomMatchers {} interface AsymmetricMatchersContaining extends CustomMatchers {} - - // Note: augmenting jest.Matchers interface will also work. } ``` diff --git a/packages/expect/src/jest-asymmetric-matchers.ts b/packages/expect/src/jest-asymmetric-matchers.ts index 7e2c0c5d73b1..df50596b3b7e 100644 --- a/packages/expect/src/jest-asymmetric-matchers.ts +++ b/packages/expect/src/jest-asymmetric-matchers.ts @@ -21,7 +21,7 @@ export abstract class AsymmetricMatcher< constructor(protected sample: T, protected inverse = false) {} - protected getMatcherContext(expect?: Vi.ExpectStatic): State { + protected getMatcherContext(expect?: Chai.ExpectStatic): State { return { ...getState(expect || (globalThis as any)[GLOBAL_EXPECT]), equals, diff --git a/packages/expect/src/jest-expect.ts b/packages/expect/src/jest-expect.ts index e51f7b8d6420..5a065a3f49f4 100644 --- a/packages/expect/src/jest-expect.ts +++ b/packages/expect/src/jest-expect.ts @@ -3,7 +3,7 @@ import { assertTypes, getColors } from '@vitest/utils' import type { Constructable } from '@vitest/utils' import type { EnhancedSpy } from '@vitest/spy' import { isMockFunction } from '@vitest/spy' -import type { ChaiPlugin } from './types' +import type { Assertion, ChaiPlugin } from './types' import { arrayBufferEquality, generateToBeMessage, iterableEquality, equals as jestEquals, sparseArrayEquality, subsetEquality, typeEquality } from './jest-utils' import type { AsymmetricMatcher } from './jest-asymmetric-matchers' import { diff, stringify } from './jest-matcher-utils' @@ -14,8 +14,8 @@ import { recordAsyncExpect } from './utils' export const JestChaiExpect: ChaiPlugin = (chai, utils) => { const c = () => getColors() - function def(name: keyof Vi.Assertion | (keyof Vi.Assertion)[], fn: ((this: Chai.AssertionStatic & Vi.Assertion, ...args: any[]) => any)) { - const addMethod = (n: keyof Vi.Assertion) => { + function def(name: keyof Assertion | (keyof Assertion)[], fn: ((this: Chai.AssertionStatic & Assertion, ...args: any[]) => any)) { + const addMethod = (n: keyof Assertion) => { utils.addMethod(chai.Assertion.prototype, n, fn) utils.addMethod((globalThis as any)[JEST_MATCHERS_OBJECT].matchers, n, fn) } diff --git a/packages/expect/src/jest-extend.ts b/packages/expect/src/jest-extend.ts index 303c841248e0..4fd9e6378033 100644 --- a/packages/expect/src/jest-extend.ts +++ b/packages/expect/src/jest-extend.ts @@ -1,6 +1,7 @@ import { util } from 'chai' import type { ChaiPlugin, + ExpectStatic, MatcherState, MatchersObject, SyncExpectationResult, @@ -17,7 +18,7 @@ import { subsetEquality, } from './jest-utils' -function getMatcherState(assertion: Chai.AssertionStatic & Chai.Assertion, expect: Vi.ExpectStatic) { +function getMatcherState(assertion: Chai.AssertionStatic & Chai.Assertion, expect: ExpectStatic) { const obj = assertion._obj const isNot = util.flag(assertion, 'negate') as boolean const promise = util.flag(assertion, 'promise') || '' @@ -52,7 +53,7 @@ class JestExtendError extends Error { } } -function JestExtendPlugin(expect: Vi.ExpectStatic, matchers: MatchersObject): ChaiPlugin { +function JestExtendPlugin(expect: ExpectStatic, matchers: MatchersObject): ChaiPlugin { return (c, utils) => { Object.entries(matchers).forEach(([expectAssertionName, expectAssertion]) => { function expectWrapper(this: Chai.AssertionStatic & Chai.Assertion, ...args: any[]) { @@ -123,7 +124,7 @@ function JestExtendPlugin(expect: Vi.ExpectStatic, matchers: MatchersObject): Ch } export const JestExtend: ChaiPlugin = (chai, utils) => { - utils.addMethod(chai.expect, 'extend', (expect: Vi.ExpectStatic, expects: MatchersObject) => { + utils.addMethod(chai.expect, 'extend', (expect: ExpectStatic, expects: MatchersObject) => { chai.use(JestExtendPlugin(expect, expects)) }) } diff --git a/packages/expect/src/state.ts b/packages/expect/src/state.ts index 711cfcd23343..4d830e926a85 100644 --- a/packages/expect/src/state.ts +++ b/packages/expect/src/state.ts @@ -1,8 +1,8 @@ -import type { MatcherState } from './types' +import type { ExpectStatic, MatcherState } from './types' import { GLOBAL_EXPECT, JEST_MATCHERS_OBJECT, MATCHERS_OBJECT } from './constants' if (!Object.prototype.hasOwnProperty.call(globalThis, MATCHERS_OBJECT)) { - const globalState = new WeakMap() + const globalState = new WeakMap() const matchers = Object.create(null) Object.defineProperty(globalThis, MATCHERS_OBJECT, { get: () => globalState, @@ -16,13 +16,13 @@ if (!Object.prototype.hasOwnProperty.call(globalThis, MATCHERS_OBJECT)) { }) } -export function getState(expect: Vi.ExpectStatic): State { +export function getState(expect: ExpectStatic): State { return (globalThis as any)[MATCHERS_OBJECT].get(expect) } export function setState( state: Partial, - expect: Vi.ExpectStatic, + expect: ExpectStatic, ): void { const map = (globalThis as any)[MATCHERS_OBJECT] const current = map.get(expect) || {} diff --git a/packages/expect/src/types.ts b/packages/expect/src/types.ts index 1b6ae780b757..c6d4b020a833 100644 --- a/packages/expect/src/types.ts +++ b/packages/expect/src/types.ts @@ -9,6 +9,7 @@ import type { use as chaiUse } from 'chai' */ import type { Formatter } from 'picocolors/types' +import type { Constructable } from '@vitest/utils' import type { diff, getMatcherUtils, stringify } from './jest-matcher-utils' export type FirstFunctionArgument = T extends (arg: infer A) => unknown ? A : never @@ -96,3 +97,106 @@ export interface RawMatcherFn { } export type MatchersObject = Record> + +export interface ExpectStatic extends Chai.ExpectStatic, AsymmetricMatchersContaining { + (actual: T, message?: string): Assertion + + extend(expects: MatchersObject): void + assertions(expected: number): void + hasAssertions(): void + anything(): any + any(constructor: unknown): any + getState(): MatcherState + setState(state: Partial): void + not: AsymmetricMatchersContaining +} + +export interface AsymmetricMatchersContaining { + stringContaining(expected: string): any + objectContaining(expected: T): any + arrayContaining(expected: Array): any + stringMatching(expected: string | RegExp): any +} + +export interface JestAssertion extends jest.Matchers { + // Jest compact + toEqual(expected: E): void + toStrictEqual(expected: E): void + toBe(expected: E): void + toMatch(expected: string | RegExp): void + toMatchObject(expected: E): void + toContain(item: E): void + toContainEqual(item: E): void + toBeTruthy(): void + toBeFalsy(): void + toBeGreaterThan(num: number | bigint): void + toBeGreaterThanOrEqual(num: number | bigint): void + toBeLessThan(num: number | bigint): void + toBeLessThanOrEqual(num: number | bigint): void + toBeNaN(): void + toBeUndefined(): void + toBeNull(): void + toBeDefined(): void + toBeInstanceOf(expected: E): void + toBeCalledTimes(times: number): void + toHaveLength(length: number): void + toHaveProperty(property: string | (string | number)[], value?: E): void + toBeCloseTo(number: number, numDigits?: number): void + toHaveBeenCalledTimes(times: number): void + toHaveBeenCalled(): void + toBeCalled(): void + toHaveBeenCalledWith(...args: E): void + toBeCalledWith(...args: E): void + toHaveBeenNthCalledWith(n: number, ...args: E): void + nthCalledWith(nthCall: number, ...args: E): void + toHaveBeenLastCalledWith(...args: E): void + lastCalledWith(...args: E): void + toThrow(expected?: string | Constructable | RegExp | Error): void + toThrowError(expected?: string | Constructable | RegExp | Error): void + toReturn(): void + toHaveReturned(): void + toReturnTimes(times: number): void + toHaveReturnedTimes(times: number): void + toReturnWith(value: E): void + toHaveReturnedWith(value: E): void + toHaveLastReturnedWith(value: E): void + lastReturnedWith(value: E): void + toHaveNthReturnedWith(nthCall: number, value: E): void + nthReturnedWith(nthCall: number, value: E): void +} + +type VitestAssertion = { + [K in keyof A]: A[K] extends Chai.Assertion + ? Assertion + : A[K] extends (...args: any[]) => any + ? A[K] // not converting function since they may contain overload + : VitestAssertion +} & ((type: string, message?: string) => Assertion) + +type Promisify = { + [K in keyof O]: O[K] extends (...args: infer A) => infer R + ? O extends R + ? Promisify + : (...args: A) => Promise + : O[K] +} + +export interface Assertion extends VitestAssertion, JestAssertion { + toBeTypeOf(expected: 'bigint' | 'boolean' | 'function' | 'number' | 'object' | 'string' | 'symbol' | 'undefined'): void + toHaveBeenCalledOnce(): void + toSatisfy(matcher: (value: E) => boolean, message?: string): void + + resolves: Promisify> + rejects: Promisify> +} + +declare global { + // support augmenting jest.Matchers by other libraries + namespace jest { + + // eslint-disable-next-line unused-imports/no-unused-vars + interface Matchers {} + } +} + +export {} diff --git a/packages/vitest/src/integrations/chai/index.ts b/packages/vitest/src/integrations/chai/index.ts index 6bc75a8b775c..01ed3d84d22d 100644 --- a/packages/vitest/src/integrations/chai/index.ts +++ b/packages/vitest/src/integrations/chai/index.ts @@ -5,21 +5,22 @@ import './setup' import type { Test } from '@vitest/runner' import { getCurrentTest } from '@vitest/runner' import { GLOBAL_EXPECT, getState, setState } from '@vitest/expect' +import type { Assertion, ExpectStatic } from '@vitest/expect' import type { MatcherState } from '../../types/chai' import { getCurrentEnvironment, getFullName } from '../../utils' export function createExpect(test?: Test) { - const expect = ((value: any, message?: string): Vi.Assertion => { + const expect = ((value: any, message?: string): Assertion => { const { assertionCalls } = getState(expect) setState({ assertionCalls: assertionCalls + 1 }, expect) - const assert = chai.expect(value, message) as unknown as Vi.Assertion + const assert = chai.expect(value, message) as unknown as Assertion const _test = test || getCurrentTest() if (_test) // @ts-expect-error internal - return assert.withTest(_test) as Vi.Assertion + return assert.withTest(_test) as Assertion else return assert - }) as Vi.ExpectStatic + }) as ExpectStatic Object.assign(expect, chai.expect) expect.getState = () => getState(expect) diff --git a/packages/vitest/src/runtime/runners/test.ts b/packages/vitest/src/runtime/runners/test.ts index 3e2eebfd1220..d3bb2017f317 100644 --- a/packages/vitest/src/runtime/runners/test.ts +++ b/packages/vitest/src/runtime/runners/test.ts @@ -1,4 +1,5 @@ import type { CancelReason, Suite, Test, TestContext, VitestRunner, VitestRunnerImportSource } from '@vitest/runner' +import type { ExpectStatic } from '@vitest/expect' import { GLOBAL_EXPECT, getState, setState } from '@vitest/expect' import { getSnapshotClient } from '../../integrations/snapshot/chai' import { vi } from '../../integrations/vi' @@ -103,7 +104,7 @@ export class VitestTestRunner implements VitestRunner { } extendTestContext(context: TestContext): TestContext { - let _expect: Vi.ExpectStatic | undefined + let _expect: ExpectStatic | undefined Object.defineProperty(context, 'expect', { get() { if (!_expect) diff --git a/packages/vitest/src/types/global.ts b/packages/vitest/src/types/global.ts index 224984c54490..2dd5eec1ca81 100644 --- a/packages/vitest/src/types/global.ts +++ b/packages/vitest/src/types/global.ts @@ -1,29 +1,37 @@ import type { Plugin as PrettyFormatPlugin } from 'pretty-format' -import type { MatchersObject } from '@vitest/expect' import type { SnapshotState } from '@vitest/snapshot' -import type { MatcherState } from './chai' -import type { Constructable, UserConsoleLog } from './general' +import type { ExpectStatic } from '@vitest/expect' +import type { UserConsoleLog } from './general' import type { VitestEnvironment } from './config' import type { BenchmarkResult } from './benchmark' -type Promisify = { - [K in keyof O]: O[K] extends (...args: infer A) => infer R - ? O extends R - ? Promisify - : (...args: A) => Promise - : O[K] -} - declare module '@vitest/expect' { interface MatcherState { environment: VitestEnvironment snapshotState: SnapshotState } + + interface ExpectStatic { + addSnapshotSerializer(plugin: PrettyFormatPlugin): void + } + + interface Assertion { + // Snapshots are extended in @vitest/snapshot and are not part of @vitest/expect + matchSnapshot(snapshot: Partial, message?: string): void + matchSnapshot(message?: string): void + toMatchSnapshot(snapshot: Partial, message?: string): void + toMatchSnapshot(message?: string): void + toMatchInlineSnapshot(properties: Partial, snapshot?: string, message?: string): void + toMatchInlineSnapshot(snapshot?: string, message?: string): void + toThrowErrorMatchingSnapshot(message?: string): void + toThrowErrorMatchingInlineSnapshot(snapshot?: string, message?: string): void + toMatchFileSnapshot(filepath: string, message?: string): Promise + } } declare module '@vitest/runner' { interface TestContext { - expect: Vi.ExpectStatic + expect: ExpectStatic } interface File { @@ -39,113 +47,3 @@ declare module '@vitest/runner' { benchmark?: BenchmarkResult } } - -declare global { - // support augmenting jest.Matchers by other libraries - namespace jest { - - // eslint-disable-next-line unused-imports/no-unused-vars - interface Matchers {} - } - - namespace Vi { - interface ExpectStatic extends Chai.ExpectStatic, AsymmetricMatchersContaining { - (actual: T, message?: string): Vi.Assertion - - extend(expects: MatchersObject): void - assertions(expected: number): void - hasAssertions(): void - anything(): any - any(constructor: unknown): any - addSnapshotSerializer(plugin: PrettyFormatPlugin): void - getState(): MatcherState - setState(state: Partial): void - not: AsymmetricMatchersContaining - } - - interface AsymmetricMatchersContaining { - stringContaining(expected: string): any - objectContaining(expected: T): any - arrayContaining(expected: Array): any - stringMatching(expected: string | RegExp): any - } - - interface JestAssertion extends jest.Matchers { - // Snapshot - matchSnapshot(snapshot: Partial, message?: string): void - matchSnapshot(message?: string): void - toMatchSnapshot(snapshot: Partial, message?: string): void - toMatchSnapshot(message?: string): void - toMatchInlineSnapshot(properties: Partial, snapshot?: string, message?: string): void - toMatchInlineSnapshot(snapshot?: string, message?: string): void - toMatchFileSnapshot(filepath: string, message?: string): Promise - toThrowErrorMatchingSnapshot(message?: string): void - toThrowErrorMatchingInlineSnapshot(snapshot?: string, message?: string): void - - // Jest compact - toEqual(expected: E): void - toStrictEqual(expected: E): void - toBe(expected: E): void - toMatch(expected: string | RegExp): void - toMatchObject(expected: E): void - toContain(item: E): void - toContainEqual(item: E): void - toBeTruthy(): void - toBeFalsy(): void - toBeGreaterThan(num: number | bigint): void - toBeGreaterThanOrEqual(num: number | bigint): void - toBeLessThan(num: number | bigint): void - toBeLessThanOrEqual(num: number | bigint): void - toBeNaN(): void - toBeUndefined(): void - toBeNull(): void - toBeDefined(): void - toBeTypeOf(expected: 'bigint' | 'boolean' | 'function' | 'number' | 'object' | 'string' | 'symbol' | 'undefined'): void - toBeInstanceOf(expected: E): void - toBeCalledTimes(times: number): void - toHaveLength(length: number): void - toHaveProperty(property: string | (string | number)[], value?: E): void - toBeCloseTo(number: number, numDigits?: number): void - toHaveBeenCalledTimes(times: number): void - toHaveBeenCalledOnce(): void - toHaveBeenCalled(): void - toBeCalled(): void - toHaveBeenCalledWith(...args: E): void - toBeCalledWith(...args: E): void - toHaveBeenNthCalledWith(n: number, ...args: E): void - nthCalledWith(nthCall: number, ...args: E): void - toHaveBeenLastCalledWith(...args: E): void - lastCalledWith(...args: E): void - toThrow(expected?: string | Constructable | RegExp | Error): void - toThrowError(expected?: string | Constructable | RegExp | Error): void - toReturn(): void - toHaveReturned(): void - toReturnTimes(times: number): void - toHaveReturnedTimes(times: number): void - toReturnWith(value: E): void - toHaveReturnedWith(value: E): void - toHaveLastReturnedWith(value: E): void - lastReturnedWith(value: E): void - toHaveNthReturnedWith(nthCall: number, value: E): void - nthReturnedWith(nthCall: number, value: E): void - toSatisfy(matcher: (value: E) => boolean, message?: string): void - } - - // eslint-disable-next-line @typescript-eslint/prefer-ts-expect-error - // @ts-ignore build namespace conflict - type VitestAssertion = { - [K in keyof A]: A[K] extends Chai.Assertion - ? Assertion - : A[K] extends (...args: any[]) => any - ? A[K] // not converting function since they may contain overload - : VitestAssertion - } & ((type: string, message?: string) => Assertion) - - interface Assertion extends VitestAssertion, JestAssertion { - resolves: Promisify> - rejects: Promisify> - } - } -} - -export {} diff --git a/packages/vitest/src/types/index.ts b/packages/vitest/src/types/index.ts index 6df7616f26ef..0111737947a3 100644 --- a/packages/vitest/src/types/index.ts +++ b/packages/vitest/src/types/index.ts @@ -24,3 +24,10 @@ export type { Mocked, MockedClass, } from '../integrations/spy' + +export type { + ExpectStatic, + AsymmetricMatchersContaining, + JestAssertion, + Assertion, +} from '@vitest/expect' diff --git a/test/core/test/jest-expect.test.ts b/test/core/test/jest-expect.test.ts index 75356b3798a2..b6da00eeebdf 100644 --- a/test/core/test/jest-expect.test.ts +++ b/test/core/test/jest-expect.test.ts @@ -13,17 +13,17 @@ interface CustomMatchers { toBeTestedPromise(): R } +declare module '@vitest/expect' { + interface Assertion extends CustomMatchers {} + interface AsymmetricMatchersContaining extends CustomMatchers {} +} + declare global { namespace jest { interface Matchers { toBeJestCompatible(): R } } - - namespace Vi { - interface JestAssertion extends CustomMatchers {} - interface AsymmetricMatchersContaining extends CustomMatchers {} - } } describe('jest-expect', () => {