diff --git a/packages/react-native/Libraries/Core/__tests__/ExceptionsManager-test.js b/packages/react-native/Libraries/Core/__tests__/ExceptionsManager-test.js index ee4bf4ebfb66..432a7c9765ba 100644 --- a/packages/react-native/Libraries/Core/__tests__/ExceptionsManager-test.js +++ b/packages/react-native/Libraries/Core/__tests__/ExceptionsManager-test.js @@ -97,13 +97,17 @@ function runExceptionsManagerTests() { error, }); + let exceptionData; if (__DEV__) { - expect(nativeReportException.mock.calls.length).toBe(0); - expect(logBoxAddException.mock.calls.length).toBe(1); - return; + expect(nativeReportException).not.toBe(0); + expect(logBoxAddException).toBeCalledTimes(1); + exceptionData = logBoxAddException.mock.calls[0][0]; + } else { + expect(logBoxAddException).not.toBeCalled(); + expect(nativeReportException.mock.calls.length).toBe(1); + exceptionData = nativeReportException.mock.calls[0][0]; } - expect(nativeReportException.mock.calls.length).toBe(1); - const exceptionData = nativeReportException.mock.calls[0][0]; + const formattedMessage = 'ReferenceError: ' + message + @@ -140,15 +144,24 @@ function runExceptionsManagerTests() { let exceptionData; if (__DEV__) { - expect(logBoxAddException.mock.calls.length).toBe(1); + expect(nativeReportException).not.toBeCalled(); + expect(logBoxAddException).toBeCalledTimes(1); exceptionData = logBoxAddException.mock.calls[0][0]; } else { - expect(nativeReportException.mock.calls.length).toBe(1); + expect(logBoxAddException).not.toBeCalled(); + expect(nativeReportException).toBeCalledTimes(1); exceptionData = nativeReportException.mock.calls[0][0]; } + const formattedMessage = + 'Error: ' + + error.message + + '\n\n' + + 'This error is located at:' + + capturedErrorDefaults.componentStack; expect(getLineFromFrame(exceptionData.stack[0])).toBe( "const error = new Error('Some error happened');", ); + expect(console.error).toBeCalledWith(formattedMessage); }); test('adds the JS engine to the message', () => { @@ -165,10 +178,12 @@ function runExceptionsManagerTests() { let exceptionData; if (__DEV__) { - expect(logBoxAddException.mock.calls.length).toBe(1); + expect(nativeReportException).not.toBeCalled(); + expect(logBoxAddException).toBeCalledTimes(1); exceptionData = logBoxAddException.mock.calls[0][0]; } else { - expect(nativeReportException.mock.calls.length).toBe(1); + expect(logBoxAddException).not.toBeCalled(); + expect(nativeReportException).toBeCalledTimes(1); exceptionData = nativeReportException.mock.calls[0][0]; } expect(exceptionData.message).toBe( @@ -202,10 +217,12 @@ function runExceptionsManagerTests() { let exceptionData; if (__DEV__) { - expect(logBoxAddException.mock.calls.length).toBe(1); + expect(nativeReportException).not.toBeCalled(); + expect(logBoxAddException).toBeCalledTimes(1); exceptionData = logBoxAddException.mock.calls[0][0]; } else { - expect(nativeReportException.mock.calls.length).toBe(1); + expect(logBoxAddException).not.toBeCalled(); + expect(nativeReportException).toBeCalledTimes(1); exceptionData = nativeReportException.mock.calls[0][0]; } const formattedMessage = @@ -235,10 +252,12 @@ function runExceptionsManagerTests() { let exceptionData; if (__DEV__) { - expect(logBoxAddException.mock.calls.length).toBe(1); + expect(nativeReportException).not.toBeCalled(); + expect(logBoxAddException).toBeCalledTimes(1); exceptionData = logBoxAddException.mock.calls[0][0]; } else { - expect(nativeReportException.mock.calls.length).toBe(1); + expect(logBoxAddException).not.toBeCalled(); + expect(nativeReportException).toBeCalledTimes(1); exceptionData = nativeReportException.mock.calls[0][0]; } const formattedMessage = @@ -271,15 +290,22 @@ function runExceptionsManagerTests() { let exceptionData; if (__DEV__) { - expect(logBoxAddException.mock.calls.length).toBe(1); + expect(nativeReportException).not.toBeCalled(); + expect(logBoxAddException).toBeCalledTimes(1); exceptionData = logBoxAddException.mock.calls[0][0]; } else { - expect(nativeReportException.mock.calls.length).toBe(1); + expect(logBoxAddException).not.toBeCalled(); + expect(nativeReportException).toBeCalledTimes(1); exceptionData = nativeReportException.mock.calls[0][0]; } expect(getLineFromFrame(exceptionData.stack[0])).toBe( "const error = Object.freeze(new Error('Some error happened'));", ); + + // No component stack. + const formattedMessage = 'Error: ' + error.message; + expect(exceptionData.message).toBe(formattedMessage); + expect(console.error).toBeCalledWith(formattedMessage); }); test('does not mutate the message', () => { @@ -291,12 +317,25 @@ function runExceptionsManagerTests() { error, }); + let exceptionData; if (__DEV__) { - expect(logBoxAddException).toHaveBeenCalled(); + expect(nativeReportException).not.toBeCalled(); + expect(logBoxAddException).toBeCalledTimes(1); + exceptionData = logBoxAddException.mock.calls[0][0]; } else { - expect(nativeReportException).toHaveBeenCalled(); + expect(logBoxAddException).not.toBeCalled(); + expect(nativeReportException).toBeCalledTimes(1); expect(error.message).toBe(message); + 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(console.error).toBeCalledWith(formattedMessage); }); test('can safely process the same error multiple times', () => { @@ -325,10 +364,12 @@ function runExceptionsManagerTests() { let exceptionData; if (__DEV__) { - expect(logBoxAddException.mock.calls.length).toBe(1); + expect(nativeReportException).not.toBeCalled(); + expect(logBoxAddException).toBeCalledTimes(1); exceptionData = logBoxAddException.mock.calls[0][0]; } else { - expect(nativeReportException.mock.calls.length).toBe(1); + expect(logBoxAddException).not.toBeCalled(); + expect(nativeReportException).toBeCalledTimes(1); exceptionData = nativeReportException.mock.calls[0][0]; } expect(exceptionData.message).toBe(formattedMessage); @@ -371,23 +412,26 @@ function runExceptionsManagerTests() { let exceptionData; if (__DEV__) { - expect(logBoxAddException.mock.calls.length).toBe(1); + expect(logBoxAddException).toBeCalledTimes(1); + expect(nativeReportException).not.toBeCalled(); exceptionData = logBoxAddException.mock.calls[0][0]; } else { - expect(nativeReportException.mock.calls.length).toBe(1); + expect(logBoxAddException).not.toBeCalled(); + expect(nativeReportException).toBeCalledTimes(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); } - 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); + expect(mockError.mock.calls[0][0].toString()).toBe('Error: ' + message); }); test('logging a string', () => { @@ -398,21 +442,23 @@ function runExceptionsManagerTests() { let exceptionData; if (__DEV__) { - expect(logBoxAddException.mock.calls.length).toBe(1); + expect(logBoxAddException).toBeCalledTimes(1); + expect(nativeReportException).not.toBeCalled(); exceptionData = logBoxAddException.mock.calls[0][0]; } else { - expect(nativeReportException.mock.calls.length).toBe(1); + expect(logBoxAddException).not.toBeCalled(); + expect(nativeReportException).toBeCalledTimes(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(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]); }); @@ -424,25 +470,27 @@ function runExceptionsManagerTests() { let exceptionData; if (__DEV__) { - expect(logBoxAddException.mock.calls.length).toBe(1); + expect(logBoxAddException).toBeCalledTimes(1); + expect(nativeReportException).not.toBeCalled(); exceptionData = logBoxAddException.mock.calls[0][0]; } else { - expect(nativeReportException.mock.calls.length).toBe(1); + expect(logBoxAddException).not.toBeCalled(); + expect(nativeReportException).toBeCalledTimes(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(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); + expect(mockError).toBeCalledTimes(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) { @@ -454,8 +502,8 @@ function runExceptionsManagerTests() { const message = 'Warning: Some mild issue happened'; console.error(message); - - expect(nativeReportException).not.toHaveBeenCalled(); + expect(logBoxAddException).not.toBeCalled(); + expect(nativeReportException).not.toBeCalled(); expect(mockError.mock.calls[0]).toEqual([message]); }); @@ -464,7 +512,8 @@ function runExceptionsManagerTests() { console.error(...args); - expect(nativeReportException).not.toHaveBeenCalled(); + expect(logBoxAddException).not.toBeCalled(); + expect(nativeReportException).not.toBeCalled(); expect(mockError.mock.calls[0]).toEqual(args); }); @@ -480,9 +529,11 @@ function runExceptionsManagerTests() { console.error(...args); if (__DEV__) { - expect(logBoxAddException).toHaveBeenCalled(); + expect(logBoxAddException).toBeCalledTimes(1); + expect(nativeReportException).not.toBeCalled(); } else { - expect(nativeReportException).toHaveBeenCalled(); + expect(logBoxAddException).not.toBeCalled(); + expect(nativeReportException).toBeCalledTimes(1); } }); @@ -492,7 +543,11 @@ function runExceptionsManagerTests() { console.error(error); - expect(nativeReportException).not.toHaveBeenCalled(); + if (__DEV__) { + expect(logBoxAddException).toBeCalledTimes(1); + } + + expect(nativeReportException).not.toBeCalled(); }); test('reportErrorsAsExceptions = false', () => { @@ -501,7 +556,8 @@ function runExceptionsManagerTests() { console.error(message); - expect(nativeReportException).not.toHaveBeenCalled(); + expect(logBoxAddException).not.toBeCalled(); + expect(nativeReportException).not.toBeCalled(); expect(mockError.mock.calls[0]).toEqual([message]); }); @@ -518,15 +574,19 @@ function runExceptionsManagerTests() { let exceptionData; if (__DEV__) { - expect(logBoxAddException.mock.calls.length).toBe(1); + // In DEV we only send the raw arguments to LogBox. + expect(logBoxAddException).toBeCalledTimes(1); + expect(nativeReportException).not.toBeCalled(); exceptionData = logBoxAddException.mock.calls[0][0]; } else { + expect(logBoxAddException).not.toBeCalled(); 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');", + ); } - expect(getLineFromFrame(exceptionData.stack[0])).toBe( - "const error = new Error('Some error happened');", - ); + expect(mockError.mock.calls[0][0]).toEqual(error); }); }); @@ -540,10 +600,12 @@ function runExceptionsManagerTests() { let exceptionData; if (__DEV__) { - expect(logBoxAddException.mock.calls.length).toBe(1); + expect(nativeReportException).not.toBeCalled(); + expect(logBoxAddException).toBeCalledTimes(1); exceptionData = logBoxAddException.mock.calls[0][0]; } else { - expect(nativeReportException.mock.calls.length).toBe(1); + expect(logBoxAddException).not.toBeCalled(); + expect(nativeReportException).toBeCalledTimes(1); exceptionData = nativeReportException.mock.calls[0][0]; } const formattedMessage = 'Error: ' + message; @@ -567,10 +629,12 @@ function runExceptionsManagerTests() { let exceptionData; if (__DEV__) { - expect(logBoxAddException.mock.calls.length).toBe(1); + expect(nativeReportException).not.toBeCalled(); + expect(logBoxAddException).toBeCalledTimes(1); exceptionData = logBoxAddException.mock.calls[0][0]; } else { - expect(nativeReportException.mock.calls.length).toBe(1); + expect(logBoxAddException).not.toBeCalled(); + expect(nativeReportException).toBeCalledTimes(1); exceptionData = nativeReportException.mock.calls[0][0]; } const formattedMessage = 'Error: ' + message; @@ -593,10 +657,12 @@ function runExceptionsManagerTests() { let exceptionData; if (__DEV__) { - expect(logBoxAddException.mock.calls.length).toBe(1); + expect(nativeReportException).not.toBeCalled(); + expect(logBoxAddException).toBeCalledTimes(1); exceptionData = logBoxAddException.mock.calls[0][0]; } else { - expect(nativeReportException.mock.calls.length).toBe(1); + expect(logBoxAddException).not.toBeCalled(); + expect(nativeReportException).toBeCalledTimes(1); exceptionData = nativeReportException.mock.calls[0][0]; } expect(exceptionData.message).toBe(message); @@ -604,6 +670,7 @@ function runExceptionsManagerTests() { expect(exceptionData.name).toBe(null); expect(exceptionData.stack[0].file).toMatch(/ExceptionsManager\.js$/); expect(exceptionData.isFatal).toBe(true); + expect(console.error.mock.calls[0]).toHaveLength(1); expect(console.error.mock.calls[0]).toEqual([message]); }); @@ -620,15 +687,21 @@ function runExceptionsManagerTests() { let exceptionData; if (__DEV__) { - expect(logBoxAddException.mock.calls.length).toBe(1); + expect(nativeReportException).not.toBeCalled(); + expect(logBoxAddException).toBeCalledTimes(1); exceptionData = logBoxAddException.mock.calls[0][0]; } else { - expect(nativeReportException.mock.calls.length).toBe(1); + expect(logBoxAddException).not.toBeCalled(); + expect(nativeReportException).toBeCalledTimes(1); exceptionData = nativeReportException.mock.calls[0][0]; } expect(getLineFromFrame(exceptionData.stack[0])).toBe( "const error = new Error('Some error happened');", ); + expect(console.error.mock.calls[0]).toHaveLength(1); + expect(console.error.mock.calls[0]).toEqual([ + 'Error: ' + error.message, + ]); }); test('logs fatal "warn"-type errors', () => { @@ -638,10 +711,16 @@ function runExceptionsManagerTests() { ExceptionsManager.handleException(error, true); if (__DEV__) { - expect(logBoxAddException).toHaveBeenCalled(); + expect(nativeReportException).not.toBeCalled(); + expect(logBoxAddException).toBeCalledTimes(1); } else { - expect(nativeReportException).toHaveBeenCalled(); + expect(logBoxAddException).not.toBeCalled(); + expect(nativeReportException).toBeCalledTimes(1); } + expect(console.error.mock.calls[0]).toHaveLength(1); + expect(console.error.mock.calls[0]).toEqual([ + 'Error: ' + error.message, + ]); }); }); @@ -682,11 +761,13 @@ function runExceptionsManagerTests() { let afterDecorator; if (__DEV__) { - expect(logBoxAddException.mock.calls.length).toBe(2); + expect(nativeReportException).not.toBeCalled(); + expect(logBoxAddException).toBeCalledTimes(2); withoutDecoratorInstalled = logBoxAddException.mock.calls[0][0]; afterDecorator = logBoxAddException.mock.calls[1][0]; } else { - expect(nativeReportException.mock.calls.length).toBe(2); + expect(logBoxAddException).not.toBeCalled(); + expect(nativeReportException).toBeCalledTimes(2); withoutDecoratorInstalled = nativeReportException.mock.calls[0][0]; afterDecorator = nativeReportException.mock.calls[1][0]; } @@ -705,6 +786,13 @@ function runExceptionsManagerTests() { ...beforeDecorator, message: 'decorated: ' + beforeDecorator.message, }); + expect(mockError).toBeCalledTimes(2); + expect(mockError.mock.calls[0][0]).toEqual( + 'Error: Some error happened', + ); + expect(mockError.mock.calls[1][0]).toMatch( + 'decorated: Error: Some error happened', + ); }); test('clearing a decorator', () => { @@ -718,12 +806,18 @@ function runExceptionsManagerTests() { ExceptionsManager.unstable_setExceptionDecorator(null); ExceptionsManager.handleException(error, true); - expect(decorator).not.toHaveBeenCalled(); + expect(decorator).not.toBeCalled(); if (__DEV__) { - expect(logBoxAddException).toHaveBeenCalled(); + expect(nativeReportException).not.toBeCalled(); + expect(logBoxAddException).toBeCalledTimes(1); } else { - expect(nativeReportException).toHaveBeenCalled(); + expect(logBoxAddException).not.toBeCalled(); + expect(nativeReportException).toBeCalledTimes(1); } + expect(mockError).toBeCalledTimes(1); + expect(mockError.mock.calls[0][0]).toEqual( + 'Error: Some error happened', + ); }); test('prevents decorator recursion from error handler', () => { @@ -740,17 +834,19 @@ function runExceptionsManagerTests() { ExceptionsManager.handleException(error, true); if (__DEV__) { - expect(logBoxAddException).toHaveBeenCalledTimes(1); + expect(nativeReportException).not.toBeCalled(); + expect(logBoxAddException).toBeCalledTimes(1); expect(logBoxAddException.mock.calls[0][0].message).toMatch( /decorated: .*Some error happened/, ); } else { - expect(nativeReportException).toHaveBeenCalledTimes(1); + expect(logBoxAddException).not.toBeCalled(); + expect(nativeReportException).toBeCalledTimes(1); expect(nativeReportException.mock.calls[0][0].message).toMatch( /decorated: .*Some error happened/, ); } - expect(mockError).toHaveBeenCalledTimes(2); + expect(mockError).toBeCalledTimes(2); expect(mockError.mock.calls[0][0]).toMatch( /Logging an error within the decorator/, ); @@ -774,6 +870,7 @@ function runExceptionsManagerTests() { if (__DEV__) { expect(logBoxAddException).toHaveBeenCalledTimes(2); + expect(nativeReportException).not.toBeCalled(); expect(logBoxAddException.mock.calls[0][0].message).toMatch( /Logging an error within the decorator/, ); @@ -781,7 +878,8 @@ function runExceptionsManagerTests() { /decorated: .*Some error happened/, ); } else { - expect(nativeReportException).toHaveBeenCalledTimes(2); + expect(logBoxAddException).not.toBeCalled(); + expect(nativeReportException).toBeCalledTimes(2); expect(nativeReportException.mock.calls[0][0].message).toMatch( /Logging an error within the decorator/, ); @@ -789,7 +887,7 @@ function runExceptionsManagerTests() { /decorated: .*Some error happened/, ); } - expect(mockError).toHaveBeenCalledTimes(2); + expect(mockError).toBeCalledTimes(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/, @@ -809,19 +907,21 @@ function runExceptionsManagerTests() { ExceptionsManager.handleException(error, true); if (__DEV__) { - expect(logBoxAddException).toHaveBeenCalledTimes(1); + expect(nativeReportException).not.toBeCalled(); + expect(logBoxAddException).toBeCalledTimes(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); + expect(logBoxAddException).not.toBeCalled(); + expect(nativeReportException).toBeCalledTimes(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).toBeCalledTimes(1); expect(mockError.mock.calls[0][0]).toMatch( /Error: Some error happened/, ); @@ -838,18 +938,20 @@ function runExceptionsManagerTests() { if (__DEV__) { expect(logBoxAddException).toHaveBeenCalledTimes(1); + expect(nativeReportException).not.toBeCalled(); // 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); + expect(logBoxAddException).not.toBeCalled(); + expect(nativeReportException).toBeCalledTimes(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).toBeCalledTimes(1); expect(mockError.mock.calls[0][0].toString()).toMatch( /Error: Some error happened/, ); @@ -863,12 +965,14 @@ function runExceptionsManagerTests() { ExceptionsManager.handleException(error, true); if (__DEV__) { - expect(logBoxAddException).toHaveBeenCalledTimes(1); + expect(nativeReportException).not.toBeCalled(); + expect(logBoxAddException).toBeCalledTimes(1); expect(logBoxAddException.mock.calls[0][0].extraData?.foo).toBe( 'bar', ); } else { - expect(nativeReportException).toHaveBeenCalledTimes(1); + expect(logBoxAddException).not.toBeCalled(); + expect(nativeReportException).toBeCalledTimes(1); expect(nativeReportException.mock.calls[0][0].extraData?.foo).toBe( 'bar', ); diff --git a/packages/react-native/Libraries/Core/setUpDeveloperTools.js b/packages/react-native/Libraries/Core/setUpDeveloperTools.js index 5cc39eae9c12..22c1a8ee4e6c 100644 --- a/packages/react-native/Libraries/Core/setUpDeveloperTools.js +++ b/packages/react-native/Libraries/Core/setUpDeveloperTools.js @@ -17,8 +17,6 @@ declare var console: {[string]: $FlowFixMe}; * You can use this module directly, or just require InitializeCore. */ if (__DEV__) { - require('./setUpReactDevTools'); - // Set up inspector const JSInspector = require('../JSInspector/JSInspector'); JSInspector.registerAgent(require('../JSInspector/NetworkAgent')); diff --git a/packages/react-native/Libraries/Core/setUpErrorHandling.js b/packages/react-native/Libraries/Core/setUpErrorHandling.js index ffbfb618b42b..b2518260edf2 100644 --- a/packages/react-native/Libraries/Core/setUpErrorHandling.js +++ b/packages/react-native/Libraries/Core/setUpErrorHandling.js @@ -10,6 +10,11 @@ 'use strict'; +if (__DEV__) { + // React DevTools need to be set up before the console.error patch. + require('./setUpReactDevTools'); +} + if (global.RN$useAlwaysAvailableJSErrorHandling !== true) { /** * Sets up the console and exception handling (redbox) for React Native. diff --git a/packages/react-native/Libraries/LogBox/Data/LogBoxLog.js b/packages/react-native/Libraries/LogBox/Data/LogBoxLog.js index 32cc3323776f..be475215db88 100644 --- a/packages/react-native/Libraries/LogBox/Data/LogBoxLog.js +++ b/packages/react-native/Libraries/LogBox/Data/LogBoxLog.js @@ -79,6 +79,7 @@ class LogBoxLog { count: number; level: LogLevel; codeFrame: ?CodeFrame; + componentCodeFrame: ?CodeFrame; isComponentError: boolean; extraData: mixed | void; symbolicated: @@ -140,8 +141,18 @@ class LogBoxLog { } retrySymbolicate(callback?: (status: SymbolicationStatus) => void): void { + let retry = false; if (this.symbolicated.status !== 'COMPLETE') { LogBoxSymbolication.deleteStack(this.stack); + retry = true; + } + if (this.symbolicatedComponentStack.status !== 'COMPLETE') { + LogBoxSymbolication.deleteStack( + convertComponentStateToStack(this.componentStack), + ); + retry = true; + } + if (retry) { this.handleSymbolicate(callback); } } @@ -153,7 +164,10 @@ class LogBoxLog { } handleSymbolicate(callback?: (status: SymbolicationStatus) => void): void { - if (this.symbolicated.status !== 'PENDING') { + if ( + this.symbolicated.status !== 'PENDING' && + this.symbolicated.status !== 'COMPLETE' + ) { this.updateStatus(null, null, null, callback); LogBoxSymbolication.symbolicate(this.stack, this.extraData).then( data => { @@ -163,25 +177,30 @@ class LogBoxLog { this.updateStatus(error, null, null, callback); }, ); - if (this.componentStack != null && this.componentStackType === 'stack') { - this.updateComponentStackStatus(null, null, null, callback); - const componentStackFrames = convertComponentStateToStack( - this.componentStack, - ); - LogBoxSymbolication.symbolicate(componentStackFrames, []).then( - data => { - this.updateComponentStackStatus( - null, - convertStackToComponentStack(data.stack), - null, - callback, - ); - }, - error => { - this.updateComponentStackStatus(error, null, null, callback); - }, - ); - } + } + if ( + this.componentStack != null && + this.componentStackType === 'stack' && + this.symbolicatedComponentStack.status !== 'PENDING' && + this.symbolicatedComponentStack.status !== 'COMPLETE' + ) { + this.updateComponentStackStatus(null, null, null, callback); + const componentStackFrames = convertComponentStateToStack( + this.componentStack, + ); + LogBoxSymbolication.symbolicate(componentStackFrames, []).then( + data => { + this.updateComponentStackStatus( + null, + convertStackToComponentStack(data.stack), + data?.codeFrame, + callback, + ); + }, + error => { + this.updateComponentStackStatus(error, null, null, callback); + }, + ); } } @@ -235,6 +254,9 @@ class LogBoxLog { status: 'FAILED', }; } else if (componentStack != null) { + if (codeFrame) { + this.componentCodeFrame = codeFrame; + } this.symbolicatedComponentStack = { error: null, componentStack, diff --git a/packages/react-native/Libraries/LogBox/Data/__tests__/LogBoxLog-test.js b/packages/react-native/Libraries/LogBox/Data/__tests__/LogBoxLog-test.js index a6d34be82c3e..364af49122a6 100644 --- a/packages/react-native/Libraries/LogBox/Data/__tests__/LogBoxLog-test.js +++ b/packages/react-native/Libraries/LogBox/Data/__tests__/LogBoxLog-test.js @@ -13,12 +13,36 @@ import type {SymbolicatedStackTrace} from '../../../Core/Devtools/symbolicateStackTrace'; import type {StackFrame} from '../../../Core/NativeExceptionsManager'; +import type {CodeFrame} from '../parseLogBoxLog'; jest.mock('../LogBoxSymbolication', () => { return {__esModule: true, symbolicate: jest.fn(), deleteStack: jest.fn()}; }); -function getLogBoxLog() { +type CodeCodeFrame = $ReadOnly<{ + content: string, + location: ?{ + row: number, + column: number, + ... + }, + fileName: string, +}>; + +const STACK_CODE_FRAME: CodeCodeFrame = { + fileName: '/path/to/Stack.js', + location: {row: 199, column: 0}, + content: '', +}; + +const COMPONENT_CODE_FRAME: CodeCodeFrame = { + fileName: '/path/to/Component.js', + location: {row: 199, column: 0}, + content: 'Component', +}; + +// We can delete this when we delete legacy component stack types. +function getLogBoxLogLegacy() { return new (require('../LogBoxLog').default)({ level: 'warn', isComponentError: false, @@ -40,6 +64,19 @@ function getLogBoxLog() { }); } +function getLogBoxLog() { + return new (require('../LogBoxLog').default)({ + level: 'warn', + isComponentError: false, + message: {content: '...', substitutions: []}, + stack: createStack(['A', 'B', 'C']), + category: 'Message category...', + componentStackType: 'stack', + componentStack: createComponentStack(['A', 'B', 'C']), + codeFrame: null, + }); +} + function getLogBoxSymbolication(): { symbolicate: JestMockFn< $ReadOnlyArray>, @@ -51,12 +88,54 @@ function getLogBoxSymbolication(): { const createStack = (methodNames: Array) => methodNames.map((methodName): StackFrame => ({ - column: null, + column: 0, file: 'file://path/to/file.js', lineNumber: 1, methodName, })); +const createStackForComponentStack = (methodNames: Array) => + methodNames.map((methodName): StackFrame => ({ + column: 0, + file: 'file://path/to/component.js', + lineNumber: 1, + methodName, + })); + +const createComponentStack = (methodNames: Array) => + methodNames.map((methodName): CodeFrame => ({ + collapse: false, + content: methodName, + location: { + row: 1, + column: 0, + }, + fileName: 'file://path/to/component.js', + })); + +function mockSymbolicate( + stack: $ReadOnlyArray, + stackCodeFrame: ?CodeCodeFrame, + componentCodeFrame: ?CodeCodeFrame, +): SymbolicatedStackTrace { + const firstFrame = stack[0]; + if ( + firstFrame != null && + firstFrame.file != null && + firstFrame.file.indexOf('component.js') > 0 + ) { + return { + stack: createStackForComponentStack( + stack.map(frame => `C(${frame.methodName})`), + ), + codeFrame: COMPONENT_CODE_FRAME, + }; + } + return { + stack: createStack(stack.map(frame => `S(${frame.methodName})`)), + codeFrame: STACK_CODE_FRAME, + }; +} // Adds a new task to the end of the microtask queue, so that awaiting this // function will run all queued immediates const runMicrotasks = async () => {}; @@ -65,244 +144,733 @@ describe('LogBoxLog', () => { beforeEach(() => { jest.resetModules(); - getLogBoxSymbolication().symbolicate.mockImplementation(async stack => ({ - stack: createStack(stack.map(frame => `S(${frame.methodName})`)), - codeFrame: null, - })); + getLogBoxSymbolication().symbolicate.mockImplementation(async stack => + mockSymbolicate(stack), + ); }); - it('creates a LogBoxLog object', () => { - const log = getLogBoxLog(); - - expect(log.level).toEqual('warn'); - expect(log.message).toEqual({content: '...', substitutions: []}); - expect(log.stack).toEqual(createStack(['A', 'B', 'C'])); - expect(log.category).toEqual('Message category...'); - expect(log.componentStack).toEqual([ - { - content: 'LogBoxLog', - fileName: 'LogBoxLog.js', - location: {column: -1, row: 1}, - }, - ]); - expect(log.codeFrame).toEqual({ - fileName: '/path/to/RKJSModules/Apps/CrashReact/CrashReactApp.js', - location: {row: 199, column: 0}, - content: '', + describe('symbolicate legacy component stacks (no symbolication)', () => { + it('creates a LogBoxLog object', () => { + const log = getLogBoxLogLegacy(); + + expect(log.level).toEqual('warn'); + expect(log.message).toEqual({content: '...', substitutions: []}); + expect(log.stack).toEqual(createStack(['A', 'B', 'C'])); + expect(log.category).toEqual('Message category...'); + expect(log.componentStack).toEqual([ + { + content: 'LogBoxLog', + fileName: 'LogBoxLog.js', + location: {column: -1, row: 1}, + }, + ]); + expect(log.codeFrame).toEqual({ + fileName: '/path/to/RKJSModules/Apps/CrashReact/CrashReactApp.js', + location: {row: 199, column: 0}, + content: '', + }); }); - }); - it('increments LogBoxLog count', () => { - const log = getLogBoxLog(); + it('increments LogBoxLog count', () => { + const log = getLogBoxLogLegacy(); - expect(log.count).toEqual(1); + expect(log.count).toEqual(1); - log.incrementCount(); + log.incrementCount(); - expect(log.count).toEqual(2); - }); + expect(log.count).toEqual(2); + }); - it('starts without a symbolicated stack', () => { - const log = getLogBoxLog(); + it('starts without a symbolicated stack', () => { + const log = getLogBoxLogLegacy(); - expect(log.symbolicated).toEqual({ - error: null, - stack: null, - status: 'NONE', + expect(log.symbolicated).toEqual({ + error: null, + stack: null, + status: 'NONE', + }); }); - }); - it('updates when symbolication is in progress', () => { - const log = getLogBoxLog(); + it('updates when symbolication is in progress', () => { + const log = getLogBoxLogLegacy(); - const callback = jest.fn(); - log.symbolicate(callback); + const callback = jest.fn(); + log.symbolicate(callback); - expect(callback).toBeCalledTimes(1); - expect(callback).toBeCalledWith('PENDING'); - expect(getLogBoxSymbolication().symbolicate).toBeCalledTimes(1); - expect(log.symbolicated).toEqual({ - error: null, - stack: null, - status: 'PENDING', + expect(callback).toBeCalledTimes(1); + expect(callback).toBeCalledWith('PENDING'); + expect(getLogBoxSymbolication().symbolicate).toBeCalledTimes(1); + expect(log.symbolicated).toEqual({ + error: null, + stack: null, + status: 'PENDING', + }); + + // Symbolicating while pending should not make more requests. + callback.mockClear(); + getLogBoxSymbolication().symbolicate.mockClear(); + + log.symbolicate(callback); + expect(callback).not.toBeCalled(); + expect(getLogBoxSymbolication().symbolicate).not.toBeCalled(); }); - // Symbolicating while pending should not make more requests. - callback.mockClear(); - getLogBoxSymbolication().symbolicate.mockClear(); + it('updates when symbolication finishes', async () => { + const log = getLogBoxLogLegacy(); - log.symbolicate(callback); - expect(callback).not.toBeCalled(); - expect(getLogBoxSymbolication().symbolicate).not.toBeCalled(); - }); + const callback = jest.fn(); + log.symbolicate(callback); + expect(callback).toBeCalledTimes(1); + expect(callback).toBeCalledWith('PENDING'); + expect(getLogBoxSymbolication().symbolicate).toBeCalled(); + + await runMicrotasks(); - it('updates when symbolication finishes', async () => { - const log = getLogBoxLog(); + expect(callback).toBeCalledTimes(2); + expect(callback).toBeCalledWith('COMPLETE'); + expect(log.symbolicated).toEqual({ + error: null, + stack: createStack(['S(A)', 'S(B)', 'S(C)']), + status: 'COMPLETE', + }); - const callback = jest.fn(); - log.symbolicate(callback); - expect(callback).toBeCalledTimes(1); - expect(callback).toBeCalledWith('PENDING'); - expect(getLogBoxSymbolication().symbolicate).toBeCalled(); + // Do not symbolicate again. + callback.mockClear(); + getLogBoxSymbolication().symbolicate.mockClear(); - await runMicrotasks(); + log.symbolicate(callback); - expect(callback).toBeCalledTimes(2); - expect(callback).toBeCalledWith('COMPLETE'); - expect(log.symbolicated).toEqual({ - error: null, - stack: createStack(['S(A)', 'S(B)', 'S(C)']), - status: 'COMPLETE', + await runMicrotasks(); + + expect(callback).toBeCalledTimes(0); + expect(getLogBoxSymbolication().symbolicate).not.toBeCalled(); }); - // Do not symbolicate again. - callback.mockClear(); - getLogBoxSymbolication().symbolicate.mockClear(); + it('updates when symbolication fails', async () => { + const error = new Error('...'); + getLogBoxSymbolication().symbolicate.mockImplementation(async stack => { + throw error; + }); - log.symbolicate(callback); + const log = getLogBoxLogLegacy(); - await runMicrotasks(); + const callback = jest.fn(); + log.symbolicate(callback); + expect(callback).toBeCalledTimes(1); + expect(callback).toBeCalledWith('PENDING'); + expect(getLogBoxSymbolication().symbolicate).toBeCalled(); - expect(callback).toBeCalledTimes(0); - expect(getLogBoxSymbolication().symbolicate).not.toBeCalled(); - }); + await runMicrotasks(); - it('updates when symbolication fails', async () => { - const error = new Error('...'); - getLogBoxSymbolication().symbolicate.mockImplementation(async stack => { - throw error; - }); + expect(callback).toBeCalledTimes(2); + expect(callback).toBeCalledWith('FAILED'); + expect(log.symbolicated).toEqual({ + error, + stack: null, + status: 'FAILED', + }); - const log = getLogBoxLog(); + // Do not symbolicate again, retry if needed. + callback.mockClear(); + getLogBoxSymbolication().symbolicate.mockClear(); - const callback = jest.fn(); - log.symbolicate(callback); - expect(callback).toBeCalledTimes(1); - expect(callback).toBeCalledWith('PENDING'); - expect(getLogBoxSymbolication().symbolicate).toBeCalled(); + log.symbolicate(callback); - await runMicrotasks(); + await runMicrotasks(); - expect(callback).toBeCalledTimes(2); - expect(callback).toBeCalledWith('FAILED'); - expect(log.symbolicated).toEqual({ - error, - stack: null, - status: 'FAILED', + expect(callback).toBeCalledTimes(0); + expect(getLogBoxSymbolication().symbolicate).not.toBeCalled(); }); - // Do not symbolicate again, retry if needed. - callback.mockClear(); - getLogBoxSymbolication().symbolicate.mockClear(); + it('retry updates when symbolication is in progress', () => { + const log = getLogBoxLogLegacy(); - log.symbolicate(callback); + const callback = jest.fn(); + log.retrySymbolicate(callback); - await runMicrotasks(); + expect(callback).toBeCalledTimes(1); + expect(callback).toBeCalledWith('PENDING'); + expect(getLogBoxSymbolication().symbolicate).toBeCalledTimes(1); + expect(log.symbolicated).toEqual({ + error: null, + stack: null, + status: 'PENDING', + }); - expect(callback).toBeCalledTimes(0); - expect(getLogBoxSymbolication().symbolicate).not.toBeCalled(); - }); + // Symbolicating while pending should not make more requests. + callback.mockClear(); + getLogBoxSymbolication().symbolicate.mockClear(); - it('retry updates when symbolication is in progress', () => { - const log = getLogBoxLog(); + log.symbolicate(callback); + expect(callback).not.toBeCalled(); + expect(getLogBoxSymbolication().symbolicate).not.toBeCalled(); + }); - const callback = jest.fn(); - log.retrySymbolicate(callback); + it('retry updates when symbolication finishes', async () => { + const log = getLogBoxLogLegacy(); - expect(callback).toBeCalledTimes(1); - expect(callback).toBeCalledWith('PENDING'); - expect(getLogBoxSymbolication().symbolicate).toBeCalledTimes(1); - expect(log.symbolicated).toEqual({ - error: null, - stack: null, - status: 'PENDING', - }); + const callback = jest.fn(); + log.retrySymbolicate(callback); + expect(callback).toBeCalledTimes(1); + expect(callback).toBeCalledWith('PENDING'); + expect(getLogBoxSymbolication().symbolicate).toBeCalled(); - // Symbolicating while pending should not make more requests. - callback.mockClear(); - getLogBoxSymbolication().symbolicate.mockClear(); + await runMicrotasks(); - log.symbolicate(callback); - expect(callback).not.toBeCalled(); - expect(getLogBoxSymbolication().symbolicate).not.toBeCalled(); - }); + expect(callback).toBeCalledTimes(2); + expect(callback).toBeCalledWith('COMPLETE'); + expect(log.symbolicated).toEqual({ + error: null, + stack: createStack(['S(A)', 'S(B)', 'S(C)']), + status: 'COMPLETE', + }); - it('retry updates when symbolication finishes', async () => { - const log = getLogBoxLog(); + // Do not symbolicate again + callback.mockClear(); + getLogBoxSymbolication().symbolicate.mockClear(); - const callback = jest.fn(); - log.retrySymbolicate(callback); - expect(callback).toBeCalledTimes(1); - expect(callback).toBeCalledWith('PENDING'); - expect(getLogBoxSymbolication().symbolicate).toBeCalled(); + log.retrySymbolicate(callback); + jest.runAllTicks(); - await runMicrotasks(); + expect(callback).toBeCalledTimes(0); + expect(getLogBoxSymbolication().symbolicate).not.toBeCalled(); + }); - expect(callback).toBeCalledTimes(2); - expect(callback).toBeCalledWith('COMPLETE'); - expect(log.symbolicated).toEqual({ - error: null, - stack: createStack(['S(A)', 'S(B)', 'S(C)']), - status: 'COMPLETE', + it('retry updates when symbolication fails', async () => { + const error = new Error('...'); + getLogBoxSymbolication().symbolicate.mockImplementation(async stack => { + throw error; + }); + + const log = getLogBoxLogLegacy(); + + const callback = jest.fn(); + log.retrySymbolicate(callback); + expect(callback).toBeCalledTimes(1); + expect(callback).toBeCalledWith('PENDING'); + expect(getLogBoxSymbolication().symbolicate).toBeCalled(); + + await runMicrotasks(); + + expect(callback).toBeCalledTimes(2); + expect(callback).toBeCalledWith('FAILED'); + expect(log.symbolicated).toEqual({ + error, + stack: null, + status: 'FAILED', + }); + + // Retry to symbolicate again. + callback.mockClear(); + getLogBoxSymbolication().symbolicate.mockClear(); + getLogBoxSymbolication().symbolicate.mockImplementation(async stack => ({ + stack: createStack(stack.map(frame => `S(${frame.methodName})`)), + codeFrame: null, + })); + + log.retrySymbolicate(callback); + + expect(callback).toBeCalledTimes(1); + expect(callback).toBeCalledWith('PENDING'); + expect(getLogBoxSymbolication().symbolicate).toBeCalled(); + + await runMicrotasks(); + + expect(callback).toBeCalledTimes(2); + expect(callback).toBeCalledWith('COMPLETE'); + expect(log.symbolicated).toEqual({ + error: null, + stack: createStack(['S(A)', 'S(B)', 'S(C)']), + status: 'COMPLETE', + }); }); + }); - // Do not symbolicate again - callback.mockClear(); - getLogBoxSymbolication().symbolicate.mockClear(); + describe('symbolicate component stacks', () => { + it('creates a LogBoxLog object', () => { + const log = getLogBoxLog(); + + expect(log.level).toEqual('warn'); + expect(log.message).toEqual({content: '...', substitutions: []}); + expect(log.stack).toEqual(createStack(['A', 'B', 'C'])); + expect(log.category).toEqual('Message category...'); + expect(log.componentStack).toEqual(createComponentStack(['A', 'B', 'C'])); + expect(log.codeFrame).toEqual(null); + expect(log.componentCodeFrame).toEqual(undefined); + }); - log.retrySymbolicate(callback); - jest.runAllTicks(); + it('increments LogBoxLog count', () => { + const log = getLogBoxLog(); - expect(callback).toBeCalledTimes(0); - expect(getLogBoxSymbolication().symbolicate).not.toBeCalled(); - }); + expect(log.count).toEqual(1); + + log.incrementCount(); + + expect(log.count).toEqual(2); + }); - it('retry updates when symbolication fails', async () => { - const error = new Error('...'); - getLogBoxSymbolication().symbolicate.mockImplementation(async stack => { - throw error; + it('starts without a symbolicated stack', () => { + const log = getLogBoxLog(); + + expect(log.symbolicated).toEqual({ + error: null, + stack: null, + status: 'NONE', + }); + expect(log.symbolicatedComponentStack).toEqual({ + error: null, + componentStack: null, + status: 'NONE', + }); }); - const log = getLogBoxLog(); + it('updates when symbolication is in progress', () => { + const log = getLogBoxLog(); + + const callback = jest.fn(); + log.symbolicate(callback); + + expect(callback).toBeCalledTimes(2); + expect(callback.mock.calls[0][0]).toBe('PENDING'); + expect(callback.mock.calls[1][0]).toBe('PENDING'); + expect(getLogBoxSymbolication().symbolicate).toBeCalledTimes(2); + expect(log.symbolicated).toEqual({ + error: null, + stack: null, + status: 'PENDING', + }); + expect(log.symbolicatedComponentStack).toEqual({ + error: null, + componentStack: null, + status: 'PENDING', + }); + + // Symbolicating while pending should not make more requests. + callback.mockClear(); + getLogBoxSymbolication().symbolicate.mockClear(); + + log.symbolicate(callback); + expect(callback).not.toBeCalled(); + expect(getLogBoxSymbolication().symbolicate).not.toBeCalled(); + }); - const callback = jest.fn(); - log.retrySymbolicate(callback); - expect(callback).toBeCalledTimes(1); - expect(callback).toBeCalledWith('PENDING'); - expect(getLogBoxSymbolication().symbolicate).toBeCalled(); + it('updates when symbolication finishes', async () => { + const log = getLogBoxLog(); + + const callback = jest.fn(); + log.symbolicate(callback); + expect(callback).toBeCalledTimes(2); + expect(callback.mock.calls[0][0]).toBe('PENDING'); + expect(callback.mock.calls[1][0]).toBe('PENDING'); + expect(getLogBoxSymbolication().symbolicate).toBeCalledTimes(2); + callback.mockClear(); + + await runMicrotasks(); + + expect(callback).toBeCalledTimes(2); + expect(callback.mock.calls[0][0]).toBe('COMPLETE'); + expect(callback.mock.calls[1][0]).toBe('COMPLETE'); + expect(log.symbolicated).toEqual({ + error: null, + stack: createStack(['S(A)', 'S(B)', 'S(C)']), + status: 'COMPLETE', + }); + expect(log.codeFrame).toBe(STACK_CODE_FRAME); + expect(log.symbolicatedComponentStack).toEqual({ + error: null, + componentStack: createComponentStack(['C(A)', 'C(B)', 'C(C)']), + status: 'COMPLETE', + }); + expect(log.componentCodeFrame).toBe(COMPONENT_CODE_FRAME); + + // Do not symbolicate again. + callback.mockClear(); + getLogBoxSymbolication().symbolicate.mockClear(); + + log.symbolicate(callback); + + await runMicrotasks(); + + expect(callback).toBeCalledTimes(0); + expect(getLogBoxSymbolication().symbolicate).not.toBeCalled(); + }); - await runMicrotasks(); + it('updates when first symbolication fails', async () => { + const error = new Error('...'); + let count = 0; + getLogBoxSymbolication().symbolicate.mockImplementation(async stack => { + count += 1; + if (count === 1) { + throw error; + } + return mockSymbolicate(stack); + }); + + const log = getLogBoxLog(); + + const callback = jest.fn(); + log.symbolicate(callback); + expect(callback).toBeCalledTimes(2); + expect(callback.mock.calls[0][0]).toBe('PENDING'); + expect(callback.mock.calls[1][0]).toBe('PENDING'); + expect(getLogBoxSymbolication().symbolicate).toBeCalled(); + callback.mockClear(); + + await runMicrotasks(); + + expect(callback).toBeCalledTimes(2); + expect(callback.mock.calls[0][0]).toBe('FAILED'); + expect(callback.mock.calls[1][0]).toBe('COMPLETE'); + expect(log.symbolicated).toEqual({ + error, + stack: null, + status: 'FAILED', + }); + expect(log.symbolicatedComponentStack).toEqual({ + error: null, + componentStack: createComponentStack(['C(A)', 'C(B)', 'C(C)']), + status: 'COMPLETE', + }); + + expect(log.componentCodeFrame).toBe(COMPONENT_CODE_FRAME); + + // Do not symbolicate again, retry if needed. + callback.mockClear(); + getLogBoxSymbolication().symbolicate.mockClear(); + + log.symbolicate(callback); + + await runMicrotasks(); + + expect(callback).toBeCalledTimes(0); + expect(getLogBoxSymbolication().symbolicate).not.toBeCalled(); + }); - expect(callback).toBeCalledTimes(2); - expect(callback).toBeCalledWith('FAILED'); - expect(log.symbolicated).toEqual({ - error, - stack: null, - status: 'FAILED', + it('updates when second symbolication fails', async () => { + const error = new Error('...'); + let count = 0; + getLogBoxSymbolication().symbolicate.mockImplementation(async stack => { + count += 1; + if (count === 2) { + throw error; + } + return mockSymbolicate(stack); + }); + + const log = getLogBoxLog(); + + const callback = jest.fn(); + log.symbolicate(callback); + expect(callback).toBeCalledTimes(2); + expect(callback.mock.calls[0][0]).toBe('PENDING'); + expect(callback.mock.calls[1][0]).toBe('PENDING'); + expect(getLogBoxSymbolication().symbolicate).toBeCalled(); + callback.mockClear(); + + await runMicrotasks(); + + expect(callback).toBeCalledTimes(2); + expect(callback.mock.calls[0][0]).toBe('COMPLETE'); + expect(callback.mock.calls[1][0]).toBe('FAILED'); + expect(log.symbolicated).toEqual({ + error: null, + stack: createStack(['S(A)', 'S(B)', 'S(C)']), + status: 'COMPLETE', + }); + expect(log.codeFrame).toBe(STACK_CODE_FRAME); + expect(log.symbolicatedComponentStack).toEqual({ + error, + componentStack: null, + status: 'FAILED', + }); + + // Do not symbolicate again, retry if needed. + callback.mockClear(); + getLogBoxSymbolication().symbolicate.mockClear(); + + log.symbolicate(callback); + + await runMicrotasks(); + + expect(callback).toBeCalledTimes(0); + expect(getLogBoxSymbolication().symbolicate).not.toBeCalled(); }); - // Retry to symbolicate again. - callback.mockClear(); - getLogBoxSymbolication().symbolicate.mockClear(); - getLogBoxSymbolication().symbolicate.mockImplementation(async stack => ({ - stack: createStack(stack.map(frame => `S(${frame.methodName})`)), - codeFrame: null, - })); + it('retry updates when symbolication is in progress', () => { + const log = getLogBoxLog(); + + const callback = jest.fn(); + log.retrySymbolicate(callback); + + expect(callback).toBeCalledTimes(2); + expect(callback.mock.calls[0][0]).toBe('PENDING'); + expect(callback.mock.calls[1][0]).toBe('PENDING'); + expect(getLogBoxSymbolication().symbolicate).toBeCalledTimes(2); + expect(log.symbolicated).toEqual({ + error: null, + stack: null, + status: 'PENDING', + }); + expect(log.symbolicatedComponentStack).toEqual({ + error: null, + componentStack: null, + status: 'PENDING', + }); + + // Symbolicating while pending should not make more requests. + callback.mockClear(); + getLogBoxSymbolication().symbolicate.mockClear(); + + log.symbolicate(callback); + expect(callback).not.toBeCalled(); + expect(getLogBoxSymbolication().symbolicate).not.toBeCalled(); + }); - log.retrySymbolicate(callback); + it('retry updates when symbolication finishes', async () => { + const log = getLogBoxLog(); + + const callback = jest.fn(); + log.retrySymbolicate(callback); + expect(callback).toBeCalledTimes(2); + expect(callback.mock.calls[0][0]).toBe('PENDING'); + expect(callback.mock.calls[1][0]).toBe('PENDING'); + expect(getLogBoxSymbolication().symbolicate).toBeCalledTimes(2); + callback.mockClear(); + + await runMicrotasks(); + + expect(callback).toBeCalledTimes(2); + expect(callback.mock.calls[0][0]).toBe('COMPLETE'); + expect(callback.mock.calls[1][0]).toBe('COMPLETE'); + expect(log.symbolicated).toEqual({ + error: null, + stack: createStack(['S(A)', 'S(B)', 'S(C)']), + status: 'COMPLETE', + }); + expect(log.codeFrame).toBe(STACK_CODE_FRAME); + expect(log.symbolicatedComponentStack).toEqual({ + error: null, + componentStack: createComponentStack(['C(A)', 'C(B)', 'C(C)']), + status: 'COMPLETE', + }); + expect(log.componentCodeFrame).toBe(COMPONENT_CODE_FRAME); + + // Do not symbolicate again + callback.mockClear(); + getLogBoxSymbolication().symbolicate.mockClear(); + + log.retrySymbolicate(callback); + jest.runAllTicks(); + + expect(callback).toBeCalledTimes(0); + expect(getLogBoxSymbolication().symbolicate).not.toBeCalled(); + }); - expect(callback).toBeCalledTimes(1); - expect(callback).toBeCalledWith('PENDING'); - expect(getLogBoxSymbolication().symbolicate).toBeCalled(); + it('retry updates when both symbolications fail', async () => { + const error = new Error('...'); + getLogBoxSymbolication().symbolicate.mockImplementation(async stack => { + throw error; + }); + + const log = getLogBoxLog(); + + const callback = jest.fn(); + log.retrySymbolicate(callback); + expect(callback).toBeCalledTimes(2); + expect(callback.mock.calls[0][0]).toBe('PENDING'); + expect(callback.mock.calls[1][0]).toBe('PENDING'); + expect(getLogBoxSymbolication().symbolicate).toBeCalledTimes(2); + callback.mockClear(); + + await runMicrotasks(); + + expect(callback).toBeCalledTimes(2); + expect(callback.mock.calls[0][0]).toBe('FAILED'); + expect(callback.mock.calls[1][0]).toBe('FAILED'); + expect(log.symbolicated).toEqual({ + error, + stack: null, + status: 'FAILED', + }); + expect(log.symbolicatedComponentStack).toEqual({ + error, + componentStack: null, + status: 'FAILED', + }); + + // Retry to symbolicate again. + callback.mockClear(); + getLogBoxSymbolication().symbolicate.mockClear(); + getLogBoxSymbolication().symbolicate.mockImplementation(async stack => + mockSymbolicate(stack), + ); + + log.retrySymbolicate(callback); + + expect(callback).toBeCalledTimes(2); + expect(callback.mock.calls[0][0]).toBe('PENDING'); + expect(callback.mock.calls[1][0]).toBe('PENDING'); + expect(getLogBoxSymbolication().symbolicate).toBeCalledTimes(2); + callback.mockClear(); + + await runMicrotasks(); + + expect(callback.mock.calls[0][0]).toBe('COMPLETE'); + expect(callback.mock.calls[1][0]).toBe('COMPLETE'); + expect(log.symbolicated).toEqual({ + error: null, + stack: createStack(['S(A)', 'S(B)', 'S(C)']), + status: 'COMPLETE', + }); + expect(log.codeFrame).toBe(STACK_CODE_FRAME); + expect(log.symbolicatedComponentStack).toEqual({ + error: null, + componentStack: createComponentStack(['C(A)', 'C(B)', 'C(C)']), + status: 'COMPLETE', + }); + expect(log.componentCodeFrame).toBe(COMPONENT_CODE_FRAME); + }); - await runMicrotasks(); + it('retry updates when stack symbolication fails', async () => { + const error = new Error('...'); + let count = 0; + getLogBoxSymbolication().symbolicate.mockImplementation(async stack => { + count += 1; + if (count === 1) { + throw error; + } + return mockSymbolicate(stack); + }); + + const log = getLogBoxLog(); + + const callback = jest.fn(); + log.retrySymbolicate(callback); + expect(callback).toBeCalledTimes(2); + expect(callback.mock.calls[0][0]).toBe('PENDING'); + expect(callback.mock.calls[1][0]).toBe('PENDING'); + expect(getLogBoxSymbolication().symbolicate).toBeCalledTimes(2); + callback.mockClear(); + + await runMicrotasks(); + + expect(callback).toBeCalledTimes(2); + expect(callback.mock.calls[0][0]).toBe('FAILED'); + expect(callback.mock.calls[1][0]).toBe('COMPLETE'); + expect(log.symbolicated).toEqual({ + error, + stack: null, + status: 'FAILED', + }); + expect(log.symbolicatedComponentStack).toEqual({ + error: null, + componentStack: createComponentStack(['C(A)', 'C(B)', 'C(C)']), + status: 'COMPLETE', + }); + expect(log.componentCodeFrame).toBe(COMPONENT_CODE_FRAME); + + // Retry to symbolicate again. + callback.mockClear(); + getLogBoxSymbolication().symbolicate.mockClear(); + getLogBoxSymbolication().symbolicate.mockImplementation(async stack => + mockSymbolicate(stack), + ); + + log.retrySymbolicate(callback); + + // Since only one symbolication failed, we should only have one pending. + expect(callback).toBeCalledTimes(1); + expect(callback.mock.calls[0][0]).toBe('PENDING'); + expect(getLogBoxSymbolication().symbolicate).toBeCalledTimes(1); + callback.mockClear(); + + await runMicrotasks(); + + expect(callback).toBeCalledTimes(1); + expect(callback.mock.calls[0][0]).toBe('COMPLETE'); + expect(log.symbolicated).toEqual({ + error: null, + stack: createStack(['S(A)', 'S(B)', 'S(C)']), + status: 'COMPLETE', + }); + expect(log.codeFrame).toBe(STACK_CODE_FRAME); + expect(log.symbolicatedComponentStack).toEqual({ + error: null, + componentStack: createComponentStack(['C(A)', 'C(B)', 'C(C)']), + status: 'COMPLETE', + }); + expect(log.componentCodeFrame).toBe(COMPONENT_CODE_FRAME); + }); - expect(callback).toBeCalledTimes(2); - expect(callback).toBeCalledWith('COMPLETE'); - expect(log.symbolicated).toEqual({ - error: null, - stack: createStack(['S(A)', 'S(B)', 'S(C)']), - status: 'COMPLETE', + it('retry updates when component symbolication fails', async () => { + const error = new Error('...'); + let count = 0; + getLogBoxSymbolication().symbolicate.mockImplementation(async stack => { + count += 1; + if (count === 2) { + throw error; + } + return mockSymbolicate(stack); + }); + + const log = getLogBoxLog(); + + const callback = jest.fn(); + log.retrySymbolicate(callback); + expect(callback).toBeCalledTimes(2); + expect(callback.mock.calls[0][0]).toBe('PENDING'); + expect(callback.mock.calls[1][0]).toBe('PENDING'); + expect(getLogBoxSymbolication().symbolicate).toBeCalledTimes(2); + callback.mockClear(); + + await runMicrotasks(); + + expect(callback).toBeCalledTimes(2); + expect(callback.mock.calls[0][0]).toBe('COMPLETE'); + expect(callback.mock.calls[1][0]).toBe('FAILED'); + expect(log.symbolicated).toEqual({ + error: null, + stack: createStack(['S(A)', 'S(B)', 'S(C)']), + status: 'COMPLETE', + }); + expect(log.codeFrame).toBe(STACK_CODE_FRAME); + expect(log.symbolicatedComponentStack).toEqual({ + error, + componentStack: null, + status: 'FAILED', + }); + + // Retry to symbolicate again. + callback.mockClear(); + getLogBoxSymbolication().symbolicate.mockClear(); + getLogBoxSymbolication().symbolicate.mockImplementation(async stack => + mockSymbolicate(stack), + ); + + log.retrySymbolicate(callback); + + // Since only one symbolication failed, we should only have one pending. + expect(callback).toBeCalledTimes(1); + expect(callback.mock.calls[0][0]).toBe('PENDING'); + expect(getLogBoxSymbolication().symbolicate).toBeCalledTimes(1); + callback.mockClear(); + + await runMicrotasks(); + + expect(callback).toBeCalledTimes(1); + expect(callback.mock.calls[0][0]).toBe('COMPLETE'); + expect(log.symbolicated).toEqual({ + error: null, + stack: createStack(['S(A)', 'S(B)', 'S(C)']), + status: 'COMPLETE', + }); + expect(log.codeFrame).toBe(STACK_CODE_FRAME); + expect(log.symbolicatedComponentStack).toEqual({ + error: null, + componentStack: createComponentStack(['C(A)', 'C(B)', 'C(C)']), + status: 'COMPLETE', + }); + expect(log.componentCodeFrame).toBe(COMPONENT_CODE_FRAME); }); }); }); diff --git a/packages/react-native/Libraries/LogBox/LogBox.js b/packages/react-native/Libraries/LogBox/LogBox.js index 473f1e1a5b21..eb329b4e67dc 100644 --- a/packages/react-native/Libraries/LogBox/LogBox.js +++ b/packages/react-native/Libraries/LogBox/LogBox.js @@ -14,6 +14,7 @@ import type {ExtendedExceptionData} from './Data/parseLogBoxLog'; import Platform from '../Utilities/Platform'; import RCTLog from '../Utilities/RCTLog'; import {hasComponentStack} from './Data/parseLogBoxLog'; +import * as React from 'react'; export type {LogData, ExtendedExceptionData, IgnorePattern}; @@ -192,9 +193,21 @@ if (__DEV__) { } try { + let stack; + // $FlowFixMe[prop-missing] Not added to flow types yet. + if (!hasComponentStack(args) && React.captureOwnerStack != null) { + stack = React.captureOwnerStack(); + if (!hasComponentStack(args)) { + console.log('hit'); + if (stack !== '') { + args[0] = args[0] += '%s'; + args.push(stack); + } + } + } if (!isWarningModuleWarning(...args) && !hasComponentStack(args)) { // Only show LogBox for the 'warning' module, or React errors with - // component stacks, otherwise pass the error through.u + // component stacks, otherwise pass the error through. // // By passing through, this will get picked up by the React console override, // potentially adding the component stack. React then passes it back to the diff --git a/packages/react-native/Libraries/LogBox/UI/LogBoxInspectorBody.js b/packages/react-native/Libraries/LogBox/UI/LogBoxInspectorBody.js index 65af7fb2cdde..f9059fa82d15 100644 --- a/packages/react-native/Libraries/LogBox/UI/LogBoxInspectorBody.js +++ b/packages/react-native/Libraries/LogBox/UI/LogBoxInspectorBody.js @@ -52,7 +52,10 @@ export default function LogBoxInspectorBody(props: { title={headerTitle} /> - + @@ -68,7 +71,10 @@ export default function LogBoxInspectorBody(props: { level={props.log.level} title={headerTitle} /> - + diff --git a/packages/react-native/Libraries/LogBox/UI/LogBoxInspectorCodeFrame.js b/packages/react-native/Libraries/LogBox/UI/LogBoxInspectorCodeFrame.js index 8061eaace4ab..0b4606cfd192 100644 --- a/packages/react-native/Libraries/LogBox/UI/LogBoxInspectorCodeFrame.js +++ b/packages/react-native/Libraries/LogBox/UI/LogBoxInspectorCodeFrame.js @@ -22,16 +22,13 @@ import LogBoxButton from './LogBoxButton'; import LogBoxInspectorSection from './LogBoxInspectorSection'; import * as LogBoxStyle from './LogBoxStyle'; import * as React from 'react'; + type Props = $ReadOnly<{ + componentCodeFrame: ?CodeFrame, codeFrame: ?CodeFrame, }>; -function LogBoxInspectorCodeFrame(props: Props): React.Node { - const codeFrame = props.codeFrame; - if (codeFrame == null) { - return null; - } - +function CodeFrameDisplay({codeFrame}: {codeFrame: CodeFrame}): React.Node { function getFileName() { // $FlowFixMe[incompatible-use] const matches = /[^/]*$/.exec(codeFrame.fileName); @@ -56,30 +53,52 @@ function LogBoxInspectorCodeFrame(props: Props): React.Node { } return ( - }> - - - - - - - { - openFileInEditor(codeFrame.fileName, codeFrame.location?.row ?? 0); - }}> - - {getFileName()} - {getLocation()} - - + + + + + + { + openFileInEditor(codeFrame.fileName, codeFrame.location?.row ?? 0); + }}> + + {getFileName()} + {getLocation()} + + + + ); +} + +function LogBoxInspectorCodeFrame(props: Props): React.Node { + const {codeFrame, componentCodeFrame} = props; + let sources = []; + if (codeFrame != null) { + sources.push(codeFrame); + } + if ( + componentCodeFrame != null && + componentCodeFrame?.content !== codeFrame?.content + ) { + sources.push(componentCodeFrame); + } + if (sources.length === 0) { + return null; + } + return ( + 1 ? 'Sources' : 'Source'} + action={}> + {sources.map((frame, index) => ( + + ))} ); } diff --git a/packages/react-native/Libraries/LogBox/UI/__tests__/LogBoxInspectorCodeFrame-test.js b/packages/react-native/Libraries/LogBox/UI/__tests__/LogBoxInspectorCodeFrame-test.js index 302fb4508b08..a9a643088151 100644 --- a/packages/react-native/Libraries/LogBox/UI/__tests__/LogBoxInspectorCodeFrame-test.js +++ b/packages/react-native/Libraries/LogBox/UI/__tests__/LogBoxInspectorCodeFrame-test.js @@ -37,7 +37,7 @@ jest.mock('../LogBoxInspectorSection', () => ({ describe('LogBoxInspectorCodeFrame', () => { it('should render null for no code frame', async () => { const output = await render.create( - , + , ); expect(output).toMatchSnapshot(); @@ -46,6 +46,7 @@ describe('LogBoxInspectorCodeFrame', () => { it('should render a code frame', async () => { const output = await render.create( { expect(output).toMatchSnapshot(); }); + it('should render both a code frame and a component frame', async () => { + const output = await render.create( + 91 | return ; + | ^ + 92 | } + 93 | + 94 |`, + location: {row: 90, column: 10}, + fileName: '/path/to/RKJSModules/Apps/CrashReact/CrashReactApp.js', + }} + codeFrame={{ + fileName: '/path/to/RKJSModules/Apps/CrashReact/CrashReactApp.js', + location: {row: 64, column: 16}, + content: ` 62 | + 63 | function ConsoleWithThrow() { +> 64 | console.error('hit'); + | ^ + 65 | throw new Error('test'); + 66 | } + 67 |`, + }} + />, + ); + + expect(output).toMatchSnapshot(); + }); + + it('should dedupe if code frames are the same', async () => { + const output = await render.create( + 65 | throw new Error('test'); + | ^ + 66 | } + 67 | + 68 |`, + location: {row: 65, column: 18}, + fileName: '/path/to/RKJSModules/Apps/CrashReact/CrashReactApp.js', + }} + codeFrame={{ + content: ` 63 | function ConsoleWithThrow() { + 64 | console.error('hit'); +> 65 | throw new Error('test'); + | ^ + 66 | } + 67 | + 68 |`, + location: {row: 65, column: 18}, + fileName: '/path/to/RKJSModules/Apps/CrashReact/CrashReactApp.js', + }} + />, + ); + + expect(output).toMatchSnapshot(); + }); + it('should render a code frame without a location', async () => { const output = await render.create( } + heading="Source" +> + + + + + + + + + CrashReactApp.js + (65:19) + + + + +`; + exports[`LogBoxInspectorCodeFrame should render a code frame 1`] = ` } @@ -173,4 +262,174 @@ exports[`LogBoxInspectorCodeFrame should render a code frame without a location `; +exports[`LogBoxInspectorCodeFrame should render both a code frame and a component frame 1`] = ` +} + heading="Sources" +> + + + + + + + + + CrashReactApp.js + (64:17) + + + + + + + + + + + + CrashReactApp.js + (90:11) + + + + +`; + exports[`LogBoxInspectorCodeFrame should render null for no code frame 1`] = `null`; diff --git a/packages/react-native/Libraries/__tests__/__snapshots__/public-api-test.js.snap b/packages/react-native/Libraries/__tests__/__snapshots__/public-api-test.js.snap index c5c4237e7182..713b5063274f 100644 --- a/packages/react-native/Libraries/__tests__/__snapshots__/public-api-test.js.snap +++ b/packages/react-native/Libraries/__tests__/__snapshots__/public-api-test.js.snap @@ -6066,6 +6066,7 @@ declare class LogBoxLog { count: number; level: LogLevel; codeFrame: ?CodeFrame; + componentCodeFrame: ?CodeFrame; isComponentError: boolean; extraData: mixed | void; symbolicated: @@ -6259,6 +6260,7 @@ exports[`public API should not change unintentionally Libraries/LogBox/UI/LogBox exports[`public API should not change unintentionally Libraries/LogBox/UI/LogBoxInspectorCodeFrame.js 1`] = ` "type Props = $ReadOnly<{ + componentCodeFrame: ?CodeFrame, codeFrame: ?CodeFrame, }>; declare function LogBoxInspectorCodeFrame(props: Props): React.Node;