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