main
js 229 lines 5.76 KB
Raw
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 */
9
10 'use strict';
11
12 let React;
13 let ReactDOMClient;
14 let Scheduler;
15 let container;
16 let act;
17
18 async function fakeAct(cb) {
19 // We don't use act/waitForThrow here because we want to observe how errors are reported for real.
20 await cb();
21 Scheduler.unstable_flushAll();
22 }
23
24 describe('ReactConfigurableErrorLogging', () => {
25 beforeEach(() => {
26 jest.resetModules();
27 React = require('react');
28 ReactDOMClient = require('react-dom/client');
29 Scheduler = require('scheduler');
30 container = document.createElement('div');
31 if (__DEV__) {
32 act = React.act;
33 }
34 });
35
36 it('should log errors that occur during the begin phase', async () => {
37 class ErrorThrowingComponent extends React.Component {
38 constructor(props) {
39 super(props);
40 throw new Error('constructor error');
41 }
42 render() {
43 return <div />;
44 }
45 }
46 const uncaughtErrors = [];
47 const caughtErrors = [];
48 const root = ReactDOMClient.createRoot(container, {
49 onUncaughtError(error, errorInfo) {
50 uncaughtErrors.push(error, errorInfo);
51 },
52 onCaughtError(error, errorInfo) {
53 caughtErrors.push(error, errorInfo);
54 },
55 });
56 await fakeAct(() => {
57 root.render(
58 <div>
59 <span>
60 <ErrorThrowingComponent />
61 </span>
62 </div>,
63 );
64 });
65
66 expect(uncaughtErrors).toEqual([
67 expect.objectContaining({
68 message: 'constructor error',
69 }),
70 expect.objectContaining({
71 componentStack: expect.stringMatching(
72 new RegExp(
73 '\\s+(in|at) ErrorThrowingComponent (.*)\n' +
74 '\\s+(in|at) span(.*)\n' +
75 '\\s+(in|at) div(.*)',
76 ),
77 ),
78 }),
79 ]);
80 expect(caughtErrors).toEqual([]);
81 });
82
83 it('should log errors that occur during the commit phase', async () => {
84 class ErrorThrowingComponent extends React.Component {
85 componentDidMount() {
86 throw new Error('componentDidMount error');
87 }
88 render() {
89 return <div />;
90 }
91 }
92 const uncaughtErrors = [];
93 const caughtErrors = [];
94 const root = ReactDOMClient.createRoot(container, {
95 onUncaughtError(error, errorInfo) {
96 uncaughtErrors.push(error, errorInfo);
97 },
98 onCaughtError(error, errorInfo) {
99 caughtErrors.push(error, errorInfo);
100 },
101 });
102 await fakeAct(() => {
103 root.render(
104 <div>
105 <span>
106 <ErrorThrowingComponent />
107 </span>
108 </div>,
109 );
110 });
111
112 expect(uncaughtErrors).toEqual([
113 expect.objectContaining({
114 message: 'componentDidMount error',
115 }),
116 expect.objectContaining({
117 componentStack: expect.stringMatching(
118 new RegExp(
119 '\\s+(in|at) ErrorThrowingComponent (.*)\n' +
120 '\\s+(in|at) span(.*)\n' +
121 '\\s+(in|at) div(.*)',
122 ),
123 ),
124 }),
125 ]);
126 expect(caughtErrors).toEqual([]);
127 });
128
129 it('should ignore errors thrown in log method to prevent cycle', async () => {
130 class ErrorBoundary extends React.Component {
131 state = {error: null};
132 componentDidCatch(error) {
133 this.setState({error});
134 }
135 render() {
136 return this.state.error ? null : this.props.children;
137 }
138 }
139 class ErrorThrowingComponent extends React.Component {
140 render() {
141 throw new Error('render error');
142 }
143 }
144
145 const uncaughtErrors = [];
146 const caughtErrors = [];
147 const root = ReactDOMClient.createRoot(container, {
148 onUncaughtError(error, errorInfo) {
149 uncaughtErrors.push(error, errorInfo);
150 },
151 onCaughtError(error, errorInfo) {
152 caughtErrors.push(error, errorInfo);
153 throw new Error('onCaughtError error');
154 },
155 });
156
157 const ref = React.createRef();
158
159 await fakeAct(() => {
160 root.render(
161 <div>
162 <ErrorBoundary ref={ref}>
163 <span>
164 <ErrorThrowingComponent />
165 </span>
166 </ErrorBoundary>
167 </div>,
168 );
169 });
170
171 expect(uncaughtErrors).toEqual([]);
172 expect(caughtErrors).toEqual([
173 expect.objectContaining({
174 message: 'render error',
175 }),
176 expect.objectContaining({
177 componentStack: expect.stringMatching(
178 new RegExp(
179 '\\s+(in|at) ErrorThrowingComponent (.*)\n' +
180 '\\s+(in|at) span(.*)\n' +
181 '\\s+(in|at) ErrorBoundary(.*)\n' +
182 '\\s+(in|at) div(.*)',
183 ),
184 ),
185 errorBoundary: ref.current,
186 }),
187 ]);
188
189 // The error thrown in caughtError should be rethrown with a clean stack
190 expect(() => {
191 jest.runAllTimers();
192 }).toThrow('onCaughtError error');
193 });
194
195 it('does not log errors when inside real act', async () => {
196 function ErrorThrowingComponent() {
197 throw new Error('render error');
198 }
199 const uncaughtErrors = [];
200 const caughtErrors = [];
201 const root = ReactDOMClient.createRoot(container, {
202 onUncaughtError(error, errorInfo) {
203 uncaughtErrors.push(error, errorInfo);
204 },
205 onCaughtError(error, errorInfo) {
206 caughtErrors.push(error, errorInfo);
207 },
208 });
209
210 if (__DEV__) {
211 global.IS_REACT_ACT_ENVIRONMENT = true;
212
213 await expect(async () => {
214 await act(() => {
215 root.render(
216 <div>
217 <span>
218 <ErrorThrowingComponent />
219 </span>
220 </div>,
221 );
222 });
223 }).rejects.toThrow('render error');
224 }
225
226 expect(uncaughtErrors).toEqual([]);
227 expect(caughtErrors).toEqual([]);
228 });
229 });