1. /**
    
  2.  * Copyright (c) Meta Platforms, Inc. and affiliates.
    
  3.  *
    
  4.  * This source code is licensed under the MIT license found in the
    
  5.  * LICENSE file in the root directory of this source tree.
    
  6.  *
    
  7.  * @emails react-core
    
  8.  * @jest-environment node
    
  9.  */
    
  10. 
    
  11. 'use strict';
    
  12. 
    
  13. let React;
    
  14. let ReactNoop;
    
  15. let Scheduler;
    
  16. let waitForAll;
    
  17. let waitForThrow;
    
  18. 
    
  19. describe('ReactIncrementalErrorLogging', () => {
    
  20.   beforeEach(() => {
    
  21.     jest.resetModules();
    
  22.     React = require('react');
    
  23.     ReactNoop = require('react-noop-renderer');
    
  24.     Scheduler = require('scheduler');
    
  25. 
    
  26.     const InternalTestUtils = require('internal-test-utils');
    
  27.     waitForAll = InternalTestUtils.waitForAll;
    
  28.     waitForThrow = InternalTestUtils.waitForThrow;
    
  29.   });
    
  30. 
    
  31.   // Note: in this test file we won't be using toErrorDev() matchers
    
  32.   // because they filter out precisely the messages we want to test for.
    
  33.   let oldConsoleError;
    
  34.   beforeEach(() => {
    
  35.     oldConsoleError = console.error;
    
  36.     console.error = jest.fn();
    
  37.   });
    
  38. 
    
  39.   afterEach(() => {
    
  40.     console.error = oldConsoleError;
    
  41.     oldConsoleError = null;
    
  42.   });
    
  43. 
    
  44.   it('should log errors that occur during the begin phase', async () => {
    
  45.     class ErrorThrowingComponent extends React.Component {
    
  46.       constructor(props) {
    
  47.         super(props);
    
  48.         throw new Error('constructor error');
    
  49.       }
    
  50.       render() {
    
  51.         return <div />;
    
  52.       }
    
  53.     }
    
  54.     ReactNoop.render(
    
  55.       <div>
    
  56.         <span>
    
  57.           <ErrorThrowingComponent />
    
  58.         </span>
    
  59.       </div>,
    
  60.     );
    
  61.     await waitForThrow('constructor error');
    
  62.     expect(console.error).toHaveBeenCalledTimes(1);
    
  63.     expect(console.error).toHaveBeenCalledWith(
    
  64.       __DEV__
    
  65.         ? expect.stringMatching(
    
  66.             new RegExp(
    
  67.               'The above error occurred in the <ErrorThrowingComponent> component:\n' +
    
  68.                 '\\s+(in|at) ErrorThrowingComponent (.*)\n' +
    
  69.                 '\\s+(in|at) span(.*)\n' +
    
  70.                 '\\s+(in|at) div(.*)\n\n' +
    
  71.                 'Consider adding an error boundary to your tree ' +
    
  72.                 'to customize error handling behavior\\.',
    
  73.             ),
    
  74.           )
    
  75.         : expect.objectContaining({
    
  76.             message: 'constructor error',
    
  77.           }),
    
  78.     );
    
  79.   });
    
  80. 
    
  81.   it('should log errors that occur during the commit phase', async () => {
    
  82.     class ErrorThrowingComponent extends React.Component {
    
  83.       componentDidMount() {
    
  84.         throw new Error('componentDidMount error');
    
  85.       }
    
  86.       render() {
    
  87.         return <div />;
    
  88.       }
    
  89.     }
    
  90.     ReactNoop.render(
    
  91.       <div>
    
  92.         <span>
    
  93.           <ErrorThrowingComponent />
    
  94.         </span>
    
  95.       </div>,
    
  96.     );
    
  97.     await waitForThrow('componentDidMount error');
    
  98.     expect(console.error).toHaveBeenCalledTimes(1);
    
  99.     expect(console.error).toHaveBeenCalledWith(
    
  100.       __DEV__
    
  101.         ? expect.stringMatching(
    
  102.             new RegExp(
    
  103.               'The above error occurred in the <ErrorThrowingComponent> component:\n' +
    
  104.                 '\\s+(in|at) ErrorThrowingComponent (.*)\n' +
    
  105.                 '\\s+(in|at) span(.*)\n' +
    
  106.                 '\\s+(in|at) div(.*)\n\n' +
    
  107.                 'Consider adding an error boundary to your tree ' +
    
  108.                 'to customize error handling behavior\\.',
    
  109.             ),
    
  110.           )
    
  111.         : expect.objectContaining({
    
  112.             message: 'componentDidMount error',
    
  113.           }),
    
  114.     );
    
  115.   });
    
  116. 
    
  117.   it('should ignore errors thrown in log method to prevent cycle', async () => {
    
  118.     const logCapturedErrorCalls = [];
    
  119.     console.error.mockImplementation(error => {
    
  120.       // Test what happens when logging itself is buggy.
    
  121.       logCapturedErrorCalls.push(error);
    
  122.       throw new Error('logCapturedError error');
    
  123.     });
    
  124.     class ErrorThrowingComponent extends React.Component {
    
  125.       render() {
    
  126.         throw new Error('render error');
    
  127.       }
    
  128.     }
    
  129.     ReactNoop.render(
    
  130.       <div>
    
  131.         <span>
    
  132.           <ErrorThrowingComponent />
    
  133.         </span>
    
  134.       </div>,
    
  135.     );
    
  136.     await waitForThrow('render error');
    
  137.     expect(logCapturedErrorCalls.length).toBe(1);
    
  138.     expect(logCapturedErrorCalls[0]).toEqual(
    
  139.       __DEV__
    
  140.         ? expect.stringMatching(
    
  141.             new RegExp(
    
  142.               'The above error occurred in the <ErrorThrowingComponent> component:\n' +
    
  143.                 '\\s+(in|at) ErrorThrowingComponent (.*)\n' +
    
  144.                 '\\s+(in|at) span(.*)\n' +
    
  145.                 '\\s+(in|at) div(.*)\n\n' +
    
  146.                 'Consider adding an error boundary to your tree ' +
    
  147.                 'to customize error handling behavior\\.',
    
  148.             ),
    
  149.           )
    
  150.         : expect.objectContaining({
    
  151.             message: 'render error',
    
  152.           }),
    
  153.     );
    
  154.     // The error thrown in logCapturedError should be rethrown with a clean stack
    
  155.     expect(() => {
    
  156.       jest.runAllTimers();
    
  157.     }).toThrow('logCapturedError error');
    
  158.   });
    
  159. 
    
  160.   it('resets instance variables before unmounting failed node', async () => {
    
  161.     class ErrorBoundary extends React.Component {
    
  162.       state = {error: null};
    
  163.       componentDidCatch(error) {
    
  164.         this.setState({error});
    
  165.       }
    
  166.       render() {
    
  167.         return this.state.error ? null : this.props.children;
    
  168.       }
    
  169.     }
    
  170.     class Foo extends React.Component {
    
  171.       state = {step: 0};
    
  172.       componentDidMount() {
    
  173.         this.setState({step: 1});
    
  174.       }
    
  175.       componentWillUnmount() {
    
  176.         Scheduler.log('componentWillUnmount: ' + this.state.step);
    
  177.       }
    
  178.       render() {
    
  179.         Scheduler.log('render: ' + this.state.step);
    
  180.         if (this.state.step > 0) {
    
  181.           throw new Error('oops');
    
  182.         }
    
  183.         return null;
    
  184.       }
    
  185.     }
    
  186. 
    
  187.     ReactNoop.render(
    
  188.       <ErrorBoundary>
    
  189.         <Foo />
    
  190.       </ErrorBoundary>,
    
  191.     );
    
  192.     await waitForAll(
    
  193.       [
    
  194.         'render: 0',
    
  195. 
    
  196.         'render: 1',
    
  197.         __DEV__ && 'render: 1', // replay due to invokeGuardedCallback
    
  198. 
    
  199.         // Retry one more time before handling error
    
  200.         'render: 1',
    
  201.         __DEV__ && 'render: 1', // replay due to invokeGuardedCallback
    
  202. 
    
  203.         'componentWillUnmount: 0',
    
  204.       ].filter(Boolean),
    
  205.     );
    
  206. 
    
  207.     expect(console.error).toHaveBeenCalledTimes(1);
    
  208.     expect(console.error).toHaveBeenCalledWith(
    
  209.       __DEV__
    
  210.         ? expect.stringMatching(
    
  211.             new RegExp(
    
  212.               'The above error occurred in the <Foo> component:\n' +
    
  213.                 '\\s+(in|at) Foo (.*)\n' +
    
  214.                 '\\s+(in|at) ErrorBoundary (.*)\n\n' +
    
  215.                 'React will try to recreate this component tree from scratch ' +
    
  216.                 'using the error boundary you provided, ErrorBoundary.',
    
  217.             ),
    
  218.           )
    
  219.         : expect.objectContaining({
    
  220.             message: 'oops',
    
  221.           }),
    
  222.     );
    
  223.   });
    
  224. });