Add more tests for suppressHydrationWarning (#24275)
* More tests for suppressHydrationWarning * Move suppressHydration tests to new file * Extract more tests * Test name * Test legacy behavior too
dan committed
Apr 5, 2022 at 00:04 UTC
5f7f5280836a44d4b81c1cb13f51e7482470cf1b
2 files changed
+698
-92
packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js
-92
@@ -2811,96 +2811,4 @@ describe('ReactDOMFizzServer', () => {
2811
</ul>,
2812
);
2813
});
2814
-
2815
- // @gate experimental
2816
- it('suppresses and fixes text mismatches with suppressHydrationWarning', async () => {
2817
- function App({isClient}) {
2818
- return (
2819
- <div>
2820
- <span
2821
- suppressHydrationWarning={true}
2822
- data-attr={isClient ? 'client-attr' : 'server-attr'}>
2823
- {isClient ? 'Client Text' : 'Server Text'}
2824
- </span>
2825
- <span suppressHydrationWarning={true}>{isClient ? 2 : 1}</span>
2826
- <span suppressHydrationWarning={true}>
2827
- hello,{isClient ? 'client' : 'server'}
2828
- </span>
2829
- </div>
2830
- );
2831
- }
2832
- await act(async () => {
2833
- const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
2834
- <App isClient={false} />,
2835
- );
2836
- pipe(writable);
2837
- });
2838
- expect(getVisibleChildren(container)).toEqual(
2839
- <div>
2840
- <span data-attr="server-attr">Server Text</span>
2841
- <span>1</span>
2842
- <span>
2843
- {'hello,'}
2844
- {'server'}
2845
- </span>
2846
- </div>,
2847
- );
2848
- ReactDOMClient.hydrateRoot(container, <App isClient={true} />, {
2849
- onRecoverableError(error) {
2850
- // Don't miss a hydration error. There should be none.
2851
- Scheduler.unstable_yieldValue(error.message);
2852
- },
2853
- });
2854
- expect(Scheduler).toFlushAndYield([]);
2855
- // The text mismatch should be *silently* fixed. Even in production.
2856
- // The attribute mismatch should be ignored and not fixed.
2857
- expect(getVisibleChildren(container)).toEqual(
2858
- <div>
2859
- <span data-attr="server-attr">Client Text</span>
2860
- <span>2</span>
2861
- <span>
2862
- {'hello,'}
2863
- {'client'}
2864
- </span>
2865
- </div>,
2866
- );
2867
- });
2868
-
2869
- // @gate experimental
2870
- it('suppresses and does not fix html mismatches with suppressHydrationWarning', async () => {
2871
- function App({isClient}) {
2872
- return (
2873
- <div>
2874
- <p
2875
- suppressHydrationWarning={true}
2876
- dangerouslySetInnerHTML={{
2877
- __html: isClient ? 'Client HTML' : 'Server HTML',
2878
- }}
2879
- />
2880
- </div>
2881
- );
2882
- }
2883
- await act(async () => {
2884
- const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
2885
- <App isClient={false} />,
2886
- );
2887
- pipe(writable);
2888
- });
2889
- expect(getVisibleChildren(container)).toEqual(
2890
- <div>
2891
- <p>Server HTML</p>
2892
- </div>,
2893
- );
2894
- ReactDOMClient.hydrateRoot(container, <App isClient={true} />, {
2895
- onRecoverableError(error) {
2896
- Scheduler.unstable_yieldValue(error.message);
2897
- },
2898
- });
2899
- expect(Scheduler).toFlushAndYield([]);
2900
- expect(getVisibleChildren(container)).toEqual(
2901
- <div>
2902
- <p>Server HTML</p>
2903
- </div>,
2904
- );
2905
- });
2814
});
packages/react-dom/src/__tests__/ReactDOMFizzSuppressHydrationWarning-test.js
new
+698
@@ -0,0 +1,698 @@
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
+ * @emails react-core
8
+ */
9
+
10
+'use strict';
11
+
12
+let JSDOM;
13
+let Stream;
14
+let Scheduler;
15
+let React;
16
+let ReactDOMClient;
17
+let ReactDOMFizzServer;
18
+let document;
19
+let writable;
20
+let container;
21
+let buffer = '';
22
+let hasErrored = false;
23
+let fatalError = undefined;
24
+
25
+describe('ReactDOMFizzServerHydrationWarning', () => {
26
+ beforeEach(() => {
27
+ jest.resetModules();
28
+ JSDOM = require('jsdom').JSDOM;
29
+ Scheduler = require('scheduler');
30
+ React = require('react');
31
+ ReactDOMClient = require('react-dom/client');
32
+ if (__EXPERIMENTAL__) {
33
+ ReactDOMFizzServer = require('react-dom/server');
34
+ }
35
+ Stream = require('stream');
36
+
37
+ // Test Environment
38
+ const jsdom = new JSDOM(
39
+ '<!DOCTYPE html><html><head></head><body><div id="container">',
40
+ {
41
+ runScripts: 'dangerously',
42
+ },
43
+ );
44
+ document = jsdom.window.document;
45
+ container = document.getElementById('container');
46
+
47
+ buffer = '';
48
+ hasErrored = false;
49
+
50
+ writable = new Stream.PassThrough();
51
+ writable.setEncoding('utf8');
52
+ writable.on('data', chunk => {
53
+ buffer += chunk;
54
+ });
55
+ writable.on('error', error => {
56
+ hasErrored = true;
57
+ fatalError = error;
58
+ });
59
+ });
60
+
61
+ async function act(callback) {
62
+ await callback();
63
+ // Await one turn around the event loop.
64
+ // This assumes that we'll flush everything we have so far.
65
+ await new Promise(resolve => {
66
+ setImmediate(resolve);
67
+ });
68
+ if (hasErrored) {
69
+ throw fatalError;
70
+ }
71
+ // JSDOM doesn't support stream HTML parser so we need to give it a proper fragment.
72
+ // We also want to execute any scripts that are embedded.
73
+ // We assume that we have now received a proper fragment of HTML.
74
+ const bufferedContent = buffer;
75
+ buffer = '';
76
+ const fakeBody = document.createElement('body');
77
+ fakeBody.innerHTML = bufferedContent;
78
+ while (fakeBody.firstChild) {
79
+ const node = fakeBody.firstChild;
80
+ if (node.nodeName === 'SCRIPT') {
81
+ const script = document.createElement('script');
82
+ script.textContent = node.textContent;
83
+ fakeBody.removeChild(node);
84
+ container.appendChild(script);
85
+ } else {
86
+ container.appendChild(node);
87
+ }
88
+ }
89
+ }
90
+
91
+ function getVisibleChildren(element) {
92
+ const children = [];
93
+ let node = element.firstChild;
94
+ while (node) {
95
+ if (node.nodeType === 1) {
96
+ if (
97
+ node.tagName !== 'SCRIPT' &&
98
+ node.tagName !== 'TEMPLATE' &&
99
+ node.tagName !== 'template' &&
100
+ !node.hasAttribute('hidden') &&
101
+ !node.hasAttribute('aria-hidden')
102
+ ) {
103
+ const props = {};
104
+ const attributes = node.attributes;
105
+ for (let i = 0; i < attributes.length; i++) {
106
+ if (
107
+ attributes[i].name === 'id' &&
108
+ attributes[i].value.includes(':')
109
+ ) {
110
+ // We assume this is a React added ID that's a non-visual implementation detail.
111
+ continue;
112
+ }
113
+ props[attributes[i].name] = attributes[i].value;
114
+ }
115
+ props.children = getVisibleChildren(node);
116
+ children.push(React.createElement(node.tagName.toLowerCase(), props));
117
+ }
118
+ } else if (node.nodeType === 3) {
119
+ children.push(node.data);
120
+ }
121
+ node = node.nextSibling;
122
+ }
123
+ return children.length === 0
124
+ ? undefined
125
+ : children.length === 1
126
+ ? children[0]
127
+ : children;
128
+ }
129
+
130
+ // @gate experimental
131
+ it('suppresses and fixes text mismatches with suppressHydrationWarning', async () => {
132
+ function App({isClient}) {
133
+ return (
134
+ <div>
135
+ <span suppressHydrationWarning={true}>
136
+ {isClient ? 'Client Text' : 'Server Text'}
137
+ </span>
138
+ <span suppressHydrationWarning={true}>{isClient ? 2 : 1}</span>
139
+ </div>
140
+ );
141
+ }
142
+ await act(async () => {
143
+ const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
144
+ <App isClient={false} />,
145
+ );
146
+ pipe(writable);
147
+ });
148
+ expect(getVisibleChildren(container)).toEqual(
149
+ <div>
150
+ <span>Server Text</span>
151
+ <span>1</span>
152
+ </div>,
153
+ );
154
+ ReactDOMClient.hydrateRoot(container, <App isClient={true} />, {
155
+ onRecoverableError(error) {
156
+ // Don't miss a hydration error. There should be none.
157
+ Scheduler.unstable_yieldValue(error.message);
158
+ },
159
+ });
160
+ expect(Scheduler).toFlushAndYield([]);
161
+ // The text mismatch should be *silently* fixed. Even in production.
162
+ expect(getVisibleChildren(container)).toEqual(
163
+ <div>
164
+ <span>Client Text</span>
165
+ <span>2</span>
166
+ </div>,
167
+ );
168
+ });
169
+
170
+ // @gate experimental
171
+ it('suppresses and fixes multiple text node mismatches with suppressHydrationWarning', async () => {
172
+ function App({isClient}) {
173
+ return (
174
+ <div>
175
+ <span suppressHydrationWarning={true}>
176
+ {isClient ? 'Client1' : 'Server1'}
177
+ {isClient ? 'Client2' : 'Server2'}
178
+ </span>
179
+ </div>
180
+ );
181
+ }
182
+ await act(async () => {
183
+ const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
184
+ <App isClient={false} />,
185
+ );
186
+ pipe(writable);
187
+ });
188
+ expect(getVisibleChildren(container)).toEqual(
189
+ <div>
190
+ <span>
191
+ {'Server1'}
192
+ {'Server2'}
193
+ </span>
194
+ </div>,
195
+ );
196
+ ReactDOMClient.hydrateRoot(container, <App isClient={true} />, {
197
+ onRecoverableError(error) {
198
+ Scheduler.unstable_yieldValue(error.message);
199
+ },
200
+ });
201
+ expect(Scheduler).toFlushAndYield([]);
202
+ expect(getVisibleChildren(container)).toEqual(
203
+ <div>
204
+ <span>
205
+ {'Client1'}
206
+ {'Client2'}
207
+ </span>
208
+ </div>,
209
+ );
210
+ });
211
+
212
+ // @gate experimental
213
+ it('errors on text-to-element mismatches with suppressHydrationWarning', async () => {
214
+ function App({isClient}) {
215
+ return (
216
+ <div>
217
+ <span suppressHydrationWarning={true}>
218
+ Hello, {isClient ? <span>Client</span> : 'Server'}!
219
+ </span>
220
+ </div>
221
+ );
222
+ }
223
+ await act(async () => {
224
+ const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
225
+ <App isClient={false} />,
226
+ );
227
+ pipe(writable);
228
+ });
229
+ expect(getVisibleChildren(container)).toEqual(
230
+ <div>
231
+ <span>
232
+ {'Hello, '}
233
+ {'Server'}
234
+ {'!'}
235
+ </span>
236
+ </div>,
237
+ );
238
+ ReactDOMClient.hydrateRoot(container, <App isClient={true} />, {
239
+ onRecoverableError(error) {
240
+ Scheduler.unstable_yieldValue(error.message);
241
+ },
242
+ });
243
+ if (gate(flags => flags.enableClientRenderFallbackOnHydrationMismatch)) {
244
+ expect(() => {
245
+ expect(Scheduler).toFlushAndYield([
246
+ 'Hydration failed because the initial UI does not match what was rendered on the server.',
247
+ 'There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.',
248
+ ]);
249
+ }).toErrorDev(
250
+ [
251
+ 'An error occurred during hydration. The server HTML was replaced with client content in <div>.',
252
+ ],
253
+ {withoutStack: true},
254
+ );
255
+ } else {
256
+ // This used to not warn.
257
+ expect(Scheduler).toFlushAndYield([]);
258
+ }
259
+ expect(getVisibleChildren(container)).toEqual(
260
+ <div>
261
+ <span>
262
+ Hello, <span>Client</span>!
263
+ </span>
264
+ </div>,
265
+ );
266
+ });
267
+
268
+ // @gate experimental
269
+ it('suppresses and fixes client-only single text node mismatches with suppressHydrationWarning', async () => {
270
+ function App({isClient}) {
271
+ return (
272
+ <div>
273
+ <span suppressHydrationWarning={true}>
274
+ {isClient ? 'Client' : null}
275
+ </span>
276
+ </div>
277
+ );
278
+ }
279
+ await act(async () => {
280
+ const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
281
+ <App isClient={false} />,
282
+ );
283
+ pipe(writable);
284
+ });
285
+ expect(getVisibleChildren(container)).toEqual(
286
+ <div>
287
+ <span />
288
+ </div>,
289
+ );
290
+ ReactDOMClient.hydrateRoot(container, <App isClient={true} />, {
291
+ onRecoverableError(error) {
292
+ Scheduler.unstable_yieldValue(error.message);
293
+ },
294
+ });
295
+ expect(Scheduler).toFlushAndYield([]);
296
+ expect(getVisibleChildren(container)).toEqual(
297
+ <div>
298
+ <span>{'Client'}</span>
299
+ </div>,
300
+ );
301
+ });
302
+
303
+ // TODO: This behavior is not consistent with client-only single text node.
304
+ // @gate experimental
305
+ it('errors on server-only single text node mismatches with suppressHydrationWarning', async () => {
306
+ function App({isClient}) {
307
+ return (
308
+ <div>
309
+ <span suppressHydrationWarning={true}>
310
+ {isClient ? null : 'Server'}
311
+ </span>
312
+ </div>
313
+ );
314
+ }
315
+ await act(async () => {
316
+ const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
317
+ <App isClient={false} />,
318
+ );
319
+ pipe(writable);
320
+ });
321
+ expect(getVisibleChildren(container)).toEqual(
322
+ <div>
323
+ <span>Server</span>
324
+ </div>,
325
+ );
326
+ ReactDOMClient.hydrateRoot(container, <App isClient={true} />, {
327
+ onRecoverableError(error) {
328
+ Scheduler.unstable_yieldValue(error.message);
329
+ },
330
+ });
331
+ if (gate(flags => flags.enableClientRenderFallbackOnHydrationMismatch)) {
332
+ expect(() => {
333
+ expect(Scheduler).toFlushAndYield([
334
+ 'Hydration failed because the initial UI does not match what was rendered on the server.',
335
+ 'There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.',
336
+ ]);
337
+ }).toErrorDev(
338
+ [
339
+ 'An error occurred during hydration. The server HTML was replaced with client content in <div>.',
340
+ ],
341
+ {withoutStack: true},
342
+ );
343
+ } else {
344
+ // This used to not warn.
345
+ expect(Scheduler).toFlushAndYield([]);
346
+ }
347
+ expect(getVisibleChildren(container)).toEqual(
348
+ <div>
349
+ <span />
350
+ </div>,
351
+ );
352
+ });
353
+
354
+ // @gate experimental
355
+ it('errors on client-only extra text node mismatches with suppressHydrationWarning', async () => {
356
+ function App({isClient}) {
357
+ return (
358
+ <div>
359
+ <span suppressHydrationWarning={true}>
360
+ <span>Shared</span>
361
+ {isClient ? 'Client' : null}
362
+ </span>
363
+ </div>
364
+ );
365
+ }
366
+ await act(async () => {
367
+ const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
368
+ <App isClient={false} />,
369
+ );
370
+ pipe(writable);
371
+ });
372
+ expect(getVisibleChildren(container)).toEqual(
373
+ <div>
374
+ <span>
375
+ <span>Shared</span>
376
+ </span>
377
+ </div>,
378
+ );
379
+ ReactDOMClient.hydrateRoot(container, <App isClient={true} />, {
380
+ onRecoverableError(error) {
381
+ Scheduler.unstable_yieldValue(error.message);
382
+ },
383
+ });
384
+ if (gate(flags => flags.enableClientRenderFallbackOnHydrationMismatch)) {
385
+ expect(() => {
386
+ expect(Scheduler).toFlushAndYield([
387
+ 'Hydration failed because the initial UI does not match what was rendered on the server.',
388
+ 'There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.',
389
+ ]);
390
+ }).toErrorDev(
391
+ [
392
+ 'An error occurred during hydration. The server HTML was replaced with client content in <div>.',
393
+ ],
394
+ {withoutStack: true},
395
+ );
396
+ } else {
397
+ // This used to not warn.
398
+ expect(Scheduler).toFlushAndYield([]);
399
+ }
400
+ expect(getVisibleChildren(container)).toEqual(
401
+ <div>
402
+ <span>
403
+ <span>Shared</span>
404
+ {'Client'}
405
+ </span>
406
+ </div>,
407
+ );
408
+ });
409
+
410
+ // @gate experimental
411
+ it('errors on server-only extra text node mismatches with suppressHydrationWarning', async () => {
412
+ function App({isClient}) {
413
+ return (
414
+ <div>
415
+ <span suppressHydrationWarning={true}>
416
+ <span>Shared</span>
417
+ {isClient ? null : 'Server'}
418
+ </span>
419
+ </div>
420
+ );
421
+ }
422
+ await act(async () => {
423
+ const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
424
+ <App isClient={false} />,
425
+ );
426
+ pipe(writable);
427
+ });
428
+ expect(getVisibleChildren(container)).toEqual(
429
+ <div>
430
+ <span>
431
+ <span>Shared</span>Server
432
+ </span>
433
+ </div>,
434
+ );
435
+ ReactDOMClient.hydrateRoot(container, <App isClient={true} />, {
436
+ onRecoverableError(error) {
437
+ Scheduler.unstable_yieldValue(error.message);
438
+ },
439
+ });
440
+ if (gate(flags => flags.enableClientRenderFallbackOnHydrationMismatch)) {
441
+ expect(() => {
442
+ expect(Scheduler).toFlushAndYield([
443
+ 'Hydration failed because the initial UI does not match what was rendered on the server.',
444
+ 'There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.',
445
+ ]);
446
+ }).toErrorDev(
447
+ [
448
+ 'An error occurred during hydration. The server HTML was replaced with client content in <div>.',
449
+ ],
450
+ {withoutStack: true},
451
+ );
452
+ } else {
453
+ // This used to not warn.
454
+ expect(Scheduler).toFlushAndYield([]);
455
+ }
456
+ expect(getVisibleChildren(container)).toEqual(
457
+ <div>
458
+ <span>
459
+ <span>Shared</span>
460
+ </span>
461
+ </div>,
462
+ );
463
+ });
464
+
465
+ // @gate experimental
466
+ it('errors on element-to-text mismatches with suppressHydrationWarning', async () => {
467
+ function App({isClient}) {
468
+ return (
469
+ <div>
470
+ <span suppressHydrationWarning={true}>
471
+ Hello, {isClient ? 'Client' : <span>Server</span>}!
472
+ </span>
473
+ </div>
474
+ );
475
+ }
476
+ await act(async () => {
477
+ const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
478
+ <App isClient={false} />,
479
+ );
480
+ pipe(writable);
481
+ });
482
+ expect(getVisibleChildren(container)).toEqual(
483
+ <div>
484
+ <span>
485
+ Hello, <span>Server</span>!
486
+ </span>
487
+ </div>,
488
+ );
489
+ ReactDOMClient.hydrateRoot(container, <App isClient={true} />, {
490
+ onRecoverableError(error) {
491
+ Scheduler.unstable_yieldValue(error.message);
492
+ },
493
+ });
494
+ if (gate(flags => flags.enableClientRenderFallbackOnHydrationMismatch)) {
495
+ expect(() => {
496
+ expect(Scheduler).toFlushAndYield([
497
+ 'Hydration failed because the initial UI does not match what was rendered on the server.',
498
+ 'Hydration failed because the initial UI does not match what was rendered on the server.',
499
+ 'There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.',
500
+ ]);
501
+ }).toErrorDev(
502
+ [
503
+ 'An error occurred during hydration. The server HTML was replaced with client content in <div>.',
504
+ ],
505
+ {withoutStack: true},
506
+ );
507
+ } else {
508
+ // This used to not warn.
509
+ expect(Scheduler).toFlushAndYield([]);
510
+ }
511
+ expect(getVisibleChildren(container)).toEqual(
512
+ <div>
513
+ <span>
514
+ {'Hello, '}
515
+ {'Client'}
516
+ {'!'}
517
+ </span>
518
+ </div>,
519
+ );
520
+ });
521
+
522
+ // @gate experimental
523
+ it('suppresses and does not fix attribute mismatches with suppressHydrationWarning', async () => {
524
+ function App({isClient}) {
525
+ return (
526
+ <div>
527
+ <span
528
+ suppressHydrationWarning={true}
529
+ className={isClient ? 'client' : 'server'}
530
+ style={{opacity: isClient ? 1 : 0}}
531
+ data-serveronly={isClient ? null : 'server-only'}
532
+ data-clientonly={isClient ? 'client-only' : null}
533
+ />
534
+ </div>
535
+ );
536
+ }
537
+ await act(async () => {
538
+ const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
539
+ <App isClient={false} />,
540
+ );
541
+ pipe(writable);
542
+ });
543
+ expect(getVisibleChildren(container)).toEqual(
544
+ <div>
545
+ <span class="server" style="opacity:0" data-serveronly="server-only" />
546
+ </div>,
547
+ );
548
+ ReactDOMClient.hydrateRoot(container, <App isClient={true} />, {
549
+ onRecoverableError(error) {
550
+ Scheduler.unstable_yieldValue(error.message);
551
+ },
552
+ });
553
+ expect(Scheduler).toFlushAndYield([]);
554
+ expect(getVisibleChildren(container)).toEqual(
555
+ <div>
556
+ <span class="server" style="opacity:0" data-serveronly="server-only" />
557
+ </div>,
558
+ );
559
+ });
560
+
561
+ // @gate experimental
562
+ it('suppresses and does not fix html mismatches with suppressHydrationWarning', async () => {
563
+ function App({isClient}) {
564
+ return (
565
+ <div>
566
+ <p
567
+ suppressHydrationWarning={true}
568
+ dangerouslySetInnerHTML={{
569
+ __html: isClient ? 'Client HTML' : 'Server HTML',
570
+ }}
571
+ />
572
+ </div>
573
+ );
574
+ }
575
+ await act(async () => {
576
+ const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
577
+ <App isClient={false} />,
578
+ );
579
+ pipe(writable);
580
+ });
581
+ expect(getVisibleChildren(container)).toEqual(
582
+ <div>
583
+ <p>Server HTML</p>
584
+ </div>,
585
+ );
586
+ ReactDOMClient.hydrateRoot(container, <App isClient={true} />, {
587
+ onRecoverableError(error) {
588
+ Scheduler.unstable_yieldValue(error.message);
589
+ },
590
+ });
591
+ expect(Scheduler).toFlushAndYield([]);
592
+ expect(getVisibleChildren(container)).toEqual(
593
+ <div>
594
+ <p>Server HTML</p>
595
+ </div>,
596
+ );
597
+ });
598
+
599
+ // @gate experimental
600
+ it('errors on insertions with suppressHydrationWarning', async () => {
601
+ function App({isClient}) {
602
+ return (
603
+ <div suppressHydrationWarning={true}>
604
+ <p>Client and server</p>
605
+ {isClient && <p>Client only</p>}
606
+ </div>
607
+ );
608
+ }
609
+ await act(async () => {
610
+ const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
611
+ <App isClient={false} />,
612
+ );
613
+ pipe(writable);
614
+ });
615
+ expect(getVisibleChildren(container)).toEqual(
616
+ <div>
617
+ <p>Client and server</p>
618
+ </div>,
619
+ );
620
+ ReactDOMClient.hydrateRoot(container, <App isClient={true} />, {
621
+ onRecoverableError(error) {
622
+ Scheduler.unstable_yieldValue(error.message);
623
+ },
624
+ });
625
+ if (gate(flags => flags.enableClientRenderFallbackOnHydrationMismatch)) {
626
+ expect(() => {
627
+ expect(Scheduler).toFlushAndYield([
628
+ 'Hydration failed because the initial UI does not match what was rendered on the server.',
629
+ 'There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.',
630
+ ]);
631
+ }).toErrorDev(
632
+ [
633
+ 'An error occurred during hydration. The server HTML was replaced with client content in <div>.',
634
+ ],
635
+ {withoutStack: true},
636
+ );
637
+ } else {
638
+ // This used to not warn.
639
+ expect(Scheduler).toFlushAndYield([]);
640
+ }
641
+ expect(getVisibleChildren(container)).toEqual(
642
+ <div>
643
+ <p>Client and server</p>
644
+ <p>Client only</p>
645
+ </div>,
646
+ );
647
+ });
648
+
649
+ // @gate experimental
650
+ it('errors on deletions with suppressHydrationWarning', async () => {
651
+ function App({isClient}) {
652
+ return (
653
+ <div suppressHydrationWarning={true}>
654
+ <p>Client and server</p>
655
+ {!isClient && <p>Server only</p>}
656
+ </div>
657
+ );
658
+ }
659
+ await act(async () => {
660
+ const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
661
+ <App isClient={false} />,
662
+ );
663
+ pipe(writable);
664
+ });
665
+ expect(getVisibleChildren(container)).toEqual(
666
+ <div>
667
+ <p>Client and server</p>
668
+ <p>Server only</p>
669
+ </div>,
670
+ );
671
+ ReactDOMClient.hydrateRoot(container, <App isClient={true} />, {
672
+ onRecoverableError(error) {
673
+ Scheduler.unstable_yieldValue(error.message);
674
+ },
675
+ });
676
+ if (gate(flags => flags.enableClientRenderFallbackOnHydrationMismatch)) {
677
+ expect(() => {
678
+ expect(Scheduler).toFlushAndYield([
679
+ 'Hydration failed because the initial UI does not match what was rendered on the server.',
680
+ 'There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.',
681
+ ]);
682
+ }).toErrorDev(
683
+ [
684
+ 'An error occurred during hydration. The server HTML was replaced with client content in <div>.',
685
+ ],
686
+ {withoutStack: true},
687
+ );
688
+ } else {
689
+ // This used to not warn.
690
+ expect(Scheduler).toFlushAndYield([]);
691
+ }
692
+ expect(getVisibleChildren(container)).toEqual(
693
+ <div>
694
+ <p>Client and server</p>
695
+ </div>,
696
+ );
697
+ });
698
+});