From f637f6a4736a9a02ab18067ef4b1c5e033c8d6f4 Mon Sep 17 00:00:00 2001 From: Sebastian Silbermann Date: Mon, 26 Feb 2024 20:59:43 +0100 Subject: [PATCH] Convert ReactServerRenderingHydration to createRoot (#28447) --- .../ReactServerRenderingHydration-test.js | 295 ++++++++++++------ 1 file changed, 202 insertions(+), 93 deletions(-) diff --git a/packages/react-dom/src/__tests__/ReactServerRenderingHydration-test.js b/packages/react-dom/src/__tests__/ReactServerRenderingHydration-test.js index c7416b255d471..982166b40545e 100644 --- a/packages/react-dom/src/__tests__/ReactServerRenderingHydration-test.js +++ b/packages/react-dom/src/__tests__/ReactServerRenderingHydration-test.js @@ -34,7 +34,7 @@ describe('ReactDOMServerHydration', () => { act = InternalTestUtils.act; }); - it('should have the correct mounting behavior (new hydrate API)', () => { + it('should have the correct mounting behavior', async () => { let mountCount = 0; let numClicks = 0; @@ -61,20 +61,29 @@ describe('ReactDOMServerHydration', () => { const element = document.createElement('div'); document.body.appendChild(element); try { - ReactDOM.render(, element); + let root = ReactDOMClient.createRoot(element); + await act(() => { + root.render(); + }); let lastMarkup = element.innerHTML; // Exercise the update path. Markup should not change, // but some lifecycle methods should be run again. - ReactDOM.render(, element); + await act(() => { + root.render(); + }); expect(mountCount).toEqual(1); // Unmount and remount. We should get another mount event and // we should get different markup, as the IDs are unique each time. - ReactDOM.unmountComponentAtNode(element); + root.unmount(); expect(element.innerHTML).toEqual(''); - ReactDOM.render(, element); + root = ReactDOMClient.createRoot(element); + await act(() => { + root.render(); + }); + expect(mountCount).toEqual(2); expect(element.innerHTML).not.toEqual(lastMarkup); @@ -82,13 +91,22 @@ describe('ReactDOMServerHydration', () => { // we used server rendering. We should mount again, but the markup should // be unchanged. We will append a sentinel at the end of innerHTML to be // sure that innerHTML was not changed. - ReactDOM.unmountComponentAtNode(element); + await act(() => { + root.unmount(); + }); expect(element.innerHTML).toEqual(''); lastMarkup = ReactDOMServer.renderToString(); element.innerHTML = lastMarkup; - let instance = ReactDOM.hydrate(, element); + let instance; + + root = await act(() => { + return ReactDOMClient.hydrateRoot( + element, + (instance = current)} />, + ); + }); expect(mountCount).toEqual(3); expect(element.innerHTML).toBe(lastMarkup); @@ -97,15 +115,41 @@ describe('ReactDOMServerHydration', () => { instance.spanRef.current.click(); expect(numClicks).toEqual(1); - ReactDOM.unmountComponentAtNode(element); + await act(() => { + root.unmount(); + }); expect(element.innerHTML).toEqual(''); // Now simulate a situation where the app is not idempotent. React should // warn but do the right thing. element.innerHTML = lastMarkup; - expect(() => { - instance = ReactDOM.hydrate(, element); - }).toErrorDev('Text content did not match. Server: "x" Client: "y"'); + const enableClientRenderFallbackOnTextMismatch = gate( + flags => flags.enableClientRenderFallbackOnTextMismatch, + ); + await expect(async () => { + root = await act(() => { + return ReactDOMClient.hydrateRoot( + element, + { + instance = current; + }} + />, + { + onRecoverableError: error => {}, + }, + ); + }); + }).toErrorDev( + enableClientRenderFallbackOnTextMismatch + ? [ + 'An error occurred during hydration. The server HTML was replaced with client content in
.', + 'Text content did not match. Server: "x" Client: "y"', + ] + : ['Text content did not match. Server: "x" Client: "y"'], + {withoutStack: enableClientRenderFallbackOnTextMismatch ? 1 : 0}, + ); expect(mountCount).toEqual(4); expect(element.innerHTML.length > 0).toBe(true); expect(element.innerHTML).not.toEqual(lastMarkup); @@ -164,25 +208,46 @@ describe('ReactDOMServerHydration', () => { }); // Regression test for /~https://github.com/facebook/react/issues/11726 - it('should not focus on either server or client with autofocus={false} even if there is a markup mismatch', () => { + it('should not focus on either server or client with autofocus={false} even if there is a markup mismatch', async () => { const element = document.createElement('div'); element.innerHTML = ReactDOMServer.renderToString( , ); expect(element.firstChild.autofocus).toBe(false); + const onFocusBeforeHydration = jest.fn(); + const onFocusAfterHydration = jest.fn(); + element.firstChild.focus = onFocusBeforeHydration; - element.firstChild.focus = jest.fn(); - - expect(() => - ReactDOM.hydrate(, element), - ).toErrorDev( - 'Warning: Text content did not match. Server: "server" Client: "client"', + const enableClientRenderFallbackOnTextMismatch = gate( + flags => flags.enableClientRenderFallbackOnTextMismatch, + ); + await expect(async () => { + await act(() => { + ReactDOMClient.hydrateRoot( + element, + , + {onRecoverableError: error => {}}, + ); + }); + }).toErrorDev( + enableClientRenderFallbackOnTextMismatch + ? [ + 'An error occurred during hydration. The server HTML was replaced with client content in
.', + 'Warning: Text content did not match. Server: "server" Client: "client"', + ] + : [ + 'Warning: Text content did not match. Server: "server" Client: "client"', + ], + {withoutStack: enableClientRenderFallbackOnTextMismatch ? 1 : 0}, ); - expect(element.firstChild.focus).not.toHaveBeenCalled(); + expect(onFocusBeforeHydration).not.toHaveBeenCalled(); + expect(onFocusAfterHydration).not.toHaveBeenCalled(); }); - it('should warn when the style property differs', () => { + it('should warn when the style property differs', async () => { const element = document.createElement('div'); element.innerHTML = ReactDOMServer.renderToString(
, @@ -190,26 +255,27 @@ describe('ReactDOMServerHydration', () => { expect(element.firstChild.style.textDecoration).toBe('none'); expect(element.firstChild.style.color).toBe('black'); - expect(() => - ReactDOM.hydrate( -
, - element, - ), - ).toErrorDev( + await expect(async () => { + await act(() => { + ReactDOMClient.hydrateRoot( + element, +
, + ); + }); + }).toErrorDev( 'Warning: Prop `style` did not match. Server: ' + '"text-decoration:none;color:black;height:10px" Client: ' + '"text-decoration:none;color:white;height:10px"', ); }); - // @gate !disableIEWorkarounds || !__DEV__ - it('should not warn when the style property differs on whitespace or order in IE', () => { + it('should not warn when the style property differs on whitespace or order in IE', async () => { document.documentMode = 11; jest.resetModules(); React = require('react'); - ReactDOM = require('react-dom'); + ReactDOMClient = require('react-dom/client'); ReactDOMServer = require('react-dom/server'); try { const element = document.createElement('div'); @@ -219,33 +285,35 @@ describe('ReactDOMServerHydration', () => { element.innerHTML = '
'; - // We don't expect to see false positive warnings. - // /~https://github.com/facebook/react/issues/11807 - ReactDOM.hydrate( -
, - element, - ); + await act(() => { + ReactDOMClient.hydrateRoot( + element, +
, + ); + }); } finally { delete document.documentMode; } }); - it('should warn when the style property differs on whitespace in non-IE browsers', () => { + it('should warn when the style property differs on whitespace in non-IE browsers', async () => { const element = document.createElement('div'); element.innerHTML = '
'; - expect(() => - ReactDOM.hydrate( -
, - element, - ), - ).toErrorDev( + await expect(async () => { + await act(() => { + ReactDOMClient.hydrateRoot( + element, +
, + ); + }); + }).toErrorDev( 'Warning: Prop `style` did not match. Server: ' + '"text-decoration: none; color: black; height: 10px;" Client: ' + '"text-decoration:none;color:black;height:10px"', @@ -264,7 +332,7 @@ describe('ReactDOMServerHydration', () => { ); }); - it('should be able to render and hydrate Mode components', () => { + it('should be able to render and hydrate Mode components', async () => { class ComponentWithWarning extends React.Component { componentWillMount() { // Expected warning @@ -286,15 +354,17 @@ describe('ReactDOMServerHydration', () => { }).toWarnDev('componentWillMount has been renamed'); expect(element.textContent).toBe('Hi'); - expect(() => { - ReactDOM.hydrate(markup, element); + await expect(async () => { + await act(() => { + ReactDOMClient.hydrateRoot(element, markup); + }); }).toWarnDev('componentWillMount has been renamed', { withoutStack: true, }); expect(element.textContent).toBe('Hi'); }); - it('should be able to render and hydrate forwardRef components', () => { + it('should be able to render and hydrate forwardRef components', async () => { const FunctionComponent = ({label, forwardedRef}) => (
{label}
); @@ -310,12 +380,14 @@ describe('ReactDOMServerHydration', () => { expect(element.textContent).toBe('Hi'); expect(ref.current).toBe(null); - ReactDOM.hydrate(markup, element); + await act(() => { + ReactDOMClient.hydrateRoot(element, markup); + }); expect(element.textContent).toBe('Hi'); expect(ref.current.tagName).toBe('DIV'); }); - it('should be able to render and hydrate Profiler components', () => { + it('should be able to render and hydrate Profiler components', async () => { const callback = jest.fn(); const markup = ( @@ -328,7 +400,9 @@ describe('ReactDOMServerHydration', () => { expect(element.textContent).toBe('Hi'); expect(callback).not.toHaveBeenCalled(); - ReactDOM.hydrate(markup, element); + await act(() => { + ReactDOMClient.hydrateRoot(element, markup); + }); expect(element.textContent).toBe('Hi'); if (__DEV__) { expect(callback).toHaveBeenCalledTimes(1); @@ -341,7 +415,7 @@ describe('ReactDOMServerHydration', () => { }); // Regression test for /~https://github.com/facebook/react/issues/11423 - it('should ignore noscript content on the client and not warn about mismatches', () => { + it('should ignore noscript content on the client and not warn about mismatches', async () => { const callback = jest.fn(); const TestComponent = ({onRender}) => { onRender(); @@ -360,10 +434,9 @@ describe('ReactDOMServerHydration', () => { '
Enable JavaScript to run this app.
', ); - // On the client we want to keep the existing markup, but not render the - // actual elements for performance reasons and to avoid for example - // downloading images. This should also not warn for hydration mismatches. - ReactDOM.hydrate(markup, element); + await act(() => { + ReactDOMClient.hydrateRoot(element, markup); + }); expect(callback).toHaveBeenCalledTimes(1); expect(element.textContent).toBe( '
Enable JavaScript to run this app.
', @@ -371,18 +444,17 @@ describe('ReactDOMServerHydration', () => { }); it('should be able to use lazy components after hydrating', async () => { + let resolveLazy; const Lazy = React.lazy( () => new Promise(resolve => { - setTimeout( - () => - resolve({ - default: function World() { - return 'world'; - }, - }), - 1000, - ); + resolveLazy = () => { + resolve({ + default: function World() { + return 'world'; + }, + }); + }; }), ); class HelloWorld extends React.Component { @@ -410,11 +482,13 @@ describe('ReactDOMServerHydration', () => { element.innerHTML = ReactDOMServer.renderToString(); expect(element.textContent).toBe('Hello '); - ReactDOM.hydrate(, element); + await act(() => { + ReactDOMClient.hydrateRoot(element, ); + }); expect(element.textContent).toBe('Hello loading'); // Resolve Lazy component - await act(() => jest.runAllTimers()); + await act(() => resolveLazy()); expect(element.textContent).toBe('Hello world'); }); @@ -497,7 +571,7 @@ describe('ReactDOMServerHydration', () => { }); // regression test for /~https://github.com/facebook/react/issues/17170 - it('should not warn if dangerouslySetInnerHtml=undefined', () => { + it('should not warn if dangerouslySetInnerHtml=undefined', async () => { const domElement = document.createElement('div'); const reactElement = (
@@ -507,49 +581,75 @@ describe('ReactDOMServerHydration', () => { const markup = ReactDOMServer.renderToStaticMarkup(reactElement); domElement.innerHTML = markup; - ReactDOM.hydrate(reactElement, domElement); + await act(() => { + ReactDOMClient.hydrateRoot(domElement, reactElement); + }); expect(domElement.innerHTML).toEqual(markup); }); - it('should warn if innerHTML mismatches with dangerouslySetInnerHTML=undefined and children on the client', () => { + it('should warn if innerHTML mismatches with dangerouslySetInnerHTML=undefined and children on the client', async () => { const domElement = document.createElement('div'); const markup = ReactDOMServer.renderToStaticMarkup(
server

'}} />, ); domElement.innerHTML = markup; - expect(() => { - ReactDOM.hydrate( -
-

client

-
, - domElement, - ); + const enableClientRenderFallbackOnTextMismatch = gate( + flags => flags.enableClientRenderFallbackOnTextMismatch, + ); + await expect(async () => { + await act(() => { + ReactDOMClient.hydrateRoot( + domElement, +
+

client

+
, + {onRecoverableError: error => {}}, + ); + }); expect(domElement.innerHTML).not.toEqual(markup); }).toErrorDev( - 'Warning: Text content did not match. Server: "server" Client: "client"', + enableClientRenderFallbackOnTextMismatch + ? [ + 'An error occurred during hydration. The server HTML was replaced with client content in
.', + 'Warning: Text content did not match. Server: "server" Client: "client"', + ] + : [ + 'Warning: Text content did not match. Server: "server" Client: "client"', + ], + {withoutStack: enableClientRenderFallbackOnTextMismatch ? 1 : 0}, ); }); - it('should warn if innerHTML mismatches with dangerouslySetInnerHTML=undefined on the client', () => { + it('should warn if innerHTML mismatches with dangerouslySetInnerHTML=undefined on the client', async () => { const domElement = document.createElement('div'); const markup = ReactDOMServer.renderToStaticMarkup(
server

'}} />, ); domElement.innerHTML = markup; - expect(() => { - ReactDOM.hydrate(
, domElement); + await expect(async () => { + await act(() => { + ReactDOMClient.hydrateRoot( + domElement, +
, + {onRecoverableError: error => {}}, + ); + }); expect(domElement.innerHTML).not.toEqual(markup); }).toErrorDev( - 'Warning: Did not expect server HTML to contain a

in

', + [ + 'An error occurred during hydration. The server HTML was replaced with client content in
.', + 'Warning: Did not expect server HTML to contain a

in

.', + ], + {withoutStack: 1}, ); }); - it('should warn when hydrating read-only properties', () => { + it('should warn when hydrating read-only properties', async () => { const readOnlyProperties = [ 'offsetParent', 'offsetTop', @@ -560,24 +660,31 @@ describe('ReactDOMServerHydration', () => { 'outerText', 'outerHTML', ]; - readOnlyProperties.forEach(readOnlyProperty => { + // eslint-disable-next-line no-for-of-loops/no-for-of-loops + for (const readOnlyProperty of readOnlyProperties) { const props = {}; props[readOnlyProperty] = 'hello'; const jsx = React.createElement('my-custom-element', props); const element = document.createElement('div'); element.innerHTML = ReactDOMServer.renderToString(jsx); if (gate(flags => flags.enableCustomElementPropertySupport)) { - expect(() => ReactDOM.hydrate(jsx, element)).toErrorDev( + await expect(async () => { + await act(() => { + ReactDOMClient.hydrateRoot(element, jsx); + }); + }).toErrorDev( `Warning: Assignment to read-only property will result in a no-op: \`${readOnlyProperty}\``, ); } else { - ReactDOM.hydrate(jsx, element); + await act(() => { + ReactDOMClient.hydrateRoot(element, jsx); + }); } - }); + } }); // @gate enableCustomElementPropertySupport - it('should not re-assign properties on hydration', () => { + it('should not re-assign properties on hydration', async () => { const container = document.createElement('div'); document.body.appendChild(container); @@ -607,7 +714,9 @@ describe('ReactDOMServerHydration', () => { }, }); - ReactDOM.hydrate(jsx, container); + await act(() => { + ReactDOMClient.hydrateRoot(container, jsx); + }); expect(customElement.getAttribute('str')).toBe('string'); expect(customElement.getAttribute('obj')).toBe(null);