main
js 382 lines 9.13 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 * @jest-environment node
9 */
10 'use strict';
11
12 let React;
13 let Suspense;
14 let Activity;
15 let ViewTransition;
16 let ReactNoop;
17 let waitForAll;
18
19 describe('ReactFragment', () => {
20 let didCatchErrors = [];
21 let rootCaughtErrors = [];
22 let SomethingThatErrors;
23 let CatchingBoundary;
24 let onCaughtError;
25
26 beforeEach(function () {
27 jest.resetModules();
28
29 React = require('react');
30 Suspense = React.Suspense;
31 Activity = React.Activity;
32 ViewTransition = React.ViewTransition;
33 ReactNoop = require('react-noop-renderer');
34 const InternalTestUtils = require('internal-test-utils');
35 waitForAll = InternalTestUtils.waitForAll;
36
37 didCatchErrors = [];
38 rootCaughtErrors = [];
39
40 onCaughtError = function (error, errorInfo) {
41 rootCaughtErrors.push(
42 error.message,
43 normalizeCodeLocInfo(errorInfo.componentStack),
44 React.captureOwnerStack
45 ? normalizeCodeLocInfo(React.captureOwnerStack())
46 : null,
47 );
48 };
49
50 SomethingThatErrors = () => {
51 throw new Error('uh oh');
52 };
53
54 // eslint-disable-next-line no-shadow
55 CatchingBoundary = class CatchingBoundary extends React.Component {
56 constructor() {
57 super();
58 this.state = {};
59 }
60
61 static getDerivedStateFromError(error) {
62 return {errored: true};
63 }
64
65 componentDidCatch(err, errInfo) {
66 didCatchErrors.push(
67 err.message,
68 normalizeCodeLocInfo(errInfo.componentStack),
69 );
70 }
71
72 render() {
73 if (this.state.errored) {
74 return null;
75 }
76 return this.props.children;
77 }
78 };
79 });
80
81 function componentStack(components) {
82 return components
83 .map(component => `\n in ${component} (at **)`)
84 .join('');
85 }
86
87 function normalizeCodeLocInfo(str) {
88 return (
89 str &&
90 str.replace(/\n +(?:at|in) ([^\(]+) [^\n]*/g, function (m, name) {
91 return '\n in ' + name + ' (at **)';
92 })
93 );
94 }
95
96 it('retains component and owner stacks when rethrowing an error', async () => {
97 class RethrowingBoundary extends React.Component {
98 static getDerivedStateFromError(error) {
99 throw error;
100 }
101
102 render() {
103 return this.props.children;
104 }
105 }
106
107 function Foo() {
108 return (
109 <RethrowingBoundary>
110 <Bar />
111 </RethrowingBoundary>
112 );
113 }
114 function Bar() {
115 return <SomethingThatErrors />;
116 }
117
118 ReactNoop.createRoot({
119 onCaughtError,
120 }).render(
121 <CatchingBoundary>
122 <Foo />
123 </CatchingBoundary>,
124 );
125 await waitForAll([]);
126 expect(didCatchErrors).toEqual([
127 'uh oh',
128 componentStack([
129 'SomethingThatErrors',
130 'Bar',
131 'RethrowingBoundary',
132 'Foo',
133 'CatchingBoundary',
134 ]),
135 ]);
136 expect(rootCaughtErrors).toEqual([
137 'uh oh',
138 componentStack([
139 'SomethingThatErrors',
140 'Bar',
141 'RethrowingBoundary',
142 'Foo',
143 'CatchingBoundary',
144 ]),
145 __DEV__ ? componentStack(['Bar', 'Foo']) : null,
146 ]);
147 });
148
149 it('includes built-in for Suspense', async () => {
150 ReactNoop.createRoot({
151 onCaughtError,
152 }).render(
153 <CatchingBoundary>
154 <Suspense>
155 <SomethingThatErrors />
156 </Suspense>
157 </CatchingBoundary>,
158 );
159 await waitForAll([]);
160 expect(didCatchErrors).toEqual([
161 'uh oh',
162 componentStack(['SomethingThatErrors', 'Suspense', 'CatchingBoundary']),
163 ]);
164 expect(rootCaughtErrors).toEqual([
165 'uh oh',
166 componentStack(['SomethingThatErrors', 'Suspense', 'CatchingBoundary']),
167 __DEV__ ? componentStack(['SomethingThatErrors']) : null,
168 ]);
169 });
170
171 it('includes built-in for Suspense fallbacks', async () => {
172 const SomethingThatSuspends = React.lazy(() => {
173 return new Promise(() => {});
174 });
175
176 ReactNoop.createRoot({
177 onCaughtError,
178 }).render(
179 <CatchingBoundary>
180 <Suspense fallback={<SomethingThatErrors />}>
181 <SomethingThatSuspends />
182 </Suspense>
183 </CatchingBoundary>,
184 );
185 await waitForAll([]);
186 expect(didCatchErrors).toEqual([
187 'uh oh',
188 componentStack([
189 'SomethingThatErrors',
190 'Suspense Fallback',
191 'CatchingBoundary',
192 ]),
193 ]);
194 expect(rootCaughtErrors).toEqual([
195 'uh oh',
196 componentStack([
197 'SomethingThatErrors',
198 'Suspense Fallback',
199 'CatchingBoundary',
200 ]),
201 __DEV__ ? componentStack(['SomethingThatErrors']) : null,
202 ]);
203 });
204
205 it('includes built-in for Activity', async () => {
206 ReactNoop.createRoot({
207 onCaughtError,
208 }).render(
209 <CatchingBoundary>
210 <Activity>
211 <SomethingThatErrors />
212 </Activity>
213 </CatchingBoundary>,
214 );
215 await waitForAll([]);
216 expect(didCatchErrors).toEqual([
217 'uh oh',
218 componentStack(['SomethingThatErrors', 'Activity', 'CatchingBoundary']),
219 ]);
220 expect(rootCaughtErrors).toEqual([
221 'uh oh',
222 componentStack(['SomethingThatErrors', 'Activity', 'CatchingBoundary']),
223 __DEV__ ? componentStack(['SomethingThatErrors']) : null,
224 ]);
225 });
226
227 // @gate enableViewTransition
228 it('includes built-in for ViewTransition', async () => {
229 ReactNoop.createRoot({
230 onCaughtError,
231 }).render(
232 <CatchingBoundary>
233 <ViewTransition>
234 <SomethingThatErrors />
235 </ViewTransition>
236 </CatchingBoundary>,
237 );
238 await waitForAll([]);
239 expect(didCatchErrors).toEqual([
240 'uh oh',
241 componentStack([
242 'SomethingThatErrors',
243 'ViewTransition',
244 'CatchingBoundary',
245 ]),
246 ]);
247 expect(rootCaughtErrors).toEqual([
248 'uh oh',
249 componentStack([
250 'SomethingThatErrors',
251 'ViewTransition',
252 'CatchingBoundary',
253 ]),
254 __DEV__ ? componentStack(['SomethingThatErrors']) : null,
255 ]);
256 });
257
258 it('includes built-in for Lazy', async () => {
259 // Lazy component throws
260 const LazyComponent = React.lazy(() => {
261 throw new Error('uh oh');
262 });
263
264 ReactNoop.createRoot({
265 onCaughtError,
266 }).render(
267 <CatchingBoundary>
268 <LazyComponent />
269 </CatchingBoundary>,
270 );
271 await waitForAll([]);
272 expect(didCatchErrors).toEqual([
273 'uh oh',
274 componentStack(['Lazy', 'CatchingBoundary']),
275 ]);
276 expect(rootCaughtErrors).toEqual([
277 'uh oh',
278 componentStack(['Lazy', 'CatchingBoundary']),
279 __DEV__ ? '' : null, // No owner stack
280 ]);
281 });
282
283 // @gate enableSuspenseList
284 it('includes built-in for SuspenseList', async () => {
285 const SuspenseList = React.unstable_SuspenseList;
286
287 ReactNoop.createRoot({
288 onCaughtError,
289 }).render(
290 <CatchingBoundary>
291 <SuspenseList revealOrder="independent">
292 <SomethingThatErrors />
293 </SuspenseList>
294 </CatchingBoundary>,
295 );
296 await waitForAll([]);
297 expect(didCatchErrors).toEqual([
298 'uh oh',
299 componentStack([
300 'SomethingThatErrors',
301 'SuspenseList',
302 'CatchingBoundary',
303 ]),
304 ]);
305 expect(rootCaughtErrors).toEqual([
306 'uh oh',
307 componentStack([
308 'SomethingThatErrors',
309 'SuspenseList',
310 'CatchingBoundary',
311 ]),
312 __DEV__ ? componentStack(['SomethingThatErrors']) : null,
313 ]);
314 });
315
316 it('does not include built-in for Fragment', async () => {
317 ReactNoop.createRoot({
318 onCaughtError,
319 }).render(
320 <CatchingBoundary>
321 <>
322 <SomethingThatErrors />
323 </>
324 </CatchingBoundary>,
325 );
326 await waitForAll([]);
327 expect(didCatchErrors).toEqual([
328 'uh oh',
329 componentStack([
330 'SomethingThatErrors',
331 // No Fragment
332 'CatchingBoundary',
333 ]),
334 ]);
335 expect(rootCaughtErrors).toEqual([
336 'uh oh',
337 componentStack([
338 'SomethingThatErrors',
339 // No Fragment
340 'CatchingBoundary',
341 ]),
342 __DEV__ ? componentStack(['SomethingThatErrors']) : null,
343 ]);
344 });
345
346 it('captures class component stacks via the no-Reflect.construct fallback', async () => {
347 class FailingClassComponent extends React.Component {
348 render() {
349 throw new Error('uh oh');
350 }
351 }
352
353 const root = ReactNoop.createRoot({onCaughtError});
354
355 const originalReflectConstruct = Reflect.construct;
356 delete Reflect.construct;
357
358 try {
359 root.render(
360 <CatchingBoundary>
361 <FailingClassComponent />
362 </CatchingBoundary>,
363 );
364 await waitForAll([]);
365 } finally {
366 Object.defineProperty(Reflect, 'construct', {
367 value: originalReflectConstruct,
368 });
369 }
370
371 expect(rootCaughtErrors).toEqual([
372 'uh oh',
373 componentStack(['FailingClassComponent', 'CatchingBoundary']),
374 __DEV__ ? componentStack(['FailingClassComponent']) : null,
375 ]);
376
377 // The throwing trap should be reset, we use props object for it
378 expect(
379 Object.getOwnPropertyDescriptor(FailingClassComponent.prototype, 'props'),
380 ).toBeUndefined();
381 });
382 });