diff --git a/packages/jest-matchers/src/__tests__/__snapshots__/matchers-test.js.snap b/packages/jest-matchers/src/__tests__/__snapshots__/matchers-test.js.snap index b9d2dd73753b..c25835ba7a79 100644 --- a/packages/jest-matchers/src/__tests__/__snapshots__/matchers-test.js.snap +++ b/packages/jest-matchers/src/__tests__/__snapshots__/matchers-test.js.snap @@ -1,5 +1,143 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`.rejects fails for promise that resolves 1`] = ` +"expect(received).rejects.toBe() + +Expected received Promise to reject, instead it resolved to value + 4" +`; + +exports[`.rejects fails non-promise value "a" 1`] = ` +"expect(received).rejects.toBeDefined() + +received value must be a Promise. +Received: + string: \\"a\\"" +`; + +exports[`.rejects fails non-promise value [1] 1`] = ` +"expect(received).rejects.toBeDefined() + +received value must be a Promise. +Received: + array: [1]" +`; + +exports[`.rejects fails non-promise value [Function anonymous] 1`] = ` +"expect(received).rejects.toBeDefined() + +received value must be a Promise. +Received: + function: [Function anonymous]" +`; + +exports[`.rejects fails non-promise value {"a": 1} 1`] = ` +"expect(received).rejects.toBeDefined() + +received value must be a Promise. +Received: + object: {\\"a\\": 1}" +`; + +exports[`.rejects fails non-promise value 4 1`] = ` +"expect(received).rejects.toBeDefined() + +received value must be a Promise. +Received: + number: 4" +`; + +exports[`.rejects fails non-promise value null 1`] = ` +"expect(received).rejects.toBeDefined() + +received value must be a Promise. +Received: null" +`; + +exports[`.rejects fails non-promise value true 1`] = ` +"expect(received).rejects.toBeDefined() + +received value must be a Promise. +Received: + boolean: true" +`; + +exports[`.rejects fails non-promise value undefined 1`] = ` +"expect(received).rejects.toBeDefined() + +received value must be a Promise. +Received: undefined" +`; + +exports[`.resolves fails for promise that rejects 1`] = ` +"expect(received).resolves.toBe() + +Expected received Promise to resolve, instead it rejected to value + undefined" +`; + +exports[`.resolves fails non-promise value "a" 1`] = ` +"expect(received).resolves.toBeDefined() + +received value must be a Promise. +Received: + string: \\"a\\"" +`; + +exports[`.resolves fails non-promise value [1] 1`] = ` +"expect(received).resolves.toBeDefined() + +received value must be a Promise. +Received: + array: [1]" +`; + +exports[`.resolves fails non-promise value [Function anonymous] 1`] = ` +"expect(received).resolves.toBeDefined() + +received value must be a Promise. +Received: + function: [Function anonymous]" +`; + +exports[`.resolves fails non-promise value {"a": 1} 1`] = ` +"expect(received).resolves.toBeDefined() + +received value must be a Promise. +Received: + object: {\\"a\\": 1}" +`; + +exports[`.resolves fails non-promise value 4 1`] = ` +"expect(received).resolves.toBeDefined() + +received value must be a Promise. +Received: + number: 4" +`; + +exports[`.resolves fails non-promise value null 1`] = ` +"expect(received).resolves.toBeDefined() + +received value must be a Promise. +Received: null" +`; + +exports[`.resolves fails non-promise value true 1`] = ` +"expect(received).resolves.toBeDefined() + +received value must be a Promise. +Received: + boolean: true" +`; + +exports[`.resolves fails non-promise value undefined 1`] = ` +"expect(received).resolves.toBeDefined() + +received value must be a Promise. +Received: undefined" +`; + exports[`.toBe() does not crash on circular references 1`] = ` "expect(received).toBe(expected) diff --git a/packages/jest-matchers/src/__tests__/matchers-test.js b/packages/jest-matchers/src/__tests__/matchers-test.js index a86b9bfd51f1..ac83201a85db 100644 --- a/packages/jest-matchers/src/__tests__/matchers-test.js +++ b/packages/jest-matchers/src/__tests__/matchers-test.js @@ -14,6 +14,95 @@ const jestExpect = require('../'); const {stringify} = require('jest-matcher-utils'); +describe('.rejects', () => { + it('should reject', async () => { + await jestExpect(Promise.reject(4)).rejects.toBe(4); + await jestExpect(Promise.reject(4)).rejects.not.toBe(5); + await jestExpect(Promise.reject(4.2)).rejects.toBeCloseTo(4.2, 5); + await jestExpect(Promise.reject((3))).rejects.not.toBeCloseTo(4.2, 5); + await jestExpect(Promise.reject({a: 1, b: 2})).rejects.toMatchObject({a: 1}); + await jestExpect(Promise.reject({a: 1, b: 2})).rejects.not.toMatchObject({c: 1}); + await jestExpect(Promise.reject(() => {throw new Error();})).rejects.toThrow(); + }); + + [ + 4, + [1], + {a: 1}, + 'a', + true, + null, + undefined, + () => {}, + ].forEach(value => { + it(`fails non-promise value ${stringify(value)}`, async () => { + let error; + try { + await jestExpect(value).rejects.toBeDefined(); + } catch (e) { + error = e; + } + expect(error).toBeDefined(); + expect(error.message).toMatchSnapshot(); + }); + }); + + it('fails for promise that resolves', async () => { + let error; + try { + await jestExpect(Promise.resolve(4)).rejects.toBe(4); + } catch (e) { + error = e; + } + expect(error).toBeDefined(); + expect(error.message).toMatchSnapshot(); + }); +}); + +describe('.resolves', () => { + it('should resolve', async () => { + await jestExpect(Promise.resolve(4)).resolves.toBe(4); + await jestExpect(Promise.resolve(4)).resolves.not.toBe(5); + await jestExpect(Promise.resolve(4.2)).resolves.toBeCloseTo(4.2, 5); + await jestExpect(Promise.resolve((3))).resolves.not.toBeCloseTo(4.2, 5); + await jestExpect(Promise.resolve({a: 1, b: 2})).resolves.toMatchObject({a: 1}); + await jestExpect(Promise.resolve({a: 1, b: 2})).resolves.not.toMatchObject({c: 1}); + await jestExpect(Promise.resolve(() => {throw new Error();})).resolves.toThrow(); + }); + + [ + 4, + [1], + {a: 1}, + 'a', + true, + null, + undefined, + () => {}, + ].forEach(value => { + it(`fails non-promise value ${stringify(value)}`, async () => { + let error; + try { + await jestExpect(value).resolves.toBeDefined(); + } catch (e) { + error = e; + } + expect(error).toBeDefined(); + expect(error.message).toMatchSnapshot(); + }); + }); + + it('fails for promise that rejects', async () => { + let error; + try { + await jestExpect(Promise.reject(4)).resolves.toBe(4); + } catch (e) { + error = e; + } + expect(error).toBeDefined(); + expect(error.message).toMatchSnapshot(); + }); +}); describe('.toBe()', () => { it('does not throw', () => { jestExpect('a').not.toBe('b'); diff --git a/packages/jest-matchers/src/index.js b/packages/jest-matchers/src/index.js index 915135e6a7ef..994d1cb1f718 100644 --- a/packages/jest-matchers/src/index.js +++ b/packages/jest-matchers/src/index.js @@ -19,6 +19,7 @@ import type { MatchersObject, RawMatcherFn, ThrowingMatcherFn, + PromiseMatcherFn, } from 'types/Matchers'; const matchers = require('./matchers'); @@ -41,6 +42,12 @@ class JestAssertionError extends Error { matcherResult: any; } +const isPromise = obj => { + return !!obj && + (typeof obj === 'object' || typeof obj === 'function') && + typeof obj.then === 'function'; +}; + if (!global[GLOBAL_STATE]) { Object.defineProperty(global, GLOBAL_STATE, { value: { @@ -56,7 +63,12 @@ if (!global[GLOBAL_STATE]) { const expect: Expect = (actual: any): ExpectationObject => { const allMatchers = global[GLOBAL_STATE].matchers; - const expectation = {not: {}}; + const expectation = { + not: {}, + rejects: {not: {}}, + resolves: {not: {}}, + }; + Object.keys(allMatchers).forEach(name => { expectation[name] = makeThrowingMatcher(allMatchers[name], false, actual); expectation.not[name] = makeThrowingMatcher( @@ -64,6 +76,16 @@ const expect: Expect = (actual: any): ExpectationObject => { true, actual, ); + + expectation.resolves[name] = + makeResolveMatcher(name, allMatchers[name], false, actual); + expectation.resolves.not[name] = + makeResolveMatcher(name, allMatchers[name], true, actual); + + expectation.rejects[name] = + makeRejectMatcher(name, allMatchers[name], false, actual); + expectation.rejects.not[name] = + makeRejectMatcher(name, allMatchers[name], true, actual); }); return expectation; @@ -84,6 +106,65 @@ const getMessage = message => { return message; }; +const makeResolveMatcher = ( + matcherName: string, + matcher: RawMatcherFn, + isNot: boolean, + actual: Promise +): PromiseMatcherFn => async (...args) => { + const matcherStatement = `.resolves.${isNot ? 'not.' : ''}${matcherName}`; + if (!isPromise(actual)) { + throw new JestAssertionError( + utils.matcherHint(matcherStatement, 'received', '') + '\n\n' + + `${utils.RECEIVED_COLOR('received')} value must be a Promise.\n` + + utils.printWithType('Received', actual, utils.printReceived), + ); + } + + let result; + try { + result = await actual; + } catch (e) { + throw new JestAssertionError( + utils.matcherHint(matcherStatement, 'received', '') + '\n\n' + + `Expected ${utils.RECEIVED_COLOR('received')} Promise to resolve, ` + + 'instead it rejected to value\n' + + ` ${utils.printReceived(result)}` + ); + } + return makeThrowingMatcher(matcher, isNot, result).apply(null, args); +}; + +const makeRejectMatcher = ( + matcherName: string, + matcher: RawMatcherFn, + isNot: boolean, + actual: Promise +): PromiseMatcherFn => async (...args) => { + const matcherStatement = `.rejects.${isNot ? 'not.' : ''}${matcherName}`; + if (!isPromise(actual)) { + throw new JestAssertionError( + utils.matcherHint(matcherStatement, 'received', '') + '\n\n' + + `${utils.RECEIVED_COLOR('received')} value must be a Promise.\n` + + utils.printWithType('Received', actual, utils.printReceived), + ); + } + + let result; + try { + result = await actual; + } catch (e) { + return makeThrowingMatcher(matcher, isNot, e).apply(null, args); + } + + throw new JestAssertionError( + utils.matcherHint(matcherStatement, 'received', '') + '\n\n' + + `Expected ${utils.RECEIVED_COLOR('received')} Promise to reject, ` + + 'instead it resolved to value\n' + + ` ${utils.printReceived(result)}` + ); +}; + const makeThrowingMatcher = ( matcher: RawMatcherFn, isNot: boolean, diff --git a/types/Matchers.js b/types/Matchers.js index 8c0d14c684ac..7bc8d43f7a3a 100644 --- a/types/Matchers.js +++ b/types/Matchers.js @@ -23,6 +23,7 @@ export type RawMatcherFn = ( ) => ExpectationResult; export type ThrowingMatcherFn = (actual: any) => void; +export type PromiseMatcherFn = (actual: any) => Promise; export type MatcherContext = {isNot: boolean}; export type MatcherState = { assertionCalls?: number, @@ -33,6 +34,14 @@ export type MatcherState = { export type MatchersObject = {[id:string]: RawMatcherFn}; export type Expect = (expected: any) => ExpectationObject; export type ExpectationObject = { + resolves: { + [id: string]: PromiseMatcherFn, + not: {[id: string]: PromiseMatcherFn}, + }, + rejects: { + [id: string]: PromiseMatcherFn, + not: {[id: string]: PromiseMatcherFn}, + }, [id: string]: ThrowingMatcherFn, not: {[id: string]: ThrowingMatcherFn}, };