Skip to content

Commit

Permalink
Add promise support to all the matchers (#3068)
Browse files Browse the repository at this point in the history
* Adds promise support to all the matchers

* avoid duplicate iteration over all the matchers

* Fixed bug in spread operator by replacing it with an apply statement

* Updated snapshots to new format.
  • Loading branch information
excitement-engineer authored and cpojer committed Mar 13, 2017
1 parent e45571c commit b49e4bf
Show file tree
Hide file tree
Showing 4 changed files with 318 additions and 1 deletion.
Original file line number Diff line number Diff line change
@@ -1,5 +1,143 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`.rejects fails for promise that resolves 1`] = `
"<dim>expect(<red>received</><dim>).rejects.toBe(<dim>)

Expected <red>received</> Promise to reject, instead it resolved to value
<red>4</>"
`;

exports[`.rejects fails non-promise value "a" 1`] = `
"<dim>expect(<red>received</><dim>).rejects.toBeDefined(<dim>)

<red>received</> value must be a Promise.
Received:
string: <red>\\"a\\"</>"
`;

exports[`.rejects fails non-promise value [1] 1`] = `
"<dim>expect(<red>received</><dim>).rejects.toBeDefined(<dim>)

<red>received</> value must be a Promise.
Received:
array: <red>[1]</>"
`;

exports[`.rejects fails non-promise value [Function anonymous] 1`] = `
"<dim>expect(<red>received</><dim>).rejects.toBeDefined(<dim>)

<red>received</> value must be a Promise.
Received:
function: <red>[Function anonymous]</>"
`;

exports[`.rejects fails non-promise value {"a": 1} 1`] = `
"<dim>expect(<red>received</><dim>).rejects.toBeDefined(<dim>)

<red>received</> value must be a Promise.
Received:
object: <red>{\\"a\\": 1}</>"
`;

exports[`.rejects fails non-promise value 4 1`] = `
"<dim>expect(<red>received</><dim>).rejects.toBeDefined(<dim>)

<red>received</> value must be a Promise.
Received:
number: <red>4</>"
`;

exports[`.rejects fails non-promise value null 1`] = `
"<dim>expect(<red>received</><dim>).rejects.toBeDefined(<dim>)

<red>received</> value must be a Promise.
Received: <red>null</>"
`;

exports[`.rejects fails non-promise value true 1`] = `
"<dim>expect(<red>received</><dim>).rejects.toBeDefined(<dim>)

<red>received</> value must be a Promise.
Received:
boolean: <red>true</>"
`;

exports[`.rejects fails non-promise value undefined 1`] = `
"<dim>expect(<red>received</><dim>).rejects.toBeDefined(<dim>)

<red>received</> value must be a Promise.
Received: <red>undefined</>"
`;

exports[`.resolves fails for promise that rejects 1`] = `
"<dim>expect(<red>received</><dim>).resolves.toBe(<dim>)

Expected <red>received</> Promise to resolve, instead it rejected to value
<red>undefined</>"
`;

exports[`.resolves fails non-promise value "a" 1`] = `
"<dim>expect(<red>received</><dim>).resolves.toBeDefined(<dim>)

<red>received</> value must be a Promise.
Received:
string: <red>\\"a\\"</>"
`;

exports[`.resolves fails non-promise value [1] 1`] = `
"<dim>expect(<red>received</><dim>).resolves.toBeDefined(<dim>)

<red>received</> value must be a Promise.
Received:
array: <red>[1]</>"
`;

exports[`.resolves fails non-promise value [Function anonymous] 1`] = `
"<dim>expect(<red>received</><dim>).resolves.toBeDefined(<dim>)

<red>received</> value must be a Promise.
Received:
function: <red>[Function anonymous]</>"
`;

exports[`.resolves fails non-promise value {"a": 1} 1`] = `
"<dim>expect(<red>received</><dim>).resolves.toBeDefined(<dim>)

<red>received</> value must be a Promise.
Received:
object: <red>{\\"a\\": 1}</>"
`;

exports[`.resolves fails non-promise value 4 1`] = `
"<dim>expect(<red>received</><dim>).resolves.toBeDefined(<dim>)

<red>received</> value must be a Promise.
Received:
number: <red>4</>"
`;

exports[`.resolves fails non-promise value null 1`] = `
"<dim>expect(<red>received</><dim>).resolves.toBeDefined(<dim>)

<red>received</> value must be a Promise.
Received: <red>null</>"
`;

exports[`.resolves fails non-promise value true 1`] = `
"<dim>expect(<red>received</><dim>).resolves.toBeDefined(<dim>)

<red>received</> value must be a Promise.
Received:
boolean: <red>true</>"
`;

exports[`.resolves fails non-promise value undefined 1`] = `
"<dim>expect(<red>received</><dim>).resolves.toBeDefined(<dim>)

<red>received</> value must be a Promise.
Received: <red>undefined</>"
`;

exports[`.toBe() does not crash on circular references 1`] = `
"<dim>expect(<red>received</><dim>).toBe(<green>expected</><dim>)

Expand Down
89 changes: 89 additions & 0 deletions packages/jest-matchers/src/__tests__/matchers-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down
83 changes: 82 additions & 1 deletion packages/jest-matchers/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import type {
MatchersObject,
RawMatcherFn,
ThrowingMatcherFn,
PromiseMatcherFn,
} from 'types/Matchers';

const matchers = require('./matchers');
Expand All @@ -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: {
Expand All @@ -56,14 +63,29 @@ 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(
allMatchers[name],
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;
Expand All @@ -84,6 +106,65 @@ const getMessage = message => {
return message;
};

const makeResolveMatcher = (
matcherName: string,
matcher: RawMatcherFn,
isNot: boolean,
actual: Promise<any>
): 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<any>
): 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,
Expand Down
9 changes: 9 additions & 0 deletions types/Matchers.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export type RawMatcherFn = (
) => ExpectationResult;

export type ThrowingMatcherFn = (actual: any) => void;
export type PromiseMatcherFn = (actual: any) => Promise<void>;
export type MatcherContext = {isNot: boolean};
export type MatcherState = {
assertionCalls?: number,
Expand All @@ -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},
};

0 comments on commit b49e4bf

Please sign in to comment.