Skip to content

Commit

Permalink
feat: create asymmetric matchers when extending expect (#934)
Browse files Browse the repository at this point in the history
  • Loading branch information
sheremet-va committed Mar 13, 2022
1 parent 89c8421 commit 086a77c
Show file tree
Hide file tree
Showing 5 changed files with 64 additions and 21 deletions.
14 changes: 7 additions & 7 deletions packages/vitest/src/index.ts
Expand Up @@ -23,13 +23,6 @@ declare module 'vite' {
}
}

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

type Promisify<O> = {
[K in keyof O]: O[K] extends (...args: infer A) => infer R
? O extends R
Expand Down Expand Up @@ -60,6 +53,13 @@ declare global {
not: AsymmetricMatchersContaining
}

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

interface JestAssertion<T = any> extends jest.Matchers<void, T> {
// Snapshot
toMatchSnapshot<U extends { [P in keyof T]: any }>(snapshot: Partial<U>, message?: string): void
Expand Down
2 changes: 1 addition & 1 deletion packages/vitest/src/integrations/chai/index.ts
Expand Up @@ -13,7 +13,7 @@ Object.assign(expect, chai.expect)
expect.getState = getState
expect.setState = setState
// @ts-expect-error untyped
expect.extend = fn => chai.expect.extend(fn)
expect.extend = matchers => chai.expect.extend(expect, matchers)

export { assert, should } from 'chai'
export { chai, expect }
@@ -1,3 +1,4 @@
import { getState } from './jest-expect'
import * as matcherUtils from './jest-matcher-utils'

import { equals, isA } from './jest-utils'
Expand All @@ -20,6 +21,7 @@ export abstract class AsymmetricMatcher<

protected getMatcherContext(): State {
return {
...getState(),
equals,
isNot: this.inverse,
utils: matcherUtils,
Expand Down
55 changes: 49 additions & 6 deletions packages/vitest/src/integrations/chai/jest-extend.ts
@@ -1,4 +1,5 @@
import chai, { util } from 'chai'
import { util } from 'chai'
import { AsymmetricMatcher } from './jest-asymmetric-matchers'
import { getState } from './jest-expect'

import * as matcherUtils from './jest-matcher-utils'
Expand Down Expand Up @@ -51,9 +52,9 @@ class JestExtendError extends Error {
}
}

function JestExtendPlugin(expects: MatchersObject): ChaiPlugin {
function JestExtendPlugin(expect: Vi.ExpectStatic, matchers: MatchersObject): ChaiPlugin {
return (c, utils) => {
Object.entries(expects).forEach(([expectAssertionName, expectAssertion]) => {
Object.entries(matchers).forEach(([expectAssertionName, expectAssertion]) => {
function expectSyncWrapper(this: Chai.AssertionStatic & Chai.Assertion, ...args: any[]) {
const { state, isNot, obj } = getMatcherState(this)

Expand All @@ -76,13 +77,55 @@ function JestExtendPlugin(expects: MatchersObject): ChaiPlugin {

const expectAssertionWrapper = isAsyncFunction(expectAssertion) ? expectAsyncWrapper : expectSyncWrapper

utils.addMethod(chai.Assertion.prototype, expectAssertionName, expectAssertionWrapper)
utils.addMethod(c.Assertion.prototype, expectAssertionName, expectAssertionWrapper)

class CustomMatcher extends AsymmetricMatcher<[unknown, ...unknown[]]> {
constructor(inverse = false, ...sample: [unknown, ...unknown[]]) {
super(sample, inverse)
}

asymmetricMatch(other: unknown) {
const { pass } = expectAssertion.call(
this.getMatcherContext(),
other,
...this.sample,
) as SyncExpectationResult

return this.inverse ? !pass : pass
}

toString() {
return `${this.inverse ? 'not.' : ''}${expectAssertionName}`
}

getExpectedType() {
return 'any'
}

toAsymmetricMatcher() {
return `${this.toString()}<${this.sample.map(String).join(', ')}>`
}
}

Object.defineProperty(expect, expectAssertionName, {
configurable: true,
enumerable: true,
value: (...sample: [unknown, ...unknown[]]) => new CustomMatcher(false, ...sample),
writable: true,
})

Object.defineProperty(expect.not, expectAssertionName, {
configurable: true,
enumerable: true,
value: (...sample: [unknown, ...unknown[]]) => new CustomMatcher(true, ...sample),
writable: true,
})
})
}
}

export const JestExtend: ChaiPlugin = (chai, utils) => {
utils.addMethod(chai.expect, 'extend', (expects: MatchersObject) => {
chai.use(JestExtendPlugin(expects))
utils.addMethod(chai.expect, 'extend', (expect: Vi.ExpectStatic, expects: MatchersObject) => {
chai.use(JestExtendPlugin(expect, expects))
})
}
12 changes: 5 additions & 7 deletions test/core/test/jest-expect.test.ts
Expand Up @@ -11,7 +11,7 @@ interface CustomMatchers<R = unknown> {
declare global {
namespace Vi {
interface JestAssertion extends CustomMatchers {}
interface ExpectStatic extends CustomMatchers {}
interface AsymmetricMatchersContaining extends CustomMatchers {}
}
}

Expand Down Expand Up @@ -148,12 +148,10 @@ describe('jest-expect', () => {

expect(5).toBeDividedBy(5)
expect(5).not.toBeDividedBy(4)

// TODO: support asymmetric matcher
// expect({ one: 1, two: 2 }).toEqual({
// one: expect.toBeDividedBy(1),
// two: expect.toBeDividedBy(1),
// })
expect({ one: 1, two: 2 }).toEqual({
one: expect.toBeDividedBy(1),
two: expect.not.toBeDividedBy(5),
})
})

it('object', () => {
Expand Down

0 comments on commit 086a77c

Please sign in to comment.