From 28fc970dcf22e402461809e1c1d29e6f33c3674b Mon Sep 17 00:00:00 2001 From: Aymeric Bouzy Date: Thu, 8 Mar 2018 19:02:07 +0100 Subject: [PATCH] Custom Asymmetric Matchers (#5503) * test * define asymmetric matchers * more * correction * trying to fix flow * flow fix --- .../__snapshots__/extend.test.js.snap | 38 ++++++++++++++ packages/expect/src/__tests__/extend.test.js | 18 +++++++ packages/expect/src/asymmetric_matchers.js | 2 +- packages/expect/src/index.js | 10 ++-- packages/expect/src/jest_matchers_object.js | 50 ++++++++++++++++++- types/Matchers.js | 2 + 6 files changed, 112 insertions(+), 8 deletions(-) diff --git a/packages/expect/src/__tests__/__snapshots__/extend.test.js.snap b/packages/expect/src/__tests__/__snapshots__/extend.test.js.snap index 7565cb732261..381b08c9f64f 100644 --- a/packages/expect/src/__tests__/__snapshots__/extend.test.js.snap +++ b/packages/expect/src/__tests__/__snapshots__/extend.test.js.snap @@ -1,5 +1,43 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`defines asymmetric matchers 1`] = ` +"expect(received).toEqual(expected) + +Expected value to equal: + {\\"value\\": toBeDivisibleBy<2>} +Received: + {\\"value\\": 3} + +Difference: + +- Expected ++ Received + + Object { +- \\"value\\": toBeDivisibleBy<2>, ++ \\"value\\": 3, + }" +`; + +exports[`defines asymmetric matchers that can be prefixed by not 1`] = ` +"expect(received).toEqual(expected) + +Expected value to equal: + {\\"value\\": not.toBeDivisibleBy<2>} +Received: + {\\"value\\": 2} + +Difference: + +- Expected ++ Received + + Object { +- \\"value\\": not.toBeDivisibleBy<2>, ++ \\"value\\": 2, + }" +`; + exports[`is available globally 1`] = `"expected 15 to be divisible by 2"`; 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.js b/packages/expect/src/__tests__/extend.test.js index 59b36aa5969e..64511d6eb243 100644 --- a/packages/expect/src/__tests__/extend.test.js +++ b/packages/expect/src/__tests__/extend.test.js @@ -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(); +}); diff --git a/packages/expect/src/asymmetric_matchers.js b/packages/expect/src/asymmetric_matchers.js index b8e7a9992264..b182fb1f1c44 100644 --- a/packages/expect/src/asymmetric_matchers.js +++ b/packages/expect/src/asymmetric_matchers.js @@ -17,7 +17,7 @@ import { import {emptyObject} from './utils'; -class AsymmetricMatcher { +export class AsymmetricMatcher { $$typeof: Symbol; inverse: boolean; diff --git a/packages/expect/src/index.js b/packages/expect/src/index.js index c4da96f53352..4d766833e593 100644 --- a/packages/expect/src/index.js +++ b/packages/expect/src/index.js @@ -258,7 +258,7 @@ const makeThrowingMatcher = ( }; expect.extend = (matchers: MatchersObject): void => - setMatchers(matchers, false); + setMatchers(matchers, false, expect); expect.anything = anything; expect.any = any; @@ -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; }; diff --git a/packages/expect/src/jest_matchers_object.js b/packages/expect/src/jest_matchers_object.js index 29c374279436..12cae657475d 100644 --- a/packages/expect/src/jest_matchers_object.js +++ b/packages/expect/src/jest_matchers_object.js @@ -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. @@ -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); diff --git a/types/Matchers.js b/types/Matchers.js index 04e11e5b0939..b8ce53f024ca 100644 --- a/types/Matchers.js +++ b/types/Matchers.js @@ -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 = {