Skip to content

Commit b49e4bf

Browse files
excitement-engineercpojer
authored andcommitted
Add promise support to all the matchers (#3068)
* 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.
1 parent e45571c commit b49e4bf

File tree

4 files changed

+318
-1
lines changed

4 files changed

+318
-1
lines changed

packages/jest-matchers/src/__tests__/__snapshots__/matchers-test.js.snap

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,143 @@
11
// Jest Snapshot v1, https://goo.gl/fbAQLP
22

3+
exports[`.rejects fails for promise that resolves 1`] = `
4+
"<dim>expect(<red>received</><dim>).rejects.toBe(<dim>)
5+
6+
Expected <red>received</> Promise to reject, instead it resolved to value
7+
<red>4</>"
8+
`;
9+
10+
exports[`.rejects fails non-promise value "a" 1`] = `
11+
"<dim>expect(<red>received</><dim>).rejects.toBeDefined(<dim>)
12+
13+
<red>received</> value must be a Promise.
14+
Received:
15+
string: <red>\\"a\\"</>"
16+
`;
17+
18+
exports[`.rejects fails non-promise value [1] 1`] = `
19+
"<dim>expect(<red>received</><dim>).rejects.toBeDefined(<dim>)
20+
21+
<red>received</> value must be a Promise.
22+
Received:
23+
array: <red>[1]</>"
24+
`;
25+
26+
exports[`.rejects fails non-promise value [Function anonymous] 1`] = `
27+
"<dim>expect(<red>received</><dim>).rejects.toBeDefined(<dim>)
28+
29+
<red>received</> value must be a Promise.
30+
Received:
31+
function: <red>[Function anonymous]</>"
32+
`;
33+
34+
exports[`.rejects fails non-promise value {"a": 1} 1`] = `
35+
"<dim>expect(<red>received</><dim>).rejects.toBeDefined(<dim>)
36+
37+
<red>received</> value must be a Promise.
38+
Received:
39+
object: <red>{\\"a\\": 1}</>"
40+
`;
41+
42+
exports[`.rejects fails non-promise value 4 1`] = `
43+
"<dim>expect(<red>received</><dim>).rejects.toBeDefined(<dim>)
44+
45+
<red>received</> value must be a Promise.
46+
Received:
47+
number: <red>4</>"
48+
`;
49+
50+
exports[`.rejects fails non-promise value null 1`] = `
51+
"<dim>expect(<red>received</><dim>).rejects.toBeDefined(<dim>)
52+
53+
<red>received</> value must be a Promise.
54+
Received: <red>null</>"
55+
`;
56+
57+
exports[`.rejects fails non-promise value true 1`] = `
58+
"<dim>expect(<red>received</><dim>).rejects.toBeDefined(<dim>)
59+
60+
<red>received</> value must be a Promise.
61+
Received:
62+
boolean: <red>true</>"
63+
`;
64+
65+
exports[`.rejects fails non-promise value undefined 1`] = `
66+
"<dim>expect(<red>received</><dim>).rejects.toBeDefined(<dim>)
67+
68+
<red>received</> value must be a Promise.
69+
Received: <red>undefined</>"
70+
`;
71+
72+
exports[`.resolves fails for promise that rejects 1`] = `
73+
"<dim>expect(<red>received</><dim>).resolves.toBe(<dim>)
74+
75+
Expected <red>received</> Promise to resolve, instead it rejected to value
76+
<red>undefined</>"
77+
`;
78+
79+
exports[`.resolves fails non-promise value "a" 1`] = `
80+
"<dim>expect(<red>received</><dim>).resolves.toBeDefined(<dim>)
81+
82+
<red>received</> value must be a Promise.
83+
Received:
84+
string: <red>\\"a\\"</>"
85+
`;
86+
87+
exports[`.resolves fails non-promise value [1] 1`] = `
88+
"<dim>expect(<red>received</><dim>).resolves.toBeDefined(<dim>)
89+
90+
<red>received</> value must be a Promise.
91+
Received:
92+
array: <red>[1]</>"
93+
`;
94+
95+
exports[`.resolves fails non-promise value [Function anonymous] 1`] = `
96+
"<dim>expect(<red>received</><dim>).resolves.toBeDefined(<dim>)
97+
98+
<red>received</> value must be a Promise.
99+
Received:
100+
function: <red>[Function anonymous]</>"
101+
`;
102+
103+
exports[`.resolves fails non-promise value {"a": 1} 1`] = `
104+
"<dim>expect(<red>received</><dim>).resolves.toBeDefined(<dim>)
105+
106+
<red>received</> value must be a Promise.
107+
Received:
108+
object: <red>{\\"a\\": 1}</>"
109+
`;
110+
111+
exports[`.resolves fails non-promise value 4 1`] = `
112+
"<dim>expect(<red>received</><dim>).resolves.toBeDefined(<dim>)
113+
114+
<red>received</> value must be a Promise.
115+
Received:
116+
number: <red>4</>"
117+
`;
118+
119+
exports[`.resolves fails non-promise value null 1`] = `
120+
"<dim>expect(<red>received</><dim>).resolves.toBeDefined(<dim>)
121+
122+
<red>received</> value must be a Promise.
123+
Received: <red>null</>"
124+
`;
125+
126+
exports[`.resolves fails non-promise value true 1`] = `
127+
"<dim>expect(<red>received</><dim>).resolves.toBeDefined(<dim>)
128+
129+
<red>received</> value must be a Promise.
130+
Received:
131+
boolean: <red>true</>"
132+
`;
133+
134+
exports[`.resolves fails non-promise value undefined 1`] = `
135+
"<dim>expect(<red>received</><dim>).resolves.toBeDefined(<dim>)
136+
137+
<red>received</> value must be a Promise.
138+
Received: <red>undefined</>"
139+
`;
140+
3141
exports[`.toBe() does not crash on circular references 1`] = `
4142
"<dim>expect(<red>received</><dim>).toBe(<green>expected</><dim>)
5143

packages/jest-matchers/src/__tests__/matchers-test.js

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,95 @@
1414
const jestExpect = require('../');
1515
const {stringify} = require('jest-matcher-utils');
1616

17+
describe('.rejects', () => {
18+
it('should reject', async () => {
19+
await jestExpect(Promise.reject(4)).rejects.toBe(4);
20+
await jestExpect(Promise.reject(4)).rejects.not.toBe(5);
21+
await jestExpect(Promise.reject(4.2)).rejects.toBeCloseTo(4.2, 5);
22+
await jestExpect(Promise.reject((3))).rejects.not.toBeCloseTo(4.2, 5);
23+
await jestExpect(Promise.reject({a: 1, b: 2})).rejects.toMatchObject({a: 1});
24+
await jestExpect(Promise.reject({a: 1, b: 2})).rejects.not.toMatchObject({c: 1});
25+
await jestExpect(Promise.reject(() => {throw new Error();})).rejects.toThrow();
26+
});
27+
28+
[
29+
4,
30+
[1],
31+
{a: 1},
32+
'a',
33+
true,
34+
null,
35+
undefined,
36+
() => {},
37+
].forEach(value => {
38+
it(`fails non-promise value ${stringify(value)}`, async () => {
39+
let error;
40+
try {
41+
await jestExpect(value).rejects.toBeDefined();
42+
} catch (e) {
43+
error = e;
44+
}
45+
expect(error).toBeDefined();
46+
expect(error.message).toMatchSnapshot();
47+
});
48+
});
49+
50+
it('fails for promise that resolves', async () => {
51+
let error;
52+
try {
53+
await jestExpect(Promise.resolve(4)).rejects.toBe(4);
54+
} catch (e) {
55+
error = e;
56+
}
57+
expect(error).toBeDefined();
58+
expect(error.message).toMatchSnapshot();
59+
});
60+
});
61+
62+
describe('.resolves', () => {
63+
it('should resolve', async () => {
64+
await jestExpect(Promise.resolve(4)).resolves.toBe(4);
65+
await jestExpect(Promise.resolve(4)).resolves.not.toBe(5);
66+
await jestExpect(Promise.resolve(4.2)).resolves.toBeCloseTo(4.2, 5);
67+
await jestExpect(Promise.resolve((3))).resolves.not.toBeCloseTo(4.2, 5);
68+
await jestExpect(Promise.resolve({a: 1, b: 2})).resolves.toMatchObject({a: 1});
69+
await jestExpect(Promise.resolve({a: 1, b: 2})).resolves.not.toMatchObject({c: 1});
70+
await jestExpect(Promise.resolve(() => {throw new Error();})).resolves.toThrow();
71+
});
72+
73+
[
74+
4,
75+
[1],
76+
{a: 1},
77+
'a',
78+
true,
79+
null,
80+
undefined,
81+
() => {},
82+
].forEach(value => {
83+
it(`fails non-promise value ${stringify(value)}`, async () => {
84+
let error;
85+
try {
86+
await jestExpect(value).resolves.toBeDefined();
87+
} catch (e) {
88+
error = e;
89+
}
90+
expect(error).toBeDefined();
91+
expect(error.message).toMatchSnapshot();
92+
});
93+
});
94+
95+
it('fails for promise that rejects', async () => {
96+
let error;
97+
try {
98+
await jestExpect(Promise.reject(4)).resolves.toBe(4);
99+
} catch (e) {
100+
error = e;
101+
}
102+
expect(error).toBeDefined();
103+
expect(error.message).toMatchSnapshot();
104+
});
105+
});
17106
describe('.toBe()', () => {
18107
it('does not throw', () => {
19108
jestExpect('a').not.toBe('b');

packages/jest-matchers/src/index.js

Lines changed: 82 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import type {
1919
MatchersObject,
2020
RawMatcherFn,
2121
ThrowingMatcherFn,
22+
PromiseMatcherFn,
2223
} from 'types/Matchers';
2324

2425
const matchers = require('./matchers');
@@ -41,6 +42,12 @@ class JestAssertionError extends Error {
4142
matcherResult: any;
4243
}
4344

45+
const isPromise = obj => {
46+
return !!obj &&
47+
(typeof obj === 'object' || typeof obj === 'function') &&
48+
typeof obj.then === 'function';
49+
};
50+
4451
if (!global[GLOBAL_STATE]) {
4552
Object.defineProperty(global, GLOBAL_STATE, {
4653
value: {
@@ -56,14 +63,29 @@ if (!global[GLOBAL_STATE]) {
5663

5764
const expect: Expect = (actual: any): ExpectationObject => {
5865
const allMatchers = global[GLOBAL_STATE].matchers;
59-
const expectation = {not: {}};
66+
const expectation = {
67+
not: {},
68+
rejects: {not: {}},
69+
resolves: {not: {}},
70+
};
71+
6072
Object.keys(allMatchers).forEach(name => {
6173
expectation[name] = makeThrowingMatcher(allMatchers[name], false, actual);
6274
expectation.not[name] = makeThrowingMatcher(
6375
allMatchers[name],
6476
true,
6577
actual,
6678
);
79+
80+
expectation.resolves[name] =
81+
makeResolveMatcher(name, allMatchers[name], false, actual);
82+
expectation.resolves.not[name] =
83+
makeResolveMatcher(name, allMatchers[name], true, actual);
84+
85+
expectation.rejects[name] =
86+
makeRejectMatcher(name, allMatchers[name], false, actual);
87+
expectation.rejects.not[name] =
88+
makeRejectMatcher(name, allMatchers[name], true, actual);
6789
});
6890

6991
return expectation;
@@ -84,6 +106,65 @@ const getMessage = message => {
84106
return message;
85107
};
86108

109+
const makeResolveMatcher = (
110+
matcherName: string,
111+
matcher: RawMatcherFn,
112+
isNot: boolean,
113+
actual: Promise<any>
114+
): PromiseMatcherFn => async (...args) => {
115+
const matcherStatement = `.resolves.${isNot ? 'not.' : ''}${matcherName}`;
116+
if (!isPromise(actual)) {
117+
throw new JestAssertionError(
118+
utils.matcherHint(matcherStatement, 'received', '') + '\n\n' +
119+
`${utils.RECEIVED_COLOR('received')} value must be a Promise.\n` +
120+
utils.printWithType('Received', actual, utils.printReceived),
121+
);
122+
}
123+
124+
let result;
125+
try {
126+
result = await actual;
127+
} catch (e) {
128+
throw new JestAssertionError(
129+
utils.matcherHint(matcherStatement, 'received', '') + '\n\n' +
130+
`Expected ${utils.RECEIVED_COLOR('received')} Promise to resolve, ` +
131+
'instead it rejected to value\n' +
132+
` ${utils.printReceived(result)}`
133+
);
134+
}
135+
return makeThrowingMatcher(matcher, isNot, result).apply(null, args);
136+
};
137+
138+
const makeRejectMatcher = (
139+
matcherName: string,
140+
matcher: RawMatcherFn,
141+
isNot: boolean,
142+
actual: Promise<any>
143+
): PromiseMatcherFn => async (...args) => {
144+
const matcherStatement = `.rejects.${isNot ? 'not.' : ''}${matcherName}`;
145+
if (!isPromise(actual)) {
146+
throw new JestAssertionError(
147+
utils.matcherHint(matcherStatement, 'received', '') + '\n\n' +
148+
`${utils.RECEIVED_COLOR('received')} value must be a Promise.\n` +
149+
utils.printWithType('Received', actual, utils.printReceived),
150+
);
151+
}
152+
153+
let result;
154+
try {
155+
result = await actual;
156+
} catch (e) {
157+
return makeThrowingMatcher(matcher, isNot, e).apply(null, args);
158+
}
159+
160+
throw new JestAssertionError(
161+
utils.matcherHint(matcherStatement, 'received', '') + '\n\n' +
162+
`Expected ${utils.RECEIVED_COLOR('received')} Promise to reject, ` +
163+
'instead it resolved to value\n' +
164+
` ${utils.printReceived(result)}`
165+
);
166+
};
167+
87168
const makeThrowingMatcher = (
88169
matcher: RawMatcherFn,
89170
isNot: boolean,

types/Matchers.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ export type RawMatcherFn = (
2323
) => ExpectationResult;
2424

2525
export type ThrowingMatcherFn = (actual: any) => void;
26+
export type PromiseMatcherFn = (actual: any) => Promise<void>;
2627
export type MatcherContext = {isNot: boolean};
2728
export type MatcherState = {
2829
assertionCalls?: number,
@@ -33,6 +34,14 @@ export type MatcherState = {
3334
export type MatchersObject = {[id:string]: RawMatcherFn};
3435
export type Expect = (expected: any) => ExpectationObject;
3536
export type ExpectationObject = {
37+
resolves: {
38+
[id: string]: PromiseMatcherFn,
39+
not: {[id: string]: PromiseMatcherFn},
40+
},
41+
rejects: {
42+
[id: string]: PromiseMatcherFn,
43+
not: {[id: string]: PromiseMatcherFn},
44+
},
3645
[id: string]: ThrowingMatcherFn,
3746
not: {[id: string]: ThrowingMatcherFn},
3847
};

0 commit comments

Comments
 (0)