diff --git a/packages/react-native/Libraries/LogBox/__tests__/LogBox-integration-test.js b/packages/react-native/Libraries/LogBox/__tests__/LogBox-integration-test.js index 801f31f315dac5..ad08d2305e2296 100644 --- a/packages/react-native/Libraries/LogBox/__tests__/LogBox-integration-test.js +++ b/packages/react-native/Libraries/LogBox/__tests__/LogBox-integration-test.js @@ -11,15 +11,18 @@ import { DoesNotUseKey, FragmentWithProp, + ManualConsoleError, + ManualConsoleErrorWithStack, } from './__fixtures__/ReactWarningFixtures'; import * as React from 'react'; const LogBoxData = require('../Data/LogBoxData'); const TestRenderer = require('react-test-renderer'); +const ExceptionsManager = require('react-native/Libraries/Core/ExceptionsManager.js'); + const installLogBox = () => { const LogBox = require('../LogBox').default; - LogBox.install(); }; @@ -46,6 +49,23 @@ const cleanLog = logs => { }); }; +const cleanError = logs => { + return logs.map(log => { + return { + ...log, + extraData: {redacted: true}, + stack: [], + componentStack: + log.componentStack == null + ? null + : log.componentStack.map(stack => ({ + ...stack, + fileName: cleanPath(stack.fileName), + })), + }; + }); +}; + // TODO(T71117418): Re-enable skipped LogBox integration tests once React component // stack frames are the same internally and in open source. // eslint-disable-next-line jest/no-disabled-tests @@ -60,6 +80,10 @@ describe('LogBox', () => { mockError.mockClear(); mockWarn.mockClear(); + // Reset ExceptionManager patching. + if (console._errorOriginal) { + console._errorOriginal = null; + } (console: any).error = mockError; (console: any).warn = mockWarn; }); @@ -128,4 +152,79 @@ describe('LogBox', () => { // The Warning: prefix is added due to a hack in LogBox to prevent double logging. expect(mockError.mock.calls[0][0].startsWith('Warning: ')).toBe(true); }); + + it('handles a manual console.error without a component stack in LogBox', () => { + const LogBox = require('../LogBox').default; + const spy = jest.spyOn(LogBox, 'addException'); + installLogBox(); + + // console.error handling depends on installing the ExceptionsManager error reporter. + ExceptionsManager.installConsoleErrorReporter(); + + // Spy console.error after LogBox is installed + // so we can assert on what React logs. + jest.spyOn(console, 'error'); + + const output = TestRenderer.create(); + + // Manual console errors should show a collapsed error dialog. + // When there is no component stack, we expect these errors to: + // - Go to the LogBox patch and fall through to console.error. + // - Get picked up by the ExceptionsManager console.error override. + // - Get passed back to LogBox via addException (non-fatal). + expect(output).toBeDefined(); + expect(mockWarn).not.toBeCalled(); + expect(console.error).toBeCalledTimes(1); + expect(console.error.mock.calls[0].map(cleanPath)).toMatchSnapshot( + 'Log sent from React', + ); + expect(spy).toBeCalledTimes(1); + expect(cleanError(spy.mock.calls[0])).toMatchSnapshot( + 'Log added to LogBox', + ); + expect(mockError.mock.calls[0].map(cleanPath)).toMatchSnapshot( + 'Log passed to console error', + ); + + // Doesn't call console.error again + expect(console.error).toBeCalledTimes(1); + }); + + it('handles a manual console.error with a component stack in LogBox', () => { + const spy = jest.spyOn(LogBoxData, 'addLog'); + installLogBox(); + + // console.error handling depends on installing the ExceptionsManager error reporter. + ExceptionsManager.installConsoleErrorReporter(); + + // Spy console.error after LogBox is installed + // so we can assert on what React logs. + jest.spyOn(console, 'error'); + + const output = TestRenderer.create(); + + // Manual console errors should show a collapsed error dialog. + // When there is a component stack, we expect these errors to: + // - Go to the LogBox patch and be detected as a React error. + // - Check the warning filter to see if there is a fiter setting. + // - Call console.error with the parsed error. + // - Get picked up by ExceptionsManager console.error override. + // - Log to console.error. + expect(output).toBeDefined(); + expect(mockWarn).not.toBeCalled(); + expect(console.error).toBeCalledTimes(1); + expect(console.error.mock.calls[0].map(cleanPath)).toMatchSnapshot( + 'Log sent from React', + ); + expect(spy).toBeCalledTimes(1); + expect(cleanError(spy.mock.calls[0])).toMatchSnapshot( + 'Log added to LogBox', + ); + expect(mockError.mock.calls[0].map(cleanPath)).toMatchSnapshot( + 'Log passed to console error', + ); + + // Doesn't call console.error again + expect(console.error).toBeCalledTimes(1); + }); }); diff --git a/packages/react-native/Libraries/LogBox/__tests__/__fixtures__/ReactWarningFixtures.js b/packages/react-native/Libraries/LogBox/__tests__/__fixtures__/ReactWarningFixtures.js index 51b85b1fbe1aef..2d13e41dfe0683 100644 --- a/packages/react-native/Libraries/LogBox/__tests__/__fixtures__/ReactWarningFixtures.js +++ b/packages/react-native/Libraries/LogBox/__tests__/__fixtures__/ReactWarningFixtures.js @@ -30,3 +30,27 @@ export const FragmentWithProp = () => { ); }; + +export const ManualConsoleError = () => { + console.error('Manual console error'); + return ( + + {['foo', 'bar'].map(item => ( + {item} + ))} + + ); +}; + +export const ManualConsoleErrorWithStack = () => { + console.error( + 'Manual console error\n at ManualConsoleErrorWithStack (/path/to/ManualConsoleErrorWithStack:30:175)\n at TestApp', + ); + return ( + + {['foo', 'bar'].map(item => ( + {item} + ))} + + ); +}; diff --git a/packages/react-native/Libraries/LogBox/__tests__/__snapshots__/LogBox-integration-test.js.snap b/packages/react-native/Libraries/LogBox/__tests__/__snapshots__/LogBox-integration-test.js.snap index 1d4392ca588d32..f45ff5cd082266 100644 --- a/packages/react-native/Libraries/LogBox/__tests__/__snapshots__/LogBox-integration-test.js.snap +++ b/packages/react-native/Libraries/LogBox/__tests__/__snapshots__/LogBox-integration-test.js.snap @@ -1,5 +1,80 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`LogBox handles a manual console.error with a component stack in LogBox: Log added to LogBox 1`] = ` +Array [ + Object { + "category": "Warning: Manual console error", + "componentStack": Array [ + Object { + "collapse": false, + "content": "ManualConsoleErrorWithStack", + "fileName": "/path/to/ManualConsoleErrorWithStack", + "location": Object { + "column": 174, + "row": 30, + }, + }, + ], + "componentStackType": "stack", + "extraData": Object { + "redacted": true, + }, + "level": "warn", + "message": Object { + "content": "Warning: Manual console error", + "substitutions": Array [], + }, + "stack": Array [], + }, +] +`; + +exports[`LogBox handles a manual console.error with a component stack in LogBox: Log passed to console error 1`] = ` +Array [ + "Warning: Manual console error + at ManualConsoleErrorWithStack (/path/to/ManualConsoleErrorWithStack:30:175) + at TestApp", +] +`; + +exports[`LogBox handles a manual console.error with a component stack in LogBox: Log sent from React 1`] = ` +Array [ + "Manual console error + at ManualConsoleErrorWithStack (/path/to/ManualConsoleErrorWithStack:30:175) + at TestApp", +] +`; + +exports[`LogBox handles a manual console.error without a component stack in LogBox: Log added to LogBox 1`] = ` +Array [ + Object { + "componentStack": null, + "extraData": Object { + "redacted": true, + }, + "id": 1, + "isComponentError": false, + "isFatal": false, + "message": "console.error: Manual console error", + "name": "console.error", + "originalMessage": "Manual console error", + "stack": Array [], + }, +] +`; + +exports[`LogBox handles a manual console.error without a component stack in LogBox: Log passed to console error 1`] = ` +Array [ + "Manual console error", +] +`; + +exports[`LogBox handles a manual console.error without a component stack in LogBox: Log sent from React 1`] = ` +Array [ + "Manual console error", +] +`; + exports[`LogBox integrates with React and handles a fragment warning in LogBox: Log added to LogBox 1`] = ` Array [ Object {