Skip to content

Commit

Permalink
Custom Asymmetric Matchers (jestjs#5503)
Browse files Browse the repository at this point in the history
* test

* define asymmetric matchers

* more

* correction

* trying to fix flow

* flow fix
  • Loading branch information
aymericbouzy authored and thomasjinlo committed Mar 9, 2018
1 parent bb6c814 commit 28fc970
Show file tree
Hide file tree
Showing 6 changed files with 112 additions and 8 deletions.
38 changes: 38 additions & 0 deletions packages/expect/src/__tests__/__snapshots__/extend.test.js.snap
Original file line number Diff line number Diff line change
@@ -1,5 +1,43 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`defines asymmetric matchers 1`] = `
"<dim>expect(</><red>received</><dim>).toEqual(</><green>expected</><dim>)</>
Expected value to equal:
<green>{\\"value\\": toBeDivisibleBy<2>}</>
Received:
<red>{\\"value\\": 3}</>
Difference:
<green>- Expected</>
<red>+ Received</>
<dim> Object {</>
<green>- \\"value\\": toBeDivisibleBy<2>,</>
<red>+ \\"value\\": 3,</>
<dim> }</>"
`;
exports[`defines asymmetric matchers that can be prefixed by not 1`] = `
"<dim>expect(</><red>received</><dim>).toEqual(</><green>expected</><dim>)</>
Expected value to equal:
<green>{\\"value\\": not.toBeDivisibleBy<2>}</>
Received:
<red>{\\"value\\": 2}</>
Difference:
<green>- Expected</>
<red>+ Received</>
<dim> Object {</>
<green>- \\"value\\": not.toBeDivisibleBy<2>,</>
<red>+ \\"value\\": 2,</>
<dim> }</>"
`;
exports[`is available globally 1`] = `"expected 15 to be divisible by 2"`;
exports[`is ok if there is no message specified 1`] = `"<red>No message was specified for this matcher.</>"`;
18 changes: 18 additions & 0 deletions packages/expect/src/__tests__/extend.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,3 +70,21 @@ it('exposes an equality function to custom matchers', () => {

expect(() => jestExpect().toBeOne()).not.toThrow();
});

it('defines asymmetric matchers', () => {
expect(() =>
jestExpect({value: 2}).toEqual({value: jestExpect.toBeDivisibleBy(2)}),
).not.toThrow();
expect(() =>
jestExpect({value: 3}).toEqual({value: jestExpect.toBeDivisibleBy(2)}),
).toThrowErrorMatchingSnapshot();
});

it('defines asymmetric matchers that can be prefixed by not', () => {
expect(() =>
jestExpect({value: 2}).toEqual({value: jestExpect.not.toBeDivisibleBy(2)}),
).toThrowErrorMatchingSnapshot();
expect(() =>
jestExpect({value: 3}).toEqual({value: jestExpect.not.toBeDivisibleBy(2)}),
).not.toThrow();
});
2 changes: 1 addition & 1 deletion packages/expect/src/asymmetric_matchers.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import {

import {emptyObject} from './utils';

class AsymmetricMatcher {
export class AsymmetricMatcher {
$$typeof: Symbol;
inverse: boolean;

Expand Down
10 changes: 5 additions & 5 deletions packages/expect/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -258,7 +258,7 @@ const makeThrowingMatcher = (
};

expect.extend = (matchers: MatchersObject): void =>
setMatchers(matchers, false);
setMatchers(matchers, false, expect);

expect.anything = anything;
expect.any = any;
Expand Down Expand Up @@ -294,15 +294,15 @@ const _validateResult = result => {
};

// add default jest matchers
setMatchers(matchers, true);
setMatchers(spyMatchers, true);
setMatchers(toThrowMatchers, true);
setMatchers(matchers, true, expect);
setMatchers(spyMatchers, true, expect);
setMatchers(toThrowMatchers, true, expect);

expect.addSnapshotSerializer = () => void 0;
expect.assertions = (expected: number) => {
getState().expectedAssertionsNumber = expected;
};
expect.hasAssertions = expected => {
expect.hasAssertions = (expected: any) => {
utils.ensureNoExpected(expected, '.hasAssertions');
getState().isExpectingAssertions = true;
};
Expand Down
50 changes: 48 additions & 2 deletions packages/expect/src/jest_matchers_object.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
* @flow
*/

import type {MatchersObject} from 'types/Matchers';
import {AsymmetricMatcher} from './asymmetric_matchers';
import type {Expect, MatchersObject} from 'types/Matchers';

// Global matchers object holds the list of available matchers and
// the state, that can hold matcher specific values that change over time.
Expand Down Expand Up @@ -39,12 +40,57 @@ export const setState = (state: Object) => {

export const getMatchers = () => global[JEST_MATCHERS_OBJECT].matchers;

export const setMatchers = (matchers: MatchersObject, isInternal: boolean) => {
export const setMatchers = (
matchers: MatchersObject,
isInternal: boolean,
expect: Expect,
) => {
Object.keys(matchers).forEach(key => {
const matcher = matchers[key];
Object.defineProperty(matcher, INTERNAL_MATCHER_FLAG, {
value: isInternal,
});

if (!isInternal) {
// expect is defined

class CustomMatcher extends AsymmetricMatcher {
sample: any;

constructor(sample: any, inverse: boolean = false) {
super();
this.sample = sample;
this.inverse = inverse;
}

asymmetricMatch(other: any) {
const {pass}: {message: () => string, pass: boolean} = matcher(
(other: any),
(this.sample: any),
);

return this.inverse ? !pass : pass;
}

toString() {
return `${this.inverse ? 'not.' : ''}${key}`;
}

getExpectedType() {
return 'any';
}

toAsymmetricMatcher() {
return `${this.toString()}<${this.sample}>`;
}
}

expect[key] = (sample: any) => new CustomMatcher(sample);
if (!expect.not) {
expect.not = {};
}
expect.not[key] = (sample: any) => new CustomMatcher(sample, true);
}
});

Object.assign(global[JEST_MATCHERS_OBJECT].matchers, matchers);
Expand Down
2 changes: 2 additions & 0 deletions types/Matchers.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ export type Expect = {
objectContaining(sample: Object): AsymmetricMatcher,
stringContaining(expected: string): AsymmetricMatcher,
stringMatching(expected: string | RegExp): AsymmetricMatcher,
[id: string]: AsymmetricMatcher,
not: {[id: string]: AsymmetricMatcher},
};

export type ExpectationObject = {
Expand Down

0 comments on commit 28fc970

Please sign in to comment.