Skip to content

Commit

Permalink
fix: pass matcher context to asymmetric matchers (#11926)
Browse files Browse the repository at this point in the history
  • Loading branch information
SimenB committed Oct 5, 2021
1 parent 2dfde56 commit f13abff
Show file tree
Hide file tree
Showing 7 changed files with 83 additions and 81 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Expand Up @@ -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))
Expand Down
Expand Up @@ -48,9 +48,9 @@ exports[`defines asymmetric variadic matchers that can be prefixed by not 1`] =
<d> }</>
`;
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 <r>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 <r>15</> to be within range 1 - 3`;
exports[`is ok if there is no message specified 1`] = `<r>No message was specified for this matcher.</>`;
Expand Down
20 changes: 16 additions & 4 deletions packages/expect/src/__tests__/extend.test.ts
Expand Up @@ -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};
},
Expand All @@ -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};
},
Expand Down
50 changes: 32 additions & 18 deletions packages/expect/src/asymmetricMatchers.ts
Expand Up @@ -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<T> {
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;
}
}

Expand Down Expand Up @@ -114,8 +133,7 @@ class Anything extends AsymmetricMatcher<void> {

class ArrayContaining extends AsymmetricMatcher<Array<unknown>> {
constructor(sample: Array<unknown>, inverse: boolean = false) {
super(sample);
this.inverse = inverse;
super(sample, inverse);
}

asymmetricMatch(other: Array<unknown>) {
Expand All @@ -134,11 +152,11 @@ class ArrayContaining extends AsymmetricMatcher<Array<unknown>> {
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() {
Expand All @@ -148,8 +166,7 @@ class ArrayContaining extends AsymmetricMatcher<Array<unknown>> {

class ObjectContaining extends AsymmetricMatcher<Record<string, unknown>> {
constructor(sample: Record<string, unknown>, inverse: boolean = false) {
super(sample);
this.inverse = inverse;
super(sample, inverse);
}

asymmetricMatch(other: any) {
Expand All @@ -173,11 +190,11 @@ class ObjectContaining extends AsymmetricMatcher<Record<string, unknown>> {
}
}

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() {
Expand All @@ -190,18 +207,17 @@ class StringContaining extends AsymmetricMatcher<string> {
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() {
Expand All @@ -214,19 +230,17 @@ class StringMatching extends AsymmetricMatcher<RegExp> {
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() {
Expand Down
10 changes: 5 additions & 5 deletions packages/expect/src/jestMatchersObject.ts
Expand Up @@ -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() {
Expand Down

0 comments on commit f13abff

Please sign in to comment.