Skip to content

Commit 0441e5f

Browse files
bvaughnyenshih
authored andcommitted
Fixed potential false-positive in toWarnDev matcher (facebook#11898)
* Warn about spying on the console * Added suppress warning flag for spyOn(console) * Nits * Removed spy-on-console guard * Fixed a potential source of false-positives in toWarnDev() matcher Also updated (most of) ReactIncrementalErrorLogging-test.internal to use the new matcher * Removed unused third param to spyOn * Improved clarity of inline comments * Removed unused normalizeCodeLocInfo() method
1 parent f3f2279 commit 0441e5f

File tree

3 files changed

+72
-78
lines changed

3 files changed

+72
-78
lines changed

packages/react-dom/src/__tests__/utils/ReactDOMServerIntegrationTestUtils.js

+2
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ module.exports = function(initModules) {
4141
if (console.error.calls && console.error.calls.reset) {
4242
console.error.calls.reset();
4343
} else {
44+
// TODO: Rewrite tests that use this helper to enumerate expeceted errors.
45+
// This will enable the helper to use the .toWarnDev() matcher instead of spying.
4446
spyOnDev(console, 'error');
4547
}
4648

packages/react-reconciler/src/__tests__/ReactIncrementalErrorLogging-test.internal.js

+53-70
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,10 @@ describe('ReactIncrementalErrorLogging', () => {
2020
ReactNoop = require('react-noop-renderer');
2121
});
2222

23-
function normalizeCodeLocInfo(str) {
24-
return str && str.replace(/\(at .+?:\d+\)/g, '(at **)');
25-
}
26-
2723
it('should log errors that occur during the begin phase', () => {
28-
spyOnDevAndProd(console, 'error');
24+
// Errors are redundantly logged in production mode by ReactFiberErrorLogger.
25+
// It's okay to ignore them for the purpose of this test.
26+
spyOnProd(console, 'error');
2927

3028
class ErrorThrowingComponent extends React.Component {
3129
componentWillMount() {
@@ -41,36 +39,29 @@ describe('ReactIncrementalErrorLogging', () => {
4139
}
4240
}
4341

44-
try {
45-
ReactNoop.render(
46-
<div>
47-
<span>
48-
<ErrorThrowingComponent />
49-
</span>
50-
</div>,
51-
);
52-
ReactNoop.flushDeferredPri();
53-
} catch (error) {}
42+
ReactNoop.render(
43+
<div>
44+
<span>
45+
<ErrorThrowingComponent />
46+
</span>
47+
</div>,
48+
);
5449

55-
expect(console.error.calls.count()).toBe(1);
56-
const errorMessage = console.error.calls.argsFor(0)[0];
57-
if (__DEV__) {
58-
expect(normalizeCodeLocInfo(errorMessage)).toContain(
50+
expect(() => {
51+
expect(ReactNoop.flushDeferredPri).toWarnDev(
5952
'The above error occurred in the <ErrorThrowingComponent> component:\n' +
6053
' in ErrorThrowingComponent (at **)\n' +
6154
' in span (at **)\n' +
62-
' in div (at **)',
55+
' in div (at **)\n\n' +
56+
'Consider adding an error boundary to your tree to customize error handling behavior.',
6357
);
64-
expect(errorMessage).toContain(
65-
'Consider adding an error boundary to your tree to customize error handling behavior.',
66-
);
67-
} else {
68-
expect(errorMessage.message).toContain('componentWillMount error');
69-
}
58+
}).toThrowError('componentWillMount error');
7059
});
7160

7261
it('should log errors that occur during the commit phase', () => {
73-
spyOnDevAndProd(console, 'error');
62+
// Errors are redundantly logged in production mode by ReactFiberErrorLogger.
63+
// It's okay to ignore them for the purpose of this test.
64+
spyOnProd(console, 'error');
7465

7566
class ErrorThrowingComponent extends React.Component {
7667
componentDidMount() {
@@ -86,32 +77,23 @@ describe('ReactIncrementalErrorLogging', () => {
8677
}
8778
}
8879

89-
try {
90-
ReactNoop.render(
91-
<div>
92-
<span>
93-
<ErrorThrowingComponent />
94-
</span>
95-
</div>,
96-
);
97-
ReactNoop.flushDeferredPri();
98-
} catch (error) {}
80+
ReactNoop.render(
81+
<div>
82+
<span>
83+
<ErrorThrowingComponent />
84+
</span>
85+
</div>,
86+
);
9987

100-
expect(console.error.calls.count()).toBe(1);
101-
const errorMessage = console.error.calls.argsFor(0)[0];
102-
if (__DEV__) {
103-
expect(normalizeCodeLocInfo(errorMessage)).toContain(
88+
expect(() => {
89+
expect(ReactNoop.flushDeferredPri).toWarnDev(
10490
'The above error occurred in the <ErrorThrowingComponent> component:\n' +
10591
' in ErrorThrowingComponent (at **)\n' +
10692
' in span (at **)\n' +
107-
' in div (at **)',
108-
);
109-
expect(errorMessage).toContain(
110-
'Consider adding an error boundary to your tree to customize error handling behavior.',
93+
' in div (at **)\n\n' +
94+
'Consider adding an error boundary to your tree to customize error handling behavior.',
11195
);
112-
} else {
113-
expect(errorMessage.message).toBe('componentDidMount error');
114-
}
96+
}).toThrowError('componentDidMount error');
11597
});
11698

11799
it('should ignore errors thrown in log method to prevent cycle', () => {
@@ -120,6 +102,8 @@ describe('ReactIncrementalErrorLogging', () => {
120102
try {
121103
React = require('react');
122104
ReactNoop = require('react-noop-renderer');
105+
106+
// TODO Update this test to use toWarnDev() matcher if possible
123107
spyOnDevAndProd(console, 'error');
124108

125109
class ErrorThrowingComponent extends React.Component {
@@ -167,7 +151,9 @@ describe('ReactIncrementalErrorLogging', () => {
167151
});
168152

169153
it('should relay info about error boundary and retry attempts if applicable', () => {
170-
spyOnDevAndProd(console, 'error');
154+
// Errors are redundantly logged in production mode by ReactFiberErrorLogger.
155+
// It's okay to ignore them for the purpose of this test.
156+
spyOnProd(console, 'error');
171157

172158
class ParentComponent extends React.Component {
173159
render() {
@@ -203,30 +189,27 @@ describe('ReactIncrementalErrorLogging', () => {
203189
}
204190
}
205191

206-
try {
207-
ReactNoop.render(<ParentComponent />);
208-
ReactNoop.flush();
209-
} catch (error) {}
192+
ReactNoop.render(<ParentComponent />);
210193

211-
expect(renderAttempts).toBe(2);
212-
expect(handleErrorCalls.length).toBe(1);
213-
expect(console.error.calls.count()).toBe(2);
214-
if (__DEV__) {
215-
expect(console.error.calls.argsFor(0)[0]).toContain(
216-
'The above error occurred in the <ErrorThrowingComponent> component:',
217-
);
218-
expect(console.error.calls.argsFor(0)[0]).toContain(
219-
'React will try to recreate this component tree from scratch ' +
194+
expect(() => {
195+
expect(ReactNoop.flush).toWarnDev([
196+
'The above error occurred in the <ErrorThrowingComponent> component:\n' +
197+
' in ErrorThrowingComponent (at **)\n' +
198+
' in ErrorBoundaryComponent (at **)\n' +
199+
' in ParentComponent (at **)\n\n' +
200+
'React will try to recreate this component tree from scratch ' +
220201
'using the error boundary you provided, ErrorBoundaryComponent.',
221-
);
222-
expect(console.error.calls.argsFor(1)[0]).toContain(
223-
'The above error occurred in the <ErrorThrowingComponent> component:',
224-
);
225-
expect(console.error.calls.argsFor(1)[0]).toContain(
226-
'This error was initially handled by the error boundary ErrorBoundaryComponent.\n' +
202+
'The above error occurred in the <ErrorThrowingComponent> component:\n' +
203+
' in ErrorThrowingComponent (at **)\n' +
204+
' in ErrorBoundaryComponent (at **)\n' +
205+
' in ParentComponent (at **)\n\n' +
206+
'This error was initially handled by the error boundary ErrorBoundaryComponent.\n' +
227207
'Recreating the tree from scratch failed so React will unmount the tree.',
228-
);
229-
}
208+
]);
209+
}).toThrowError('componentDidMount error');
210+
211+
expect(renderAttempts).toBe(2);
212+
expect(handleErrorCalls.length).toBe(1);
230213
});
231214
});
232215

scripts/jest/matchers/toWarnDev.js

+17-8
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,12 @@ const createMatcherFor = consoleMethod =>
1919

2020
const unexpectedWarnings = [];
2121

22+
// Catch errors thrown by the callback,
23+
// But only rethrow them if all test expectations have been satisfied.
24+
// Otherwise an Error in the callback can mask a failed expectation,
25+
// and result in a test that passes when it shouldn't.
26+
let caughtError;
27+
2228
const consoleSpy = message => {
2329
const normalizedMessage = normalizeCodeLocInfo(message);
2430

@@ -58,6 +64,11 @@ const createMatcherFor = consoleMethod =>
5864

5965
try {
6066
callback();
67+
} catch (error) {
68+
caughtError = error;
69+
} finally {
70+
// Restore the unspied method so that unexpected errors fail tests.
71+
console[consoleMethod] = originalMethod;
6172

6273
// Any unexpected warnings should be treated as a failure.
6374
if (unexpectedWarnings.length > 0) {
@@ -72,20 +83,18 @@ const createMatcherFor = consoleMethod =>
7283
return {
7384
message: () =>
7485
`Expected warning was not recorded:\n ${this.utils.printReceived(
75-
expectedMessages.join('\n')
86+
expectedMessages[0]
7687
)}`,
7788
pass: false,
7889
};
7990
}
8091

92+
// Any unexpected Errors thrown by the callback should fail the test.
93+
if (caughtError) {
94+
throw caughtError;
95+
}
96+
8197
return {pass: true};
82-
} catch (error) {
83-
// TODO Flag this error so Jest doesn't override its stack
84-
// See https://tinyurl.com/y9unakwb
85-
throw error;
86-
} finally {
87-
// Restore the unspied method so that unexpected errors fail tests.
88-
console[consoleMethod] = originalMethod;
8998
}
9099
} else {
91100
// Any uncaught errors or warnings should fail tests in production mode.

0 commit comments

Comments
 (0)