Skip to content

Commit 84abf60

Browse files
bvaughncpojer
authored andcommitted
[Discussion] Expect only overrides error stack for built-in matchers (#5162)
* Expect only overrides error stack for built-in matchers * Fixed naggy lint issue * Replaced for...of with Object.keys().forEach() * Removed JestAssertionError export (as it is no longer needed) * Added custom matcher test * Adjusted custom_matcher test to use prettified, non-absolute stack * test: refactor integration test * update with correct snapshot * test: skip on windows * Clarified comment * Further clarified comment * test: fix test for node 6 * test: move custom matcher stack to separate file to be able to skip it on windows * Revert "test: fix test for node 6" This reverts commit f902b4b. * test: really fix node 6 * docs: add package prefix to changelog also format all markdown files * Reverted unrelated Markdown changes that had made their way into the branch * Replaced '__jestInternal' string attribute with a Symbol * Updated snapshot
1 parent 9cbc26b commit 84abf60

File tree

9 files changed

+169
-8
lines changed

9 files changed

+169
-8
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
Windows platform. ([#5161](https://github.com/facebook/jest/pull/5161))
1010
* `[jest-regex-util]` Fix breaking change in `--testPathPattern`
1111
([#5230](https://github.com/facebook/jest/pull/5230))
12+
* `[expect]` Do not override `Error` stack (with `Error.captureStackTrace`) for
13+
custom matchers. ([#5162](https://github.com/facebook/jest/pull/5162))
1214

1315
### Features
1416

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
// Jest Snapshot v1, https://goo.gl/fbAQLP
2+
3+
exports[`works with custom matchers 1`] = `
4+
"FAIL __tests__/custom_matcher.test.js
5+
Custom matcher
6+
✓ passes
7+
✓ fails
8+
✕ preserves error stack
9+
10+
● Custom matcher › preserves error stack
11+
12+
qux
13+
14+
43 | const bar = () => baz();
15+
44 | const baz = () => {
16+
> 45 | throw Error('qux');
17+
46 | };
18+
47 |
19+
48 | // This expecation fails due to an error we throw (intentionally)
20+
21+
at __tests__/custom_matcher.test.js:45:13
22+
at __tests__/custom_matcher.test.js:43:23
23+
at __tests__/custom_matcher.test.js:42:23
24+
at __tests__/custom_matcher.test.js:52:7
25+
at __tests__/custom_matcher.test.js:11:18
26+
at __tests__/custom_matcher.test.js:53:8
27+
28+
"
29+
`;

integration_tests/__tests__/__snapshots__/failures.test.js.snap

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ exports[`works with async failures 1`] = `
118118
+ \\"foo\\": \\"bar\\",
119119
}
120120
121-
at ../../packages/expect/build/index.js:155:54
121+
at ../../packages/expect/build/index.js:156:54
122122
123123
"
124124
`;
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
/**
2+
* Copyright (c) 2014-present, Facebook, Inc. All rights reserved.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @flow
8+
*/
9+
'use strict';
10+
11+
const runJest = require('../runJest');
12+
const {extractSummary} = require('../utils');
13+
const skipOnWindows = require('../../scripts/skip_on_windows');
14+
15+
skipOnWindows.suite();
16+
17+
test('works with custom matchers', () => {
18+
const {stderr} = runJest('custom_matcher_stack_trace');
19+
20+
let {rest} = extractSummary(stderr);
21+
22+
rest = rest
23+
.split('\n')
24+
.filter(line => line.indexOf('at Error (native)') < 0)
25+
.join('\n');
26+
27+
expect(rest).toMatchSnapshot();
28+
});
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
/**
2+
* Copyright (c) 2014-present, Facebook, Inc. All rights reserved.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
*/
8+
'use strict';
9+
10+
function toCustomMatch(callback, expectation) {
11+
const actual = callback();
12+
13+
if (actual !== expectation) {
14+
return {
15+
message: () => `Expected "${expectation}" but got "${actual}"`,
16+
pass: false,
17+
};
18+
} else {
19+
return {pass: true};
20+
}
21+
}
22+
23+
expect.extend({
24+
toCustomMatch,
25+
});
26+
27+
describe('Custom matcher', () => {
28+
it('passes', () => {
29+
// This expectation should pass
30+
expect(() => 'foo').toCustomMatch('foo');
31+
});
32+
33+
it('fails', () => {
34+
expect(() => {
35+
// This expectation should fail,
36+
// Which is why it's wrapped in a .toThrow() block.
37+
expect(() => 'foo').toCustomMatch('bar');
38+
}).toThrow();
39+
});
40+
41+
it('preserves error stack', () => {
42+
const foo = () => bar();
43+
const bar = () => baz();
44+
const baz = () => {
45+
throw Error('qux');
46+
};
47+
48+
// This expecation fails due to an error we throw (intentionally)
49+
// The stack trace should point to the line that throws the error though,
50+
// Not to the line that calls the matcher.
51+
expect(() => {
52+
foo();
53+
}).toCustomMatch('test');
54+
});
55+
});
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"jest": {
3+
"testEnvironment": "node"
4+
}
5+
}

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

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,18 @@
99
const jestExpect = require('../');
1010

1111
jestExpect.extend({
12+
toCustomMatch(callback, expectation) {
13+
const actual = callback();
14+
15+
if (actual !== expectation) {
16+
return {
17+
message: () => `Expected "${expectation}" but got "${actual}"`,
18+
pass: false,
19+
};
20+
}
21+
22+
return {pass: true};
23+
},
1224
toMatchPredicate(received, argument) {
1325
argument(received);
1426
return {
@@ -22,7 +34,7 @@ it('stack trace points to correct location when using matchers', () => {
2234
try {
2335
jestExpect(true).toBe(false);
2436
} catch (error) {
25-
expect(error.stack).toContain('stacktrace.test.js:23');
37+
expect(error.stack).toContain('stacktrace.test.js:35');
2638
}
2739
});
2840

@@ -32,6 +44,22 @@ it('stack trace points to correct location when using nested matchers', () => {
3244
jestExpect(value).toBe(false);
3345
});
3446
} catch (error) {
35-
expect(error.stack).toContain('stacktrace.test.js:32');
47+
expect(error.stack).toContain('stacktrace.test.js:44');
48+
}
49+
});
50+
51+
it('stack trace points to correct location when throwing from a custom matcher', () => {
52+
try {
53+
jestExpect(() => {
54+
const foo = () => bar();
55+
const bar = () => baz();
56+
const baz = () => {
57+
throw new Error('Expected');
58+
};
59+
60+
foo();
61+
}).toCustomMatch('bar');
62+
} catch (error) {
63+
expect(error.stack).toContain('stacktrace.test.js:57');
3664
}
3765
});

packages/expect/src/index.js

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import {
3434
stringMatching,
3535
} from './asymmetric_matchers';
3636
import {
37+
INTERNAL_MATCHER_FLAG,
3738
getState,
3839
setState,
3940
getMatchers,
@@ -214,6 +215,7 @@ const makeThrowingMatcher = (
214215
result = matcher.apply(matcherContext, [actual].concat(args));
215216
} catch (error) {
216217
if (
218+
matcher[INTERNAL_MATCHER_FLAG] === true &&
217219
!(error instanceof JestAssertionError) &&
218220
error.name !== 'PrettyFormatPluginError' &&
219221
// Guard for some environments (browsers) that do not support this feature.
@@ -252,7 +254,8 @@ const makeThrowingMatcher = (
252254
};
253255
};
254256

255-
expect.extend = (matchers: MatchersObject): void => setMatchers(matchers);
257+
expect.extend = (matchers: MatchersObject): void =>
258+
setMatchers(matchers, false);
256259

257260
expect.anything = anything;
258261
expect.any = any;
@@ -280,9 +283,9 @@ const _validateResult = result => {
280283
};
281284

282285
// add default jest matchers
283-
expect.extend(matchers);
284-
expect.extend(spyMatchers);
285-
expect.extend(toThrowMatchers);
286+
setMatchers(matchers, true);
287+
setMatchers(spyMatchers, true);
288+
setMatchers(toThrowMatchers, true);
286289

287290
expect.addSnapshotSerializer = () => void 0;
288291
expect.assertions = (expected: number) => {

packages/expect/src/jest_matchers_object.js

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@ import type {MatchersObject} from 'types/Matchers';
1313
// the state, that can hold matcher specific values that change over time.
1414
const JEST_MATCHERS_OBJECT = Symbol.for('$$jest-matchers-object');
1515

16+
// Notes a built-in/internal Jest matcher.
17+
// Jest may override the stack trace of Errors thrown by internal matchers.
18+
export const INTERNAL_MATCHER_FLAG = Symbol.for('$$jest-internal-matcher');
19+
1620
if (!global[JEST_MATCHERS_OBJECT]) {
1721
Object.defineProperty(global, JEST_MATCHERS_OBJECT, {
1822
value: {
@@ -35,6 +39,13 @@ export const setState = (state: Object) => {
3539

3640
export const getMatchers = () => global[JEST_MATCHERS_OBJECT].matchers;
3741

38-
export const setMatchers = (matchers: MatchersObject) => {
42+
export const setMatchers = (matchers: MatchersObject, isInternal: boolean) => {
43+
Object.keys(matchers).forEach(key => {
44+
const matcher = matchers[key];
45+
Object.defineProperty(matcher, INTERNAL_MATCHER_FLAG, {
46+
value: isInternal,
47+
});
48+
});
49+
3950
Object.assign(global[JEST_MATCHERS_OBJECT].matchers, matchers);
4051
};

0 commit comments

Comments
 (0)