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
+});