Skip to content

Commit

Permalink
Remove global mocks by adding support for "suppressReactErrorLogging"…
Browse files Browse the repository at this point in the history
… property (facebook#11636)

* Remove global mocks

They are making it harder to test compiled bundles.

One of them (FeatureFlags) is not used. It is mocked in some specific test files (and that's fine).

The other (FiberErrorLogger) is mocked to silence its output. I'll look if there's some other way to achieve this.

* Add error.suppressReactErrorLogging and use it in tests

This adds an escape hatch to *not* log errors that go through React to the console.
We will enable it for our own tests.
  • Loading branch information
gaearon authored and raphamorim committed Nov 27, 2017
1 parent f3510cf commit 5dbba9b
Show file tree
Hide file tree
Showing 6 changed files with 53 additions and 73 deletions.
1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,6 @@
".*": "./scripts/jest/preprocessor.js"
},
"setupFiles": [
"./scripts/jest/setup.js",
"./scripts/jest/environment.js"
],
"setupTestFrameworkScriptFile": "./scripts/jest/test-framework-setup.js",
Expand Down
5 changes: 5 additions & 0 deletions packages/react-reconciler/src/ReactFiberErrorLogger.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@ export function logCapturedError(capturedError: CapturedError): void {
}

const error = (capturedError.error: any);
const suppressLogging = error && error.suppressReactErrorLogging;
if (suppressLogging) {
return;
}

if (__DEV__) {
const {
componentName,
Expand Down
5 changes: 4 additions & 1 deletion packages/react-reconciler/src/ReactFiberScheduler.js
Original file line number Diff line number Diff line change
Expand Up @@ -1004,7 +1004,10 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
} catch (e) {
// Prevent cycle if logCapturedError() throws.
// A cycle may still occur if logCapturedError renders a component that throws.
console.error(e);
const suppressLogging = e && e.suppressReactErrorLogging;
if (!suppressLogging) {
console.error(e);
}
}

// If we're in the commit phase, defer scheduling an update on the
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ describe('ReactIncrementalErrorHandling', () => {
ReactNoop = require('react-noop-renderer');
});

afterEach(() => {
jest.unmock('../ReactFiberErrorLogger');
});

function div(...children) {
children = children.map(c => (typeof c === 'string' ? {text: c} : c));
return {type: 'div', children, prop: undefined};
Expand Down Expand Up @@ -952,49 +956,22 @@ describe('ReactIncrementalErrorHandling', () => {
});

describe('ReactFiberErrorLogger', () => {
function initReactFiberErrorLoggerMock(mock) {
jest.resetModules();
if (mock) {
jest.mock('../ReactFiberErrorLogger');
} else {
jest.unmock('../ReactFiberErrorLogger');
}
React = require('react');
ReactNoop = require('react-noop-renderer');
}

beforeEach(() => {
// Assert that we're mocking this file by default.
// If this ever changes, we'll need to update this test suite anyway.
expect(
require('../ReactFiberErrorLogger').logCapturedError._isMockFunction,
).toBe(true);
});

afterEach(() => {
// Restore the default (we verified it was being mocked in beforeEach).
jest.mock('../ReactFiberErrorLogger');
});

function normalizeCodeLocInfo(str) {
return str && str.replace(/\(at .+?:\d+\)/g, '(at **)');
}

it('should log errors that occur during the begin phase', () => {
initReactFiberErrorLoggerMock(false);
// Intentionally spy in production too.
// TODO: It is confusing how these tests correctly "see" console.error()s
// from exceptions becase they called initReactFiberErrorLoggerMock(false)
// and thus unmocked the error logger. It is globally mocked in all other
// tests. We did this to silence warnings from intentionally caught errors
// in tests. But perhaps we should instead make a pass through all tests,
// intentionally assert console.error() is always called for uncaught
// exceptions, and then remove the global mock of ReactFiberErrorLogger.
spyOn(console, 'error');

class ErrorThrowingComponent extends React.Component {
componentWillMount() {
throw Error('componentWillMount error');
const error = new Error('componentWillMount error');
// Note: it's `true` on the Error prototype our test environment.
// That lets us avoid asserting on warnings for each expected error.
// Here we intentionally shadow it to test logging, like in real apps.
error.suppressReactErrorLogging = undefined;
throw error;
}
render() {
return <div />;
Expand Down Expand Up @@ -1030,19 +1007,17 @@ describe('ReactIncrementalErrorHandling', () => {
});

it('should log errors that occur during the commit phase', () => {
initReactFiberErrorLoggerMock(false);
// TODO: It is confusing how these tests correctly "see" console.error()s
// from exceptions becase they called initReactFiberErrorLoggerMock(false)
// and thus unmocked the error logger. It is globally mocked in all other
// tests. We did this to silence warnings from intentionally caught errors
// in tests. But perhaps we should instead make a pass through all tests,
// intentionally assert console.error() is always called for uncaught
// exceptions, and then remove the global mock of ReactFiberErrorLogger.
// Intentionally spy in production too.
spyOn(console, 'error');

class ErrorThrowingComponent extends React.Component {
componentDidMount() {
throw Error('componentDidMount error');
const error = new Error('componentDidMount error');
// Note: it's `true` on the Error prototype our test environment.
// That lets us avoid asserting on warnings for each expected error.
// Here we intentionally shadow it to test logging, like in real apps.
error.suppressReactErrorLogging = undefined;
throw error;
}
render() {
return <div />;
Expand Down Expand Up @@ -1078,18 +1053,16 @@ describe('ReactIncrementalErrorHandling', () => {
});

it('should ignore errors thrown in log method to prevent cycle', () => {
initReactFiberErrorLoggerMock(true);
jest.resetModules();
jest.mock('../ReactFiberErrorLogger');
React = require('react');
ReactNoop = require('react-noop-renderer');
// Intentionally spy in production too.
// Note: we're seeing console.error() even though ReactFiberErrorLogger is
// mocked because we're currently testing the console.error() call inside
// ReactFiberScheduler (for cycles). It doesn't get affected by mocking.
// TODO: mocking is confusing and we should simplify this and remove
// special cases.
spyOn(console, 'error');

class ErrorThrowingComponent extends React.Component {
render() {
throw Error('render error');
throw new Error('render error');
}
}

Expand All @@ -1099,7 +1072,12 @@ describe('ReactIncrementalErrorHandling', () => {
ReactFiberErrorLogger.logCapturedError.mockImplementation(
capturedError => {
logCapturedErrorCalls.push(capturedError);
throw Error('logCapturedError error');
const error = new Error('logCapturedError error');
// Note: it's `true` on the Error prototype our test environment.
// That lets us avoid asserting on warnings for each expected error.
// Here we intentionally shadow it to test logging, like in real apps.
error.suppressReactErrorLogging = undefined;
throw error;
},
);

Expand All @@ -1124,14 +1102,7 @@ describe('ReactIncrementalErrorHandling', () => {
});

it('should relay info about error boundary and retry attempts if applicable', () => {
initReactFiberErrorLoggerMock(false);
// TODO: It is confusing how these tests correctly "see" console.error()s
// from exceptions becase they called initReactFiberErrorLoggerMock(false)
// and thus unmocked the error logger. It is globally mocked in all other
// tests. We did this to silence warnings from intentionally caught errors
// in tests. But perhaps we should instead make a pass through all tests,
// intentionally assert console.error() is always called for uncaught
// exceptions, and then remove the global mock of ReactFiberErrorLogger.
// Intentionally spy in production too.
spyOn(console, 'error');

class ParentComponent extends React.Component {
Expand All @@ -1155,7 +1126,12 @@ describe('ReactIncrementalErrorHandling', () => {

class ErrorThrowingComponent extends React.Component {
componentDidMount() {
throw Error('componentDidMount error');
const error = new Error('componentDidMount error');
// Note: it's `true` on the Error prototype our test environment.
// That lets us avoid asserting on warnings for each expected error.
// Here we intentionally shadow it to test logging, like in real apps.
error.suppressReactErrorLogging = undefined;
throw error;
}
render() {
renderAttempts++;
Expand Down
9 changes: 9 additions & 0 deletions scripts/jest/environment.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,12 @@ global.requestIdleCallback = function(callback) {
global.cancelIdleCallback = function(callbackID) {
clearTimeout(callbackID);
};

// By default React console.error()'s any errors, caught or uncaught.
// However it is annoying to assert that a warning fired each time
// we assert that there is an exception in our tests. This lets us
// opt out of extra console error reporting for most tests except
// for the few that specifically test the logging by shadowing this
// property. In real apps, it would usually not be defined at all.
Error.prototype.suppressReactErrorLogging = true;
DOMException.prototype.suppressReactErrorLogging = true;
12 changes: 0 additions & 12 deletions scripts/jest/setup.js

This file was deleted.

0 comments on commit 5dbba9b

Please sign in to comment.