@samitouri / QOS-React / commits / b2adcfba32

Don't suppress jsdom error reporting in our tests (#13401)

* Don't suppress jsdom error reporting * Address review

Dan Abramov committed Aug 15, 2018 at 17:44 UTC b2adcfba32d133aabdbc182bba1001e729060abe
8 files changed +259 -225
packages/react-dom/src/__tests__/ReactErrorLoggingRecovery-test.js
-6
@@ -19,12 +19,6 @@ if (global.window) {
19 const React = require('react');
20 const ReactDOM = require('react-dom');
21
22 -// Unlike other tests, we want to enable error logging.
23 -// Note this is not a real Error prototype property,
24 -// it's only set in our Jest environment.
25 -// eslint-disable-next-line no-extend-native
26 -Error.prototype.suppressReactErrorLogging = false;
27 -
22 // Initialize JSDOM separately.
23 // We don't use our normal JSDOM setup because we want to load React first.
24 const {JSDOM} = require('jsdom');
packages/react-reconciler/src/ReactFiberErrorLogger.js
-5
@@ -21,11 +21,6 @@ export function logCapturedError(capturedError: CapturedError): void {
21 }
22
23 const error = (capturedError.error: any);
24 - const suppressLogging = error && error.suppressReactErrorLogging;
25 - if (suppressLogging) {
26 - return;
27 - }
28 -
24 if (__DEV__) {
25 const {
26 componentName,
packages/react-reconciler/src/__tests__/ReactIncrementalErrorLogging-test.internal.js deleted
-194
@@ -1,194 +0,0 @@
1 -/**
2 - * Copyright (c) 2013-present, Facebook, Inc.
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 - * @emails react-core
8 - * @jest-environment node
9 - */
10 -
11 -'use strict';
12 -
13 -let React;
14 -let ReactFeatureFlags;
15 -let ReactNoop;
16 -
17 -describe('ReactIncrementalErrorLogging', () => {
18 - beforeEach(() => {
19 - jest.resetModules();
20 - ReactFeatureFlags = require('shared/ReactFeatureFlags');
21 - ReactFeatureFlags.debugRenderPhaseSideEffectsForStrictMode = false;
22 - ReactFeatureFlags.replayFailedUnitOfWorkWithInvokeGuardedCallback = false;
23 - React = require('react');
24 - ReactNoop = require('react-noop-renderer');
25 - });
26 -
27 - it('should log errors that occur during the begin phase', () => {
28 - // Errors are redundantly logged in production mode by ReactFiberErrorLogger.
29 - // It's okay to ignore them for the purpose of this test.
30 - spyOnProd(console, 'error');
31 -
32 - class ErrorThrowingComponent extends React.Component {
33 - UNSAFE_componentWillMount() {
34 - const error = new Error('componentWillMount error');
35 - // Note: it's `true` on the Error prototype our test environment.
36 - // That lets us avoid asserting on warnings for each expected error.
37 - // Here we intentionally shadow it to test logging, like in real apps.
38 - error.suppressReactErrorLogging = undefined;
39 - throw error;
40 - }
41 - render() {
42 - return <div />;
43 - }
44 - }
45 -
46 - ReactNoop.render(
47 - <div>
48 - <span>
49 - <ErrorThrowingComponent />
50 - </span>
51 - </div>,
52 - );
53 -
54 - expect(() => {
55 - expect(ReactNoop.flushDeferredPri).toWarnDev(
56 - 'The above error occurred in the <ErrorThrowingComponent> component:\n' +
57 - ' in ErrorThrowingComponent (at **)\n' +
58 - ' in span (at **)\n' +
59 - ' in div (at **)\n\n' +
60 - 'Consider adding an error boundary to your tree to customize error handling behavior.',
61 - );
62 - }).toThrowError('componentWillMount error');
63 - });
64 -
65 - it('should log errors that occur during the commit phase', () => {
66 - // Errors are redundantly logged in production mode by ReactFiberErrorLogger.
67 - // It's okay to ignore them for the purpose of this test.
68 - spyOnProd(console, 'error');
69 -
70 - class ErrorThrowingComponent extends React.Component {
71 - componentDidMount() {
72 - const error = new Error('componentDidMount error');
73 - // Note: it's `true` on the Error prototype our test environment.
74 - // That lets us avoid asserting on warnings for each expected error.
75 - // Here we intentionally shadow it to test logging, like in real apps.
76 - error.suppressReactErrorLogging = undefined;
77 - throw error;
78 - }
79 - render() {
80 - return <div />;
81 - }
82 - }
83 -
84 - ReactNoop.render(
85 - <div>
86 - <span>
87 - <ErrorThrowingComponent />
88 - </span>
89 - </div>,
90 - );
91 -
92 - expect(() => {
93 - expect(ReactNoop.flushDeferredPri).toWarnDev(
94 - 'The above error occurred in the <ErrorThrowingComponent> component:\n' +
95 - ' in ErrorThrowingComponent (at **)\n' +
96 - ' in span (at **)\n' +
97 - ' in div (at **)\n\n' +
98 - 'Consider adding an error boundary to your tree to customize error handling behavior.',
99 - );
100 - }).toThrowError('componentDidMount error');
101 - });
102 -
103 - it('should ignore errors thrown in log method to prevent cycle', () => {
104 - jest.resetModules();
105 - jest.mock('../ReactFiberErrorLogger');
106 - try {
107 - React = require('react');
108 - ReactNoop = require('react-noop-renderer');
109 -
110 - // TODO Update this test to use toWarnDev() matcher if possible
111 - spyOnDevAndProd(console, 'error');
112 -
113 - class ErrorThrowingComponent extends React.Component {
114 - render() {
115 - throw new Error('render error');
116 - }
117 - }
118 -
119 - const logCapturedErrorCalls = [];
120 -
121 - const ReactFiberErrorLogger = require('../ReactFiberErrorLogger');
122 - ReactFiberErrorLogger.logCapturedError.mockImplementation(
123 - capturedError => {
124 - logCapturedErrorCalls.push(capturedError);
125 - const error = new Error('logCapturedError error');
126 - // Note: it's `true` on the Error prototype our test environment.
127 - // That lets us avoid asserting on warnings for each expected error.
128 - // Here we intentionally shadow it to test logging, like in real apps.
129 - error.suppressReactErrorLogging = undefined;
130 - throw error;
131 - },
132 - );
133 -
134 - try {
135 - ReactNoop.render(
136 - <div>
137 - <span>
138 - <ErrorThrowingComponent />
139 - </span>
140 - </div>,
141 - );
142 - ReactNoop.flushDeferredPri();
143 - } catch (error) {}
144 -
145 - expect(logCapturedErrorCalls.length).toBe(1);
146 -
147 - // The error thrown in logCapturedError should be rethrown with a clean stack
148 - expect(() => {
149 - jest.runAllTimers();
150 - }).toThrow('logCapturedError error');
151 - } finally {
152 - jest.unmock('../ReactFiberErrorLogger');
153 - }
154 - });
155 -
156 - it('resets instance variables before unmounting failed node', () => {
157 - class ErrorBoundary extends React.Component {
158 - state = {error: null};
159 - componentDidCatch(error) {
160 - this.setState({error});
161 - }
162 - render() {
163 - return this.state.error ? null : this.props.children;
164 - }
165 - }
166 - class Foo extends React.Component {
167 - state = {step: 0};
168 - componentDidMount() {
169 - this.setState({step: 1});
170 - }
171 - componentWillUnmount() {
172 - ReactNoop.yield('componentWillUnmount: ' + this.state.step);
173 - }
174 - render() {
175 - ReactNoop.yield('render: ' + this.state.step);
176 - if (this.state.step > 0) {
177 - throw new Error('oops');
178 - }
179 - return null;
180 - }
181 - }
182 -
183 - ReactNoop.render(
184 - <ErrorBoundary>
185 - <Foo />
186 - </ErrorBoundary>,
187 - );
188 - expect(ReactNoop.flush()).toEqual([
189 - 'render: 0',
190 - 'render: 1',
191 - 'componentWillUnmount: 0',
192 - ]);
193 - });
194 -});
packages/react-reconciler/src/__tests__/ReactIncrementalErrorLogging-test.js new
+211
@@ -0,0 +1,211 @@
1 +/**
2 + * Copyright (c) 2013-present, Facebook, Inc.
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 + * @emails react-core
8 + * @jest-environment node
9 + */
10 +
11 +'use strict';
12 +
13 +let React;
14 +let ReactNoop;
15 +
16 +describe('ReactIncrementalErrorLogging', () => {
17 + beforeEach(() => {
18 + jest.resetModules();
19 + React = require('react');
20 + ReactNoop = require('react-noop-renderer');
21 + });
22 +
23 + // Note: in this test file we won't be using toWarnDev() matchers
24 + // because they filter out precisely the messages we want to test for.
25 + let oldConsoleError;
26 + beforeEach(() => {
27 + oldConsoleError = console.error;
28 + console.error = jest.fn();
29 + });
30 +
31 + afterEach(() => {
32 + console.error = oldConsoleError;
33 + oldConsoleError = null;
34 + });
35 +
36 + it('should log errors that occur during the begin phase', () => {
37 + class ErrorThrowingComponent extends React.Component {
38 + constructor(props) {
39 + super(props);
40 + throw new Error('constructor error');
41 + }
42 + render() {
43 + return <div />;
44 + }
45 + }
46 + ReactNoop.render(
47 + <div>
48 + <span>
49 + <ErrorThrowingComponent />
50 + </span>
51 + </div>,
52 + );
53 + expect(ReactNoop.flushDeferredPri).toThrowError('constructor error');
54 + expect(console.error).toHaveBeenCalledTimes(1);
55 + expect(console.error).toHaveBeenCalledWith(
56 + __DEV__
57 + ? expect.stringMatching(
58 + new RegExp(
59 + 'The above error occurred in the <ErrorThrowingComponent> component:\n' +
60 + '\\s+in ErrorThrowingComponent (.*)\n' +
61 + '\\s+in span (.*)\n' +
62 + '\\s+in div (.*)\n\n' +
63 + 'Consider adding an error boundary to your tree ' +
64 + 'to customize error handling behavior\\.',
65 + ),
66 + )
67 + : expect.objectContaining({
68 + message: 'constructor error',
69 + }),
70 + );
71 + });
72 +
73 + it('should log errors that occur during the commit phase', () => {
74 + class ErrorThrowingComponent extends React.Component {
75 + componentDidMount() {
76 + throw new Error('componentDidMount error');
77 + }
78 + render() {
79 + return <div />;
80 + }
81 + }
82 + ReactNoop.render(
83 + <div>
84 + <span>
85 + <ErrorThrowingComponent />
86 + </span>
87 + </div>,
88 + );
89 + expect(ReactNoop.flushDeferredPri).toThrowError('componentDidMount error');
90 + expect(console.error).toHaveBeenCalledTimes(1);
91 + expect(console.error).toHaveBeenCalledWith(
92 + __DEV__
93 + ? expect.stringMatching(
94 + new RegExp(
95 + 'The above error occurred in the <ErrorThrowingComponent> component:\n' +
96 + '\\s+in ErrorThrowingComponent (.*)\n' +
97 + '\\s+in span (.*)\n' +
98 + '\\s+in div (.*)\n\n' +
99 + 'Consider adding an error boundary to your tree ' +
100 + 'to customize error handling behavior\\.',
101 + ),
102 + )
103 + : expect.objectContaining({
104 + message: 'componentDidMount error',
105 + }),
106 + );
107 + });
108 +
109 + it('should ignore errors thrown in log method to prevent cycle', () => {
110 + const logCapturedErrorCalls = [];
111 + console.error.mockImplementation(error => {
112 + // Test what happens when logging itself is buggy.
113 + logCapturedErrorCalls.push(error);
114 + throw new Error('logCapturedError error');
115 + });
116 + class ErrorThrowingComponent extends React.Component {
117 + render() {
118 + throw new Error('render error');
119 + }
120 + }
121 + ReactNoop.render(
122 + <div>
123 + <span>
124 + <ErrorThrowingComponent />
125 + </span>
126 + </div>,
127 + );
128 + expect(ReactNoop.flushDeferredPri).toThrow('render error');
129 + expect(logCapturedErrorCalls.length).toBe(1);
130 + expect(logCapturedErrorCalls[0]).toEqual(
131 + __DEV__
132 + ? expect.stringMatching(
133 + new RegExp(
134 + 'The above error occurred in the <ErrorThrowingComponent> component:\n' +
135 + '\\s+in ErrorThrowingComponent (.*)\n' +
136 + '\\s+in span (.*)\n' +
137 + '\\s+in div (.*)\n\n' +
138 + 'Consider adding an error boundary to your tree ' +
139 + 'to customize error handling behavior\\.',
140 + ),
141 + )
142 + : expect.objectContaining({
143 + message: 'render error',
144 + }),
145 + );
146 + // The error thrown in logCapturedError should be rethrown with a clean stack
147 + expect(() => {
148 + jest.runAllTimers();
149 + }).toThrow('logCapturedError error');
150 + });
151 +
152 + it('resets instance variables before unmounting failed node', () => {
153 + class ErrorBoundary extends React.Component {
154 + state = {error: null};
155 + componentDidCatch(error) {
156 + this.setState({error});
157 + }
158 + render() {
159 + return this.state.error ? null : this.props.children;
160 + }
161 + }
162 + class Foo extends React.Component {
163 + state = {step: 0};
164 + componentDidMount() {
165 + this.setState({step: 1});
166 + }
167 + componentWillUnmount() {
168 + ReactNoop.yield('componentWillUnmount: ' + this.state.step);
169 + }
170 + render() {
171 + ReactNoop.yield('render: ' + this.state.step);
172 + if (this.state.step > 0) {
173 + throw new Error('oops');
174 + }
175 + return null;
176 + }
177 + }
178 +
179 + ReactNoop.render(
180 + <ErrorBoundary>
181 + <Foo />
182 + </ErrorBoundary>,
183 + );
184 + expect(ReactNoop.flush()).toEqual(
185 + [
186 + 'render: 0',
187 + __DEV__ && 'render: 0', // replay
188 + 'render: 1',
189 + __DEV__ && 'render: 1', // replay
190 + 'componentWillUnmount: 0',
191 + ].filter(Boolean),
192 + );
193 +
194 + expect(console.error).toHaveBeenCalledTimes(1);
195 + expect(console.error).toHaveBeenCalledWith(
196 + __DEV__
197 + ? expect.stringMatching(
198 + new RegExp(
199 + 'The above error occurred in the <Foo> component:\n' +
200 + '\\s+in Foo (.*)\n' +
201 + '\\s+in ErrorBoundary (.*)\n\n' +
202 + 'React will try to recreate this component tree from scratch ' +
203 + 'using the error boundary you provided, ErrorBoundary.',
204 + ),
205 + )
206 + : expect.objectContaining({
207 + message: 'oops',
208 + }),
209 + );
210 + });
211 +});
scripts/jest/matchers/toWarnDev.js
+10
@@ -2,6 +2,7 @@
2
3 const jestDiff = require('jest-diff');
4 const util = require('util');
5 +const shouldIgnoreConsoleError = require('../shouldIgnoreConsoleError');
6
7 function normalizeCodeLocInfo(str) {
8 return str && str.replace(/at .+?:\d+/g, 'at **');
@@ -54,6 +55,15 @@ const createMatcherFor = consoleMethod =>
55 typeof message === 'string' && message.includes('\n in ');
56
57 const consoleSpy = (format, ...args) => {
58 + // Ignore uncaught errors reported by jsdom
59 + // and React addendums because they're too noisy.
60 + if (
61 + consoleMethod === 'error' &&
62 + shouldIgnoreConsoleError(format, args)
63 + ) {
64 + return;
65 + }
66 +
67 const message = util.format(format, ...args);
68 const normalizedMessage = normalizeCodeLocInfo(message);
69
scripts/jest/setupEnvironment.js
-20
@@ -7,14 +7,6 @@ if (NODE_ENV !== 'development' && NODE_ENV !== 'production') {
7 global.__DEV__ = NODE_ENV === 'development';
8 global.__PROFILE__ = NODE_ENV === 'development';
9
10 -// By default React console.error()'s any errors, caught or uncaught.
11 -// However it is annoying to assert that a warning fired each time
12 -// we assert that there is an exception in our tests. This lets us
13 -// opt out of extra console error reporting for most tests except
14 -// for the few that specifically test the logging by shadowing this
15 -// property. In real apps, it would usually not be defined at all.
16 -Error.prototype.suppressReactErrorLogging = true;
17 -
10 if (typeof window !== 'undefined') {
11 global.requestAnimationFrame = function(callback) {
12 setTimeout(callback);
@@ -33,16 +25,4 @@ if (typeof window !== 'undefined') {
25 global.cancelIdleCallback = function(callbackID) {
26 clearTimeout(callbackID);
27 };
36 -
37 - // Same as we did with Error.prototype above.
38 - DOMException.prototype.suppressReactErrorLogging = true;
39 -
40 - // Also prevent JSDOM from logging intentionally thrown errors.
41 - // TODO: it might make sense to do it the other way around.
42 - // https://github.com/facebook/react/issues/11098#issuecomment-355032539
43 - window.addEventListener('error', event => {
44 - if (event.error != null && event.error.suppressReactErrorLogging) {
45 - event.preventDefault();
46 - }
47 - });
28 }
scripts/jest/setupTests.js
+7
@@ -2,6 +2,7 @@
2
3 const chalk = require('chalk');
4 const util = require('util');
5 +const shouldIgnoreConsoleError = require('./shouldIgnoreConsoleError');
6
7 if (process.env.REACT_CLASS_EQUIVALENCE_TEST) {
8 // Inside the class equivalence tester, we have a custom environment, let's
@@ -66,6 +67,12 @@ if (process.env.REACT_CLASS_EQUIVALENCE_TEST) {
67 ['error', 'warn'].forEach(methodName => {
68 const unexpectedConsoleCallStacks = [];
69 const newMethod = function(format, ...args) {
70 + // Ignore uncaught errors reported by jsdom
71 + // and React addendums because they're too noisy.
72 + if (methodName === 'error' && shouldIgnoreConsoleError(format, args)) {
73 + return;
74 + }
75 +
76 // Capture the call stack now so we can warn about it later.
77 // The call stack has helpful information for the test author.
78 // Don't throw yet though b'c it might be accidentally caught and suppressed.
scripts/jest/shouldIgnoreConsoleError.js new
+31
@@ -0,0 +1,31 @@
1 +'use strict';
2 +
3 +module.exports = function shouldIgnoreConsoleError(format, args) {
4 + if (__DEV__) {
5 + if (typeof format === 'string') {
6 + if (format.indexOf('Error: Uncaught [') === 0) {
7 + // This looks like an uncaught error from invokeGuardedCallback() wrapper
8 + // in development that is reported by jsdom. Ignore because it's noisy.
9 + return true;
10 + }
11 + if (format.indexOf('The above error occurred') === 0) {
12 + // This looks like an error addendum from ReactFiberErrorLogger.
13 + // Ignore it too.
14 + return true;
15 + }
16 + }
17 + } else {
18 + if (
19 + format != null &&
20 + typeof format.message === 'string' &&
21 + typeof format.stack === 'string' &&
22 + args.length === 0
23 + ) {
24 + // In production, ReactFiberErrorLogger logs error objects directly.
25 + // They are noisy too so we'll try to ignore them.
26 + return true;
27 + }
28 + }
29 + // Looks legit
30 + return false;
31 +};