react-native-macos/Libraries/Core/__tests__/ExceptionsManager-test.js

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(/^.+? \((?=\/)/, '');
}