From 1b40d3f86da6c29ec6824bc98b99a5d0a16bbdd5 Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Tue, 19 Dec 2017 13:29:37 -0800 Subject: [PATCH 1/4] Expose JestAssertionError to custom matchers --- packages/expect/src/index.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/expect/src/index.js b/packages/expect/src/index.js index 832eca8d4e52..c9b1cf960b3f 100644 --- a/packages/expect/src/index.js +++ b/packages/expect/src/index.js @@ -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); From 97b7e3d3dec63e121acac96e3a923e9c36bf05c6 Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Tue, 19 Dec 2017 13:36:00 -0800 Subject: [PATCH 2/4] Updated CHANGELOG --- CHANGELOG.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5e4503e4dfeb..05821aa5da12 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 From 8aeb67d3b760f966b65434cc43831d41204b835b Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Tue, 19 Dec 2017 15:39:18 -0800 Subject: [PATCH 3/4] Added test for throwing expect.JestAssertionError --- .../expect/src/__tests__/stacktrace.test.js | 29 +++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/packages/expect/src/__tests__/stacktrace.test.js b/packages/expect/src/__tests__/stacktrace.test.js index 0d054206cc60..bf13312394a6 100644 --- a/packages/expect/src/__tests__/stacktrace.test.js +++ b/packages/expect/src/__tests__/stacktrace.test.js @@ -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'); } }); @@ -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'); } }); From 9f64ea7e73441343154159a027e9669ed4fadf96 Mon Sep 17 00:00:00 2001 From: Simen Bekkhus Date: Wed, 20 Dec 2017 08:20:39 +0100 Subject: [PATCH 4/4] test: add integration test --- .../__snapshots__/failures.test.js.snap | 41 +++++++++++++ integration_tests/__tests__/failures.test.js | 6 ++ .../failures/__tests__/custom_matcher.test.js | 60 +++++++++++++++++++ 3 files changed, 107 insertions(+) create mode 100644 integration_tests/failures/__tests__/custom_matcher.test.js diff --git a/integration_tests/__tests__/__snapshots__/failures.test.js.snap b/integration_tests/__tests__/__snapshots__/failures.test.js.snap index 77081dc783e5..d16edae7558b 100644 --- a/integration_tests/__tests__/__snapshots__/failures.test.js.snap +++ b/integration_tests/__tests__/__snapshots__/failures.test.js.snap @@ -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 diff --git a/integration_tests/__tests__/failures.test.js b/integration_tests/__tests__/failures.test.js index c8bc3f421136..265fa80df269 100644 --- a/integration_tests/__tests__/failures.test.js +++ b/integration_tests/__tests__/failures.test.js @@ -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(); +}); diff --git a/integration_tests/failures/__tests__/custom_matcher.test.js b/integration_tests/failures/__tests__/custom_matcher.test.js new file mode 100644 index 000000000000..82c0c463ba90 --- /dev/null +++ b/integration_tests/failures/__tests__/custom_matcher.test.js @@ -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'); + }); +});