@samitouri / QOS-React / commits / f637f6a473

Convert ReactServerRenderingHydration to createRoot (#28447)

Sebastian Silbermann committed Feb 26, 2024 at 20:59 UTC f637f6a4736a9a02ab18067ef4b1c5e033c8d6f4
1 file changed +202 -93
packages/react-dom/src/__tests__/ReactServerRenderingHydration-test.js
+202 -93
@@ -34,7 +34,7 @@ describe('ReactDOMServerHydration', () => {
34 act = InternalTestUtils.act;
35 });
36
37 - it('should have the correct mounting behavior (new hydrate API)', () => {
37 + it('should have the correct mounting behavior', async () => {
38 let mountCount = 0;
39 let numClicks = 0;
40
@@ -61,20 +61,29 @@ describe('ReactDOMServerHydration', () => {
61 const element = document.createElement('div');
62 document.body.appendChild(element);
63 try {
64 - ReactDOM.render(<TestComponent />, element);
64 + let root = ReactDOMClient.createRoot(element);
65 + await act(() => {
66 + root.render(<TestComponent />);
67 + });
68
69 let lastMarkup = element.innerHTML;
70
71 // Exercise the update path. Markup should not change,
72 // but some lifecycle methods should be run again.
70 - ReactDOM.render(<TestComponent name="x" />, element);
73 + await act(() => {
74 + root.render(<TestComponent name="x" />);
75 + });
76 expect(mountCount).toEqual(1);
77
78 // Unmount and remount. We should get another mount event and
79 // we should get different markup, as the IDs are unique each time.
75 - ReactDOM.unmountComponentAtNode(element);
80 + root.unmount();
81 expect(element.innerHTML).toEqual('');
77 - ReactDOM.render(<TestComponent name="x" />, element);
82 + root = ReactDOMClient.createRoot(element);
83 + await act(() => {
84 + root.render(<TestComponent name="x" />);
85 + });
86 +
87 expect(mountCount).toEqual(2);
88 expect(element.innerHTML).not.toEqual(lastMarkup);
89
@@ -82,13 +91,22 @@ describe('ReactDOMServerHydration', () => {
91 // we used server rendering. We should mount again, but the markup should
92 // be unchanged. We will append a sentinel at the end of innerHTML to be
93 // sure that innerHTML was not changed.
85 - ReactDOM.unmountComponentAtNode(element);
94 + await act(() => {
95 + root.unmount();
96 + });
97 expect(element.innerHTML).toEqual('');
98
99 lastMarkup = ReactDOMServer.renderToString(<TestComponent name="x" />);
100 element.innerHTML = lastMarkup;
101
91 - let instance = ReactDOM.hydrate(<TestComponent name="x" />, element);
102 + let instance;
103 +
104 + root = await act(() => {
105 + return ReactDOMClient.hydrateRoot(
106 + element,
107 + <TestComponent name="x" ref={current => (instance = current)} />,
108 + );
109 + });
110 expect(mountCount).toEqual(3);
111 expect(element.innerHTML).toBe(lastMarkup);
112
@@ -97,15 +115,41 @@ describe('ReactDOMServerHydration', () => {
115 instance.spanRef.current.click();
116 expect(numClicks).toEqual(1);
117
100 - ReactDOM.unmountComponentAtNode(element);
118 + await act(() => {
119 + root.unmount();
120 + });
121 expect(element.innerHTML).toEqual('');
122
123 // Now simulate a situation where the app is not idempotent. React should
124 // warn but do the right thing.
125 element.innerHTML = lastMarkup;
106 - expect(() => {
107 - instance = ReactDOM.hydrate(<TestComponent name="y" />, element);
108 - }).toErrorDev('Text content did not match. Server: "x" Client: "y"');
126 + const enableClientRenderFallbackOnTextMismatch = gate(
127 + flags => flags.enableClientRenderFallbackOnTextMismatch,
128 + );
129 + await expect(async () => {
130 + root = await act(() => {
131 + return ReactDOMClient.hydrateRoot(
132 + element,
133 + <TestComponent
134 + name="y"
135 + ref={current => {
136 + instance = current;
137 + }}
138 + />,
139 + {
140 + onRecoverableError: error => {},
141 + },
142 + );
143 + });
144 + }).toErrorDev(
145 + enableClientRenderFallbackOnTextMismatch
146 + ? [
147 + 'An error occurred during hydration. The server HTML was replaced with client content in <div>.',
148 + 'Text content did not match. Server: "x" Client: "y"',
149 + ]
150 + : ['Text content did not match. Server: "x" Client: "y"'],
151 + {withoutStack: enableClientRenderFallbackOnTextMismatch ? 1 : 0},
152 + );
153 expect(mountCount).toEqual(4);
154 expect(element.innerHTML.length > 0).toBe(true);
155 expect(element.innerHTML).not.toEqual(lastMarkup);
@@ -164,25 +208,46 @@ describe('ReactDOMServerHydration', () => {
208 });
209
210 // Regression test for https://github.com/facebook/react/issues/11726
167 - it('should not focus on either server or client with autofocus={false} even if there is a markup mismatch', () => {
211 + it('should not focus on either server or client with autofocus={false} even if there is a markup mismatch', async () => {
212 const element = document.createElement('div');
213 element.innerHTML = ReactDOMServer.renderToString(
214 <button autoFocus={false}>server</button>,
215 );
216 expect(element.firstChild.autofocus).toBe(false);
217 + const onFocusBeforeHydration = jest.fn();
218 + const onFocusAfterHydration = jest.fn();
219 + element.firstChild.focus = onFocusBeforeHydration;
220
174 - element.firstChild.focus = jest.fn();
175 -
176 - expect(() =>
177 - ReactDOM.hydrate(<button autoFocus={false}>client</button>, element),
178 - ).toErrorDev(
179 - 'Warning: Text content did not match. Server: "server" Client: "client"',
221 + const enableClientRenderFallbackOnTextMismatch = gate(
222 + flags => flags.enableClientRenderFallbackOnTextMismatch,
223 + );
224 + await expect(async () => {
225 + await act(() => {
226 + ReactDOMClient.hydrateRoot(
227 + element,
228 + <button autoFocus={false} onFocus={onFocusAfterHydration}>
229 + client
230 + </button>,
231 + {onRecoverableError: error => {}},
232 + );
233 + });
234 + }).toErrorDev(
235 + enableClientRenderFallbackOnTextMismatch
236 + ? [
237 + 'An error occurred during hydration. The server HTML was replaced with client content in <div>.',
238 + 'Warning: Text content did not match. Server: "server" Client: "client"',
239 + ]
240 + : [
241 + 'Warning: Text content did not match. Server: "server" Client: "client"',
242 + ],
243 + {withoutStack: enableClientRenderFallbackOnTextMismatch ? 1 : 0},
244 );
245
182 - expect(element.firstChild.focus).not.toHaveBeenCalled();
246 + expect(onFocusBeforeHydration).not.toHaveBeenCalled();
247 + expect(onFocusAfterHydration).not.toHaveBeenCalled();
248 });
249
185 - it('should warn when the style property differs', () => {
250 + it('should warn when the style property differs', async () => {
251 const element = document.createElement('div');
252 element.innerHTML = ReactDOMServer.renderToString(
253 <div style={{textDecoration: 'none', color: 'black', height: '10px'}} />,
@@ -190,26 +255,27 @@ describe('ReactDOMServerHydration', () => {
255 expect(element.firstChild.style.textDecoration).toBe('none');
256 expect(element.firstChild.style.color).toBe('black');
257
193 - expect(() =>
194 - ReactDOM.hydrate(
195 - <div
196 - style={{textDecoration: 'none', color: 'white', height: '10px'}}
197 - />,
198 - element,
199 - ),
200 - ).toErrorDev(
258 + await expect(async () => {
259 + await act(() => {
260 + ReactDOMClient.hydrateRoot(
261 + element,
262 + <div
263 + style={{textDecoration: 'none', color: 'white', height: '10px'}}
264 + />,
265 + );
266 + });
267 + }).toErrorDev(
268 'Warning: Prop `style` did not match. Server: ' +
269 '"text-decoration:none;color:black;height:10px" Client: ' +
270 '"text-decoration:none;color:white;height:10px"',
271 );
272 });
273
207 - // @gate !disableIEWorkarounds || !__DEV__
208 - it('should not warn when the style property differs on whitespace or order in IE', () => {
274 + it('should not warn when the style property differs on whitespace or order in IE', async () => {
275 document.documentMode = 11;
276 jest.resetModules();
277 React = require('react');
212 - ReactDOM = require('react-dom');
278 + ReactDOMClient = require('react-dom/client');
279 ReactDOMServer = require('react-dom/server');
280 try {
281 const element = document.createElement('div');
@@ -219,33 +285,35 @@ describe('ReactDOMServerHydration', () => {
285 element.innerHTML =
286 '<div style="height: 10px; color: black; text-decoration: none;"></div>';
287
222 - // We don't expect to see false positive warnings.
223 - // https://github.com/facebook/react/issues/11807
224 - ReactDOM.hydrate(
225 - <div
226 - style={{textDecoration: 'none', color: 'black', height: '10px'}}
227 - />,
228 - element,
229 - );
288 + await act(() => {
289 + ReactDOMClient.hydrateRoot(
290 + element,
291 + <div
292 + style={{textDecoration: 'none', color: 'black', height: '10px'}}
293 + />,
294 + );
295 + });
296 } finally {
297 delete document.documentMode;
298 }
299 });
300
235 - it('should warn when the style property differs on whitespace in non-IE browsers', () => {
301 + it('should warn when the style property differs on whitespace in non-IE browsers', async () => {
302 const element = document.createElement('div');
303
304 element.innerHTML =
305 '<div style="text-decoration: none; color: black; height: 10px;"></div>';
306
241 - expect(() =>
242 - ReactDOM.hydrate(
243 - <div
244 - style={{textDecoration: 'none', color: 'black', height: '10px'}}
245 - />,
246 - element,
247 - ),
248 - ).toErrorDev(
307 + await expect(async () => {
308 + await act(() => {
309 + ReactDOMClient.hydrateRoot(
310 + element,
311 + <div
312 + style={{textDecoration: 'none', color: 'black', height: '10px'}}
313 + />,
314 + );
315 + });
316 + }).toErrorDev(
317 'Warning: Prop `style` did not match. Server: ' +
318 '"text-decoration: none; color: black; height: 10px;" Client: ' +
319 '"text-decoration:none;color:black;height:10px"',
@@ -264,7 +332,7 @@ describe('ReactDOMServerHydration', () => {
332 );
333 });
334
267 - it('should be able to render and hydrate Mode components', () => {
335 + it('should be able to render and hydrate Mode components', async () => {
336 class ComponentWithWarning extends React.Component {
337 componentWillMount() {
338 // Expected warning
@@ -286,15 +354,17 @@ describe('ReactDOMServerHydration', () => {
354 }).toWarnDev('componentWillMount has been renamed');
355 expect(element.textContent).toBe('Hi');
356
289 - expect(() => {
290 - ReactDOM.hydrate(markup, element);
357 + await expect(async () => {
358 + await act(() => {
359 + ReactDOMClient.hydrateRoot(element, markup);
360 + });
361 }).toWarnDev('componentWillMount has been renamed', {
362 withoutStack: true,
363 });
364 expect(element.textContent).toBe('Hi');
365 });
366
297 - it('should be able to render and hydrate forwardRef components', () => {
367 + it('should be able to render and hydrate forwardRef components', async () => {
368 const FunctionComponent = ({label, forwardedRef}) => (
369 <div ref={forwardedRef}>{label}</div>
370 );
@@ -310,12 +380,14 @@ describe('ReactDOMServerHydration', () => {
380 expect(element.textContent).toBe('Hi');
381 expect(ref.current).toBe(null);
382
313 - ReactDOM.hydrate(markup, element);
383 + await act(() => {
384 + ReactDOMClient.hydrateRoot(element, markup);
385 + });
386 expect(element.textContent).toBe('Hi');
387 expect(ref.current.tagName).toBe('DIV');
388 });
389
318 - it('should be able to render and hydrate Profiler components', () => {
390 + it('should be able to render and hydrate Profiler components', async () => {
391 const callback = jest.fn();
392 const markup = (
393 <React.Profiler id="profiler" onRender={callback}>
@@ -328,7 +400,9 @@ describe('ReactDOMServerHydration', () => {
400 expect(element.textContent).toBe('Hi');
401 expect(callback).not.toHaveBeenCalled();
402
331 - ReactDOM.hydrate(markup, element);
403 + await act(() => {
404 + ReactDOMClient.hydrateRoot(element, markup);
405 + });
406 expect(element.textContent).toBe('Hi');
407 if (__DEV__) {
408 expect(callback).toHaveBeenCalledTimes(1);
@@ -341,7 +415,7 @@ describe('ReactDOMServerHydration', () => {
415 });
416
417 // Regression test for https://github.com/facebook/react/issues/11423
344 - it('should ignore noscript content on the client and not warn about mismatches', () => {
418 + it('should ignore noscript content on the client and not warn about mismatches', async () => {
419 const callback = jest.fn();
420 const TestComponent = ({onRender}) => {
421 onRender();
@@ -360,10 +434,9 @@ describe('ReactDOMServerHydration', () => {
434 '<div>Enable JavaScript to run this app.</div>',
435 );
436
363 - // On the client we want to keep the existing markup, but not render the
364 - // actual elements for performance reasons and to avoid for example
365 - // downloading images. This should also not warn for hydration mismatches.
366 - ReactDOM.hydrate(markup, element);
437 + await act(() => {
438 + ReactDOMClient.hydrateRoot(element, markup);
439 + });
440 expect(callback).toHaveBeenCalledTimes(1);
441 expect(element.textContent).toBe(
442 '<div>Enable JavaScript to run this app.</div>',
@@ -371,18 +444,17 @@ describe('ReactDOMServerHydration', () => {
444 });
445
446 it('should be able to use lazy components after hydrating', async () => {
447 + let resolveLazy;
448 const Lazy = React.lazy(
449 () =>
450 new Promise(resolve => {
377 - setTimeout(
378 - () =>
379 - resolve({
380 - default: function World() {
381 - return 'world';
382 - },
383 - }),
384 - 1000,
385 - );
451 + resolveLazy = () => {
452 + resolve({
453 + default: function World() {
454 + return 'world';
455 + },
456 + });
457 + };
458 }),
459 );
460 class HelloWorld extends React.Component {
@@ -410,11 +482,13 @@ describe('ReactDOMServerHydration', () => {
482 element.innerHTML = ReactDOMServer.renderToString(<HelloWorld />);
483 expect(element.textContent).toBe('Hello ');
484
413 - ReactDOM.hydrate(<HelloWorld />, element);
485 + await act(() => {
486 + ReactDOMClient.hydrateRoot(element, <HelloWorld />);
487 + });
488 expect(element.textContent).toBe('Hello loading');
489
490 // Resolve Lazy component
417 - await act(() => jest.runAllTimers());
491 + await act(() => resolveLazy());
492 expect(element.textContent).toBe('Hello world');
493 });
494
@@ -497,7 +571,7 @@ describe('ReactDOMServerHydration', () => {
571 });
572
573 // regression test for https://github.com/facebook/react/issues/17170
500 - it('should not warn if dangerouslySetInnerHtml=undefined', () => {
574 + it('should not warn if dangerouslySetInnerHtml=undefined', async () => {
575 const domElement = document.createElement('div');
576 const reactElement = (
577 <div dangerouslySetInnerHTML={undefined}>
@@ -507,49 +581,75 @@ describe('ReactDOMServerHydration', () => {
581 const markup = ReactDOMServer.renderToStaticMarkup(reactElement);
582 domElement.innerHTML = markup;
583
510 - ReactDOM.hydrate(reactElement, domElement);
584 + await act(() => {
585 + ReactDOMClient.hydrateRoot(domElement, reactElement);
586 + });
587
588 expect(domElement.innerHTML).toEqual(markup);
589 });
590
515 - it('should warn if innerHTML mismatches with dangerouslySetInnerHTML=undefined and children on the client', () => {
591 + it('should warn if innerHTML mismatches with dangerouslySetInnerHTML=undefined and children on the client', async () => {
592 const domElement = document.createElement('div');
593 const markup = ReactDOMServer.renderToStaticMarkup(
594 <div dangerouslySetInnerHTML={{__html: '<p>server</p>'}} />,
595 );
596 domElement.innerHTML = markup;
597
522 - expect(() => {
523 - ReactDOM.hydrate(
524 - <div dangerouslySetInnerHTML={undefined}>
525 - <p>client</p>
526 - </div>,
527 - domElement,
528 - );
598 + const enableClientRenderFallbackOnTextMismatch = gate(
599 + flags => flags.enableClientRenderFallbackOnTextMismatch,
600 + );
601 + await expect(async () => {
602 + await act(() => {
603 + ReactDOMClient.hydrateRoot(
604 + domElement,
605 + <div dangerouslySetInnerHTML={undefined}>
606 + <p>client</p>
607 + </div>,
608 + {onRecoverableError: error => {}},
609 + );
610 + });
611
612 expect(domElement.innerHTML).not.toEqual(markup);
613 }).toErrorDev(
532 - 'Warning: Text content did not match. Server: "server" Client: "client"',
614 + enableClientRenderFallbackOnTextMismatch
615 + ? [
616 + 'An error occurred during hydration. The server HTML was replaced with client content in <div>.',
617 + 'Warning: Text content did not match. Server: "server" Client: "client"',
618 + ]
619 + : [
620 + 'Warning: Text content did not match. Server: "server" Client: "client"',
621 + ],
622 + {withoutStack: enableClientRenderFallbackOnTextMismatch ? 1 : 0},
623 );
624 });
625
536 - it('should warn if innerHTML mismatches with dangerouslySetInnerHTML=undefined on the client', () => {
626 + it('should warn if innerHTML mismatches with dangerouslySetInnerHTML=undefined on the client', async () => {
627 const domElement = document.createElement('div');
628 const markup = ReactDOMServer.renderToStaticMarkup(
629 <div dangerouslySetInnerHTML={{__html: '<p>server</p>'}} />,
630 );
631 domElement.innerHTML = markup;
632
543 - expect(() => {
544 - ReactDOM.hydrate(<div dangerouslySetInnerHTML={undefined} />, domElement);
633 + await expect(async () => {
634 + await act(() => {
635 + ReactDOMClient.hydrateRoot(
636 + domElement,
637 + <div dangerouslySetInnerHTML={undefined} />,
638 + {onRecoverableError: error => {}},
639 + );
640 + });
641
642 expect(domElement.innerHTML).not.toEqual(markup);
643 }).toErrorDev(
548 - 'Warning: Did not expect server HTML to contain a <p> in <div>',
644 + [
645 + 'An error occurred during hydration. The server HTML was replaced with client content in <div>.',
646 + 'Warning: Did not expect server HTML to contain a <p> in <div>.',
647 + ],
648 + {withoutStack: 1},
649 );
650 });
651
552 - it('should warn when hydrating read-only properties', () => {
652 + it('should warn when hydrating read-only properties', async () => {
653 const readOnlyProperties = [
654 'offsetParent',
655 'offsetTop',
@@ -560,24 +660,31 @@ describe('ReactDOMServerHydration', () => {
660 'outerText',
661 'outerHTML',
662 ];
563 - readOnlyProperties.forEach(readOnlyProperty => {
663 + // eslint-disable-next-line no-for-of-loops/no-for-of-loops
664 + for (const readOnlyProperty of readOnlyProperties) {
665 const props = {};
666 props[readOnlyProperty] = 'hello';
667 const jsx = React.createElement('my-custom-element', props);
668 const element = document.createElement('div');
669 element.innerHTML = ReactDOMServer.renderToString(jsx);
670 if (gate(flags => flags.enableCustomElementPropertySupport)) {
570 - expect(() => ReactDOM.hydrate(jsx, element)).toErrorDev(
671 + await expect(async () => {
672 + await act(() => {
673 + ReactDOMClient.hydrateRoot(element, jsx);
674 + });
675 + }).toErrorDev(
676 `Warning: Assignment to read-only property will result in a no-op: \`${readOnlyProperty}\``,
677 );
678 } else {
574 - ReactDOM.hydrate(jsx, element);
679 + await act(() => {
680 + ReactDOMClient.hydrateRoot(element, jsx);
681 + });
682 }
576 - });
683 + }
684 });
685
686 // @gate enableCustomElementPropertySupport
580 - it('should not re-assign properties on hydration', () => {
687 + it('should not re-assign properties on hydration', async () => {
688 const container = document.createElement('div');
689 document.body.appendChild(container);
690
@@ -607,7 +714,9 @@ describe('ReactDOMServerHydration', () => {
714 },
715 });
716
610 - ReactDOM.hydrate(jsx, container);
717 + await act(() => {
718 + ReactDOMClient.hydrateRoot(container, jsx);
719 + });
720
721 expect(customElement.getAttribute('str')).toBe('string');
722 expect(customElement.getAttribute('obj')).toBe(null);