@samitouri / QOS-React-2 / commits / 4db3ff6c1a

Test suite for hydration diff warnings (#24229)

* Test suite for hydration diff warnings * Test both variants * Add more edge cases

dan committed Mar 31, 2022 at 17:35 UTC 4db3ff6c1ac092676dffc2251f5dec908eca9c74
1 file changed +1329
packages/react-dom/src/__tests__/ReactDOMHydrationDiff-test.js new
+1329
@@ -0,0 +1,1329 @@
1 +/**
2 + * Copyright (c) Facebook, Inc. and its 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 +let React;
11 +let ReactDOMClient;
12 +let ReactDOMServer;
13 +let act;
14 +
15 +const util = require('util');
16 +const realConsoleError = console.error;
17 +
18 +describe('ReactDOMServerHydration', () => {
19 + let container;
20 +
21 + beforeEach(() => {
22 + jest.resetModules();
23 + React = require('react');
24 + ReactDOMClient = require('react-dom/client');
25 + ReactDOMServer = require('react-dom/server');
26 + act = require('react-dom/test-utils').act;
27 +
28 + console.error = jest.fn();
29 + container = document.createElement('div');
30 + document.body.appendChild(container);
31 + });
32 +
33 + afterEach(() => {
34 + document.body.removeChild(container);
35 + console.error = realConsoleError;
36 + });
37 +
38 + function normalizeCodeLocInfo(str) {
39 + return (
40 + typeof str === 'string' &&
41 + str.replace(/\n +(?:at|in) ([\S]+)[^\n]*/g, function(m, name) {
42 + return '\n in ' + name + ' (at **)';
43 + })
44 + );
45 + }
46 +
47 + function formatMessage(args) {
48 + const [format, ...rest] = args;
49 + if (format instanceof Error) {
50 + return 'Caught [' + format.message + ']';
51 + }
52 + if (format.indexOf('Error: Uncaught [') === 0) {
53 + // Ignore errors captured by jsdom and their stacks.
54 + // We only want console errors in this suite.
55 + return null;
56 + }
57 + rest[rest.length - 1] = normalizeCodeLocInfo(rest[rest.length - 1]);
58 + return util.format(format, ...rest);
59 + }
60 +
61 + function formatConsoleErrors() {
62 + return console.error.mock.calls.map(formatMessage).filter(Boolean);
63 + }
64 +
65 + function testMismatch(Mismatch) {
66 + const htmlString = ReactDOMServer.renderToString(
67 + <Mismatch isClient={false} />,
68 + );
69 + container.innerHTML = htmlString;
70 + act(() => {
71 + ReactDOMClient.hydrateRoot(container, <Mismatch isClient={true} />);
72 + });
73 + return formatConsoleErrors();
74 + }
75 +
76 + describe('text mismatch', () => {
77 + // @gate __DEV__
78 + it('warns when client and server render different text', () => {
79 + function Mismatch({isClient}) {
80 + return (
81 + <div className="parent">
82 + <main className="child">{isClient ? 'client' : 'server'}</main>
83 + </div>
84 + );
85 + }
86 + if (gate(flags => flags.enableClientRenderFallbackOnHydrationMismatch)) {
87 + expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
88 + Array [
89 + "Warning: Text content did not match. Server: \\"server\\" Client: \\"client\\"
90 + in main (at **)
91 + in div (at **)
92 + in Mismatch (at **)",
93 + "Warning: An error occurred during hydration. The server HTML was replaced with client content in <div>.",
94 + "Caught [Text content does not match server-rendered HTML.]",
95 + "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]",
96 + ]
97 + `);
98 + } else {
99 + expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
100 + Array [
101 + "Warning: Text content did not match. Server: \\"server\\" Client: \\"client\\"
102 + in main (at **)
103 + in div (at **)
104 + in Mismatch (at **)",
105 + ]
106 + `);
107 + }
108 + });
109 +
110 + // @gate __DEV__
111 + it('warns when client and server render different html', () => {
112 + function Mismatch({isClient}) {
113 + return (
114 + <div className="parent">
115 + <main
116 + className="child"
117 + dangerouslySetInnerHTML={{
118 + __html: isClient
119 + ? '<span>client</span>'
120 + : '<span>server</span>',
121 + }}
122 + />
123 + </div>
124 + );
125 + }
126 + expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
127 + Array [
128 + "Warning: Prop \`dangerouslySetInnerHTML\` did not match. Server: \\"<span>server</span>\\" Client: \\"<span>client</span>\\"
129 + in main (at **)
130 + in div (at **)
131 + in Mismatch (at **)",
132 + ]
133 + `);
134 + });
135 + });
136 +
137 + describe('attribute mismatch', () => {
138 + // @gate __DEV__
139 + it('warns when client and server render different attributes', () => {
140 + function Mismatch({isClient}) {
141 + return (
142 + <div className="parent">
143 + <main
144 + className={isClient ? 'child client' : 'child server'}
145 + dir={isClient ? 'ltr' : 'rtl'}
146 + />
147 + </div>
148 + );
149 + }
150 + expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
151 + Array [
152 + "Warning: Prop \`className\` did not match. Server: \\"child server\\" Client: \\"child client\\"
153 + in main (at **)
154 + in div (at **)
155 + in Mismatch (at **)",
156 + ]
157 + `);
158 + });
159 +
160 + // @gate __DEV__
161 + it('warns when client renders extra attributes', () => {
162 + function Mismatch({isClient}) {
163 + return (
164 + <div className="parent">
165 + <main
166 + className="child"
167 + tabIndex={isClient ? 1 : null}
168 + dir={isClient ? 'ltr' : null}
169 + />
170 + </div>
171 + );
172 + }
173 + expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
174 + Array [
175 + "Warning: Prop \`tabIndex\` did not match. Server: \\"null\\" Client: \\"1\\"
176 + in main (at **)
177 + in div (at **)
178 + in Mismatch (at **)",
179 + ]
180 + `);
181 + });
182 +
183 + // @gate __DEV__
184 + it('warns when server renders extra attributes', () => {
185 + function Mismatch({isClient}) {
186 + return (
187 + <div className="parent">
188 + <main
189 + className="child"
190 + tabIndex={isClient ? null : 1}
191 + dir={isClient ? null : 'rtl'}
192 + />
193 + </div>
194 + );
195 + }
196 + expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
197 + Array [
198 + "Warning: Extra attributes from the server: tabindex,dir
199 + in main (at **)
200 + in div (at **)
201 + in Mismatch (at **)",
202 + ]
203 + `);
204 + });
205 +
206 + // @gate __DEV__
207 + it('warns when both client and server render extra attributes', () => {
208 + function Mismatch({isClient}) {
209 + return (
210 + <div className="parent">
211 + <main
212 + className="child"
213 + tabIndex={isClient ? 1 : null}
214 + dir={isClient ? null : 'rtl'}
215 + />
216 + </div>
217 + );
218 + }
219 + expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
220 + Array [
221 + "Warning: Prop \`tabIndex\` did not match. Server: \\"null\\" Client: \\"1\\"
222 + in main (at **)
223 + in div (at **)
224 + in Mismatch (at **)",
225 + ]
226 + `);
227 + });
228 +
229 + // @gate __DEV__
230 + it('warns when client and server render different styles', () => {
231 + function Mismatch({isClient}) {
232 + return (
233 + <div className="parent">
234 + <main
235 + className="child"
236 + style={{
237 + opacity: isClient ? 1 : 0,
238 + }}
239 + />
240 + </div>
241 + );
242 + }
243 + expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
244 + Array [
245 + "Warning: Prop \`style\` did not match. Server: \\"opacity:0\\" Client: \\"opacity:1\\"
246 + in main (at **)
247 + in div (at **)
248 + in Mismatch (at **)",
249 + ]
250 + `);
251 + });
252 + });
253 +
254 + describe('extra nodes on the client', () => {
255 + describe('extra elements on the client', () => {
256 + // @gate __DEV__
257 + it('warns when client renders an extra element as only child', () => {
258 + function Mismatch({isClient}) {
259 + return (
260 + <div className="parent">
261 + {isClient && <main className="only" />}
262 + </div>
263 + );
264 + }
265 + if (
266 + gate(flags => flags.enableClientRenderFallbackOnHydrationMismatch)
267 + ) {
268 + expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
269 + Array [
270 + "Warning: Expected server HTML to contain a matching <main> in <div>.
271 + in main (at **)
272 + in div (at **)
273 + in Mismatch (at **)",
274 + "Warning: An error occurred during hydration. The server HTML was replaced with client content in <div>.",
275 + "Caught [Hydration failed because the initial UI does not match what was rendered on the server.]",
276 + "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]",
277 + ]
278 + `);
279 + } else {
280 + expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
281 + Array [
282 + "Warning: Expected server HTML to contain a matching <main> in <div>.
283 + in main (at **)
284 + in div (at **)
285 + in Mismatch (at **)",
286 + ]
287 + `);
288 + }
289 + });
290 +
291 + // @gate __DEV__
292 + it('warns when client renders an extra element in the beginning', () => {
293 + function Mismatch({isClient}) {
294 + return (
295 + <div className="parent">
296 + {isClient && <header className="1" />}
297 + <main className="2" />
298 + <footer className="3" />
299 + </div>
300 + );
301 + }
302 + if (
303 + gate(flags => flags.enableClientRenderFallbackOnHydrationMismatch)
304 + ) {
305 + expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
306 + Array [
307 + "Warning: Expected server HTML to contain a matching <header> in <div>.
308 + in header (at **)
309 + in div (at **)
310 + in Mismatch (at **)",
311 + "Warning: An error occurred during hydration. The server HTML was replaced with client content in <div>.",
312 + "Caught [Hydration failed because the initial UI does not match what was rendered on the server.]",
313 + "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]",
314 + ]
315 + `);
316 + } else {
317 + expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
318 + Array [
319 + "Warning: Expected server HTML to contain a matching <header> in <div>.
320 + in header (at **)
321 + in div (at **)
322 + in Mismatch (at **)",
323 + ]
324 + `);
325 + }
326 + });
327 +
328 + // @gate __DEV__
329 + it('warns when client renders an extra element in the middle', () => {
330 + function Mismatch({isClient}) {
331 + return (
332 + <div className="parent">
333 + <header className="1" />
334 + {isClient && <main className="2" />}
335 + <footer className="3" />
336 + </div>
337 + );
338 + }
339 + if (
340 + gate(flags => flags.enableClientRenderFallbackOnHydrationMismatch)
341 + ) {
342 + expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
343 + Array [
344 + "Warning: Expected server HTML to contain a matching <main> in <div>.
345 + in main (at **)
346 + in div (at **)
347 + in Mismatch (at **)",
348 + "Warning: An error occurred during hydration. The server HTML was replaced with client content in <div>.",
349 + "Caught [Hydration failed because the initial UI does not match what was rendered on the server.]",
350 + "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]",
351 + ]
352 + `);
353 + } else {
354 + expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
355 + Array [
356 + "Warning: Expected server HTML to contain a matching <main> in <div>.
357 + in main (at **)
358 + in div (at **)
359 + in Mismatch (at **)",
360 + ]
361 + `);
362 + }
363 + });
364 +
365 + // @gate __DEV__
366 + it('warns when client renders an extra element in the end', () => {
367 + function Mismatch({isClient}) {
368 + return (
369 + <div className="parent">
370 + <header className="1" />
371 + <main className="2" />
372 + {isClient && <footer className="3" />}
373 + </div>
374 + );
375 + }
376 + if (
377 + gate(flags => flags.enableClientRenderFallbackOnHydrationMismatch)
378 + ) {
379 + expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
380 + Array [
381 + "Warning: Expected server HTML to contain a matching <footer> in <div>.
382 + in footer (at **)
383 + in div (at **)
384 + in Mismatch (at **)",
385 + "Warning: An error occurred during hydration. The server HTML was replaced with client content in <div>.",
386 + "Caught [Hydration failed because the initial UI does not match what was rendered on the server.]",
387 + "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]",
388 + ]
389 + `);
390 + } else {
391 + expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
392 + Array [
393 + "Warning: Expected server HTML to contain a matching <footer> in <div>.
394 + in footer (at **)
395 + in div (at **)
396 + in Mismatch (at **)",
397 + ]
398 + `);
399 + }
400 + });
401 + });
402 +
403 + describe('extra text nodes on the client', () => {
404 + // @gate __DEV__
405 + it('warns when client renders an extra text node as only child', () => {
406 + function Mismatch({isClient}) {
407 + return <div className="parent">{isClient && 'only'}</div>;
408 + }
409 + if (
410 + gate(flags => flags.enableClientRenderFallbackOnHydrationMismatch)
411 + ) {
412 + expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
413 + Array [
414 + "Warning: Text content did not match. Server: \\"\\" Client: \\"only\\"
415 + in div (at **)
416 + in Mismatch (at **)",
417 + "Warning: An error occurred during hydration. The server HTML was replaced with client content in <div>.",
418 + "Caught [Text content does not match server-rendered HTML.]",
419 + "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]",
420 + ]
421 + `);
422 + } else {
423 + expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
424 + Array [
425 + "Warning: Text content did not match. Server: \\"\\" Client: \\"only\\"
426 + in div (at **)
427 + in Mismatch (at **)",
428 + ]
429 + `);
430 + }
431 + });
432 +
433 + // @gate __DEV__
434 + it('warns when client renders an extra text node in the beginning', () => {
435 + function Mismatch({isClient}) {
436 + return (
437 + <div className="parent">
438 + <header className="1" />
439 + {isClient && 'second'}
440 + <footer className="3" />
441 + </div>
442 + );
443 + }
444 + if (
445 + gate(flags => flags.enableClientRenderFallbackOnHydrationMismatch)
446 + ) {
447 + expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
448 + Array [
449 + "Warning: Expected server HTML to contain a matching text node for \\"second\\" in <div>.
450 + in div (at **)
451 + in Mismatch (at **)",
452 + "Warning: An error occurred during hydration. The server HTML was replaced with client content in <div>.",
453 + "Caught [Hydration failed because the initial UI does not match what was rendered on the server.]",
454 + "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]",
455 + ]
456 + `);
457 + } else {
458 + expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
459 + Array [
460 + "Warning: Expected server HTML to contain a matching text node for \\"second\\" in <div>.
461 + in div (at **)
462 + in Mismatch (at **)",
463 + ]
464 + `);
465 + }
466 + });
467 +
468 + // @gate __DEV__
469 + it('warns when client renders an extra text node in the beginning', () => {
470 + function Mismatch({isClient}) {
471 + return (
472 + <div className="parent">
473 + {isClient && 'first'}
474 + <main className="2" />
475 + <footer className="3" />
476 + </div>
477 + );
478 + }
479 + if (
480 + gate(flags => flags.enableClientRenderFallbackOnHydrationMismatch)
481 + ) {
482 + expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
483 + Array [
484 + "Warning: Expected server HTML to contain a matching text node for \\"first\\" in <div>.
485 + in div (at **)
486 + in Mismatch (at **)",
487 + "Warning: An error occurred during hydration. The server HTML was replaced with client content in <div>.",
488 + "Caught [Hydration failed because the initial UI does not match what was rendered on the server.]",
489 + "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]",
490 + ]
491 + `);
492 + } else {
493 + expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
494 + Array [
495 + "Warning: Expected server HTML to contain a matching text node for \\"first\\" in <div>.
496 + in div (at **)
497 + in Mismatch (at **)",
498 + ]
499 + `);
500 + }
501 + });
502 +
503 + // @gate __DEV__
504 + it('warns when client renders an extra text node in the end', () => {
505 + function Mismatch({isClient}) {
506 + return (
507 + <div className="parent">
508 + <header className="1" />
509 + <main className="2" />
510 + {isClient && 'third'}
511 + </div>
512 + );
513 + }
514 + if (
515 + gate(flags => flags.enableClientRenderFallbackOnHydrationMismatch)
516 + ) {
517 + expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
518 + Array [
519 + "Warning: Expected server HTML to contain a matching text node for \\"third\\" in <div>.
520 + in div (at **)
521 + in Mismatch (at **)",
522 + "Warning: An error occurred during hydration. The server HTML was replaced with client content in <div>.",
523 + "Caught [Hydration failed because the initial UI does not match what was rendered on the server.]",
524 + "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]",
525 + ]
526 + `);
527 + } else {
528 + expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
529 + Array [
530 + "Warning: Expected server HTML to contain a matching text node for \\"third\\" in <div>.
531 + in div (at **)
532 + in Mismatch (at **)",
533 + ]
534 + `);
535 + }
536 + });
537 + });
538 + });
539 +
540 + describe('extra nodes on the server', () => {
541 + describe('extra elements on the server', () => {
542 + // @gate __DEV__
543 + it('warns when server renders an extra element as only child', () => {
544 + function Mismatch({isClient}) {
545 + return (
546 + <div className="parent">
547 + {!isClient && <main className="only" />}
548 + </div>
549 + );
550 + }
551 + if (
552 + gate(flags => flags.enableClientRenderFallbackOnHydrationMismatch)
553 + ) {
554 + expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
555 + Array [
556 + "Warning: Did not expect server HTML to contain a <main> in <div>.
557 + in div (at **)
558 + in Mismatch (at **)",
559 + "Warning: An error occurred during hydration. The server HTML was replaced with client content in <div>.",
560 + "Caught [Hydration failed because the initial UI does not match what was rendered on the server.]",
561 + "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]",
562 + ]
563 + `);
564 + } else {
565 + expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
566 + Array [
567 + "Warning: Did not expect server HTML to contain a <main> in <div>.
568 + in div (at **)
569 + in Mismatch (at **)",
570 + ]
571 + `);
572 + }
573 + });
574 +
575 + // @gate __DEV__
576 + it('warns when server renders an extra element in the beginning', () => {
577 + function Mismatch({isClient}) {
578 + return (
579 + <div className="parent">
580 + {!isClient && <header className="1" />}
581 + <main className="2" />
582 + <footer className="3" />
583 + </div>
584 + );
585 + }
586 + if (
587 + gate(flags => flags.enableClientRenderFallbackOnHydrationMismatch)
588 + ) {
589 + expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
590 + Array [
591 + "Warning: Expected server HTML to contain a matching <main> in <div>.
592 + in main (at **)
593 + in div (at **)
594 + in Mismatch (at **)",
595 + "Warning: An error occurred during hydration. The server HTML was replaced with client content in <div>.",
596 + "Caught [Hydration failed because the initial UI does not match what was rendered on the server.]",
597 + "Caught [Hydration failed because the initial UI does not match what was rendered on the server.]",
598 + "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]",
599 + ]
600 + `);
601 + } else {
602 + expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
603 + Array [
604 + "Warning: Did not expect server HTML to contain a <header> in <div>.
605 + in main (at **)
606 + in div (at **)
607 + in Mismatch (at **)",
608 + ]
609 + `);
610 + }
611 + });
612 +
613 + // @gate __DEV__
614 + it('warns when server renders an extra element in the middle', () => {
615 + function Mismatch({isClient}) {
616 + return (
617 + <div className="parent">
618 + <header className="1" />
619 + {!isClient && <main className="2" />}
620 + <footer className="3" />
621 + </div>
622 + );
623 + }
624 + if (
625 + gate(flags => flags.enableClientRenderFallbackOnHydrationMismatch)
626 + ) {
627 + expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
628 + Array [
629 + "Warning: Expected server HTML to contain a matching <footer> in <div>.
630 + in footer (at **)
631 + in div (at **)
632 + in Mismatch (at **)",
633 + "Warning: An error occurred during hydration. The server HTML was replaced with client content in <div>.",
634 + "Caught [Hydration failed because the initial UI does not match what was rendered on the server.]",
635 + "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]",
636 + ]
637 + `);
638 + } else {
639 + expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
640 + Array [
641 + "Warning: Did not expect server HTML to contain a <main> in <div>.
642 + in footer (at **)
643 + in div (at **)
644 + in Mismatch (at **)",
645 + ]
646 + `);
647 + }
648 + });
649 +
650 + // @gate __DEV__
651 + it('warns when server renders an extra element in the end', () => {
652 + function Mismatch({isClient}) {
653 + return (
654 + <div className="parent">
655 + <header className="1" />
656 + <main className="2" />
657 + {!isClient && <footer className="3" />}
658 + </div>
659 + );
660 + }
661 + if (
662 + gate(flags => flags.enableClientRenderFallbackOnHydrationMismatch)
663 + ) {
664 + expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
665 + Array [
666 + "Warning: Did not expect server HTML to contain a <footer> in <div>.
667 + in div (at **)
668 + in Mismatch (at **)",
669 + "Warning: An error occurred during hydration. The server HTML was replaced with client content in <div>.",
670 + "Caught [Hydration failed because the initial UI does not match what was rendered on the server.]",
671 + "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]",
672 + ]
673 + `);
674 + } else {
675 + expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
676 + Array [
677 + "Warning: Did not expect server HTML to contain a <footer> in <div>.
678 + in div (at **)
679 + in Mismatch (at **)",
680 + ]
681 + `);
682 + }
683 + });
684 + });
685 +
686 + describe('extra text nodes on the server', () => {
687 + // @gate __DEV__
688 + it('warns when server renders an extra text node as only child', () => {
689 + function Mismatch({isClient}) {
690 + return <div className="parent">{!isClient && 'only'}</div>;
691 + }
692 + if (
693 + gate(flags => flags.enableClientRenderFallbackOnHydrationMismatch)
694 + ) {
695 + expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
696 + Array [
697 + "Warning: Did not expect server HTML to contain the text node \\"only\\" in <div>.
698 + in div (at **)
699 + in Mismatch (at **)",
700 + "Warning: An error occurred during hydration. The server HTML was replaced with client content in <div>.",
701 + "Caught [Hydration failed because the initial UI does not match what was rendered on the server.]",
702 + "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]",
703 + ]
704 + `);
705 + } else {
706 + expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
707 + Array [
708 + "Warning: Did not expect server HTML to contain the text node \\"only\\" in <div>.
709 + in div (at **)
710 + in Mismatch (at **)",
711 + ]
712 + `);
713 + }
714 + });
715 +
716 + // @gate __DEV__
717 + it('warns when server renders an extra text node in the beginning', () => {
718 + function Mismatch({isClient}) {
719 + return (
720 + <div className="parent">
721 + {!isClient && 'first'}
722 + <main className="2" />
723 + <footer className="3" />
724 + </div>
725 + );
726 + }
727 + if (
728 + gate(flags => flags.enableClientRenderFallbackOnHydrationMismatch)
729 + ) {
730 + expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
731 + Array [
732 + "Warning: Expected server HTML to contain a matching <main> in <div>.
733 + in main (at **)
734 + in div (at **)
735 + in Mismatch (at **)",
736 + "Warning: An error occurred during hydration. The server HTML was replaced with client content in <div>.",
737 + "Caught [Hydration failed because the initial UI does not match what was rendered on the server.]",
738 + "Caught [Hydration failed because the initial UI does not match what was rendered on the server.]",
739 + "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]",
740 + ]
741 + `);
742 + } else {
743 + expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
744 + Array [
745 + "Warning: Did not expect server HTML to contain the text node \\"first\\" in <div>.
746 + in main (at **)
747 + in div (at **)
748 + in Mismatch (at **)",
749 + ]
750 + `);
751 + }
752 + });
753 +
754 + // @gate __DEV__
755 + it('warns when server renders an extra text node in the middle', () => {
756 + function Mismatch({isClient}) {
757 + return (
758 + <div className="parent">
759 + <header className="1" />
760 + {!isClient && 'second'}
761 + <footer className="3" />
762 + </div>
763 + );
764 + }
765 + if (
766 + gate(flags => flags.enableClientRenderFallbackOnHydrationMismatch)
767 + ) {
768 + expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
769 + Array [
770 + "Warning: Expected server HTML to contain a matching <footer> in <div>.
771 + in footer (at **)
772 + in div (at **)
773 + in Mismatch (at **)",
774 + "Warning: An error occurred during hydration. The server HTML was replaced with client content in <div>.",
775 + "Caught [Hydration failed because the initial UI does not match what was rendered on the server.]",
776 + "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]",
777 + ]
778 + `);
779 + } else {
780 + expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
781 + Array [
782 + "Warning: Did not expect server HTML to contain the text node \\"second\\" in <div>.
783 + in footer (at **)
784 + in div (at **)
785 + in Mismatch (at **)",
786 + ]
787 + `);
788 + }
789 + });
790 +
791 + // @gate __DEV__
792 + it('warns when server renders an extra text node in the end', () => {
793 + function Mismatch({isClient}) {
794 + return (
795 + <div className="parent">
796 + <header className="1" />
797 + <main className="2" />
798 + {!isClient && 'third'}
799 + </div>
800 + );
801 + }
802 + if (
803 + gate(flags => flags.enableClientRenderFallbackOnHydrationMismatch)
804 + ) {
805 + expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
806 + Array [
807 + "Warning: Did not expect server HTML to contain the text node \\"third\\" in <div>.
808 + in div (at **)
809 + in Mismatch (at **)",
810 + "Warning: An error occurred during hydration. The server HTML was replaced with client content in <div>.",
811 + "Caught [Hydration failed because the initial UI does not match what was rendered on the server.]",
812 + "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]",
813 + ]
814 + `);
815 + } else {
816 + expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
817 + Array [
818 + "Warning: Did not expect server HTML to contain the text node \\"third\\" in <div>.
819 + in div (at **)
820 + in Mismatch (at **)",
821 + ]
822 + `);
823 + }
824 + });
825 + });
826 + });
827 +
828 + describe('special nodes', () => {
829 + describe('Suspense', () => {
830 + function Never() {
831 + throw new Promise(resolve => {});
832 + }
833 +
834 + // @gate __DEV__
835 + it('warns when client renders an extra Suspense node in content mode', () => {
836 + function Mismatch({isClient}) {
837 + return (
838 + <div className="parent">
839 + {isClient && (
840 + <React.Suspense fallback={<p>Loading...</p>}>
841 + <main className="only" />
842 + </React.Suspense>
843 + )}
844 + </div>
845 + );
846 + }
847 + if (
848 + gate(flags => flags.enableClientRenderFallbackOnHydrationMismatch)
849 + ) {
850 + // TODO: This message doesn't seem to have any useful details.
851 + expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
852 + Array [
853 + "Warning: An error occurred during hydration. The server HTML was replaced with client content in <div>.",
854 + "Caught [Hydration failed because the initial UI does not match what was rendered on the server.]",
855 + "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]",
856 + ]
857 + `);
858 + } else {
859 + expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`Array []`);
860 + }
861 + });
862 +
863 + // @gate __DEV__
864 + it('warns when server renders an extra Suspense node in content mode', () => {
865 + function Mismatch({isClient}) {
866 + return (
867 + <div className="parent">
868 + {!isClient && (
869 + <React.Suspense fallback={<p>Loading...</p>}>
870 + <main className="only" />
871 + </React.Suspense>
872 + )}
873 + </div>
874 + );
875 + }
876 + if (
877 + gate(flags => flags.enableClientRenderFallbackOnHydrationMismatch)
878 + ) {
879 + expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
880 + Array [
881 + "Warning: Did not expect server HTML to contain a <main> in <div>.
882 + in div (at **)
883 + in Mismatch (at **)",
884 + "Warning: An error occurred during hydration. The server HTML was replaced with client content in <div>.",
885 + "Caught [Hydration failed because the initial UI does not match what was rendered on the server.]",
886 + "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]",
887 + ]
888 + `);
889 + } else {
890 + expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
891 + Array [
892 + "Warning: Did not expect server HTML to contain a <main> in <div>.
893 + in div (at **)
894 + in Mismatch (at **)",
895 + ]
896 + `);
897 + }
898 + });
899 +
900 + // @gate __DEV__
901 + it('warns when client renders an extra Suspense node in fallback mode', () => {
902 + function Mismatch({isClient}) {
903 + return (
904 + <div className="parent">
905 + {isClient && (
906 + <React.Suspense fallback={<p>Loading...</p>}>
907 + <main className="only" />
908 + <Never />
909 + </React.Suspense>
910 + )}
911 + </div>
912 + );
913 + }
914 + if (
915 + gate(flags => flags.enableClientRenderFallbackOnHydrationMismatch)
916 + ) {
917 + // TODO: This message doesn't seem to have any useful details.
918 + expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
919 + Array [
920 + "Warning: An error occurred during hydration. The server HTML was replaced with client content in <div>.",
921 + "Caught [Hydration failed because the initial UI does not match what was rendered on the server.]",
922 + "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]",
923 + ]
924 + `);
925 + } else {
926 + expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`Array []`);
927 + }
928 + });
929 +
930 + // @gate __DEV__
931 + it('warns when server renders an extra Suspense node in fallback mode', () => {
932 + function Mismatch({isClient}) {
933 + return (
934 + <div className="parent">
935 + {!isClient && (
936 + <React.Suspense fallback={<p>Loading...</p>}>
937 + <main className="only" />
938 + <Never />
939 + </React.Suspense>
940 + )}
941 + </div>
942 + );
943 + }
944 + if (
945 + gate(flags => flags.enableClientRenderFallbackOnHydrationMismatch)
946 + ) {
947 + expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
948 + Array [
949 + "Warning: Did not expect server HTML to contain a <p> in <div>.
950 + in div (at **)
951 + in Mismatch (at **)",
952 + "Warning: An error occurred during hydration. The server HTML was replaced with client content in <div>.",
953 + "Caught [Hydration failed because the initial UI does not match what was rendered on the server.]",
954 + "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]",
955 + ]
956 + `);
957 + } else {
958 + expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
959 + Array [
960 + "Warning: Did not expect server HTML to contain a <p> in <div>.
961 + in div (at **)
962 + in Mismatch (at **)",
963 + ]
964 + `);
965 + }
966 + });
967 +
968 + // @gate __DEV__
969 + it('warns when client renders an extra node inside Suspense content', () => {
970 + function Mismatch({isClient}) {
971 + return (
972 + <div className="parent">
973 + <React.Suspense fallback={<p>Loading...</p>}>
974 + <header className="1" />
975 + {isClient && <main className="second" />}
976 + <footer className="3" />
977 + </React.Suspense>
978 + </div>
979 + );
980 + }
981 + if (
982 + gate(flags => flags.enableClientRenderFallbackOnHydrationMismatch)
983 + ) {
984 + expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
985 + Array [
986 + "Warning: Expected server HTML to contain a matching <main> in <div>.
987 + in main (at **)
988 + in Suspense (at **)
989 + in div (at **)
990 + in Mismatch (at **)",
991 + "Caught [Hydration failed because the initial UI does not match what was rendered on the server.]",
992 + "Caught [There was an error while hydrating this Suspense boundary. Switched to client rendering.]",
993 + ]
994 + `);
995 + } else {
996 + expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
997 + Array [
998 + "Warning: Expected server HTML to contain a matching <main> in <div>.
999 + in main (at **)
1000 + in Suspense (at **)
1001 + in div (at **)
1002 + in Mismatch (at **)",
1003 + ]
1004 + `);
1005 + }
1006 + });
1007 +
1008 + // @gate __DEV__
1009 + it('warns when server renders an extra node inside Suspense content', () => {
1010 + function Mismatch({isClient}) {
1011 + return (
1012 + <div className="parent">
1013 + <React.Suspense fallback={<p>Loading...</p>}>
1014 + <header className="1" />
1015 + {!isClient && <main className="second" />}
1016 + <footer className="3" />
1017 + </React.Suspense>
1018 + </div>
1019 + );
1020 + }
1021 + if (
1022 + gate(flags => flags.enableClientRenderFallbackOnHydrationMismatch)
1023 + ) {
1024 + expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
1025 + Array [
1026 + "Warning: Expected server HTML to contain a matching <footer> in <div>.
1027 + in footer (at **)
1028 + in Suspense (at **)
1029 + in div (at **)
1030 + in Mismatch (at **)",
1031 + "Caught [Hydration failed because the initial UI does not match what was rendered on the server.]",
1032 + "Caught [There was an error while hydrating this Suspense boundary. Switched to client rendering.]",
1033 + ]
1034 + `);
1035 + } else {
1036 + expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
1037 + Array [
1038 + "Warning: Did not expect server HTML to contain a <main> in <div>.
1039 + in footer (at **)
1040 + in Suspense (at **)
1041 + in div (at **)
1042 + in Mismatch (at **)",
1043 + ]
1044 + `);
1045 + }
1046 + });
1047 +
1048 + // @gate __DEV__
1049 + it('warns when client renders an extra node inside Suspense fallback', () => {
1050 + function Mismatch({isClient}) {
1051 + return (
1052 + <div className="parent">
1053 + <React.Suspense
1054 + fallback={
1055 + <>
1056 + <p>Loading...</p>
1057 + {isClient && <br />}
1058 + </>
1059 + }>
1060 + <main className="only" />
1061 + <Never />
1062 + </React.Suspense>
1063 + </div>
1064 + );
1065 + }
1066 + // TODO: Why does this not show a fallback mismatch?
1067 + // And why is this message different from the other ones?
1068 + if (
1069 + gate(flags => flags.enableClientRenderFallbackOnHydrationMismatch)
1070 + ) {
1071 + expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
1072 + Array [
1073 + "Caught [The server could not finish this Suspense boundary, likely due to an error during server rendering. Switched to client rendering.]",
1074 + ]
1075 + `);
1076 + } else {
1077 + expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
1078 + Array [
1079 + "Caught [The server could not finish this Suspense boundary, likely due to an error during server rendering. Switched to client rendering.]",
1080 + ]
1081 + `);
1082 + }
1083 + });
1084 +
1085 + // @gate __DEV__
1086 + it('warns when server renders an extra node inside Suspense fallback', () => {
1087 + function Mismatch({isClient}) {
1088 + return (
1089 + <div className="parent">
1090 + <React.Suspense
1091 + fallback={
1092 + <>
1093 + <p>Loading...</p>
1094 + {!isClient && <br />}
1095 + </>
1096 + }>
1097 + <main className="only" />
1098 + <Never />
1099 + </React.Suspense>
1100 + </div>
1101 + );
1102 + }
1103 + // TODO: Why does this not show a fallback mismatch?
1104 + // And why is this message different from the other ones?
1105 + if (
1106 + gate(flags => flags.enableClientRenderFallbackOnHydrationMismatch)
1107 + ) {
1108 + expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
1109 + Array [
1110 + "Caught [The server could not finish this Suspense boundary, likely due to an error during server rendering. Switched to client rendering.]",
1111 + ]
1112 + `);
1113 + } else {
1114 + expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
1115 + Array [
1116 + "Caught [The server could not finish this Suspense boundary, likely due to an error during server rendering. Switched to client rendering.]",
1117 + ]
1118 + `);
1119 + }
1120 + });
1121 + });
1122 +
1123 + describe('Fragment', () => {
1124 + // @gate __DEV__
1125 + it('warns when client renders an extra Fragment node', () => {
1126 + function Mismatch({isClient}) {
1127 + return (
1128 + <div className="parent">
1129 + {isClient && (
1130 + <>
1131 + <header className="1" />
1132 + <main className="2" />
1133 + <footer className="3" />
1134 + </>
1135 + )}
1136 + </div>
1137 + );
1138 + }
1139 + if (
1140 + gate(flags => flags.enableClientRenderFallbackOnHydrationMismatch)
1141 + ) {
1142 + expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
1143 + Array [
1144 + "Warning: Expected server HTML to contain a matching <header> in <div>.
1145 + in header (at **)
1146 + in div (at **)
1147 + in Mismatch (at **)",
1148 + "Warning: An error occurred during hydration. The server HTML was replaced with client content in <div>.",
1149 + "Caught [Hydration failed because the initial UI does not match what was rendered on the server.]",
1150 + "Caught [Hydration failed because the initial UI does not match what was rendered on the server.]",
1151 + "Caught [Hydration failed because the initial UI does not match what was rendered on the server.]",
1152 + "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]",
1153 + ]
1154 + `);
1155 + } else {
1156 + expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
1157 + Array [
1158 + "Warning: Expected server HTML to contain a matching <header> in <div>.
1159 + in header (at **)
1160 + in div (at **)
1161 + in Mismatch (at **)",
1162 + ]
1163 + `);
1164 + }
1165 + });
1166 +
1167 + // @gate __DEV__
1168 + it('warns when server renders an extra Fragment node', () => {
1169 + function Mismatch({isClient}) {
1170 + return (
1171 + <div className="parent">
1172 + {!isClient && (
1173 + <>
1174 + <header className="1" />
1175 + <main className="2" />
1176 + <footer className="3" />
1177 + </>
1178 + )}
1179 + </div>
1180 + );
1181 + }
1182 + if (
1183 + gate(flags => flags.enableClientRenderFallbackOnHydrationMismatch)
1184 + ) {
1185 + expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
1186 + Array [
1187 + "Warning: Did not expect server HTML to contain a <header> in <div>.
1188 + in div (at **)
1189 + in Mismatch (at **)",
1190 + "Warning: An error occurred during hydration. The server HTML was replaced with client content in <div>.",
1191 + "Caught [Hydration failed because the initial UI does not match what was rendered on the server.]",
1192 + "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]",
1193 + ]
1194 + `);
1195 + } else {
1196 + expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
1197 + Array [
1198 + "Warning: Did not expect server HTML to contain a <header> in <div>.
1199 + in div (at **)
1200 + in Mismatch (at **)",
1201 + ]
1202 + `);
1203 + }
1204 + });
1205 + });
1206 + });
1207 +
1208 + describe('misc cases', () => {
1209 + // @gate __DEV__
1210 + it('warns when client renders an extra node deeper in the tree', () => {
1211 + function Mismatch({isClient}) {
1212 + return isClient ? <ProfileSettings /> : <MediaSettings />;
1213 + }
1214 +
1215 + function ProfileSettings() {
1216 + return (
1217 + <div className="parent">
1218 + <input />
1219 + <Panel type="profile" />
1220 + </div>
1221 + );
1222 + }
1223 +
1224 + function MediaSettings() {
1225 + return (
1226 + <div className="parent">
1227 + <input />
1228 + <Panel type="media" />
1229 + </div>
1230 + );
1231 + }
1232 +
1233 + function Panel({type}) {
1234 + return (
1235 + <>
1236 + <header className="1" />
1237 + <main className="2" />
1238 + {type === 'profile' && <footer className="3" />}
1239 + </>
1240 + );
1241 + }
1242 +
1243 + if (gate(flags => flags.enableClientRenderFallbackOnHydrationMismatch)) {
1244 + expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
1245 + Array [
1246 + "Warning: Expected server HTML to contain a matching <footer> in <div>.
1247 + in footer (at **)
1248 + in Panel (at **)
1249 + in div (at **)
1250 + in ProfileSettings (at **)
1251 + in Mismatch (at **)",
1252 + "Warning: An error occurred during hydration. The server HTML was replaced with client content in <div>.",
1253 + "Caught [Hydration failed because the initial UI does not match what was rendered on the server.]",
1254 + "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]",
1255 + ]
1256 + `);
1257 + } else {
1258 + expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
1259 + Array [
1260 + "Warning: Expected server HTML to contain a matching <footer> in <div>.
1261 + in footer (at **)
1262 + in Panel (at **)
1263 + in div (at **)
1264 + in ProfileSettings (at **)
1265 + in Mismatch (at **)",
1266 + ]
1267 + `);
1268 + }
1269 + });
1270 +
1271 + // @gate __DEV__
1272 + it('warns when server renders an extra node deeper in the tree', () => {
1273 + function Mismatch({isClient}) {
1274 + return isClient ? <ProfileSettings /> : <MediaSettings />;
1275 + }
1276 +
1277 + function ProfileSettings() {
1278 + return (
1279 + <div className="parent">
1280 + <input />
1281 + <Panel type="profile" />
1282 + </div>
1283 + );
1284 + }
1285 +
1286 + function MediaSettings() {
1287 + return (
1288 + <div className="parent">
1289 + <input />
1290 + <Panel type="media" />
1291 + </div>
1292 + );
1293 + }
1294 +
1295 + function Panel({type}) {
1296 + return (
1297 + <>
1298 + <header className="1" />
1299 + <main className="2" />
1300 + {type !== 'profile' && <footer className="3" />}
1301 + </>
1302 + );
1303 + }
1304 +
1305 + if (gate(flags => flags.enableClientRenderFallbackOnHydrationMismatch)) {
1306 + expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
1307 + Array [
1308 + "Warning: Did not expect server HTML to contain a <footer> in <div>.
1309 + in div (at **)
1310 + in ProfileSettings (at **)
1311 + in Mismatch (at **)",
1312 + "Warning: An error occurred during hydration. The server HTML was replaced with client content in <div>.",
1313 + "Caught [Hydration failed because the initial UI does not match what was rendered on the server.]",
1314 + "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]",
1315 + ]
1316 + `);
1317 + } else {
1318 + expect(testMismatch(Mismatch)).toMatchInlineSnapshot(`
1319 + Array [
1320 + "Warning: Did not expect server HTML to contain a <footer> in <div>.
1321 + in div (at **)
1322 + in ProfileSettings (at **)
1323 + in Mismatch (at **)",
1324 + ]
1325 + `);
1326 + }
1327 + });
1328 + });
1329 +});