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
+};