Skip to content

Commit

Permalink
feat!: move assertion declarations to expect package (#3294)
Browse files Browse the repository at this point in the history
  • Loading branch information
sheremet-va authored May 3, 2023
1 parent 1f1189b commit cf3afe2
Show file tree
Hide file tree
Showing 13 changed files with 162 additions and 153 deletions.
1 change: 1 addition & 0 deletions .eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -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",
{
Expand Down
8 changes: 3 additions & 5 deletions docs/api/expect.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
}
```

Expand Down
8 changes: 3 additions & 5 deletions docs/guide/extending-matchers.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
}
```

Expand Down
2 changes: 1 addition & 1 deletion packages/expect/src/jest-asymmetric-matchers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
6 changes: 3 additions & 3 deletions packages/expect/src/jest-expect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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)
}
Expand Down
7 changes: 4 additions & 3 deletions packages/expect/src/jest-extend.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { util } from 'chai'
import type {
ChaiPlugin,
ExpectStatic,
MatcherState,
MatchersObject,
SyncExpectationResult,
Expand All @@ -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') || ''
Expand Down Expand Up @@ -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[]) {
Expand Down Expand Up @@ -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))
})
}
8 changes: 4 additions & 4 deletions packages/expect/src/state.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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) || {}
Expand Down
104 changes: 104 additions & 0 deletions packages/expect/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 {}
9 changes: 5 additions & 4 deletions packages/vitest/src/integrations/chai/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
3 changes: 2 additions & 1 deletion packages/vitest/src/runtime/runners/test.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -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)
Expand Down
Loading

0 comments on commit cf3afe2

Please sign in to comment.