Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: more expect compatible APIs #360

Merged
merged 6 commits into from Dec 29, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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> {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is T needed?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I used it before writing Promisify helper and was lazy to remove 👀

// 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 @@ -373,6 +377,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 @@ -387,4 +446,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