Skip to content

Commit 27a1dc6

Browse files
peterdaniscpojer
authored andcommitted
[WIP] Make toThrow matcher pass only if Error-like object is returned from promises (#5670)
* Fix rejects.not matcher * Check if resolved/rejected value is Error instance * Add test * Changelog * Change instanceof check * Add additional tests & update snaps * Add isError fn * Change err msg to also test partial string match * Move isError to expect utils * Add isError test * Update changelog
1 parent aef82a2 commit 27a1dc6

File tree

7 files changed

+231
-26
lines changed

7 files changed

+231
-26
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@
88
([#5558](https://github.com/facebook/jest/pull/5558))
99
* `[jest-matcher-utils]` Add `isNot` option to `matcherHint` function
1010
([#5512](https://github.com/facebook/jest/pull/5512))
11+
* `[expect]` Make toThrow matcher pass only if Error object is returned from
12+
promises ([#5670](https://github.com/facebook/jest/pull/5670))
13+
* `[expect]` Add isError to utils
14+
([#5670](https://github.com/facebook/jest/pull/5670))
1115

1216
### Fixes
1317

@@ -21,6 +25,8 @@
2125
([#5692](https://github.com/facebook/jest/pull/5692))
2226
* `[jest-cli]` Fix update snapshot issue when using watchAll
2327
([#5696](https://github.com/facebook/jest/pull/5696))
28+
* `[expect]` Fix rejects.not matcher
29+
([#5670](https://github.com/facebook/jest/pull/5670))
2430

2531
### Chore & Maintenance
2632

packages/expect/src/__tests__/__snapshots__/to_throw_matchers.test.js.snap

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,32 @@ Got:
4343
string: <green>\\"111\\"</>"
4444
`;
4545
46+
exports[`.toThrow() promise/async throws if Error-like object is returned did not throw at all 1`] = `
47+
[Error: expect(function).toThrow(undefined)
48+
49+
Expected the function to throw an error.
50+
But it didn't throw anything.]
51+
`;
52+
53+
exports[`.toThrow() promise/async throws if Error-like object is returned threw, but class did not match 1`] = `
54+
[Error: expect(function).toThrow(type)
55+
56+
Expected the function to throw an error of type:
57+
"Err2"
58+
Instead, it threw:
59+
 Error 
60+
 at jestExpect (packages/expect/src/__tests__/toThrowMatchers-test.js:24:74)]
61+
`;
62+
63+
exports[`.toThrow() promise/async throws if Error-like object is returned threw, but should not have 1`] = `
64+
[Error: expect(function).not.toThrow()
65+
66+
Expected the function not to throw an error.
67+
Instead, it threw:
68+
 Error 
69+
 at jestExpect (packages/expect/src/__tests__/toThrowMatchers-test.js:24:74)]
70+
`;
71+
4672
exports[`.toThrow() regexp did not throw at all 1`] = `
4773
"<dim>expect(</><red>function</><dim>).toThrow(</><green>regexp</><dim>)</>
4874
@@ -142,6 +168,32 @@ Got:
142168
string: <green>\\"111\\"</>"
143169
`;
144170
171+
exports[`.toThrowError() promise/async throws if Error-like object is returned did not throw at all 1`] = `
172+
[Error: expect(function).toThrow(undefined)
173+
174+
Expected the function to throw an error.
175+
But it didn't throw anything.]
176+
`;
177+
178+
exports[`.toThrowError() promise/async throws if Error-like object is returned threw, but class did not match 1`] = `
179+
[Error: expect(function).toThrow(type)
180+
181+
Expected the function to throw an error of type:
182+
"Err2"
183+
Instead, it threw:
184+
 Error 
185+
 at jestExpect (packages/expect/src/__tests__/toThrowMatchers-test.js:24:74)]
186+
`;
187+
188+
exports[`.toThrowError() promise/async throws if Error-like object is returned threw, but should not have 1`] = `
189+
[Error: expect(function).not.toThrow()
190+
191+
Expected the function not to throw an error.
192+
Instead, it threw:
193+
 Error 
194+
 at jestExpect (packages/expect/src/__tests__/toThrowMatchers-test.js:24:74)]
195+
`;
196+
145197
exports[`.toThrowError() regexp did not throw at all 1`] = `
146198
"<dim>expect(</><red>function</><dim>).toThrowError(</><green>regexp</><dim>)</>
147199
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
/**
2+
* @jest-environment jsdom
3+
*/
4+
/* eslint-env browser */
5+
6+
import {isError} from '../utils';
7+
8+
// Copied from https://github.com/graingert/angular.js/blob/a43574052e9775cbc1d7dd8a086752c979b0f020/test/AngularSpec.js#L1883
9+
describe('isError', () => {
10+
function testErrorFromDifferentContext(createError) {
11+
const iframe = document.createElement('iframe');
12+
document.body.appendChild(iframe);
13+
try {
14+
const error = createError(iframe.contentWindow);
15+
expect(isError(error)).toBe(true);
16+
} finally {
17+
iframe.parentElement.removeChild(iframe);
18+
}
19+
}
20+
21+
it('should not assume objects are errors', () => {
22+
const fakeError = {message: 'A fake error', stack: 'no stack here'};
23+
expect(isError(fakeError)).toBe(false);
24+
});
25+
26+
it('should detect simple error instances', () => {
27+
expect(isError(new Error())).toBe(true);
28+
});
29+
30+
it('should detect errors from another context', () => {
31+
testErrorFromDifferentContext(win => {
32+
return new win.Error();
33+
});
34+
});
35+
36+
it('should detect DOMException errors from another context', () => {
37+
testErrorFromDifferentContext(win => {
38+
try {
39+
win.document.querySelectorAll('');
40+
} catch (e) {
41+
return e;
42+
}
43+
});
44+
});
45+
});

packages/expect/src/__tests__/to_throw_matchers.test.js

Lines changed: 97 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,9 @@
1111
const jestExpect = require('../');
1212

1313
// Custom Error class because node versions have different stack trace strings.
14-
class Error {
14+
class customError extends Error {
1515
constructor(message) {
16+
super();
1617
this.message = message;
1718
this.name = 'Error';
1819
this.stack =
@@ -24,23 +25,23 @@ class Error {
2425

2526
['toThrowError', 'toThrow'].forEach(toThrow => {
2627
describe('.' + toThrow + '()', () => {
27-
class Err extends Error {}
28-
class Err2 extends Error {}
28+
class Err extends customError {}
29+
class Err2 extends customError {}
2930

3031
test('to throw or not to throw', () => {
3132
jestExpect(() => {
32-
throw new Error('apple');
33+
throw new customError('apple');
3334
})[toThrow]();
3435
jestExpect(() => {}).not[toThrow]();
3536
});
3637

3738
describe('strings', () => {
3839
it('passes', () => {
3940
jestExpect(() => {
40-
throw new Error('apple');
41+
throw new customError('apple');
4142
})[toThrow]('apple');
4243
jestExpect(() => {
43-
throw new Error('banana');
44+
throw new customError('banana');
4445
}).not[toThrow]('apple');
4546
jestExpect(() => {}).not[toThrow]('apple');
4647
});
@@ -54,7 +55,7 @@ class Error {
5455
test('threw, but message did not match', () => {
5556
expect(() => {
5657
jestExpect(() => {
57-
throw new Error('apple');
58+
throw new customError('apple');
5859
})[toThrow]('banana');
5960
}).toThrowErrorMatchingSnapshot();
6061
});
@@ -68,7 +69,7 @@ class Error {
6869
test('threw, but should not have', () => {
6970
expect(() => {
7071
jestExpect(() => {
71-
throw new Error('apple');
72+
throw new customError('apple');
7273
}).not[toThrow]('apple');
7374
}).toThrowErrorMatchingSnapshot();
7475
});
@@ -77,10 +78,10 @@ class Error {
7778
describe('regexp', () => {
7879
it('passes', () => {
7980
expect(() => {
80-
throw new Error('apple');
81+
throw new customError('apple');
8182
})[toThrow](/apple/);
8283
expect(() => {
83-
throw new Error('banana');
84+
throw new customError('banana');
8485
}).not[toThrow](/apple/);
8586
expect(() => {}).not[toThrow](/apple/);
8687
});
@@ -94,15 +95,15 @@ class Error {
9495
test('threw, but message did not match', () => {
9596
expect(() => {
9697
jestExpect(() => {
97-
throw new Error('apple');
98+
throw new customError('apple');
9899
})[toThrow](/banana/);
99100
}).toThrowErrorMatchingSnapshot();
100101
});
101102

102103
test('threw, but should not have', () => {
103104
expect(() => {
104105
jestExpect(() => {
105-
throw new Error('apple');
106+
throw new customError('apple');
106107
}).not[toThrow](/apple/);
107108
}).toThrowErrorMatchingSnapshot();
108109
});
@@ -115,7 +116,7 @@ class Error {
115116
})[toThrow](Err);
116117
jestExpect(() => {
117118
throw new Err();
118-
})[toThrow](Error);
119+
})[toThrow](customError);
119120
jestExpect(() => {
120121
throw new Err();
121122
}).not[toThrow](Err2);
@@ -145,6 +146,89 @@ class Error {
145146
});
146147
});
147148

149+
describe('promise/async throws if Error-like object is returned', () => {
150+
const asyncFn = async (shouldThrow?: boolean, resolve?: boolean) => {
151+
let err;
152+
if (shouldThrow) {
153+
err = new Err('async apple');
154+
}
155+
if (resolve) {
156+
return await Promise.resolve(err || 'apple');
157+
} else {
158+
return await Promise.reject(err || 'apple');
159+
}
160+
};
161+
162+
test('passes', async () => {
163+
expect.assertions(24);
164+
await jestExpect(Promise.reject(new Error())).rejects[toThrow]();
165+
166+
await jestExpect(asyncFn(true)).rejects[toThrow]();
167+
await jestExpect(asyncFn(true)).rejects[toThrow](Err);
168+
await jestExpect(asyncFn(true)).rejects[toThrow](Error);
169+
await jestExpect(asyncFn(true)).rejects[toThrow]('apple');
170+
await jestExpect(asyncFn(true)).rejects[toThrow](/app/);
171+
172+
await jestExpect(asyncFn(true)).rejects.not[toThrow](Err2);
173+
await jestExpect(asyncFn(true)).rejects.not[toThrow]('banana');
174+
await jestExpect(asyncFn(true)).rejects.not[toThrow](/banana/);
175+
176+
await jestExpect(asyncFn(true, true)).resolves[toThrow]();
177+
178+
await jestExpect(asyncFn(false, true)).resolves.not[toThrow]();
179+
await jestExpect(asyncFn(false, true)).resolves.not[toThrow](Error);
180+
await jestExpect(asyncFn(false, true)).resolves.not[toThrow]('apple');
181+
await jestExpect(asyncFn(false, true)).resolves.not[toThrow](/apple/);
182+
await jestExpect(asyncFn(false, true)).resolves.not[toThrow]('banana');
183+
await jestExpect(asyncFn(false, true)).resolves.not[toThrow](/banana/);
184+
185+
await jestExpect(asyncFn()).rejects.not[toThrow]();
186+
await jestExpect(asyncFn()).rejects.not[toThrow](Error);
187+
await jestExpect(asyncFn()).rejects.not[toThrow]('apple');
188+
await jestExpect(asyncFn()).rejects.not[toThrow](/apple/);
189+
await jestExpect(asyncFn()).rejects.not[toThrow]('banana');
190+
await jestExpect(asyncFn()).rejects.not[toThrow](/banana/);
191+
192+
// Works with nested functions inside promises
193+
await jestExpect(
194+
Promise.reject(() => {
195+
throw new Error();
196+
}),
197+
).rejects[toThrow]();
198+
await jestExpect(Promise.reject(() => {})).rejects.not[toThrow]();
199+
});
200+
201+
test('did not throw at all', async () => {
202+
let err;
203+
try {
204+
await jestExpect(asyncFn()).rejects.toThrow();
205+
} catch (error) {
206+
err = error;
207+
}
208+
expect(err).toMatchSnapshot();
209+
});
210+
211+
test('threw, but class did not match', async () => {
212+
let err;
213+
try {
214+
await jestExpect(asyncFn(true)).rejects.toThrow(Err2);
215+
} catch (error) {
216+
err = error;
217+
}
218+
expect(err).toMatchSnapshot();
219+
});
220+
221+
test('threw, but should not have', async () => {
222+
let err;
223+
try {
224+
await jestExpect(asyncFn(true)).rejects.not.toThrow();
225+
} catch (error) {
226+
err = error;
227+
}
228+
expect(err).toMatchSnapshot();
229+
});
230+
});
231+
148232
test('invalid arguments', () => {
149233
expect(() =>
150234
jestExpect(() => {})[toThrow](111),

packages/expect/src/index.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ const expect = (actual: any, ...rest): ExpectationObject => {
109109
);
110110
expectation.rejects.not[name] = makeRejectMatcher(
111111
name,
112-
matcher,
112+
promiseMatcher,
113113
true,
114114
actual,
115115
);

packages/expect/src/to_throw_matchers.js

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {
2020
printWithType,
2121
} from 'jest-matcher-utils';
2222
import {equals} from './jasmine_utils';
23+
import {isError} from './utils';
2324

2425
export const createMatcher = (matcherName: string, fromPromise?: boolean) => (
2526
actual: Function,
@@ -28,21 +29,24 @@ export const createMatcher = (matcherName: string, fromPromise?: boolean) => (
2829
const value = expected;
2930
let error;
3031

31-
if (fromPromise) {
32+
if (fromPromise && isError(actual)) {
3233
error = actual;
3334
} else {
3435
if (typeof actual !== 'function') {
35-
throw new Error(
36-
matcherHint(matcherName, 'function', getType(value)) +
37-
'\n\n' +
38-
'Received value must be a function, but instead ' +
39-
`"${getType(actual)}" was found`,
40-
);
41-
}
42-
try {
43-
actual();
44-
} catch (e) {
45-
error = e;
36+
if (!fromPromise) {
37+
throw new Error(
38+
matcherHint(matcherName, 'function', getType(value)) +
39+
'\n\n' +
40+
'Received value must be a function, but instead ' +
41+
`"${getType(actual)}" was found`,
42+
);
43+
}
44+
} else {
45+
try {
46+
actual();
47+
} catch (e) {
48+
error = e;
49+
}
4650
}
4751
}
4852

packages/expect/src/utils.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,3 +195,17 @@ export const partition = <T>(
195195

196196
return result;
197197
};
198+
199+
// Copied from https://github.com/graingert/angular.js/blob/a43574052e9775cbc1d7dd8a086752c979b0f020/src/Angular.js#L685-L693
200+
export const isError = (value: any) => {
201+
switch (Object.prototype.toString.call(value)) {
202+
case '[object Error]':
203+
return true;
204+
case '[object Exception]':
205+
return true;
206+
case '[object DOMException]':
207+
return true;
208+
default:
209+
return value instanceof Error;
210+
}
211+
};

0 commit comments

Comments
 (0)