main
js 586 lines 16.3 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
8 'use strict';
9
10 describe('ReactDOMConsoleErrorReporting', () => {
11 let act;
12 let React;
13 let ReactDOM;
14
15 let ErrorBoundary;
16 let NoError;
17 let container;
18 let windowOnError;
19 let waitForThrow;
20
21 beforeEach(() => {
22 jest.resetModules();
23 act = require('internal-test-utils').act;
24 React = require('react');
25 ReactDOM = require('react-dom');
26
27 const InternalTestUtils = require('internal-test-utils');
28 waitForThrow = InternalTestUtils.waitForThrow;
29
30 ErrorBoundary = class extends React.Component {
31 state = {error: null};
32 static getDerivedStateFromError(error) {
33 return {error};
34 }
35 render() {
36 if (this.state.error) {
37 return <h1>Caught: {this.state.error.message}</h1>;
38 }
39 return this.props.children;
40 }
41 };
42 NoError = function () {
43 return <h1>OK</h1>;
44 };
45 container = document.createElement('div');
46 document.body.appendChild(container);
47 windowOnError = jest.fn();
48 window.addEventListener('error', windowOnError);
49 spyOnDevAndProd(console, 'error');
50 spyOnDevAndProd(console, 'warn');
51 });
52
53 afterEach(() => {
54 document.body.removeChild(container);
55 window.removeEventListener('error', windowOnError);
56 jest.restoreAllMocks();
57 });
58
59 describe('ReactDOM.render', () => {
60 // @gate !disableLegacyMode
61 it('logs errors during event handlers', async () => {
62 function Foo() {
63 return (
64 <button
65 onClick={() => {
66 throw Error('Boom');
67 }}>
68 click me
69 </button>
70 );
71 }
72
73 await act(() => {
74 ReactDOM.render(<Foo />, container);
75 });
76
77 await expect(async () => {
78 await act(() => {
79 container.firstChild.dispatchEvent(
80 new MouseEvent('click', {
81 bubbles: true,
82 }),
83 );
84 });
85 }).rejects.toThrow(
86 expect.objectContaining({
87 message: 'Boom',
88 }),
89 );
90
91 // Reported because we're in a browser click event:
92 expect(windowOnError.mock.calls).toEqual([
93 [
94 expect.objectContaining({
95 message: 'Boom',
96 }),
97 ],
98 ]);
99 expect(console.warn).not.toHaveBeenCalled();
100
101 if (__DEV__) {
102 expect(console.error.mock.calls).toEqual([
103 [
104 expect.stringContaining(
105 'ReactDOM.render has not been supported since React 18',
106 ),
107 ],
108 ]);
109 } else {
110 expect(console.error).not.toHaveBeenCalled();
111 }
112
113 // Check next render doesn't throw.
114 windowOnError.mockReset();
115 console.warn.mockReset();
116 console.error.mockReset();
117 await act(() => {
118 ReactDOM.render(<NoError />, container);
119 });
120 expect(container.textContent).toBe('OK');
121 expect(windowOnError).not.toHaveBeenCalled();
122 expect(console.warn).not.toHaveBeenCalled();
123 if (__DEV__) {
124 expect(console.error.mock.calls).toEqual([
125 [
126 expect.stringContaining(
127 'ReactDOM.render has not been supported since React 18',
128 ),
129 ],
130 ]);
131 } else {
132 expect(console.error).not.toHaveBeenCalled();
133 }
134 });
135
136 // @gate !disableLegacyMode
137 it('logs render errors without an error boundary', async () => {
138 function Foo() {
139 throw Error('Boom');
140 }
141
142 await expect(async () => {
143 await act(() => {
144 ReactDOM.render(<Foo />, container);
145 });
146 }).rejects.toThrow('Boom');
147
148 // Reported because errors without a boundary are reported to window.
149 expect(windowOnError.mock.calls).toEqual([
150 [
151 expect.objectContaining({
152 message: 'Boom',
153 }),
154 ],
155 ]);
156
157 if (__DEV__) {
158 expect(console.warn.mock.calls).toEqual([
159 [
160 // Formatting
161 expect.stringContaining('%s'),
162 // Addendum by React:
163 expect.stringContaining('An error occurred in the <Foo> component'),
164 expect.stringContaining('Consider adding an error boundary'),
165 // The component stack is not added without the polyfill/devtools.
166 // expect.stringContaining('Foo'),
167 ],
168 ]);
169
170 expect(console.error.mock.calls).toEqual([
171 [
172 expect.stringContaining(
173 'ReactDOM.render has not been supported since React 18',
174 ),
175 ],
176 ]);
177 } else {
178 expect(console.warn).not.toHaveBeenCalled();
179 expect(console.error).not.toHaveBeenCalled();
180 }
181
182 // Check next render doesn't throw.
183 windowOnError.mockReset();
184 console.warn.mockReset();
185 console.error.mockReset();
186 await act(() => {
187 ReactDOM.render(<NoError />, container);
188 });
189 expect(container.textContent).toBe('OK');
190 expect(console.warn).not.toHaveBeenCalled();
191 expect(windowOnError).not.toHaveBeenCalled();
192 if (__DEV__) {
193 expect(console.error.mock.calls).toEqual([
194 [
195 expect.stringContaining(
196 'ReactDOM.render has not been supported since React 18',
197 ),
198 ],
199 ]);
200 } else {
201 expect(console.error).not.toHaveBeenCalled();
202 }
203 });
204
205 // @gate !disableLegacyMode
206 it('logs render errors with an error boundary', async () => {
207 function Foo() {
208 throw Error('Boom');
209 }
210
211 await act(() => {
212 ReactDOM.render(
213 <ErrorBoundary>
214 <Foo />
215 </ErrorBoundary>,
216 container,
217 );
218 });
219
220 // The top-level error was caught with try/catch,
221 // so we don't see an error event.
222 expect(windowOnError).not.toHaveBeenCalled();
223 expect(console.warn).not.toHaveBeenCalled();
224
225 if (__DEV__) {
226 expect(console.error.mock.calls).toEqual([
227 [
228 expect.stringContaining(
229 'ReactDOM.render has not been supported since React 18',
230 ),
231 ],
232 [
233 // Formatting
234 expect.stringContaining('%o'),
235 expect.objectContaining({
236 message: 'Boom',
237 }),
238 // Addendum by React:
239 expect.stringContaining(
240 'The above error occurred in the <Foo> component',
241 ),
242 expect.stringContaining('ErrorBoundary'),
243 // The component stack is not added without the polyfill/devtools.
244 // expect.stringContaining('Foo'),
245 ],
246 ]);
247 } else {
248 expect(console.error.mock.calls).toEqual([
249 [
250 // Reported by React with no extra message:
251 expect.objectContaining({
252 message: 'Boom',
253 }),
254 ],
255 ]);
256 }
257
258 // Check next render doesn't throw.
259 windowOnError.mockReset();
260 console.error.mockReset();
261 console.warn.mockReset();
262 await act(() => {
263 ReactDOM.render(<NoError />, container);
264 });
265 expect(container.textContent).toBe('OK');
266 expect(windowOnError).not.toHaveBeenCalled();
267 expect(console.warn).not.toHaveBeenCalled();
268 if (__DEV__) {
269 expect(console.error.mock.calls).toEqual([
270 [
271 expect.stringContaining(
272 'ReactDOM.render has not been supported since React 18',
273 ),
274 ],
275 ]);
276 } else {
277 expect(console.error).not.toHaveBeenCalled();
278 }
279 });
280
281 // @gate !disableLegacyMode
282 it('logs layout effect errors without an error boundary', async () => {
283 function Foo() {
284 React.useLayoutEffect(() => {
285 throw Error('Boom');
286 }, []);
287 return null;
288 }
289
290 await expect(async () => {
291 await act(() => {
292 ReactDOM.render(<Foo />, container);
293 });
294 }).rejects.toThrow('Boom');
295
296 // Reported because errors without a boundary are reported to window.
297 expect(windowOnError.mock.calls).toEqual([
298 [
299 expect.objectContaining({
300 message: 'Boom',
301 }),
302 ],
303 ]);
304
305 if (__DEV__) {
306 expect(console.warn.mock.calls).toEqual([
307 [
308 // Formatting
309 expect.stringContaining('%s'),
310
311 // Addendum by React:
312 expect.stringContaining('An error occurred in the <Foo> component'),
313 expect.stringContaining('Consider adding an error boundary'),
314 // The component stack is not added without the polyfill/devtools.
315 // expect.stringContaining('Foo'),
316 ],
317 ]);
318
319 expect(console.error.mock.calls).toEqual([
320 [
321 expect.stringContaining(
322 'ReactDOM.render has not been supported since React 18',
323 ),
324 ],
325 ]);
326 } else {
327 expect(console.warn).not.toHaveBeenCalled();
328 expect(console.error).not.toHaveBeenCalled();
329 }
330
331 // Check next render doesn't throw.
332 windowOnError.mockReset();
333 console.warn.mockReset();
334 console.error.mockReset();
335 await act(() => {
336 ReactDOM.render(<NoError />, container);
337 });
338 expect(container.textContent).toBe('OK');
339 expect(console.warn).not.toHaveBeenCalled();
340 expect(windowOnError).not.toHaveBeenCalled();
341
342 if (__DEV__) {
343 expect(console.error.mock.calls).toEqual([
344 [
345 expect.stringContaining(
346 'ReactDOM.render has not been supported since React 18',
347 ),
348 ],
349 ]);
350 } else {
351 expect(console.error).not.toHaveBeenCalled();
352 }
353 });
354
355 // @gate !disableLegacyMode
356 it('logs layout effect errors with an error boundary', async () => {
357 function Foo() {
358 React.useLayoutEffect(() => {
359 throw Error('Boom');
360 }, []);
361 return null;
362 }
363
364 await act(() => {
365 ReactDOM.render(
366 <ErrorBoundary>
367 <Foo />
368 </ErrorBoundary>,
369 container,
370 );
371 });
372
373 // The top-level error was caught with try/catch,
374 // so we don't see an error event.
375 expect(windowOnError).not.toHaveBeenCalled();
376 expect(console.warn).not.toHaveBeenCalled();
377
378 if (__DEV__) {
379 expect(console.error.mock.calls).toEqual([
380 [
381 expect.stringContaining(
382 'ReactDOM.render has not been supported since React 18',
383 ),
384 ],
385 [
386 // Formatting
387 expect.stringContaining('%o'),
388 expect.objectContaining({
389 message: 'Boom',
390 }),
391 // Addendum by React:
392 expect.stringContaining(
393 'The above error occurred in the <Foo> component',
394 ),
395 expect.stringContaining('ErrorBoundary'),
396 // The component stack is not added without the polyfill/devtools.
397 // expect.stringContaining('Foo'),
398 ],
399 ]);
400 } else {
401 expect(console.error.mock.calls).toEqual([
402 [
403 // Reported by React with no extra message:
404 expect.objectContaining({
405 message: 'Boom',
406 }),
407 ],
408 ]);
409 }
410
411 // Check next render doesn't throw.
412 windowOnError.mockReset();
413 console.warn.mockReset();
414 console.error.mockReset();
415 await act(() => {
416 ReactDOM.render(<NoError />, container);
417 });
418 expect(container.textContent).toBe('OK');
419 expect(windowOnError).not.toHaveBeenCalled();
420 expect(console.warn).not.toHaveBeenCalled();
421 if (__DEV__) {
422 expect(console.error.mock.calls).toEqual([
423 [
424 expect.stringContaining(
425 'ReactDOM.render has not been supported since React 18',
426 ),
427 ],
428 ]);
429 } else {
430 expect(console.error).not.toHaveBeenCalled();
431 }
432 });
433
434 // @gate !disableLegacyMode
435 it('logs passive effect errors without an error boundary', async () => {
436 function Foo() {
437 React.useEffect(() => {
438 throw Error('Boom');
439 }, []);
440 return null;
441 }
442
443 await act(async () => {
444 ReactDOM.render(<Foo />, container);
445 await waitForThrow('Boom');
446 });
447
448 // The top-level error was caught with try/catch,
449 // so we don't see an error event.
450 expect(windowOnError.mock.calls).toEqual([
451 [
452 expect.objectContaining({
453 message: 'Boom',
454 }),
455 ],
456 ]);
457
458 if (__DEV__) {
459 expect(console.warn.mock.calls).toEqual([
460 [
461 // Formatting
462 expect.stringContaining('%s'),
463
464 // Addendum by React:
465 expect.stringContaining('An error occurred in the <Foo> component'),
466 expect.stringContaining('Consider adding an error boundary'),
467 // The component stack is not added without the polyfill/devtools.
468 // expect.stringContaining('Foo'),
469 ],
470 ]);
471
472 expect(console.error.mock.calls).toEqual([
473 [
474 expect.stringContaining(
475 'ReactDOM.render has not been supported since React 18',
476 ),
477 ],
478 ]);
479 } else {
480 expect(console.warn).not.toHaveBeenCalled();
481 expect(console.error).not.toHaveBeenCalled();
482 }
483
484 // Check next render doesn't throw.
485 windowOnError.mockReset();
486 console.warn.mockReset();
487 console.error.mockReset();
488 await act(() => {
489 ReactDOM.render(<NoError />, container);
490 });
491 expect(container.textContent).toBe('OK');
492 expect(windowOnError).not.toHaveBeenCalled();
493 expect(console.warn).not.toHaveBeenCalled();
494 if (__DEV__) {
495 expect(console.error.mock.calls).toEqual([
496 [
497 expect.stringContaining(
498 'ReactDOM.render has not been supported since React 18',
499 ),
500 ],
501 ]);
502 } else {
503 expect(console.error).not.toHaveBeenCalled();
504 }
505 });
506
507 // @gate !disableLegacyMode
508 it('logs passive effect errors with an error boundary', async () => {
509 function Foo() {
510 React.useEffect(() => {
511 throw Error('Boom');
512 }, []);
513 return null;
514 }
515
516 await act(() => {
517 ReactDOM.render(
518 <ErrorBoundary>
519 <Foo />
520 </ErrorBoundary>,
521 container,
522 );
523 });
524
525 // The top-level error was caught with try/catch,
526 // so we don't see an error event.
527 expect(windowOnError).not.toHaveBeenCalled();
528 expect(console.warn).not.toHaveBeenCalled();
529
530 if (__DEV__) {
531 expect(console.error.mock.calls).toEqual([
532 [
533 expect.stringContaining(
534 'ReactDOM.render has not been supported since React 18',
535 ),
536 ],
537 [
538 // Formatting
539 expect.stringContaining('%o'),
540 expect.objectContaining({
541 message: 'Boom',
542 }),
543 // Addendum by React:
544 expect.stringContaining(
545 'The above error occurred in the <Foo> component',
546 ),
547 expect.stringContaining('ErrorBoundary'),
548 // The component stack is not added without the polyfill/devtools.
549 // expect.stringContaining('Foo'),
550 ],
551 ]);
552 } else {
553 expect(console.error.mock.calls).toEqual([
554 [
555 // Reported by React with no extra message:
556 expect.objectContaining({
557 message: 'Boom',
558 }),
559 ],
560 ]);
561 }
562
563 // Check next render doesn't throw.
564 windowOnError.mockReset();
565 console.warn.mockReset();
566 console.error.mockReset();
567 await act(() => {
568 ReactDOM.render(<NoError />, container);
569 });
570 expect(container.textContent).toBe('OK');
571 expect(windowOnError).not.toHaveBeenCalled();
572 expect(console.warn).not.toHaveBeenCalled();
573 if (__DEV__) {
574 expect(console.error.mock.calls).toEqual([
575 [
576 expect.stringContaining(
577 'ReactDOM.render has not been supported since React 18',
578 ),
579 ],
580 ]);
581 } else {
582 expect(console.warn).not.toHaveBeenCalled();
583 }
584 });
585 });
586 });