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:
Родитель
bcc482e655
Коммит
a9cab21010
|
@ -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() {
|
get flattenStyle() {
|
||||||
return require('../StyleSheet/flattenStyle');
|
return require('../StyleSheet/flattenStyle');
|
||||||
},
|
},
|
||||||
|
get ReactFiberErrorDialog() {
|
||||||
|
return require('../Core/ReactFiberErrorDialog');
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
Загрузка…
Ссылка в новой задаче