905 строки
31 KiB
JavaScript
905 строки
31 KiB
JavaScript
/**
|
|
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
*
|
|
* This source code is licensed under the MIT license found in the
|
|
* LICENSE file in the root directory of this source tree.
|
|
*
|
|
* @format
|
|
* @oncall react_native
|
|
*/
|
|
|
|
'use strict';
|
|
|
|
const LogBox = require('../../LogBox/LogBox');
|
|
const ExceptionsManager = require('../ExceptionsManager');
|
|
const NativeExceptionsManager = require('../NativeExceptionsManager').default;
|
|
const ReactFiberErrorDialog = require('../ReactFiberErrorDialog').default;
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
|
|
const capturedErrorDefaults = {
|
|
componentName: 'A',
|
|
componentStack: '\n in A\n in B\n in C',
|
|
errorBoundary: null,
|
|
errorBoundaryFound: false,
|
|
errorBoundaryName: null,
|
|
willRetry: false,
|
|
};
|
|
|
|
describe('checkVersion', () => {
|
|
describe('in development', () => {
|
|
setDevelopmentModeForTests(true);
|
|
runExceptionsManagerTests();
|
|
});
|
|
|
|
describe('in production', () => {
|
|
setDevelopmentModeForTests(false);
|
|
runExceptionsManagerTests();
|
|
});
|
|
});
|
|
|
|
function setDevelopmentModeForTests(dev) {
|
|
let originalDev;
|
|
|
|
beforeAll(() => {
|
|
originalDev = global.__DEV__;
|
|
global.__DEV__ = dev;
|
|
});
|
|
|
|
afterAll(() => {
|
|
global.__DEV__ = originalDev;
|
|
});
|
|
}
|
|
|
|
function runExceptionsManagerTests() {
|
|
describe('ExceptionsManager', () => {
|
|
let nativeReportException;
|
|
let logBoxAddException;
|
|
|
|
beforeEach(() => {
|
|
jest.resetModules();
|
|
jest.mock('../../LogBox/LogBox', () => ({
|
|
addException: jest.fn(),
|
|
}));
|
|
jest.mock('../NativeExceptionsManager', () => {
|
|
return {
|
|
default: {
|
|
reportException: jest.fn(),
|
|
// Used to show symbolicated messages, not part of this test.
|
|
updateExceptionMessage: () => {},
|
|
},
|
|
};
|
|
});
|
|
// Make symbolication a no-op.
|
|
jest.mock(
|
|
'../Devtools/symbolicateStackTrace',
|
|
() =>
|
|
async function symbolicateStackTrace(stack) {
|
|
return {stack};
|
|
},
|
|
);
|
|
jest.spyOn(console, 'error').mockReturnValue(undefined);
|
|
nativeReportException = NativeExceptionsManager.reportException;
|
|
logBoxAddException = LogBox.addException;
|
|
});
|
|
|
|
afterEach(() => {
|
|
jest.restoreAllMocks();
|
|
});
|
|
|
|
describe('ReactFiberErrorDialog.showErrorDialog', () => {
|
|
test('forwards error instance to reportException', () => {
|
|
const error = new ReferenceError('Some error happened');
|
|
// Copy all the data we care about before any possible mutation.
|
|
const {message, name} = error;
|
|
|
|
const logToConsoleInReact = ReactFiberErrorDialog.showErrorDialog({
|
|
...capturedErrorDefaults,
|
|
error,
|
|
});
|
|
|
|
if (__DEV__) {
|
|
expect(nativeReportException.mock.calls.length).toBe(0);
|
|
expect(logBoxAddException.mock.calls.length).toBe(1);
|
|
return;
|
|
}
|
|
expect(nativeReportException.mock.calls.length).toBe(1);
|
|
const exceptionData = nativeReportException.mock.calls[0][0];
|
|
const formattedMessage =
|
|
'ReferenceError: ' +
|
|
message +
|
|
'\n\n' +
|
|
'This error is located at:' +
|
|
capturedErrorDefaults.componentStack;
|
|
expect(exceptionData.message).toBe(formattedMessage);
|
|
expect(exceptionData.originalMessage).toBe(message);
|
|
expect(exceptionData.name).toBe(name);
|
|
expect(exceptionData.componentStack).toBe(
|
|
capturedErrorDefaults.componentStack,
|
|
);
|
|
expect(getLineFromFrame(exceptionData.stack[0])).toBe(
|
|
"const error = new ReferenceError('Some error happened');",
|
|
);
|
|
expect(exceptionData.isFatal).toBe(false);
|
|
expect(logToConsoleInReact).toBe(false);
|
|
expect(console.error).toBeCalledWith(formattedMessage);
|
|
});
|
|
|
|
test('does not pop frames off the stack with framesToPop', () => {
|
|
function createError() {
|
|
const error = new Error('Some error happened');
|
|
error.framesToPop = 1;
|
|
return error;
|
|
}
|
|
const error = createError();
|
|
|
|
ReactFiberErrorDialog.showErrorDialog({
|
|
...capturedErrorDefaults,
|
|
error,
|
|
});
|
|
|
|
let exceptionData;
|
|
|
|
if (__DEV__) {
|
|
expect(logBoxAddException.mock.calls.length).toBe(1);
|
|
exceptionData = logBoxAddException.mock.calls[0][0];
|
|
} else {
|
|
expect(nativeReportException.mock.calls.length).toBe(1);
|
|
exceptionData = nativeReportException.mock.calls[0][0];
|
|
}
|
|
expect(getLineFromFrame(exceptionData.stack[0])).toBe(
|
|
"const error = new Error('Some error happened');",
|
|
);
|
|
});
|
|
|
|
test('adds the JS engine to the message', () => {
|
|
const error = new Error('Some error happened');
|
|
error.jsEngine = 'hermes';
|
|
// Copy all the data we care about before any possible mutation.
|
|
const {message, jsEngine} = error;
|
|
|
|
ReactFiberErrorDialog.showErrorDialog({
|
|
...capturedErrorDefaults,
|
|
error,
|
|
});
|
|
|
|
let exceptionData;
|
|
|
|
if (__DEV__) {
|
|
expect(logBoxAddException.mock.calls.length).toBe(1);
|
|
exceptionData = logBoxAddException.mock.calls[0][0];
|
|
} else {
|
|
expect(nativeReportException.mock.calls.length).toBe(1);
|
|
exceptionData = nativeReportException.mock.calls[0][0];
|
|
}
|
|
expect(exceptionData.message).toBe(
|
|
'Error: ' +
|
|
message +
|
|
'\n\n' +
|
|
'This error is located at:' +
|
|
capturedErrorDefaults.componentStack +
|
|
', js engine: ' +
|
|
jsEngine,
|
|
);
|
|
expect(console.error).toBeCalledWith(
|
|
'Error: ' +
|
|
message +
|
|
'\n\n' +
|
|
'This error is located at:' +
|
|
capturedErrorDefaults.componentStack +
|
|
', js engine: ' +
|
|
jsEngine,
|
|
);
|
|
});
|
|
|
|
test('wraps string in an Error and sends to handleException', () => {
|
|
const message = 'Some error happened';
|
|
|
|
const logToConsoleInReact = ReactFiberErrorDialog.showErrorDialog({
|
|
...capturedErrorDefaults,
|
|
error: message,
|
|
});
|
|
|
|
let exceptionData;
|
|
|
|
if (__DEV__) {
|
|
expect(logBoxAddException.mock.calls.length).toBe(1);
|
|
exceptionData = logBoxAddException.mock.calls[0][0];
|
|
} else {
|
|
expect(nativeReportException.mock.calls.length).toBe(1);
|
|
exceptionData = nativeReportException.mock.calls[0][0];
|
|
}
|
|
const formattedMessage =
|
|
message +
|
|
'\n\n' +
|
|
'This error is located at:' +
|
|
capturedErrorDefaults.componentStack;
|
|
expect(exceptionData.message).toBe(formattedMessage);
|
|
expect(exceptionData.originalMessage).toBe(message);
|
|
expect(exceptionData.componentStack).toBe(
|
|
capturedErrorDefaults.componentStack,
|
|
);
|
|
expect(exceptionData.stack[0].file).toMatch(
|
|
/ReactFiberErrorDialog\.js$/,
|
|
);
|
|
expect(exceptionData.isFatal).toBe(false);
|
|
expect(logToConsoleInReact).toBe(false);
|
|
expect(console.error).toBeCalledWith(formattedMessage);
|
|
});
|
|
|
|
test('reports "Unspecified error" if error is null', () => {
|
|
const logToConsoleInReact = ReactFiberErrorDialog.showErrorDialog({
|
|
...capturedErrorDefaults,
|
|
error: null,
|
|
});
|
|
|
|
let exceptionData;
|
|
|
|
if (__DEV__) {
|
|
expect(logBoxAddException.mock.calls.length).toBe(1);
|
|
exceptionData = logBoxAddException.mock.calls[0][0];
|
|
} else {
|
|
expect(nativeReportException.mock.calls.length).toBe(1);
|
|
exceptionData = nativeReportException.mock.calls[0][0];
|
|
}
|
|
const formattedMessage =
|
|
'Unspecified error' +
|
|
'\n\n' +
|
|
'This error is located at:' +
|
|
capturedErrorDefaults.componentStack;
|
|
expect(exceptionData.message).toBe(formattedMessage);
|
|
expect(exceptionData.originalMessage).toBe('Unspecified error');
|
|
expect(exceptionData.name).toBe(null);
|
|
expect(exceptionData.componentStack).toBe(
|
|
capturedErrorDefaults.componentStack,
|
|
);
|
|
expect(exceptionData.stack[0].file).toMatch(
|
|
/ReactFiberErrorDialog\.js$/,
|
|
);
|
|
expect(exceptionData.isFatal).toBe(false);
|
|
expect(logToConsoleInReact).toBe(false);
|
|
expect(console.error).toBeCalledWith(formattedMessage);
|
|
});
|
|
|
|
test('works with a frozen error object', () => {
|
|
const error = Object.freeze(new Error('Some error happened'));
|
|
|
|
ReactFiberErrorDialog.showErrorDialog({
|
|
...capturedErrorDefaults,
|
|
error,
|
|
});
|
|
|
|
let exceptionData;
|
|
|
|
if (__DEV__) {
|
|
expect(logBoxAddException.mock.calls.length).toBe(1);
|
|
exceptionData = logBoxAddException.mock.calls[0][0];
|
|
} else {
|
|
expect(nativeReportException.mock.calls.length).toBe(1);
|
|
exceptionData = nativeReportException.mock.calls[0][0];
|
|
}
|
|
expect(getLineFromFrame(exceptionData.stack[0])).toBe(
|
|
"const error = Object.freeze(new Error('Some error happened'));",
|
|
);
|
|
});
|
|
|
|
test('does not mutate the message', () => {
|
|
const error = new ReferenceError('Some error happened');
|
|
const {message} = error;
|
|
|
|
ReactFiberErrorDialog.showErrorDialog({
|
|
...capturedErrorDefaults,
|
|
error,
|
|
});
|
|
|
|
if (__DEV__) {
|
|
expect(logBoxAddException).toHaveBeenCalled();
|
|
} else {
|
|
expect(nativeReportException).toHaveBeenCalled();
|
|
expect(error.message).toBe(message);
|
|
}
|
|
});
|
|
|
|
test('can safely process the same error multiple times', () => {
|
|
const error = new ReferenceError('Some error happened');
|
|
// Copy all the data we care about before any possible mutation.
|
|
const {message} = error;
|
|
const componentStacks = [
|
|
'\n in A\n in B\n in C',
|
|
'\n in X\n in Y\n in Z',
|
|
];
|
|
for (const componentStack of componentStacks) {
|
|
nativeReportException.mockClear();
|
|
logBoxAddException.mockClear();
|
|
const formattedMessage =
|
|
'ReferenceError: ' +
|
|
message +
|
|
'\n\n' +
|
|
'This error is located at:' +
|
|
componentStack;
|
|
const logToConsoleInReact = ReactFiberErrorDialog.showErrorDialog({
|
|
...capturedErrorDefaults,
|
|
componentStack,
|
|
error,
|
|
});
|
|
|
|
let exceptionData;
|
|
|
|
if (__DEV__) {
|
|
expect(logBoxAddException.mock.calls.length).toBe(1);
|
|
exceptionData = logBoxAddException.mock.calls[0][0];
|
|
} else {
|
|
expect(nativeReportException.mock.calls.length).toBe(1);
|
|
exceptionData = nativeReportException.mock.calls[0][0];
|
|
}
|
|
expect(exceptionData.message).toBe(formattedMessage);
|
|
expect(exceptionData.originalMessage).toBe(message);
|
|
expect(exceptionData.componentStack).toBe(componentStack);
|
|
expect(getLineFromFrame(exceptionData.stack[0])).toBe(
|
|
"const error = new ReferenceError('Some error happened');",
|
|
);
|
|
expect(exceptionData.isFatal).toBe(false);
|
|
expect(logToConsoleInReact).toBe(false);
|
|
expect(console.error).toBeCalledWith(formattedMessage);
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('console.error handler', () => {
|
|
let mockError;
|
|
beforeEach(() => {
|
|
// NOTE: We initialise a fresh mock every time using spyOn, above.
|
|
// We can't use `console._errorOriginal` for this, because that's a bound
|
|
// (=wrapped) version of the mock and Jest does not approve.
|
|
mockError = console.error;
|
|
ExceptionsManager.installConsoleErrorReporter();
|
|
});
|
|
|
|
afterEach(() => {
|
|
// There is no uninstallConsoleErrorReporter. Do this so the next install
|
|
// works.
|
|
console.error = console._errorOriginal;
|
|
delete console._errorOriginal;
|
|
delete console.reportErrorsAsExceptions;
|
|
});
|
|
|
|
test('logging an Error', () => {
|
|
const error = new Error('Some error happened');
|
|
const {message, name} = error;
|
|
|
|
console.error(error);
|
|
|
|
let exceptionData;
|
|
|
|
if (__DEV__) {
|
|
expect(logBoxAddException.mock.calls.length).toBe(1);
|
|
exceptionData = logBoxAddException.mock.calls[0][0];
|
|
} else {
|
|
expect(nativeReportException.mock.calls.length).toBe(1);
|
|
exceptionData = nativeReportException.mock.calls[0][0];
|
|
}
|
|
const formattedMessage = 'Error: ' + message;
|
|
expect(exceptionData.message).toBe(formattedMessage);
|
|
expect(exceptionData.originalMessage).toBe(message);
|
|
expect(exceptionData.name).toBe(name);
|
|
expect(getLineFromFrame(exceptionData.stack[0])).toBe(
|
|
"const error = new Error('Some error happened');",
|
|
);
|
|
expect(exceptionData.isFatal).toBe(false);
|
|
expect(mockError.mock.calls[0]).toHaveLength(1);
|
|
expect(mockError.mock.calls[0][0]).toBeInstanceOf(Error);
|
|
expect(mockError.mock.calls[0][0].toString()).toBe(formattedMessage);
|
|
});
|
|
|
|
test('logging a string', () => {
|
|
const message = 'Some error happened';
|
|
|
|
console.error(message);
|
|
|
|
let exceptionData;
|
|
|
|
if (__DEV__) {
|
|
expect(logBoxAddException.mock.calls.length).toBe(1);
|
|
exceptionData = logBoxAddException.mock.calls[0][0];
|
|
} else {
|
|
expect(nativeReportException.mock.calls.length).toBe(1);
|
|
exceptionData = nativeReportException.mock.calls[0][0];
|
|
}
|
|
expect(exceptionData.message).toBe(
|
|
'console.error: Some error happened',
|
|
);
|
|
expect(exceptionData.originalMessage).toBe('Some error happened');
|
|
expect(exceptionData.name).toBe('console.error');
|
|
expect(
|
|
getLineFromFrame(getFirstFrameInThisFile(exceptionData.stack)),
|
|
).toBe('console.error(message);');
|
|
expect(exceptionData.isFatal).toBe(false);
|
|
expect(mockError.mock.calls[0]).toEqual([message]);
|
|
});
|
|
|
|
test('logging arbitrary arguments', () => {
|
|
const args = [42, true, Symbol(), {x: undefined, y: null}];
|
|
|
|
console.error(...args);
|
|
|
|
let exceptionData;
|
|
|
|
if (__DEV__) {
|
|
expect(logBoxAddException.mock.calls.length).toBe(1);
|
|
exceptionData = logBoxAddException.mock.calls[0][0];
|
|
} else {
|
|
expect(nativeReportException.mock.calls.length).toBe(1);
|
|
exceptionData = nativeReportException.mock.calls[0][0];
|
|
}
|
|
expect(exceptionData.message).toBe(
|
|
'console.error: 42 true ["symbol" failed to stringify] {"y":null}',
|
|
);
|
|
expect(exceptionData.originalMessage).toBe(
|
|
'42 true ["symbol" failed to stringify] {"y":null}',
|
|
);
|
|
expect(exceptionData.name).toBe('console.error');
|
|
expect(
|
|
getLineFromFrame(getFirstFrameInThisFile(exceptionData.stack)),
|
|
).toBe('console.error(...args);');
|
|
expect(exceptionData.isFatal).toBe(false);
|
|
|
|
expect(mockError).toHaveBeenCalledTimes(1);
|
|
// Shallowly compare the mock call arguments with `args`
|
|
expect(mockError.mock.calls[0]).toHaveLength(args.length);
|
|
for (let i = 0; i < args.length; ++i) {
|
|
expect(mockError.mock.calls[0][i]).toBe(args[i]);
|
|
}
|
|
});
|
|
|
|
test('logging a warning', () => {
|
|
const message = 'Warning: Some mild issue happened';
|
|
|
|
console.error(message);
|
|
|
|
expect(nativeReportException).not.toHaveBeenCalled();
|
|
expect(mockError.mock.calls[0]).toEqual([message]);
|
|
});
|
|
|
|
test('logging a warning with more arguments', () => {
|
|
const args = ['Warning: Some mild issue happened', 42];
|
|
|
|
console.error(...args);
|
|
|
|
expect(nativeReportException).not.toHaveBeenCalled();
|
|
expect(mockError.mock.calls[0]).toEqual(args);
|
|
});
|
|
|
|
test('logging a warning-looking object', () => {
|
|
// Forces `strignifySafe` to invoke `toString()`.
|
|
const object = {
|
|
toString: () => 'Warning: Some error may have happened',
|
|
};
|
|
object.cycle = object;
|
|
|
|
const args = [object];
|
|
|
|
console.error(...args);
|
|
|
|
if (__DEV__) {
|
|
expect(logBoxAddException).toHaveBeenCalled();
|
|
} else {
|
|
expect(nativeReportException).toHaveBeenCalled();
|
|
}
|
|
});
|
|
|
|
test('does not log "warn"-type errors', () => {
|
|
const error = new Error('This is a warning.');
|
|
error.type = 'warn';
|
|
|
|
console.error(error);
|
|
|
|
expect(nativeReportException).not.toHaveBeenCalled();
|
|
});
|
|
|
|
test('reportErrorsAsExceptions = false', () => {
|
|
console.reportErrorsAsExceptions = false;
|
|
const message = 'Some error happened';
|
|
|
|
console.error(message);
|
|
|
|
expect(nativeReportException).not.toHaveBeenCalled();
|
|
expect(mockError.mock.calls[0]).toEqual([message]);
|
|
});
|
|
|
|
test('does not pop frames off the stack with framesToPop', () => {
|
|
function createError() {
|
|
const error = new Error('Some error happened');
|
|
error.framesToPop = 1;
|
|
return error;
|
|
}
|
|
const error = createError();
|
|
|
|
console.error(error);
|
|
|
|
let exceptionData;
|
|
|
|
if (__DEV__) {
|
|
expect(logBoxAddException.mock.calls.length).toBe(1);
|
|
exceptionData = logBoxAddException.mock.calls[0][0];
|
|
} else {
|
|
expect(nativeReportException.mock.calls.length).toBe(1);
|
|
exceptionData = nativeReportException.mock.calls[0][0];
|
|
}
|
|
expect(getLineFromFrame(exceptionData.stack[0])).toBe(
|
|
"const error = new Error('Some error happened');",
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('handleException', () => {
|
|
test('handling a fatal Error', () => {
|
|
const error = new Error('Some error happened');
|
|
const {message} = error;
|
|
|
|
ExceptionsManager.handleException(error, true);
|
|
|
|
let exceptionData;
|
|
|
|
if (__DEV__) {
|
|
expect(logBoxAddException.mock.calls.length).toBe(1);
|
|
exceptionData = logBoxAddException.mock.calls[0][0];
|
|
} else {
|
|
expect(nativeReportException.mock.calls.length).toBe(1);
|
|
exceptionData = nativeReportException.mock.calls[0][0];
|
|
}
|
|
const formattedMessage = 'Error: ' + message;
|
|
expect(exceptionData.message).toBe(formattedMessage);
|
|
expect(exceptionData.originalMessage).toBe(message);
|
|
expect(exceptionData.name).toBe('Error');
|
|
expect(getLineFromFrame(exceptionData.stack[0])).toBe(
|
|
"const error = new Error('Some error happened');",
|
|
);
|
|
expect(exceptionData.isFatal).toBe(true);
|
|
expect(console.error.mock.calls[0]).toHaveLength(1);
|
|
expect(console.error.mock.calls[0][0]).toBe(formattedMessage);
|
|
});
|
|
|
|
test('handling a non-fatal Error', () => {
|
|
const error = new Error('Some error happened');
|
|
const {message} = error;
|
|
|
|
ExceptionsManager.handleException(error, false);
|
|
|
|
let exceptionData;
|
|
|
|
if (__DEV__) {
|
|
expect(logBoxAddException.mock.calls.length).toBe(1);
|
|
exceptionData = logBoxAddException.mock.calls[0][0];
|
|
} else {
|
|
expect(nativeReportException.mock.calls.length).toBe(1);
|
|
exceptionData = nativeReportException.mock.calls[0][0];
|
|
}
|
|
const formattedMessage = 'Error: ' + message;
|
|
expect(exceptionData.message).toBe(formattedMessage);
|
|
expect(exceptionData.originalMessage).toBe(message);
|
|
expect(exceptionData.name).toBe('Error');
|
|
expect(getLineFromFrame(exceptionData.stack[0])).toBe(
|
|
"const error = new Error('Some error happened');",
|
|
);
|
|
expect(exceptionData.isFatal).toBe(false);
|
|
expect(console.error.mock.calls[0]).toHaveLength(1);
|
|
expect(console.error.mock.calls[0][0]).toBe(formattedMessage);
|
|
});
|
|
|
|
test('handling a thrown string', () => {
|
|
const message = 'Some error happened';
|
|
|
|
ExceptionsManager.handleException(message, true);
|
|
|
|
let exceptionData;
|
|
|
|
if (__DEV__) {
|
|
expect(logBoxAddException.mock.calls.length).toBe(1);
|
|
exceptionData = logBoxAddException.mock.calls[0][0];
|
|
} else {
|
|
expect(nativeReportException.mock.calls.length).toBe(1);
|
|
exceptionData = nativeReportException.mock.calls[0][0];
|
|
}
|
|
expect(exceptionData.message).toBe(message);
|
|
expect(exceptionData.originalMessage).toBe(null);
|
|
expect(exceptionData.name).toBe(null);
|
|
expect(exceptionData.stack[0].file).toMatch(/ExceptionsManager\.js$/);
|
|
expect(exceptionData.isFatal).toBe(true);
|
|
expect(console.error.mock.calls[0]).toEqual([message]);
|
|
});
|
|
|
|
test('does not pop frames off the stack with framesToPop', () => {
|
|
function createError() {
|
|
const error = new Error('Some error happened');
|
|
error.framesToPop = 1;
|
|
return error;
|
|
}
|
|
const error = createError();
|
|
|
|
ExceptionsManager.handleException(error, true);
|
|
|
|
let exceptionData;
|
|
|
|
if (__DEV__) {
|
|
expect(logBoxAddException.mock.calls.length).toBe(1);
|
|
exceptionData = logBoxAddException.mock.calls[0][0];
|
|
} else {
|
|
expect(nativeReportException.mock.calls.length).toBe(1);
|
|
exceptionData = nativeReportException.mock.calls[0][0];
|
|
}
|
|
expect(getLineFromFrame(exceptionData.stack[0])).toBe(
|
|
"const error = new Error('Some error happened');",
|
|
);
|
|
});
|
|
|
|
test('logs fatal "warn"-type errors', () => {
|
|
const error = new Error('This is a fatal... warning?');
|
|
error.type = 'warn';
|
|
|
|
ExceptionsManager.handleException(error, true);
|
|
|
|
if (__DEV__) {
|
|
expect(logBoxAddException).toHaveBeenCalled();
|
|
} else {
|
|
expect(nativeReportException).toHaveBeenCalled();
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('unstable_setExceptionDecorator', () => {
|
|
let mockError;
|
|
beforeEach(() => {
|
|
// NOTE: We initialise a fresh mock every time using spyOn, above.
|
|
// We can't use `console._errorOriginal` for this, because that's a bound
|
|
// (=wrapped) version of the mock and Jest does not approve.
|
|
mockError = console.error;
|
|
ExceptionsManager.installConsoleErrorReporter();
|
|
});
|
|
|
|
afterEach(() => {
|
|
// There is no uninstallConsoleErrorReporter. Do this so the next install
|
|
// works.
|
|
console.error = console._errorOriginal;
|
|
delete console._errorOriginal;
|
|
delete console.reportErrorsAsExceptions;
|
|
});
|
|
|
|
test('modifying the exception data', () => {
|
|
const error = new Error('Some error happened');
|
|
const decorator = jest.fn().mockImplementation(data => ({
|
|
...data,
|
|
message: 'decorated: ' + data.message,
|
|
}));
|
|
|
|
// Report the same exception with and without the decorator
|
|
ExceptionsManager.handleException(error, true);
|
|
ExceptionsManager.unstable_setExceptionDecorator(decorator);
|
|
ExceptionsManager.handleException(error, true);
|
|
|
|
expect(decorator.mock.calls.length).toBe(1);
|
|
const beforeDecorator = decorator.mock.calls[0][0];
|
|
|
|
let withoutDecoratorInstalled;
|
|
let afterDecorator;
|
|
|
|
if (__DEV__) {
|
|
expect(logBoxAddException.mock.calls.length).toBe(2);
|
|
withoutDecoratorInstalled = logBoxAddException.mock.calls[0][0];
|
|
afterDecorator = logBoxAddException.mock.calls[1][0];
|
|
} else {
|
|
expect(nativeReportException.mock.calls.length).toBe(2);
|
|
withoutDecoratorInstalled = nativeReportException.mock.calls[0][0];
|
|
afterDecorator = nativeReportException.mock.calls[1][0];
|
|
}
|
|
|
|
expect(afterDecorator.id).toEqual(beforeDecorator.id);
|
|
|
|
// id will change between successive exceptions
|
|
delete withoutDecoratorInstalled.id;
|
|
delete beforeDecorator.id;
|
|
delete afterDecorator.id;
|
|
delete withoutDecoratorInstalled.isComponentError;
|
|
delete afterDecorator.isComponentError;
|
|
|
|
expect(withoutDecoratorInstalled).toEqual(beforeDecorator);
|
|
expect(afterDecorator).toEqual({
|
|
...beforeDecorator,
|
|
message: 'decorated: ' + beforeDecorator.message,
|
|
});
|
|
});
|
|
|
|
test('clearing a decorator', () => {
|
|
const error = new Error('Some error happened');
|
|
const decorator = jest.fn().mockImplementation(data => ({
|
|
...data,
|
|
message: 'decorated: ' + data.message,
|
|
}));
|
|
|
|
ExceptionsManager.unstable_setExceptionDecorator(decorator);
|
|
ExceptionsManager.unstable_setExceptionDecorator(null);
|
|
ExceptionsManager.handleException(error, true);
|
|
|
|
expect(decorator).not.toHaveBeenCalled();
|
|
if (__DEV__) {
|
|
expect(logBoxAddException).toHaveBeenCalled();
|
|
} else {
|
|
expect(nativeReportException).toHaveBeenCalled();
|
|
}
|
|
});
|
|
|
|
test('prevents decorator recursion from error handler', () => {
|
|
const error = new Error('Some error happened');
|
|
const decorator = jest.fn().mockImplementation(data => {
|
|
console.error('Logging an error within the decorator');
|
|
return {
|
|
...data,
|
|
message: 'decorated: ' + data.message,
|
|
};
|
|
});
|
|
|
|
ExceptionsManager.unstable_setExceptionDecorator(decorator);
|
|
ExceptionsManager.handleException(error, true);
|
|
|
|
if (__DEV__) {
|
|
expect(logBoxAddException).toHaveBeenCalledTimes(1);
|
|
expect(logBoxAddException.mock.calls[0][0].message).toMatch(
|
|
/decorated: .*Some error happened/,
|
|
);
|
|
} else {
|
|
expect(nativeReportException).toHaveBeenCalledTimes(1);
|
|
expect(nativeReportException.mock.calls[0][0].message).toMatch(
|
|
/decorated: .*Some error happened/,
|
|
);
|
|
}
|
|
expect(mockError).toHaveBeenCalledTimes(2);
|
|
expect(mockError.mock.calls[0][0]).toMatch(
|
|
/Logging an error within the decorator/,
|
|
);
|
|
expect(mockError.mock.calls[1][0]).toMatch(
|
|
/decorated: .*Some error happened/,
|
|
);
|
|
});
|
|
|
|
test('prevents decorator recursion from console.error', () => {
|
|
const error = new Error('Some error happened');
|
|
const decorator = jest.fn().mockImplementation(data => {
|
|
console.error('Logging an error within the decorator');
|
|
return {
|
|
...data,
|
|
message: 'decorated: ' + data.message,
|
|
};
|
|
});
|
|
|
|
ExceptionsManager.unstable_setExceptionDecorator(decorator);
|
|
console.error(error);
|
|
|
|
if (__DEV__) {
|
|
expect(logBoxAddException).toHaveBeenCalledTimes(2);
|
|
expect(logBoxAddException.mock.calls[0][0].message).toMatch(
|
|
/Logging an error within the decorator/,
|
|
);
|
|
expect(logBoxAddException.mock.calls[1][0].message).toMatch(
|
|
/decorated: .*Some error happened/,
|
|
);
|
|
} else {
|
|
expect(nativeReportException).toHaveBeenCalledTimes(2);
|
|
expect(nativeReportException.mock.calls[0][0].message).toMatch(
|
|
/Logging an error within the decorator/,
|
|
);
|
|
expect(nativeReportException.mock.calls[1][0].message).toMatch(
|
|
/decorated: .*Some error happened/,
|
|
);
|
|
}
|
|
expect(mockError).toHaveBeenCalledTimes(2);
|
|
// console.error calls are chained without exception pre-processing, so decorator doesn't apply
|
|
expect(mockError.mock.calls[0][0].toString()).toMatch(
|
|
/Error: Some error happened/,
|
|
);
|
|
expect(mockError.mock.calls[1][0]).toMatch(
|
|
/Logging an error within the decorator/,
|
|
);
|
|
});
|
|
|
|
test('can handle throwing decorators recursion when exception is thrown', () => {
|
|
const error = new Error('Some error happened');
|
|
const decorator = jest.fn().mockImplementation(data => {
|
|
throw new Error('Throwing an error within the decorator');
|
|
});
|
|
|
|
ExceptionsManager.unstable_setExceptionDecorator(decorator);
|
|
ExceptionsManager.handleException(error, true);
|
|
|
|
if (__DEV__) {
|
|
expect(logBoxAddException).toHaveBeenCalledTimes(1);
|
|
// Exceptions in decorators are ignored and the decorator is not applied
|
|
expect(logBoxAddException.mock.calls[0][0].message).toMatch(
|
|
/Error: Some error happened/,
|
|
);
|
|
} else {
|
|
expect(nativeReportException).toHaveBeenCalledTimes(1);
|
|
// Exceptions in decorators are ignored and the decorator is not applied
|
|
expect(nativeReportException.mock.calls[0][0].message).toMatch(
|
|
/Error: Some error happened/,
|
|
);
|
|
}
|
|
expect(mockError).toHaveBeenCalledTimes(1);
|
|
expect(mockError.mock.calls[0][0]).toMatch(
|
|
/Error: Some error happened/,
|
|
);
|
|
});
|
|
|
|
test('can handle throwing decorators recursion when exception is logged', () => {
|
|
const error = new Error('Some error happened');
|
|
const decorator = jest.fn().mockImplementation(data => {
|
|
throw new Error('Throwing an error within the decorator');
|
|
});
|
|
|
|
ExceptionsManager.unstable_setExceptionDecorator(decorator);
|
|
console.error(error);
|
|
|
|
if (__DEV__) {
|
|
expect(logBoxAddException).toHaveBeenCalledTimes(1);
|
|
// Exceptions in decorators are ignored and the decorator is not applied
|
|
expect(logBoxAddException.mock.calls[0][0].message).toMatch(
|
|
/Error: Some error happened/,
|
|
);
|
|
} else {
|
|
expect(nativeReportException).toHaveBeenCalledTimes(1);
|
|
// Exceptions in decorators are ignored and the decorator is not applied
|
|
expect(nativeReportException.mock.calls[0][0].message).toMatch(
|
|
/Error: Some error happened/,
|
|
);
|
|
}
|
|
expect(mockError).toHaveBeenCalledTimes(1);
|
|
expect(mockError.mock.calls[0][0].toString()).toMatch(
|
|
/Error: Some error happened/,
|
|
);
|
|
});
|
|
});
|
|
describe('Errors with custom extraData', () => {
|
|
test('ExtendedErrors may pass custom extraData using the decoratedExtraDataKey symbol', () => {
|
|
const error = new Error('Some error happened');
|
|
// Annotates the error with some custom extra data.
|
|
error[ExceptionsManager.decoratedExtraDataKey] = {foo: 'bar'};
|
|
ExceptionsManager.handleException(error, true);
|
|
|
|
if (__DEV__) {
|
|
expect(logBoxAddException).toHaveBeenCalledTimes(1);
|
|
expect(logBoxAddException.mock.calls[0][0].extraData?.foo).toBe(
|
|
'bar',
|
|
);
|
|
} else {
|
|
expect(nativeReportException).toHaveBeenCalledTimes(1);
|
|
expect(nativeReportException.mock.calls[0][0].extraData?.foo).toBe(
|
|
'bar',
|
|
);
|
|
}
|
|
});
|
|
});
|
|
});
|
|
}
|
|
const linesByFile = new Map();
|
|
|
|
function getLineFromFrame({lineNumber /* 1-based */, file}) {
|
|
if (file == null) {
|
|
return null;
|
|
}
|
|
const cleanedFile = cleanFileName(file);
|
|
const lines =
|
|
linesByFile.get(cleanedFile) ||
|
|
fs.readFileSync(cleanedFile, 'utf8').split('\n');
|
|
if (!linesByFile.has(cleanedFile)) {
|
|
linesByFile.set(cleanedFile, lines);
|
|
}
|
|
return (lines[lineNumber - 1] || '').trim();
|
|
}
|
|
|
|
function getFirstFrameInThisFile(stack) {
|
|
return stack.find(({file}) => file.endsWith(path.basename(module.filename)));
|
|
}
|
|
|
|
// Works around a parseErrorStack bug involving `new X` stack frames.
|
|
function cleanFileName(file) {
|
|
return file.replace(/^.+? \((?=\/)/, '');
|
|
}
|