Add ReactFiberErrorDialog from React + tests (#25671)

Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/25671

Moves the RN-specific `ReactFiberErrorDialog` implementation from React to RN for easier iteration. Also adds new unit tests.

This current change is additive, so we're compatible with the current React renderer which still uses `ExceptionsManager` and not the file added here. After the corresponding React update we can remove `ExceptionsManager` from the RN private interface entirely.

Reviewed By: cpojer

Differential Revision: D16278938

fbshipit-source-id: 0c2c0c3e65e524e079730ae3b0cc23e0c0bdc5fd
This commit is contained in:
Moti Zilberman 2019-07-16 03:17:35 -07:00 коммит произвёл Facebook Github Bot
Родитель bcc482e655
Коммит a9cab21010
3 изменённых файлов: 170 добавлений и 0 удалений

Просмотреть файл

@ -0,0 +1,58 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
* @flow strict-local
*/
export type CapturedError = {
+componentName: ?string,
+componentStack: string,
+error: mixed,
+errorBoundary: ?{},
+errorBoundaryFound: boolean,
+errorBoundaryName: string | null,
+willRetry: boolean,
};
import {handleException} from './ExceptionsManager';
/**
* Intercept lifecycle errors and ensure they are shown with the correct stack
* trace within the native redbox component.
*/
export function showErrorDialog(capturedError: CapturedError): boolean {
const {componentStack, error} = capturedError;
let errorToHandle: Error;
// Typically Errors are thrown but eg strings or null can be thrown as well.
if (error instanceof Error) {
const {message, name} = error;
const summary = message ? `${name}: ${message}` : name;
errorToHandle = error;
try {
errorToHandle.message = `${summary}\n\nThis error is located at:${componentStack}`;
} catch (e) {}
} else if (typeof error === 'string') {
errorToHandle = new Error(
`${error}\n\nThis error is located at:${componentStack}`,
);
} else {
errorToHandle = new Error(`Unspecified error at:${componentStack}`);
}
handleException(errorToHandle, false);
// Return false here to prevent ReactFiberErrorLogger default behavior of
// logging error details to console.error. Calls to console.error are
// automatically routed to the native redbox controller, which we've already
// done above by calling ExceptionsManager.
return false;
}

Просмотреть файл

@ -0,0 +1,109 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
* @emails oncall+react_native
*/
'use strict';
const capturedErrorDefaults = {
componentName: 'A',
componentStack: '\n in A\n in B\n in C',
errorBoundary: null,
errorBoundaryFound: false,
errorBoundaryName: null,
willRetry: false,
};
describe('ReactFiberErrorDialog', () => {
let ReactFiberErrorDialog, ExceptionsManager;
beforeEach(() => {
jest.resetModules();
jest.mock('../ExceptionsManager', () => {
return {
handleException: jest.fn(),
};
});
ReactFiberErrorDialog = require('../ReactFiberErrorDialog');
ExceptionsManager = require('../ExceptionsManager');
});
describe('showErrorDialog', () => {
test('forwards error instance to handleException', () => {
const error = new ReferenceError('Some error happened');
error.someCustomProp = 42;
// Copy all the data we care about before any possible mutation.
const {name, stack, message, someCustomProp} = error;
const logToConsole = ReactFiberErrorDialog.showErrorDialog({
...capturedErrorDefaults,
error,
});
expect(ExceptionsManager.handleException.mock.calls.length).toBe(1);
const errorArg = ExceptionsManager.handleException.mock.calls[0][0];
const isFatalArg = ExceptionsManager.handleException.mock.calls[0][1];
// We intentionally don't test whether errorArg === error, because this
// implementation detail might change. Instead, we test that they are
// functionally equivalent.
expect(errorArg).toBeInstanceOf(ReferenceError);
expect(errorArg).toHaveProperty('name', name);
expect(errorArg).toHaveProperty('stack', stack);
expect(errorArg).toHaveProperty('someCustomProp', someCustomProp);
expect(errorArg).toHaveProperty(
'message',
'ReferenceError: ' +
message +
'\n\n' +
'This error is located at:' +
capturedErrorDefaults.componentStack,
);
expect(isFatalArg).toBe(false);
expect(logToConsole).toBe(false);
});
test('wraps string in an Error and sends to handleException', () => {
const message = 'Some error happened';
const logToConsole = ReactFiberErrorDialog.showErrorDialog({
...capturedErrorDefaults,
error: message,
});
expect(ExceptionsManager.handleException.mock.calls.length).toBe(1);
const errorArg = ExceptionsManager.handleException.mock.calls[0][0];
const isFatalArg = ExceptionsManager.handleException.mock.calls[0][1];
expect(errorArg).toBeInstanceOf(Error);
expect(errorArg).toHaveProperty(
'message',
message +
'\n\n' +
'This error is located at:' +
capturedErrorDefaults.componentStack,
);
expect(isFatalArg).toBe(false);
expect(logToConsole).toBe(false);
});
test('reports "Unspecified error" if error is null', () => {
const logToConsole = ReactFiberErrorDialog.showErrorDialog({
...capturedErrorDefaults,
error: null,
});
expect(ExceptionsManager.handleException.mock.calls.length).toBe(1);
const errorArg = ExceptionsManager.handleException.mock.calls[0][0];
const isFatalArg = ExceptionsManager.handleException.mock.calls[0][1];
expect(errorArg).toBeInstanceOf(Error);
expect(errorArg).toHaveProperty(
'message',
'Unspecified error at:' + capturedErrorDefaults.componentStack,
);
expect(isFatalArg).toBe(false);
expect(logToConsole).toBe(false);
});
});
});

Просмотреть файл

@ -40,4 +40,7 @@ module.exports = {
get flattenStyle() {
return require('../StyleSheet/flattenStyle');
},
get ReactFiberErrorDialog() {
return require('../Core/ReactFiberErrorDialog');
},
};