@samitouri / QOS-React-2 / commits / 5f7f528083

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