feat!: move assertion declarations to expect package (#3294)

This commit is contained in:
Vladimir 2023-05-03 18:25:38 +02:00 committed by GitHub
parent 1f1189bc6b
commit cf3afe2bea
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 162 additions and 153 deletions

View File

@ -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",
{

View File

@ -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<R = unknown> {
toBeFoo(): R
}
declare namespace Vi {
interface Assertion extends CustomMatchers {}
declare module '@vitest/expect' {
interface Assertion<T = any> extends CustomMatchers<T> {}
interface AsymmetricMatchersContaining extends CustomMatchers {}
// Note: augmenting jest.Matchers interface will also work.
}
```

View File

@ -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<R = unknown> {
toBeFoo(): R
}
declare namespace Vi {
interface Assertion extends CustomMatchers {}
declare module '@vitest/expect' {
interface Assertion<T = any> extends CustomMatchers<T> {}
interface AsymmetricMatchersContaining extends CustomMatchers {}
// Note: augmenting jest.Matchers interface will also work.
}
```

View File

@ -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,

View File

@ -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)
}

View File

@ -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))
})
}

View File

@ -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<Vi.ExpectStatic, MatcherState>()
const globalState = new WeakMap<ExpectStatic, MatcherState>()
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<State extends MatcherState = MatcherState>(expect: Vi.ExpectStatic): State {
export function getState<State extends MatcherState = MatcherState>(expect: ExpectStatic): State {
return (globalThis as any)[MATCHERS_OBJECT].get(expect)
}
export function setState<State extends MatcherState = MatcherState>(
state: Partial<State>,
expect: Vi.ExpectStatic,
expect: ExpectStatic,
): void {
const map = (globalThis as any)[MATCHERS_OBJECT]
const current = map.get(expect) || {}

View File

@ -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> = T extends (arg: infer A) => unknown ? A : never
@ -96,3 +97,106 @@ export interface RawMatcherFn<T extends MatcherState = MatcherState> {
}
export type MatchersObject<T extends MatcherState = MatcherState> = Record<string, RawMatcherFn<T>>
export interface ExpectStatic extends Chai.ExpectStatic, AsymmetricMatchersContaining {
<T>(actual: T, message?: string): Assertion<T>
extend(expects: MatchersObject): void
assertions(expected: number): void
hasAssertions(): void
anything(): any
any(constructor: unknown): any
getState(): MatcherState
setState(state: Partial<MatcherState>): void
not: AsymmetricMatchersContaining
}
export interface AsymmetricMatchersContaining {
stringContaining(expected: string): any
objectContaining<T = any>(expected: T): any
arrayContaining<T = unknown>(expected: Array<T>): any
stringMatching(expected: string | RegExp): any
}
export interface JestAssertion<T = any> extends jest.Matchers<void, T> {
// Jest compact
toEqual<E>(expected: E): void
toStrictEqual<E>(expected: E): void
toBe<E>(expected: E): void
toMatch(expected: string | RegExp): void
toMatchObject<E extends {} | any[]>(expected: E): void
toContain<E>(item: E): void
toContainEqual<E>(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<E>(expected: E): void
toBeCalledTimes(times: number): void
toHaveLength(length: number): void
toHaveProperty<E>(property: string | (string | number)[], value?: E): void
toBeCloseTo(number: number, numDigits?: number): void
toHaveBeenCalledTimes(times: number): void
toHaveBeenCalled(): void
toBeCalled(): void
toHaveBeenCalledWith<E extends any[]>(...args: E): void
toBeCalledWith<E extends any[]>(...args: E): void
toHaveBeenNthCalledWith<E extends any[]>(n: number, ...args: E): void
nthCalledWith<E extends any[]>(nthCall: number, ...args: E): void
toHaveBeenLastCalledWith<E extends any[]>(...args: E): void
lastCalledWith<E extends any[]>(...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<E>(value: E): void
toHaveReturnedWith<E>(value: E): void
toHaveLastReturnedWith<E>(value: E): void
lastReturnedWith<E>(value: E): void
toHaveNthReturnedWith<E>(nthCall: number, value: E): void
nthReturnedWith<E>(nthCall: number, value: E): void
}
type VitestAssertion<A, T> = {
[K in keyof A]: A[K] extends Chai.Assertion
? Assertion<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)
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]
}
export interface Assertion<T = any> extends VitestAssertion<Chai.Assertion, T>, JestAssertion<T> {
toBeTypeOf(expected: 'bigint' | 'boolean' | 'function' | 'number' | 'object' | 'string' | 'symbol' | 'undefined'): void
toHaveBeenCalledOnce(): void
toSatisfy<E>(matcher: (value: E) => boolean, message?: string): void
resolves: Promisify<Assertion<T>>
rejects: Promisify<Assertion<T>>
}
declare global {
// support augmenting jest.Matchers by other libraries
namespace jest {
// eslint-disable-next-line unused-imports/no-unused-vars
interface Matchers<R, T = {}> {}
}
}
export {}

View File

@ -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<MatcherState>(expect)

View File

@ -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)

View File

@ -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<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]
}
declare module '@vitest/expect' {
interface MatcherState {
environment: VitestEnvironment
snapshotState: SnapshotState
}
interface ExpectStatic {
addSnapshotSerializer(plugin: PrettyFormatPlugin): void
}
interface Assertion<T> {
// Snapshots are extended in @vitest/snapshot and are not part of @vitest/expect
matchSnapshot<U extends { [P in keyof T]: any }>(snapshot: Partial<U>, message?: string): void
matchSnapshot(message?: string): void
toMatchSnapshot<U extends { [P in keyof T]: any }>(snapshot: Partial<U>, message?: string): void
toMatchSnapshot(message?: string): void
toMatchInlineSnapshot<U extends { [P in keyof T]: any }>(properties: Partial<U>, 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<void>
}
}
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<R, T = {}> {}
}
namespace Vi {
interface ExpectStatic extends Chai.ExpectStatic, AsymmetricMatchersContaining {
<T>(actual: T, message?: string): Vi.Assertion<T>
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<MatcherState>): void
not: AsymmetricMatchersContaining
}
interface AsymmetricMatchersContaining {
stringContaining(expected: string): any
objectContaining<T = any>(expected: T): any
arrayContaining<T = unknown>(expected: Array<T>): any
stringMatching(expected: string | RegExp): any
}
interface JestAssertion<T = any> extends jest.Matchers<void, T> {
// Snapshot
matchSnapshot<U extends { [P in keyof T]: any }>(snapshot: Partial<U>, message?: string): void
matchSnapshot(message?: string): void
toMatchSnapshot<U extends { [P in keyof T]: any }>(snapshot: Partial<U>, message?: string): void
toMatchSnapshot(message?: string): void
toMatchInlineSnapshot<U extends { [P in keyof T]: any }>(properties: Partial<U>, snapshot?: string, message?: string): void
toMatchInlineSnapshot(snapshot?: string, message?: string): void
toMatchFileSnapshot(filepath: string, message?: string): Promise<void>
toThrowErrorMatchingSnapshot(message?: string): void
toThrowErrorMatchingInlineSnapshot(snapshot?: string, message?: string): void
// Jest compact
toEqual<E>(expected: E): void
toStrictEqual<E>(expected: E): void
toBe<E>(expected: E): void
toMatch(expected: string | RegExp): void
toMatchObject<E extends {} | any[]>(expected: E): void
toContain<E>(item: E): void
toContainEqual<E>(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<E>(expected: E): void
toBeCalledTimes(times: number): void
toHaveLength(length: number): void
toHaveProperty<E>(property: string | (string | number)[], value?: E): void
toBeCloseTo(number: number, numDigits?: number): void
toHaveBeenCalledTimes(times: number): void
toHaveBeenCalledOnce(): void
toHaveBeenCalled(): void
toBeCalled(): void
toHaveBeenCalledWith<E extends any[]>(...args: E): void
toBeCalledWith<E extends any[]>(...args: E): void
toHaveBeenNthCalledWith<E extends any[]>(n: number, ...args: E): void
nthCalledWith<E extends any[]>(nthCall: number, ...args: E): void
toHaveBeenLastCalledWith<E extends any[]>(...args: E): void
lastCalledWith<E extends any[]>(...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<E>(value: E): void
toHaveReturnedWith<E>(value: E): void
toHaveLastReturnedWith<E>(value: E): void
lastReturnedWith<E>(value: E): void
toHaveNthReturnedWith<E>(nthCall: number, value: E): void
nthReturnedWith<E>(nthCall: number, value: E): void
toSatisfy<E>(matcher: (value: E) => boolean, message?: string): void
}
// eslint-disable-next-line @typescript-eslint/prefer-ts-expect-error
// @ts-ignore build namespace conflict
type VitestAssertion<A, T> = {
[K in keyof A]: A[K] extends Chai.Assertion
? Assertion<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)
interface Assertion<T = any> extends VitestAssertion<Chai.Assertion, T>, JestAssertion<T> {
resolves: Promisify<Assertion<T>>
rejects: Promisify<Assertion<T>>
}
}
}
export {}

View File

@ -24,3 +24,10 @@ export type {
Mocked,
MockedClass,
} from '../integrations/spy'
export type {
ExpectStatic,
AsymmetricMatchersContaining,
JestAssertion,
Assertion,
} from '@vitest/expect'

View File

@ -13,17 +13,17 @@ interface CustomMatchers<R = unknown> {
toBeTestedPromise(): R
}
declare module '@vitest/expect' {
interface Assertion<T = any> extends CustomMatchers<T> {}
interface AsymmetricMatchersContaining extends CustomMatchers {}
}
declare global {
namespace jest {
interface Matchers<R> {
toBeJestCompatible(): R
}
}
namespace Vi {
interface JestAssertion extends CustomMatchers {}
interface AsymmetricMatchersContaining extends CustomMatchers {}
}
}
describe('jest-expect', () => {