From 9fe38737984113a7e26bdf855ac8104ef1aeaba4 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Thu, 2 Nov 2023 23:10:34 +0900 Subject: [PATCH] fix: copy custom asymmetric matchers to local `expect` (#4405) --- packages/expect/src/constants.ts | 1 + packages/expect/src/jest-extend.ts | 15 ++++++- packages/expect/src/state.ts | 6 ++- .../vitest/src/integrations/chai/index.ts | 3 +- test/core/test/local-context.test.ts | 39 +++++++++++++++++++ 5 files changed, 60 insertions(+), 4 deletions(-) diff --git a/packages/expect/src/constants.ts b/packages/expect/src/constants.ts index 97a47089fd1a..70055a68b521 100644 --- a/packages/expect/src/constants.ts +++ b/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') diff --git a/packages/expect/src/jest-extend.ts b/packages/expect/src/jest-extend.ts index f6160c63fb83..fba626bcefb4 100644 --- a/packages/expect/src/jest-extend.ts +++ b/packages/expect/src/jest-extend.ts @@ -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' @@ -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, }) @@ -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, + }) }) } } diff --git a/packages/expect/src/state.ts b/packages/expect/src/state.ts index 4d830e926a85..7600a6d8dda0 100644 --- a/packages/expect/src/state.ts +++ b/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() const matchers = Object.create(null) + const assymetricMatchers = Object.create(null) Object.defineProperty(globalThis, MATCHERS_OBJECT, { get: () => globalState, }) @@ -14,6 +15,9 @@ if (!Object.prototype.hasOwnProperty.call(globalThis, MATCHERS_OBJECT)) { matchers, }), }) + Object.defineProperty(globalThis, ASYMMETRIC_MATCHERS_OBJECT, { + get: () => assymetricMatchers, + }) } export function getState(expect: ExpectStatic): State { diff --git a/packages/vitest/src/integrations/chai/index.ts b/packages/vitest/src/integrations/chai/index.ts index 41b64d16a700..3422094956d5 100644 --- a/packages/vitest/src/integrations/chai/index.ts +++ b/packages/vitest/src/integrations/chai/index.ts @@ -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' @@ -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(expect) expect.setState = state => setState(state as Partial, expect) diff --git a/test/core/test/local-context.test.ts b/test/core/test/local-context.test.ts index 8ace7b7e3489..65fa3bae2f87 100644 --- a/test/core/test/local-context.test.ts +++ b/test/core/test/local-context.test.ts @@ -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) + }) +})