diff --git a/CHANGELOG.md b/CHANGELOG.md index 013b611055fe..ff05fda5d1e8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,8 @@ * `[pretty-format]` Prevent error in pretty-format for window in jsdom test env ([#4750](https://github.com/facebook/jest/pull/4750)) * `[jest-resolve]` Preserve module identity for symlinks ([#4761](https://github.com/facebook/jest/pull/4761)) * `[jest-config]` Include error message for `preset` json ([#4766](https://github.com/facebook/jest/pull/4766)) +* `[pretty-format]` Throw `PrettyFormatPluginError` if a plugin halts with an exception ([#4787](https://github.com/facebook/jest/pull/4787)) +* `[expect]` Keep the stack trace unchanged when `PrettyFormatPluginError` is thrown by pretty-format ([#4787](https://github.com/facebook/jest/pull/4787)) ### Features * `[jest-environment-jsdom]` [**BREAKING**] Upgrade to JSDOM@11 ([#4770](https://github.com/facebook/jest/pull/4770)) diff --git a/packages/expect/src/index.js b/packages/expect/src/index.js index 8a1f3164f4cc..e43ebb343987 100644 --- a/packages/expect/src/index.js +++ b/packages/expect/src/index.js @@ -197,12 +197,14 @@ const makeThrowingMatcher = ( try { result = matcher.apply(matcherContext, [actual].concat(args)); } catch (error) { - if (!(error instanceof JestAssertionError)) { - // Try to remove this and deeper functions from the stack trace frame. + if ( + !(error instanceof JestAssertionError) && + error.name !== 'PrettyFormatPluginError' && // Guard for some environments (browsers) that do not support this feature. - if (Error.captureStackTrace) { - Error.captureStackTrace(error, throwingMatcher); - } + Error.captureStackTrace + ) { + // Try to remove this and deeper functions from the stack trace frame. + Error.captureStackTrace(error, throwingMatcher); } throw error; } diff --git a/packages/pretty-format/src/__tests__/pretty_format.test.js b/packages/pretty-format/src/__tests__/pretty_format.test.js index 28ea1dd05b94..41a833b0e798 100644 --- a/packages/pretty-format/src/__tests__/pretty_format.test.js +++ b/packages/pretty-format/src/__tests__/pretty_format.test.js @@ -549,6 +549,66 @@ describe('prettyFormat()', () => { }).toThrow(); }); + it('throws PrettyFormatPluginError if test throws an error', () => { + expect.hasAssertions(); + const options = { + plugins: [ + { + print: () => '', + test() { + throw new Error('Where is the error?'); + }, + }, + ], + }; + + try { + prettyFormat('', options); + } catch (error) { + expect(error.name).toBe('PrettyFormatPluginError'); + } + }); + + it('throws PrettyFormatPluginError if print throws an error', () => { + expect.hasAssertions(); + const options = { + plugins: [ + { + print: () => { + throw new Error('Where is the error?'); + }, + test: () => true, + }, + ], + }; + + try { + prettyFormat('', options); + } catch (error) { + expect(error.name).toBe('PrettyFormatPluginError'); + } + }); + + it('throws PrettyFormatPluginError if serialize throws an error', () => { + expect.hasAssertions(); + const options = { + plugins: [ + { + serialize: () => { + throw new Error('Where is the error?'); + }, + test: () => true, + }, + ], + }; + + try { + prettyFormat('', options); + } catch (error) { + expect(error.name).toBe('PrettyFormatPluginError'); + } + }); + it('supports plugins with deeply nested arrays (#24)', () => { const val = [[1, 2], [3, 4]]; expect( diff --git a/packages/pretty-format/src/index.js b/packages/pretty-format/src/index.js index c1df9ac4ebcd..f48206364729 100644 --- a/packages/pretty-format/src/index.js +++ b/packages/pretty-format/src/index.js @@ -48,6 +48,14 @@ const isWindow = val => typeof window !== 'undefined' && val === window; const SYMBOL_REGEXP = /^Symbol\((.*)\)(.*)$/; const NEWLINE_REGEXP = /\n/gi; +class PrettyFormatPluginError extends Error { + constructor(message, stack) { + super(message); + this.stack = stack; + this.name = this.constructor.name; + } +} + function isToStringedArrayType(toStringed: string): boolean { return ( toStringed === '[object Array]' || @@ -246,25 +254,31 @@ function printPlugin( depth: number, refs: Refs, ): string { - const printed = plugin.serialize - ? plugin.serialize(val, config, indentation, depth, refs, printer) - : plugin.print( - val, - valChild => printer(valChild, config, indentation, depth, refs), - str => { - const indentationNext = indentation + config.indent; - return ( - indentationNext + - str.replace(NEWLINE_REGEXP, '\n' + indentationNext) - ); - }, - { - edgeSpacing: config.spacingOuter, - min: config.min, - spacing: config.spacingInner, - }, - config.colors, - ); + let printed; + + try { + printed = plugin.serialize + ? plugin.serialize(val, config, indentation, depth, refs, printer) + : plugin.print( + val, + valChild => printer(valChild, config, indentation, depth, refs), + str => { + const indentationNext = indentation + config.indent; + return ( + indentationNext + + str.replace(NEWLINE_REGEXP, '\n' + indentationNext) + ); + }, + { + edgeSpacing: config.spacingOuter, + min: config.min, + spacing: config.spacingInner, + }, + config.colors, + ); + } catch (error) { + throw new PrettyFormatPluginError(error.message, error.stack); + } if (typeof printed !== 'string') { throw new Error( `pretty-format: Plugin must return type "string" but instead returned "${typeof printed}".`, @@ -275,8 +289,12 @@ function printPlugin( function findPlugin(plugins: Plugins, val: any) { for (let p = 0; p < plugins.length; p++) { - if (plugins[p].test(val)) { - return plugins[p]; + try { + if (plugins[p].test(val)) { + return plugins[p]; + } + } catch (error) { + throw new PrettyFormatPluginError(error.message, error.stack); } }