From f13abff8df9a0e1148baf3584bcde6d1b479edc7 Mon Sep 17 00:00:00 2001 From: Simen Bekkhus Date: Tue, 5 Oct 2021 13:41:28 +0200 Subject: [PATCH] fix: pass matcher context to asymmetric matchers (#11926) --- CHANGELOG.md | 1 + .../__snapshots__/extend.test.ts.snap | 4 +- packages/expect/src/__tests__/extend.test.ts | 20 ++++-- packages/expect/src/asymmetricMatchers.ts | 50 ++++++++----- packages/expect/src/jestMatchersObject.ts | 10 +-- packages/expect/src/matchers.ts | 72 ++++++------------- packages/expect/src/types.ts | 7 +- 7 files changed, 83 insertions(+), 81 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 828a9175cba3..5377b630f12e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ ### Fixes +- `[expect]` Pass matcher context to asymmetric matchers ([#11926](https://github.com/facebook/jest/pull/11926)) - `[@jest/types]` Mark deprecated configuration options as `@deprecated` ([#11913](https://github.com/facebook/jest/pull/11913)) - `[jest-cli]` Improve `--help` printout by removing defunct `--browser` option ([#11914](https://github.com/facebook/jest/pull/11914)) - `[jest-haste-map]` Use distinct cache paths for different values of `computeDependencies` ([#11916](https://github.com/facebook/jest/pull/11916)) diff --git a/packages/expect/src/__tests__/__snapshots__/extend.test.ts.snap b/packages/expect/src/__tests__/__snapshots__/extend.test.ts.snap index 68129f9c16d7..0dcdf8adf905 100644 --- a/packages/expect/src/__tests__/__snapshots__/extend.test.ts.snap +++ b/packages/expect/src/__tests__/__snapshots__/extend.test.ts.snap @@ -48,9 +48,9 @@ exports[`defines asymmetric variadic matchers that can be prefixed by not 1`] = } `; -exports[`is available globally when matcher is unary 1`] = `expected 15 to be divisible by 2`; +exports[`is available globally when matcher is unary 1`] = `expected 15 to be divisible by 2`; -exports[`is available globally when matcher is variadic 1`] = `expected 15 to be within range 1 - 3`; +exports[`is available globally when matcher is variadic 1`] = `expected 15 to be within range 1 - 3`; exports[`is ok if there is no message specified 1`] = `No message was specified for this matcher.`; diff --git a/packages/expect/src/__tests__/extend.test.ts b/packages/expect/src/__tests__/extend.test.ts index 898fa98640d5..8fdfd1d856c1 100644 --- a/packages/expect/src/__tests__/extend.test.ts +++ b/packages/expect/src/__tests__/extend.test.ts @@ -18,8 +18,14 @@ jestExpect.extend({ toBeDivisibleBy(actual: number, expected: number) { const pass = actual % expected === 0; const message = pass - ? () => `expected ${actual} not to be divisible by ${expected}` - : () => `expected ${actual} to be divisible by ${expected}`; + ? () => + `expected ${this.utils.printReceived( + actual, + )} not to be divisible by ${expected}` + : () => + `expected ${this.utils.printReceived( + actual, + )} to be divisible by ${expected}`; return {message, pass}; }, @@ -33,8 +39,14 @@ jestExpect.extend({ toBeWithinRange(actual: number, floor: number, ceiling: number) { const pass = actual >= floor && actual <= ceiling; const message = pass - ? () => `expected ${actual} not to be within range ${floor} - ${ceiling}` - : () => `expected ${actual} to be within range ${floor} - ${ceiling}`; + ? () => + `expected ${this.utils.printReceived( + actual, + )} not to be within range ${floor} - ${ceiling}` + : () => + `expected ${this.utils.printReceived( + actual, + )} to be within range ${floor} - ${ceiling}`; return {message, pass}; }, diff --git a/packages/expect/src/asymmetricMatchers.ts b/packages/expect/src/asymmetricMatchers.ts index c906f247e742..5d56358192bb 100644 --- a/packages/expect/src/asymmetricMatchers.ts +++ b/packages/expect/src/asymmetricMatchers.ts @@ -6,16 +6,35 @@ * */ +import * as matcherUtils from 'jest-matcher-utils'; import {equals, fnNameFor, hasProperty, isA, isUndefined} from './jasmineUtils'; +import {getState} from './jestMatchersObject'; +import type {MatcherState} from './types'; +import {iterableEquality, subsetEquality} from './utils'; export class AsymmetricMatcher { protected sample: T; + protected readonly matcherState: MatcherState; $$typeof: symbol; + // TODO: remove this field in Jest 28 (use `matcherState`) inverse?: boolean; - constructor(sample: T) { + constructor(sample: T, isNot = false) { this.$$typeof = Symbol.for('jest.asymmetricMatcher'); this.sample = sample; + + const utils = {...matcherUtils, iterableEquality, subsetEquality}; + + const matcherContext: MatcherState = { + ...getState(), + equals, + isNot, + utils, + }; + + this.inverse = matcherContext.isNot; + + this.matcherState = matcherContext; } } @@ -114,8 +133,7 @@ class Anything extends AsymmetricMatcher { class ArrayContaining extends AsymmetricMatcher> { constructor(sample: Array, inverse: boolean = false) { - super(sample); - this.inverse = inverse; + super(sample, inverse); } asymmetricMatch(other: Array) { @@ -134,11 +152,11 @@ class ArrayContaining extends AsymmetricMatcher> { other.some(another => equals(item, another)), )); - return this.inverse ? !result : result; + return this.matcherState.isNot ? !result : result; } toString() { - return `Array${this.inverse ? 'Not' : ''}Containing`; + return `Array${this.matcherState.isNot ? 'Not' : ''}Containing`; } getExpectedType() { @@ -148,8 +166,7 @@ class ArrayContaining extends AsymmetricMatcher> { class ObjectContaining extends AsymmetricMatcher> { constructor(sample: Record, inverse: boolean = false) { - super(sample); - this.inverse = inverse; + super(sample, inverse); } asymmetricMatch(other: any) { @@ -173,11 +190,11 @@ class ObjectContaining extends AsymmetricMatcher> { } } - return this.inverse ? !result : result; + return this.matcherState.isNot ? !result : result; } toString() { - return `Object${this.inverse ? 'Not' : ''}Containing`; + return `Object${this.matcherState.isNot ? 'Not' : ''}Containing`; } getExpectedType() { @@ -190,18 +207,17 @@ class StringContaining extends AsymmetricMatcher { if (!isA('String', sample)) { throw new Error('Expected is not a string'); } - super(sample); - this.inverse = inverse; + super(sample, inverse); } asymmetricMatch(other: string) { const result = isA('String', other) && other.includes(this.sample); - return this.inverse ? !result : result; + return this.matcherState.isNot ? !result : result; } toString() { - return `String${this.inverse ? 'Not' : ''}Containing`; + return `String${this.matcherState.isNot ? 'Not' : ''}Containing`; } getExpectedType() { @@ -214,19 +230,17 @@ class StringMatching extends AsymmetricMatcher { if (!isA('String', sample) && !isA('RegExp', sample)) { throw new Error('Expected is not a String or a RegExp'); } - super(new RegExp(sample)); - - this.inverse = inverse; + super(new RegExp(sample), inverse); } asymmetricMatch(other: string) { const result = isA('String', other) && this.sample.test(other); - return this.inverse ? !result : result; + return this.matcherState.isNot ? !result : result; } toString() { - return `String${this.inverse ? 'Not' : ''}Matching`; + return `String${this.matcherState.isNot ? 'Not' : ''}Matching`; } getExpectedType() { diff --git a/packages/expect/src/jestMatchersObject.ts b/packages/expect/src/jestMatchersObject.ts index 93876b220d60..68aa1bfd659e 100644 --- a/packages/expect/src/jestMatchersObject.ts +++ b/packages/expect/src/jestMatchersObject.ts @@ -63,21 +63,21 @@ export const setMatchers = ( class CustomMatcher extends AsymmetricMatcher<[unknown, unknown]> { constructor(inverse: boolean = false, ...sample: [unknown, unknown]) { - super(sample); - this.inverse = inverse; + super(sample, inverse); } asymmetricMatch(other: unknown) { - const {pass} = matcher( + const {pass} = matcher.call( + this.matcherState, other, ...this.sample, ) as SyncExpectationResult; - return this.inverse ? !pass : pass; + return this.matcherState.isNot ? !pass : pass; } toString() { - return `${this.inverse ? 'not.' : ''}${key}`; + return `${this.matcherState.isNot ? 'not.' : ''}${key}`; } getExpectedType() { diff --git a/packages/expect/src/matchers.ts b/packages/expect/src/matchers.ts index 344374330cb6..f7f019129d51 100644 --- a/packages/expect/src/matchers.ts +++ b/packages/expect/src/matchers.ts @@ -38,7 +38,7 @@ import { printReceivedStringContainExpectedResult, printReceivedStringContainExpectedSubstring, } from './print'; -import type {MatcherState, MatchersObject} from './types'; +import type {MatchersObject} from './types'; import { arrayBufferEquality, getObjectSubset, @@ -73,7 +73,7 @@ type ContainIterable = | HTMLCollectionOf; const matchers: MatchersObject = { - toBe(this: MatcherState, received: unknown, expected: unknown) { + toBe(received: unknown, expected: unknown) { const matcherName = 'toBe'; const options: MatcherHintOptions = { comment: 'Object.is equality', @@ -126,12 +126,7 @@ const matchers: MatchersObject = { return {actual: received, expected, message, name: matcherName, pass}; }, - toBeCloseTo( - this: MatcherState, - received: number, - expected: number, - precision: number = 2, - ) { + toBeCloseTo(received: number, expected: number, precision: number = 2) { const matcherName = 'toBeCloseTo'; const secondArgument = arguments.length === 3 ? 'precision' : undefined; const isNot = this.isNot; @@ -197,7 +192,7 @@ const matchers: MatchersObject = { return {message, pass}; }, - toBeDefined(this: MatcherState, received: unknown, expected: void) { + toBeDefined(received: unknown, expected: void) { const matcherName = 'toBeDefined'; const options: MatcherHintOptions = { isNot: this.isNot, @@ -215,7 +210,7 @@ const matchers: MatchersObject = { return {message, pass}; }, - toBeFalsy(this: MatcherState, received: unknown, expected: void) { + toBeFalsy(received: unknown, expected: void) { const matcherName = 'toBeFalsy'; const options: MatcherHintOptions = { isNot: this.isNot, @@ -233,11 +228,7 @@ const matchers: MatchersObject = { return {message, pass}; }, - toBeGreaterThan( - this: MatcherState, - received: number | bigint, - expected: number | bigint, - ) { + toBeGreaterThan(received: number | bigint, expected: number | bigint) { const matcherName = 'toBeGreaterThan'; const isNot = this.isNot; const options: MatcherHintOptions = { @@ -257,11 +248,7 @@ const matchers: MatchersObject = { return {message, pass}; }, - toBeGreaterThanOrEqual( - this: MatcherState, - received: number | bigint, - expected: number | bigint, - ) { + toBeGreaterThanOrEqual(received: number | bigint, expected: number | bigint) { const matcherName = 'toBeGreaterThanOrEqual'; const isNot = this.isNot; const options: MatcherHintOptions = { @@ -281,7 +268,7 @@ const matchers: MatchersObject = { return {message, pass}; }, - toBeInstanceOf(this: MatcherState, received: any, expected: Function) { + toBeInstanceOf(received: any, expected: Function) { const matcherName = 'toBeInstanceOf'; const options: MatcherHintOptions = { isNot: this.isNot, @@ -331,11 +318,7 @@ const matchers: MatchersObject = { return {message, pass}; }, - toBeLessThan( - this: MatcherState, - received: number | bigint, - expected: number | bigint, - ) { + toBeLessThan(received: number | bigint, expected: number | bigint) { const matcherName = 'toBeLessThan'; const isNot = this.isNot; const options: MatcherHintOptions = { @@ -355,11 +338,7 @@ const matchers: MatchersObject = { return {message, pass}; }, - toBeLessThanOrEqual( - this: MatcherState, - received: number | bigint, - expected: number | bigint, - ) { + toBeLessThanOrEqual(received: number | bigint, expected: number | bigint) { const matcherName = 'toBeLessThanOrEqual'; const isNot = this.isNot; const options: MatcherHintOptions = { @@ -379,7 +358,7 @@ const matchers: MatchersObject = { return {message, pass}; }, - toBeNaN(this: MatcherState, received: any, expected: void) { + toBeNaN(received: any, expected: void) { const matcherName = 'toBeNaN'; const options: MatcherHintOptions = { isNot: this.isNot, @@ -397,7 +376,7 @@ const matchers: MatchersObject = { return {message, pass}; }, - toBeNull(this: MatcherState, received: unknown, expected: void) { + toBeNull(received: unknown, expected: void) { const matcherName = 'toBeNull'; const options: MatcherHintOptions = { isNot: this.isNot, @@ -415,7 +394,7 @@ const matchers: MatchersObject = { return {message, pass}; }, - toBeTruthy(this: MatcherState, received: unknown, expected: void) { + toBeTruthy(received: unknown, expected: void) { const matcherName = 'toBeTruthy'; const options: MatcherHintOptions = { isNot: this.isNot, @@ -433,7 +412,7 @@ const matchers: MatchersObject = { return {message, pass}; }, - toBeUndefined(this: MatcherState, received: unknown, expected: void) { + toBeUndefined(received: unknown, expected: void) { const matcherName = 'toBeUndefined'; const options: MatcherHintOptions = { isNot: this.isNot, @@ -451,11 +430,7 @@ const matchers: MatchersObject = { return {message, pass}; }, - toContain( - this: MatcherState, - received: ContainIterable | string, - expected: unknown, - ) { + toContain(received: ContainIterable | string, expected: unknown) { const matcherName = 'toContain'; const isNot = this.isNot; const options: MatcherHintOptions = { @@ -556,11 +531,7 @@ const matchers: MatchersObject = { return {message, pass}; }, - toContainEqual( - this: MatcherState, - received: ContainIterable, - expected: unknown, - ) { + toContainEqual(received: ContainIterable, expected: unknown) { const matcherName = 'toContainEqual'; const isNot = this.isNot; const options: MatcherHintOptions = { @@ -606,7 +577,7 @@ const matchers: MatchersObject = { return {message, pass}; }, - toEqual(this: MatcherState, received: unknown, expected: unknown) { + toEqual(received: unknown, expected: unknown) { const matcherName = 'toEqual'; const options: MatcherHintOptions = { comment: 'deep equality', @@ -641,7 +612,7 @@ const matchers: MatchersObject = { return {actual: received, expected, message, name: matcherName, pass}; }, - toHaveLength(this: MatcherState, received: any, expected: number) { + toHaveLength(received: any, expected: number) { const matcherName = 'toHaveLength'; const isNot = this.isNot; const options: MatcherHintOptions = { @@ -696,7 +667,6 @@ const matchers: MatchersObject = { }, toHaveProperty( - this: MatcherState, received: object, expectedPath: string | Array, expectedValue?: unknown, @@ -818,7 +788,7 @@ const matchers: MatchersObject = { return {message, pass}; }, - toMatch(this: MatcherState, received: string, expected: string | RegExp) { + toMatch(received: string, expected: string | RegExp) { const matcherName = 'toMatch'; const options: MatcherHintOptions = { isNot: this.isNot, @@ -893,7 +863,7 @@ const matchers: MatchersObject = { return {message, pass}; }, - toMatchObject(this: MatcherState, received: object, expected: object) { + toMatchObject(received: object, expected: object) { const matcherName = 'toMatchObject'; const options: MatcherHintOptions = { isNot: this.isNot, @@ -944,7 +914,7 @@ const matchers: MatchersObject = { return {message, pass}; }, - toStrictEqual(this: MatcherState, received: unknown, expected: unknown) { + toStrictEqual(received: unknown, expected: unknown) { const matcherName = 'toStrictEqual'; const options: MatcherHintOptions = { comment: 'deep equality', diff --git a/packages/expect/src/types.ts b/packages/expect/src/types.ts index 29e9df1dc352..0828eeca49a3 100644 --- a/packages/expect/src/types.ts +++ b/packages/expect/src/types.ts @@ -22,7 +22,12 @@ export type AsyncExpectationResult = Promise; export type ExpectationResult = SyncExpectationResult | AsyncExpectationResult; export type RawMatcherFn = { - (received: any, expected: any, options?: any): ExpectationResult; + ( + this: MatcherState, + received: any, + expected: any, + options?: any, + ): ExpectationResult; [INTERNAL_MATCHER_FLAG]?: boolean; };