Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Expose JestAssertionError to custom matchers #5138

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
## master

None for now

### Fixes

### Features

* Expose `expect.JestAssertionError` to enable custom matchers to throw errors
with the call stack preserved.
([#5138](https://github.com/facebook/jest/pull/5138))

### Chore & Maintenance

## jest 22.0.1
Expand Down
41 changes: 41 additions & 0 deletions integration_tests/__tests__/__snapshots__/failures.test.js.snap
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,47 @@ exports[`works with async failures 1`] = `
"
`;

exports[`works with custom matchers 1`] = `
"FAIL __tests__/custom_matcher.test.js
Custom matcher
✓ passes
✕ fails
✕ preserves error stack

● Custom matcher › fails

Expected \\"bar\\" but got \\"foo\\"

43 | // This test should fail
44 | it('fails', () => {
> 45 | expect(() => 'foo').toCustomMatch('bar');
46 | });
47 |
48 | // This test fails due to an unrelated/unexpected error

at __tests__/custom_matcher.test.js:45:25

● Custom matcher › preserves error stack

ReferenceError: qux is not defined

52 | const bar = () => baz();
53 | // eslint-disable-next-line no-undef
> 54 | const baz = () => qux();
55 |
56 | expect(() => {
57 | foo();

at __tests__/custom_matcher.test.js:54:23
at __tests__/custom_matcher.test.js:52:23
at __tests__/custom_matcher.test.js:51:23
at __tests__/custom_matcher.test.js:57:7
at __tests__/custom_matcher.test.js:13:20
at __tests__/custom_matcher.test.js:58:8

"
`;

exports[`works with node assert 1`] = `
"FAIL __tests__/node_assertion_error.test.js
✕ assert
Expand Down
6 changes: 6 additions & 0 deletions integration_tests/__tests__/failures.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -119,3 +119,9 @@ test('works with snapshot failures', () => {
result.substring(0, result.indexOf('Snapshot Summary')),
).toMatchSnapshot();
});

test('works with custom matchers', () => {
const {stderr} = runJest(dir, ['custom_matcher.test.js']);

expect(normalizeDots(extractSummary(stderr).rest)).toMatchSnapshot();
});
60 changes: 60 additions & 0 deletions integration_tests/failures/__tests__/custom_matcher.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/**
* Copyright (c) 2014-present, Facebook, Inc. All rights reserved.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @emails oncall+jsinfra
*/
'use strict';

function toCustomMatch(callback, expectation) {
try {
const actual = callback();

if (actual !== expectation) {
return {
message: () => `Expected "${expectation}" but got "${actual}"`,
pass: false,
};
}
} catch (error) {
// Explicitly wrap caught errors to preserve their stack
// Without this, Jest will override stack to point to the matcher
const assertionError = new expect.JestAssertionError();
assertionError.message = error.message;
assertionError.stack = error.stack;
throw assertionError;
}

return {pass: true};
}

expect.extend({
toCustomMatch,
});

describe('Custom matcher', () => {
// This test will pass
it('passes', () => {
expect(() => 'foo').toCustomMatch('foo');
});

// This test should fail
it('fails', () => {
expect(() => 'foo').toCustomMatch('bar');
});

// This test fails due to an unrelated/unexpected error
// It will show a helpful stack trace though
it('preserves error stack', () => {
const foo = () => bar();
const bar = () => baz();
// eslint-disable-next-line no-undef
const baz = () => qux();

expect(() => {
foo();
}).toCustomMatch('test');
});
});
29 changes: 27 additions & 2 deletions packages/expect/src/__tests__/stacktrace.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,22 @@ jestExpect.extend({
pass: true,
};
},
toPreserveErrorCallStack(callback) {
try {
callback();
} catch (error) {
const assertionError = new jestExpect.JestAssertionError(error.message);
assertionError.stack = error.stack;
throw assertionError;
}
},
});

it('stack trace points to correct location when using matchers', () => {
try {
jestExpect(true).toBe(false);
} catch (error) {
expect(error.stack).toContain('stacktrace.test.js:23');
expect(error.stack).toContain('stacktrace.test.js:32');
}
});

Expand All @@ -32,6 +41,22 @@ it('stack trace points to correct location when using nested matchers', () => {
jestExpect(value).toBe(false);
});
} catch (error) {
expect(error.stack).toContain('stacktrace.test.js:32');
expect(error.stack).toContain('stacktrace.test.js:41');
}
});

it('stack trace points to correct location when throwing an instance of JestAssertionError', () => {
try {
jestExpect(() => {
const foo = () => bar();
const bar = () => baz();
const baz = () => {
throw new Error('Expected');
};

foo();
}).toPreserveErrorCallStack();
} catch (error) {
expect(error.stack).toContain('stacktrace.test.js:54');
}
});
4 changes: 4 additions & 0 deletions packages/expect/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -296,4 +296,8 @@ expect.getState = getState;
expect.setState = setState;
expect.extractExpectedAssertionsErrors = extractExpectedAssertionsErrors;

// Expose JestAssertionError for custom matchers
// This enables them to preserve the stack for specific errors
expect.JestAssertionError = JestAssertionError;

module.exports = (expect: Expect);