Skip to content

Commit

Permalink
fix: copy custom asymmetric matchers to local expect (#4405)
Browse files Browse the repository at this point in the history
  • Loading branch information
hi-ogawa committed Nov 2, 2023
1 parent 970038b commit 9fe3873
Show file tree
Hide file tree
Showing 5 changed files with 60 additions and 4 deletions.
1 change: 1 addition & 0 deletions packages/expect/src/constants.ts
@@ -1,3 +1,4 @@
export const MATCHERS_OBJECT = Symbol.for('matchers-object')
export const JEST_MATCHERS_OBJECT = Symbol.for('$$jest-matchers-object')
export const GLOBAL_EXPECT = Symbol.for('expect-global')
export const ASYMMETRIC_MATCHERS_OBJECT = Symbol.for('asymmetric-matchers-object')
15 changes: 13 additions & 2 deletions packages/expect/src/jest-extend.ts
Expand Up @@ -6,7 +6,7 @@ import type {
MatchersObject,
SyncExpectationResult,
} from './types'
import { JEST_MATCHERS_OBJECT } from './constants'
import { ASYMMETRIC_MATCHERS_OBJECT, JEST_MATCHERS_OBJECT } from './constants'
import { AsymmetricMatcher } from './jest-asymmetric-matchers'
import { getState } from './state'

Expand Down Expand Up @@ -108,10 +108,12 @@ function JestExtendPlugin(expect: ExpectStatic, matchers: MatchersObject): ChaiP
}
}

const customMatcher = (...sample: [unknown, ...unknown[]]) => new CustomMatcher(false, ...sample)

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

Expand All @@ -121,6 +123,15 @@ function JestExtendPlugin(expect: ExpectStatic, matchers: MatchersObject): ChaiP
value: (...sample: [unknown, ...unknown[]]) => new CustomMatcher(true, ...sample),
writable: true,
})

// keep track of asymmetric matchers on global so that it can be copied over to local context's `expect`.
// note that the negated variant is automatically shared since it's assigned on the single `expect.not` object.
Object.defineProperty(((globalThis as any)[ASYMMETRIC_MATCHERS_OBJECT]), expectAssertionName, {
configurable: true,
enumerable: true,
value: customMatcher,
writable: true,
})
})
}
}
Expand Down
6 changes: 5 additions & 1 deletion packages/expect/src/state.ts
@@ -1,9 +1,10 @@
import type { ExpectStatic, MatcherState } from './types'
import { GLOBAL_EXPECT, JEST_MATCHERS_OBJECT, MATCHERS_OBJECT } from './constants'
import { ASYMMETRIC_MATCHERS_OBJECT, GLOBAL_EXPECT, JEST_MATCHERS_OBJECT, MATCHERS_OBJECT } from './constants'

if (!Object.prototype.hasOwnProperty.call(globalThis, MATCHERS_OBJECT)) {
const globalState = new WeakMap<ExpectStatic, MatcherState>()
const matchers = Object.create(null)
const assymetricMatchers = Object.create(null)
Object.defineProperty(globalThis, MATCHERS_OBJECT, {
get: () => globalState,
})
Expand All @@ -14,6 +15,9 @@ if (!Object.prototype.hasOwnProperty.call(globalThis, MATCHERS_OBJECT)) {
matchers,
}),
})
Object.defineProperty(globalThis, ASYMMETRIC_MATCHERS_OBJECT, {
get: () => assymetricMatchers,
})
}

export function getState<State extends MatcherState = MatcherState>(expect: ExpectStatic): State {
Expand Down
3 changes: 2 additions & 1 deletion packages/vitest/src/integrations/chai/index.ts
Expand Up @@ -4,7 +4,7 @@ import * as chai from 'chai'
import './setup'
import type { TaskPopulated, Test } from '@vitest/runner'
import { getCurrentTest } from '@vitest/runner'
import { GLOBAL_EXPECT, getState, setState } from '@vitest/expect'
import { ASYMMETRIC_MATCHERS_OBJECT, GLOBAL_EXPECT, getState, setState } from '@vitest/expect'
import type { Assertion, ExpectStatic } from '@vitest/expect'
import type { MatcherState } from '../../types/chai'
import { getFullName } from '../../utils/tasks'
Expand All @@ -23,6 +23,7 @@ export function createExpect(test?: TaskPopulated) {
return assert
}) as ExpectStatic
Object.assign(expect, chai.expect)
Object.assign(expect, (globalThis as any)[ASYMMETRIC_MATCHERS_OBJECT])

expect.getState = () => getState<MatcherState>(expect)
expect.setState = state => setState(state as Partial<MatcherState>, expect)
Expand Down
39 changes: 39 additions & 0 deletions test/core/test/local-context.test.ts
Expand Up @@ -36,3 +36,42 @@ describe('context expect', () => {
expect(localExpect.getState().snapshotState).toBeDefined()
})
})

describe('custom matcher are inherited by local context', () => {
expect.extend({
toEqual_testCustom(received, expected) {
return {
pass: received === expected,
message: () => `test`,
}
},
})

it('basic', ({ expect: localExpect }) => {
// as assertion
expect(expect('test')).toHaveProperty('toEqual_testCustom')
expect(expect.soft('test')).toHaveProperty('toEqual_testCustom')
expect(localExpect('test')).toHaveProperty('toEqual_testCustom')
expect(localExpect.soft('test')).toHaveProperty('toEqual_testCustom')

// as asymmetric matcher
expect(expect).toHaveProperty('toEqual_testCustom')
expect(expect.not).toHaveProperty('toEqual_testCustom')
expect(localExpect).toHaveProperty('toEqual_testCustom')
expect(localExpect.not).toHaveProperty('toEqual_testCustom');

(expect(0) as any).toEqual_testCustom(0);
(expect(0) as any).not.toEqual_testCustom(1);
(localExpect(0) as any).toEqual_testCustom(0);
(localExpect(0) as any).not.toEqual_testCustom(1)

expect(0).toEqual((expect as any).toEqual_testCustom(0))
localExpect(0).toEqual((localExpect as any).toEqual_testCustom(0))
expect(0).toEqual((expect.not as any).toEqual_testCustom(1))
localExpect(0).toEqual((localExpect.not as any).toEqual_testCustom(1))

// asymmetric matcher function is identical
expect((expect as any).toEqual_testCustom).toBe((localExpect as any).toEqual_testCustom)
expect((expect.not as any).toEqual_testCustom).toBe((localExpect.not as any).toEqual_testCustom)
})
})

0 comments on commit 9fe3873

Please sign in to comment.