Skip to content

Commit

Permalink
feat: more expect compatible APIs (#360)
Browse files Browse the repository at this point in the history
  • Loading branch information
sheremet-va committed Dec 29, 2021
1 parent 0b4f19e commit 577ff67
Show file tree
Hide file tree
Showing 7 changed files with 226 additions and 84 deletions.
128 changes: 71 additions & 57 deletions packages/vitest/src/index.ts
Expand Up @@ -24,73 +24,87 @@ declare module 'vite' {
}
}

interface AsymmetricMatchersContaining {
stringContaining(expected: string): void
objectContaining(expected: any): ObjectContaining
arrayContaining(expected: unknown[]): ArrayContaining
stringMatching(expected: string | RegExp): StringMatching
}

declare global {
namespace Chai {
interface ExpectStatic {
interface ExpectStatic extends AsymmetricMatchersContaining {
extend(expects: MatchersObject): void
stringContaining(expected: string): void
assertions(expected: number): void
hasAssertions(): void
anything(): Anything
objectContaining(expected: any): ObjectContaining
any(constructor: unknown): Any
arrayContaining(expected: any): ArrayContaining
stringMatching(expected: RegExp): StringMatching
assertions(expected: number): void
not: AsymmetricMatchersContaining
}

interface Assertion {
// Chai
chaiEqual(expected: any): void

interface JestAssertions<T = void> {
// Snapshot
toMatchSnapshot(message?: string): Assertion
toMatchInlineSnapshot(snapshot?: string, message?: string): Assertion
matchSnapshot(message?: string): Assertion
toMatchSnapshot(message?: string): T
toMatchInlineSnapshot(snapshot?: string, message?: string): T
matchSnapshot(message?: string): T

// Jest compact
toEqual(expected: any): void
toStrictEqual(expected: any): void
toBe(expected: any): void
toMatch(expected: string | RegExp): void
toMatchObject(expected: any): void
toContain(item: any): void
toContainEqual(item: any): void
toBeTruthy(): void
toBeFalsy(): void
toBeGreaterThan(num: number): void
toBeGreaterThanOrEqual(num: number): void
toBeLessThan(num: number): void
toBeLessThanOrEqual(num: number): void
toBeNaN(): void
toBeUndefined(): void
toBeNull(): void
toBeDefined(): void
toBeInstanceOf(c: any): void
toBeCalledTimes(n: number): void
toHaveLength(l: number): void
toHaveProperty(p: string, value?: any): void
toBeCloseTo(number: number, numDigits?: number): void
toHaveBeenCalledTimes(n: number): void
toHaveBeenCalledOnce(): void
toHaveBeenCalled(): void
toBeCalled(): void
toHaveBeenCalledWith(...args: any[]): void
toBeCalledWith(...args: any[]): void
toHaveBeenNthCalledWith(n: number, ...args: any[]): void
nthCalledWith(n: number, ...args: any[]): void
toHaveBeenLastCalledWith(...args: any[]): void
lastCalledWith(...args: any[]): void
toThrow(expected?: string | RegExp): void
toThrowError(expected?: string | RegExp): void
toReturn(): void
toHaveReturned(): void
toReturnTimes(times: number): void
toHaveReturnedTimes(times: number): void
toReturnWith(value: any): void
toHaveReturnedWith(value: any): void
toHaveLastReturnedWith(value: any): void
lastReturnedWith(value: any): void
toHaveNthReturnedWith(nthCall: number, value: any): void
nthReturnedWith(nthCall: number, value: any): void
toEqual(expected: any): T
toStrictEqual(expected: any): T
toBe(expected: any): T
toMatch(expected: string | RegExp): T
toMatchObject(expected: any): T
toContain(item: any): T
toContainEqual(item: any): T
toBeTruthy(): T
toBeFalsy(): T
toBeGreaterThan(num: number): T
toBeGreaterThanOrEqual(num: number): T
toBeLessThan(num: number): T
toBeLessThanOrEqual(num: number): T
toBeNaN(): T
toBeUndefined(): T
toBeNull(): T
toBeDefined(): T
toBeInstanceOf(c: any): T
toBeCalledTimes(n: number): T
toHaveLength(l: number): T
toHaveProperty(p: string, value?: any): T
toBeCloseTo(number: number, numDigits?: number): T
toHaveBeenCalledTimes(n: number): T
toHaveBeenCalledOnce(): T
toHaveBeenCalled(): T
toBeCalled(): T
toHaveBeenCalledWith(...args: any[]): T
toBeCalledWith(...args: any[]): T
toHaveBeenNthCalledWith(n: number, ...args: any[]): T
nthCalledWith(n: number, ...args: any[]): T
toHaveBeenLastCalledWith(...args: any[]): T
lastCalledWith(...args: any[]): T
toThrow(expected?: string | RegExp): T
toThrowError(expected?: string | RegExp): T
toReturn(): T
toHaveReturned(): T
toReturnTimes(times: number): T
toHaveReturnedTimes(times: number): T
toReturnWith(value: any): T
toHaveReturnedWith(value: any): T
toHaveLastReturnedWith(value: any): T
lastReturnedWith(value: any): T
toHaveNthReturnedWith(nthCall: number, value: any): T
nthReturnedWith(nthCall: number, value: any): T
}

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]
}

interface Assertion extends JestAssertions {
resolves: Promisify<Assertion>
rejects: Promisify<Assertion>

// Chai
chaiEqual(expected: any): void
}
}
}
37 changes: 17 additions & 20 deletions packages/vitest/src/integrations/chai/jest-asymmetric-matchers.ts
Expand Up @@ -260,47 +260,44 @@ export class StringMatching extends AsymmetricMatcher<RegExp> {
export const JestAsymmetricMatchers: ChaiPlugin = (chai, utils) => {
utils.addMethod(
chai.expect,
'stringContaining',
(expected: string) => new StringContaining(expected),
'anything',
() => new Anything(),
)

utils.addMethod(
chai.expect,
'anything',
() => {
return new Anything()
},
'any',
(expected: unknown) => new Any(expected),
)

utils.addMethod(
chai.expect,
'objectContaining',
(expected: any) => {
return new ObjectContaining(expected)
},
'stringContaining',
(expected: string) => new StringContaining(expected),
)

utils.addMethod(
chai.expect,
'any',
(expected: unknown) => {
return new Any(expected)
},
'objectContaining',
(expected: any) => new ObjectContaining(expected),
)

utils.addMethod(
chai.expect,
'arrayContaining',
(expected: any) => {
return new ArrayContaining(expected)
},
(expected: any) => new ArrayContaining(expected),
)

utils.addMethod(
chai.expect,
'stringMatching',
(expected: any) => {
return new StringMatching(expected)
},
(expected: any) => new StringMatching(expected),
)

chai.expect.not = {
stringContaining: (expected: string) => new StringContaining(expected, true),
objectContaining: (expected: any) => new ObjectContaining(expected, true),
arrayContaining: (expected: unknown[]) => new ArrayContaining(expected, true),
stringMatching: (expected: string | RegExp) => new StringMatching(expected, true),
}
}
74 changes: 74 additions & 0 deletions packages/vitest/src/integrations/chai/jest-expect.ts
Expand Up @@ -4,6 +4,8 @@ import { arrayBufferEquality, equals as asymmetricEquals, hasAsymmetric, iterabl

type MatcherState = {
assertionCalls: number
isExpectingAssertions: boolean
isExpectingAssertionsError: Error | null
expectedAssertionsNumber: number | null
expectedAssertionsNumberError: Error | null
}
Expand All @@ -12,6 +14,8 @@ const MATCHERS_OBJECT = Symbol.for('matchers-object')
if (!Object.prototype.hasOwnProperty.call(global, MATCHERS_OBJECT)) {
const defaultState: Partial<MatcherState> = {
assertionCalls: 0,
isExpectingAssertions: false,
isExpectingAssertionsError: null,
expectedAssertionsNumber: null,
expectedAssertionsNumberError: null,
}
Expand Down Expand Up @@ -384,6 +388,61 @@ export const JestChaiExpect: ChaiPlugin = (chai, utils) => {
)
})

utils.addProperty(chai.Assertion.prototype, 'resolves', function(this: any) {
utils.flag(this, 'promise', 'resolves')
const obj = utils.flag(this, 'object')
const proxy: any = new Proxy(this, {
get: (target, key, reciever) => {
const result = Reflect.get(target, key, reciever)

if (typeof result !== 'function')
return result instanceof chai.Assertion ? proxy : result

return async(...args: any[]) => {
return obj.then(
(value: any) => {
utils.flag(this, 'object', value)
return result.call(this, ...args)
},
(err: any) => {
throw new Error(`promise rejected ${err} instead of resolving`)
},
)
}
},
})

return proxy
})

utils.addProperty(chai.Assertion.prototype, 'rejects', function(this: any) {
utils.flag(this, 'promise', 'rejects')
const obj = utils.flag(this, 'object')
const wrapper = typeof obj === 'function' ? obj() : obj
const proxy: any = new Proxy(this, {
get: (target, key, reciever) => {
const result = Reflect.get(target, key, reciever)

if (typeof result !== 'function')
return result instanceof chai.Assertion ? proxy : result

return async(...args: any[]) => {
return wrapper.then(
(value: any) => {
throw new Error(`promise resolved ${value} instead of rejecting`)
},
(err: any) => {
utils.flag(this, 'object', err)
return result.call(this, ...args)
},
)
}
},
})

return proxy
})

utils.addMethod(
chai.expect,
'assertions',
Expand All @@ -398,4 +457,19 @@ export const JestChaiExpect: ChaiPlugin = (chai, utils) => {
})
},
)

utils.addMethod(
chai.expect,
'hasAssertions',
function hasAssertions() {
const error = new Error('expected any number of assertion, but got none')
if (Error.captureStackTrace)
Error.captureStackTrace(error, hasAssertions)

setState({
isExpectingAssertions: true,
isExpectingAssertionsError: error,
})
},
)
}
7 changes: 4 additions & 3 deletions packages/vitest/src/integrations/chai/jest-extend.ts
@@ -1,4 +1,5 @@
import chai, { util } from 'chai'
import { getState } from './jest-expect'

import * as matcherUtils from './jest-matcher-utils'

Expand All @@ -20,6 +21,7 @@ const isAsyncFunction = (fn: unknown) =>
const getMatcherState = (assertion: Chai.AssertionStatic & Chai.Assertion) => {
const actual = assertion._obj
const isNot = util.flag(assertion, 'negate') as boolean
const promise = util.flag(assertion, 'promise') || ''
const jestUtils = {
...matcherUtils,
iterableEquality,
Expand All @@ -29,10 +31,9 @@ const getMatcherState = (assertion: Chai.AssertionStatic & Chai.Assertion) => {
const matcherState: MatcherState = {
isNot,
utils: jestUtils,
// global assertionCalls? needed for built-in jest function, but we don't use it
assertionCalls: 0,
promise: '',
promise,
equals,
...getState(),
// needed for built-in jest-snapshots, but we don't use it
suppressedErrors: [],
}
Expand Down
4 changes: 2 additions & 2 deletions packages/vitest/src/integrations/chai/types.ts
Expand Up @@ -28,9 +28,9 @@ export type MatcherState = {
) => boolean
expand?: boolean
expectedAssertionsNumber?: number | null
expectedAssertionsNumberError?: Error
expectedAssertionsNumberError?: Error | null
isExpectingAssertions?: boolean
isExpectingAssertionsError?: Error
isExpectingAssertionsError?: Error | null
isNot: boolean
promise: string
suppressedErrors: Array<Error>
Expand Down
12 changes: 10 additions & 2 deletions packages/vitest/src/runtime/run.ts
Expand Up @@ -60,11 +60,19 @@ export async function runTest(test: Test) {

try {
await callSuiteHook(test.suite, 'beforeEach', [test, test.suite])
setState({ assertionCalls: 0, expectedAssertionsNumber: null, expectedAssertionsNumberError: null })
setState({
assertionCalls: 0,
isExpectingAssertions: false,
isExpectingAssertionsError: null,
expectedAssertionsNumber: null,
expectedAssertionsNumberError: null,
})
await getFn(test)()
const { assertionCalls, expectedAssertionsNumber, expectedAssertionsNumberError } = getState()
const { assertionCalls, expectedAssertionsNumber, expectedAssertionsNumberError, isExpectingAssertions, isExpectingAssertionsError } = getState()
if (expectedAssertionsNumber !== null && assertionCalls !== expectedAssertionsNumber)
throw expectedAssertionsNumberError
if (isExpectingAssertions === true && assertionCalls === 0)
throw isExpectingAssertionsError

test.result.state = 'pass'
}
Expand Down

0 comments on commit 577ff67

Please sign in to comment.