From b5b31e797cbf52e27e1b8283565817d9d66837d5 Mon Sep 17 00:00:00 2001 From: Ricky Hanlon Date: Tue, 30 Jan 2024 00:05:08 -0500 Subject: [PATCH 1/2] Convert ReactRenderDocument to hydrateRoot --- .../src/__tests__/ReactRenderDocument-test.js | 197 ++++++++++++++---- 1 file changed, 157 insertions(+), 40 deletions(-) diff --git a/packages/react-dom/src/__tests__/ReactRenderDocument-test.js b/packages/react-dom/src/__tests__/ReactRenderDocument-test.js index 1ca24a5cdb39a..c90e4286a5538 100644 --- a/packages/react-dom/src/__tests__/ReactRenderDocument-test.js +++ b/packages/react-dom/src/__tests__/ReactRenderDocument-test.js @@ -11,7 +11,11 @@ let React; let ReactDOM; +let ReactDOMClient; let ReactDOMServer; +let act; +let Scheduler; +let assertLog; function getTestDocument(markup) { const doc = document.implementation.createHTMLDocument(''); @@ -30,11 +34,15 @@ describe('rendering React components at document', () => { React = require('react'); ReactDOM = require('react-dom'); + ReactDOMClient = require('react-dom/client'); ReactDOMServer = require('react-dom/server'); + act = require('internal-test-utils').act; + assertLog = require('internal-test-utils').assertLog; + Scheduler = require('scheduler'); }); describe('with new explicit hydration API', () => { - it('should be able to adopt server markup', () => { + it('should be able to adopt server markup', async () => { class Root extends React.Component { render() { return ( @@ -53,16 +61,21 @@ describe('rendering React components at document', () => { const testDocument = getTestDocument(markup); const body = testDocument.body; - ReactDOM.hydrate(, testDocument); + let root; + await act(() => { + root = ReactDOMClient.hydrateRoot(testDocument, ); + }); expect(testDocument.body.innerHTML).toBe('Hello world'); - ReactDOM.hydrate(, testDocument); + await act(() => { + root.render(); + }); expect(testDocument.body.innerHTML).toBe('Hello moon'); expect(body === testDocument.body).toBe(true); }); - it('should be able to unmount component from document node, but leaves singleton nodes intact', () => { + it('should be able to unmount component from document node, but leaves singleton nodes intact', async () => { class Root extends React.Component { render() { return ( @@ -78,7 +91,10 @@ describe('rendering React components at document', () => { const markup = ReactDOMServer.renderToString(); const testDocument = getTestDocument(markup); - ReactDOM.hydrate(, testDocument); + let root; + await act(() => { + root = ReactDOMClient.hydrateRoot(testDocument, ); + }); expect(testDocument.body.innerHTML).toBe('Hello world'); const originalDocEl = testDocument.documentElement; @@ -86,7 +102,7 @@ describe('rendering React components at document', () => { const originalBody = testDocument.body; // When we unmount everything is removed except the singleton nodes of html, head, and body - ReactDOM.unmountComponentAtNode(testDocument); + root.unmount(); expect(testDocument.firstChild).toBe(originalDocEl); expect(testDocument.head).toBe(originalHead); expect(testDocument.body).toBe(originalBody); @@ -94,7 +110,7 @@ describe('rendering React components at document', () => { expect(originalHead.firstChild).toEqual(null); }); - it('should not be able to switch root constructors', () => { + it('should not be able to switch root constructors', async () => { class Component extends React.Component { render() { return ( @@ -124,17 +140,21 @@ describe('rendering React components at document', () => { const markup = ReactDOMServer.renderToString(); const testDocument = getTestDocument(markup); - ReactDOM.hydrate(, testDocument); + let root; + await act(() => { + root = ReactDOMClient.hydrateRoot(testDocument, ); + }); expect(testDocument.body.innerHTML).toBe('Hello world'); - // This works but is probably a bad idea. - ReactDOM.hydrate(, testDocument); + await act(() => { + root.render(); + }); expect(testDocument.body.innerHTML).toBe('Goodbye world'); }); - it('should be able to mount into document', () => { + it('should be able to mount into document', async () => { class Component extends React.Component { render() { return ( @@ -153,40 +173,80 @@ describe('rendering React components at document', () => { ); const testDocument = getTestDocument(markup); - ReactDOM.hydrate(, testDocument); + await act(() => { + ReactDOMClient.hydrateRoot( + testDocument, + , + ); + }); expect(testDocument.body.innerHTML).toBe('Hello world'); }); - it('cannot render over an existing text child at the root', () => { + it('cannot render over an existing text child at the root', async () => { const container = document.createElement('div'); container.textContent = 'potato'; - expect(() => ReactDOM.hydrate(
parsnip
, container)).toErrorDev( - 'Expected server HTML to contain a matching
in
.', + + expect(() => { + ReactDOM.flushSync(() => { + ReactDOMClient.hydrateRoot(container,
parsnip
, { + onRecoverableError: error => { + Scheduler.log('Log recoverable error: ' + error.message); + }, + }); + }); + }).toErrorDev( + [ + 'Warning: An error occurred during hydration. The server HTML was replaced with client content in
.', + 'Expected server HTML to contain a matching
in
.', + ], + {withoutStack: 1}, ); + + assertLog([ + 'Log recoverable error: Hydration failed because the initial UI does not match what was rendered on the server.', + 'Log recoverable error: There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.', + ]); + // This creates an unfortunate double text case. - expect(container.textContent).toBe('potatoparsnip'); + expect(container.textContent).toBe('parsnip'); }); - it('renders over an existing nested text child without throwing', () => { + it('renders over an existing nested text child without throwing', async () => { const container = document.createElement('div'); const wrapper = document.createElement('div'); wrapper.textContent = 'potato'; container.appendChild(wrapper); - expect(() => - ReactDOM.hydrate( -
-
parsnip
-
, - container, - ), - ).toErrorDev( - 'Expected server HTML to contain a matching
in
.', + expect(() => { + ReactDOM.flushSync(() => { + ReactDOMClient.hydrateRoot( + container, +
+
parsnip
+
, + { + onRecoverableError: error => { + Scheduler.log('Log recoverable error: ' + error.message); + }, + }, + ); + }); + }).toErrorDev( + [ + 'Warning: An error occurred during hydration. The server HTML was replaced with client content in
.', + 'Expected server HTML to contain a matching
in
.', + ], + {withoutStack: 1}, ); + + assertLog([ + 'Log recoverable error: Hydration failed because the initial UI does not match what was rendered on the server.', + 'Log recoverable error: There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.', + ]); expect(container.textContent).toBe('parsnip'); }); - it('should give helpful errors on state desync', () => { + it('should give helpful errors on state desync', async () => { class Component extends React.Component { render() { return ( @@ -205,13 +265,34 @@ describe('rendering React components at document', () => { ); const testDocument = getTestDocument(markup); - expect(() => - ReactDOM.hydrate(, testDocument), - ).toErrorDev('Warning: Text content did not match.'); + expect(() => { + ReactDOM.flushSync(() => { + ReactDOMClient.hydrateRoot( + testDocument, + , + { + onRecoverableError: error => { + Scheduler.log('Log recoverable error: ' + error.message); + }, + }, + ); + }); + }).toErrorDev( + [ + 'Warning: An error occurred during hydration. The server HTML was replaced with client content in <#document>.', + 'Warning: Text content did not match.', + ], + {withoutStack: 1}, + ); + + assertLog([ + 'Log recoverable error: Text content does not match server-rendered HTML.', + 'Log recoverable error: There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.', + ]); expect(testDocument.body.innerHTML).toBe('Hello world'); }); - it('should render w/ no markup to full document', () => { + it('should render w/ no markup to full document', async () => { const testDocument = getTestDocument(); class Component extends React.Component { @@ -229,23 +310,59 @@ describe('rendering React components at document', () => { if (gate(flags => flags.enableFloat)) { // with float the title no longer is a hydration mismatch so we get an error on the body mismatch - expect(() => - ReactDOM.hydrate(, testDocument), - ).toErrorDev( - 'Expected server HTML to contain a matching text node for "Hello world" in ', + expect(() => { + ReactDOM.flushSync(() => { + ReactDOMClient.hydrateRoot( + testDocument, + , + { + onRecoverableError: error => { + Scheduler.log('Log recoverable error: ' + error.message); + }, + }, + ); + }); + }).toErrorDev( + [ + 'Warning: An error occurred during hydration. The server HTML was replaced with client content in <#document>.', + 'Expected server HTML to contain a matching text node for "Hello world" in ', + ], + {withoutStack: 1}, ); + assertLog([ + 'Log recoverable error: Hydration failed because the initial UI does not match what was rendered on the server.', + 'Log recoverable error: There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.', + ]); } else { // getTestDocument() has an extra that we didn't render. - expect(() => - ReactDOM.hydrate(, testDocument), - ).toErrorDev( - 'Did not expect server HTML to contain a in .', + expect(() => { + ReactDOM.flushSync(() => { + ReactDOMClient.hydrateRoot( + testDocument, + , + { + onRecoverableError: error => { + Scheduler.log('Log recoverable error: ' + error.message); + }, + }, + ); + }); + }).toErrorDev( + [ + 'Warning: An error occurred during hydration. The server HTML was replaced with client content in <#document>.', + 'Warning: Text content did not match. Server: "test doc" Client: "Hello World"', + ], + {withoutStack: 1}, ); + assertLog([ + 'Log recoverable error: Text content does not match server-rendered HTML.', + 'Log recoverable error: There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.', + ]); } expect(testDocument.body.innerHTML).toBe('Hello world'); }); - it('supports findDOMNode on full-page components', () => { + it('supports findDOMNode on full-page components in legacy mode', () => { const tree = ( From da2c623e2b9d33c41b0c330b5f95cde28ea84d22 Mon Sep 17 00:00:00 2001 From: Sebastian Silbermann Date: Wed, 7 Feb 2024 15:27:16 +0100 Subject: [PATCH 2/2] Fix tests for www --- .../src/__tests__/ReactRenderDocument-test.js | 29 +++++++++++++------ 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/packages/react-dom/src/__tests__/ReactRenderDocument-test.js b/packages/react-dom/src/__tests__/ReactRenderDocument-test.js index c90e4286a5538..3eab0e6c3661b 100644 --- a/packages/react-dom/src/__tests__/ReactRenderDocument-test.js +++ b/packages/react-dom/src/__tests__/ReactRenderDocument-test.js @@ -265,6 +265,9 @@ describe('rendering React components at document', () => { ); const testDocument = getTestDocument(markup); + const enableClientRenderFallbackOnTextMismatch = gate( + flags => flags.enableClientRenderFallbackOnTextMismatch, + ); expect(() => { ReactDOM.flushSync(() => { ReactDOMClient.hydrateRoot( @@ -278,17 +281,25 @@ describe('rendering React components at document', () => { ); }); }).toErrorDev( - [ - 'Warning: An error occurred during hydration. The server HTML was replaced with client content in <#document>.', - 'Warning: Text content did not match.', - ], - {withoutStack: 1}, + enableClientRenderFallbackOnTextMismatch + ? [ + 'Warning: An error occurred during hydration. The server HTML was replaced with client content in <#document>.', + 'Warning: Text content did not match.', + ] + : ['Warning: Text content did not match.'], + { + withoutStack: enableClientRenderFallbackOnTextMismatch ? 1 : 0, + }, ); - assertLog([ - 'Log recoverable error: Text content does not match server-rendered HTML.', - 'Log recoverable error: There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.', - ]); + assertLog( + enableClientRenderFallbackOnTextMismatch + ? [ + 'Log recoverable error: Text content does not match server-rendered HTML.', + 'Log recoverable error: There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.', + ] + : [], + ); expect(testDocument.body.innerHTML).toBe('Hello world'); });