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

refactor(expect): define matchers in separate files #4857

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions packages/expect/src/jest-assertions/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import toMatchObject from './toMatchObject'

export default [
toMatchObject,
]
21 changes: 21 additions & 0 deletions packages/expect/src/jest-assertions/toEqual.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { defineAssertion } from '../utils'
import { iterableEquality, equals as jestEquals } from '../jest-utils'
import { getCustomEqualityTesters } from '../jest-matcher-utils'

export default defineAssertion('toEqual', function (expected: unknown) {
const customTesters = getCustomEqualityTesters()
const actual = this._obj
const equal = jestEquals(
actual,
expected,
[...customTesters, iterableEquality],
)

return this.assert(
equal,
'expected #{this} to deeply equal #{exp}',
'expected #{this} to not deeply equal #{exp}',
expected,
actual,
)
})
27 changes: 27 additions & 0 deletions packages/expect/src/jest-assertions/toMatchObject.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { AssertionError, util } from 'chai'
import { defineAssertion } from '../utils'
import { getObjectSubset, iterableEquality, equals as jestEquals, subsetEquality } from '../jest-utils'
import { getCustomEqualityTesters } from '../jest-matcher-utils'

export default defineAssertion('toMatchObject', function (expected: unknown) {
const customTesters = getCustomEqualityTesters()
const actual = this._obj
const pass = jestEquals(actual, expected, [...customTesters, iterableEquality, subsetEquality])
const isNot = util.flag(this, 'negate') as boolean
const { subset: actualSubset, stripped } = getObjectSubset(actual, expected)
if ((pass && isNot) || (!pass && !isNot)) {
const msg = util.getMessage(
this,
[
pass,
'expected #{this} to match object #{exp}',
'expected #{this} to not match object #{exp}',
expected,
actualSubset,
false,
],
)
const message = stripped === 0 ? msg : `${msg}\n(${stripped} matching ${stripped === 1 ? 'property' : 'properties'} omitted from actual)`
throw new AssertionError(message, { showDiff: true, expected, actual: actualSubset })
}
})
28 changes: 28 additions & 0 deletions packages/expect/src/jest-assertions/toStrictEqual.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { defineAssertion } from '../utils'
import { arrayBufferEquality, iterableEquality, equals as jestEquals, sparseArrayEquality, typeEquality } from '../jest-utils'
import { getCustomEqualityTesters } from '../jest-matcher-utils'

export default defineAssertion('toStrictEqual', function (expected: unknown) {
const customTesters = getCustomEqualityTesters()
const obj = this._obj
const equal = jestEquals(
obj,
expected,
[
...customTesters,
iterableEquality,
typeEquality,
sparseArrayEquality,
arrayBufferEquality,
],
true,
)

return this.assert(
equal,
'expected #{this} to strictly equal #{exp}',
'expected #{this} to not strictly equal #{exp}',
expected,
obj,
)
})
67 changes: 5 additions & 62 deletions packages/expect/src/jest-expect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,14 @@ import type { MockInstance } from '@vitest/spy'
import { isMockFunction } from '@vitest/spy'
import type { Test } from '@vitest/runner'
import type { Assertion, ChaiPlugin } from './types'
import { arrayBufferEquality, generateToBeMessage, getObjectSubset, iterableEquality, equals as jestEquals, sparseArrayEquality, subsetEquality, typeEquality } from './jest-utils'
import { arrayBufferEquality, generateToBeMessage, iterableEquality, equals as jestEquals, sparseArrayEquality, typeEquality } from './jest-utils'
import type { AsymmetricMatcher } from './jest-asymmetric-matchers'
import { diff, getCustomEqualityTesters, stringify } from './jest-matcher-utils'
import { JEST_MATCHERS_OBJECT } from './constants'
import { recordAsyncExpect, wrapSoft } from './utils'

import matchers from './jest-assertions/index'

// polyfill globals because expect can be used in node environment
declare class Node {
contains(item: unknown): boolean
Expand All @@ -25,6 +27,8 @@ export const JestChaiExpect: ChaiPlugin = (chai, utils) => {
const c = () => getColors()
const customTesters = getCustomEqualityTesters()

matchers.forEach(define => define(chai, utils))

function def(name: keyof Assertion | (keyof Assertion)[], fn: ((this: Chai.AssertionStatic & Assertion, ...args: any[]) => any)) {
const addMethod = (n: keyof Assertion) => {
const softWrapper = wrapSoft(utils, fn)
Expand Down Expand Up @@ -76,46 +80,6 @@ export const JestChaiExpect: ChaiPlugin = (chai, utils) => {
return this
})

def('toEqual', function (expected) {
const actual = utils.flag(this, 'object')
const equal = jestEquals(
actual,
expected,
[...customTesters, iterableEquality],
)

return this.assert(
equal,
'expected #{this} to deeply equal #{exp}',
'expected #{this} to not deeply equal #{exp}',
expected,
actual,
)
})

def('toStrictEqual', function (expected) {
const obj = utils.flag(this, 'object')
const equal = jestEquals(
obj,
expected,
[
...customTesters,
iterableEquality,
typeEquality,
sparseArrayEquality,
arrayBufferEquality,
],
true,
)

return this.assert(
equal,
'expected #{this} to strictly equal #{exp}',
'expected #{this} to not strictly equal #{exp}',
expected,
obj,
)
})
def('toBe', function (expected) {
const actual = this._obj
const pass = Object.is(actual, expected)
Expand Down Expand Up @@ -159,27 +123,6 @@ export const JestChaiExpect: ChaiPlugin = (chai, utils) => {
actual,
)
})
def('toMatchObject', function (expected) {
const actual = this._obj
const pass = jestEquals(actual, expected, [...customTesters, iterableEquality, subsetEquality])
const isNot = utils.flag(this, 'negate') as boolean
const { subset: actualSubset, stripped } = getObjectSubset(actual, expected)
if ((pass && isNot) || (!pass && !isNot)) {
const msg = utils.getMessage(
this,
[
pass,
'expected #{this} to match object #{exp}',
'expected #{this} to not match object #{exp}',
expected,
actualSubset,
false,
],
)
const message = stripped === 0 ? msg : `${msg}\n(${stripped} matching ${stripped === 1 ? 'property' : 'properties'} omitted from actual)`
throw new AssertionError(message, { showDiff: true, expected, actual: actualSubset })
}
})
def('toMatch', function (expected: string | RegExp) {
const actual = this._obj as string
if (typeof actual !== 'string')
Expand Down
19 changes: 17 additions & 2 deletions packages/expect/src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { processError } from '@vitest/utils/error'
import type { Test } from '@vitest/runner/types'
import { GLOBAL_EXPECT } from './constants'
import { GLOBAL_EXPECT, JEST_MATCHERS_OBJECT } from './constants'
import { getState } from './state'
import type { Assertion, MatcherState } from './types'
import type { Assertion, ChaiPlugin, MatcherState } from './types'

export function recordAsyncExpect(test: any, promise: Promise<any> | PromiseLike<any>) {
// record promise for test, that resolves before test ends
Expand Down Expand Up @@ -49,3 +49,18 @@ export function wrapSoft(utils: Chai.ChaiUtils, fn: (this: Chai.AssertionStatic
}
}
}

export function defineAssertion<T>(name: keyof Assertion, fn: (this: Omit<Chai.AssertionStatic & Assertion, '_obj'> & { _obj: T }, ...args: any) => any): ChaiPlugin {
return (chai, utils) => {
const addMethod = (n: keyof Assertion) => {
const softWrapper = wrapSoft(utils, fn)
utils.addMethod(chai.Assertion.prototype, n, softWrapper)
utils.addMethod((globalThis as any)[JEST_MATCHERS_OBJECT].matchers, n, softWrapper)
}

if (Array.isArray(name))
name.forEach(n => addMethod(n))
else
addMethod(name)
}
}
Loading