From 9a716ebd6ea5e8a36384cf1f220378390db4afcd Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Mon, 4 Apr 2022 16:45:24 +0100 Subject: [PATCH 1/5] More tests for suppressHydrationWarning --- .../src/__tests__/ReactDOMFizzServer-test.js | 90 +++++++++++++++++++ 1 file changed, 90 insertions(+) diff --git a/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js b/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js index d4687ab4d365b..f3166083dd22c 100644 --- a/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js @@ -2903,4 +2903,94 @@ describe('ReactDOMFizzServer', () => { , ); }); + + // @gate experimental + it('does not suppress insertions with suppressHydrationWarning', async () => { + function App({isClient}) { + return ( +
+

Client and server

+ {isClient &&

Client only

} +
+ ); + } + await act(async () => { + const {pipe} = ReactDOMFizzServer.renderToPipeableStream( + , + ); + pipe(writable); + }); + expect(getVisibleChildren(container)).toEqual( +
+

Client and server

+
, + ); + ReactDOMClient.hydrateRoot(container, , { + onRecoverableError(error) { + Scheduler.unstable_yieldValue(error.message); + }, + }); + expect(() => { + expect(Scheduler).toFlushAndYield([ + 'Hydration failed because the initial UI does not match what was rendered on the server.', + 'There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.', + ]); + }).toErrorDev( + [ + 'An error occurred during hydration. The server HTML was replaced with client content in
.', + ], + {withoutStack: true}, + ); + expect(getVisibleChildren(container)).toEqual( +
+

Client and server

+

Client only

+
, + ); + }); + + // @gate experimental + it('does not suppress deletions with suppressHydrationWarning', async () => { + function App({isClient}) { + return ( +
+

Client and server

+ {!isClient &&

Server only

} +
+ ); + } + await act(async () => { + const {pipe} = ReactDOMFizzServer.renderToPipeableStream( + , + ); + pipe(writable); + }); + expect(getVisibleChildren(container)).toEqual( +
+

Client and server

+

Server only

+
, + ); + ReactDOMClient.hydrateRoot(container, , { + onRecoverableError(error) { + Scheduler.unstable_yieldValue(error.message); + }, + }); + expect(() => { + expect(Scheduler).toFlushAndYield([ + 'Hydration failed because the initial UI does not match what was rendered on the server.', + 'There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.', + ]); + }).toErrorDev( + [ + 'An error occurred during hydration. The server HTML was replaced with client content in
.', + ], + {withoutStack: true}, + ); + expect(getVisibleChildren(container)).toEqual( +
+

Client and server

+
, + ); + }); }); From ea6c70b08b3eefd03d8e0b6924a352e2b87dbf82 Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Mon, 4 Apr 2022 21:58:55 +0100 Subject: [PATCH 2/5] Move suppressHydration tests to new file --- .../src/__tests__/ReactDOMFizzServer-test.js | 182 ---------- ...actDOMFizzSuppressHydrationWarning-test.js | 311 ++++++++++++++++++ 2 files changed, 311 insertions(+), 182 deletions(-) create mode 100644 packages/react-dom/src/__tests__/ReactDOMFizzSuppressHydrationWarning-test.js diff --git a/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js b/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js index f3166083dd22c..295e99b26a691 100644 --- a/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js @@ -2811,186 +2811,4 @@ describe('ReactDOMFizzServer', () => { , ); }); - - // @gate experimental - it('suppresses and fixes text mismatches with suppressHydrationWarning', async () => { - function App({isClient}) { - return ( -
- - {isClient ? 'Client Text' : 'Server Text'} - - {isClient ? 2 : 1} - - hello,{isClient ? 'client' : 'server'} - -
- ); - } - await act(async () => { - const {pipe} = ReactDOMFizzServer.renderToPipeableStream( - , - ); - pipe(writable); - }); - expect(getVisibleChildren(container)).toEqual( -
- Server Text - 1 - - {'hello,'} - {'server'} - -
, - ); - ReactDOMClient.hydrateRoot(container, , { - onRecoverableError(error) { - // Don't miss a hydration error. There should be none. - Scheduler.unstable_yieldValue(error.message); - }, - }); - expect(Scheduler).toFlushAndYield([]); - // The text mismatch should be *silently* fixed. Even in production. - // The attribute mismatch should be ignored and not fixed. - expect(getVisibleChildren(container)).toEqual( -
- Client Text - 2 - - {'hello,'} - {'client'} - -
, - ); - }); - - // @gate experimental - it('suppresses and does not fix html mismatches with suppressHydrationWarning', async () => { - function App({isClient}) { - return ( -
-

-

- ); - } - await act(async () => { - const {pipe} = ReactDOMFizzServer.renderToPipeableStream( - , - ); - pipe(writable); - }); - expect(getVisibleChildren(container)).toEqual( -
-

Server HTML

-
, - ); - ReactDOMClient.hydrateRoot(container, , { - onRecoverableError(error) { - Scheduler.unstable_yieldValue(error.message); - }, - }); - expect(Scheduler).toFlushAndYield([]); - expect(getVisibleChildren(container)).toEqual( -
-

Server HTML

-
, - ); - }); - - // @gate experimental - it('does not suppress insertions with suppressHydrationWarning', async () => { - function App({isClient}) { - return ( -
-

Client and server

- {isClient &&

Client only

} -
- ); - } - await act(async () => { - const {pipe} = ReactDOMFizzServer.renderToPipeableStream( - , - ); - pipe(writable); - }); - expect(getVisibleChildren(container)).toEqual( -
-

Client and server

-
, - ); - ReactDOMClient.hydrateRoot(container, , { - onRecoverableError(error) { - Scheduler.unstable_yieldValue(error.message); - }, - }); - expect(() => { - expect(Scheduler).toFlushAndYield([ - 'Hydration failed because the initial UI does not match what was rendered on the server.', - 'There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.', - ]); - }).toErrorDev( - [ - 'An error occurred during hydration. The server HTML was replaced with client content in
.', - ], - {withoutStack: true}, - ); - expect(getVisibleChildren(container)).toEqual( -
-

Client and server

-

Client only

-
, - ); - }); - - // @gate experimental - it('does not suppress deletions with suppressHydrationWarning', async () => { - function App({isClient}) { - return ( -
-

Client and server

- {!isClient &&

Server only

} -
- ); - } - await act(async () => { - const {pipe} = ReactDOMFizzServer.renderToPipeableStream( - , - ); - pipe(writable); - }); - expect(getVisibleChildren(container)).toEqual( -
-

Client and server

-

Server only

-
, - ); - ReactDOMClient.hydrateRoot(container, , { - onRecoverableError(error) { - Scheduler.unstable_yieldValue(error.message); - }, - }); - expect(() => { - expect(Scheduler).toFlushAndYield([ - 'Hydration failed because the initial UI does not match what was rendered on the server.', - 'There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.', - ]); - }).toErrorDev( - [ - 'An error occurred during hydration. The server HTML was replaced with client content in
.', - ], - {withoutStack: true}, - ); - expect(getVisibleChildren(container)).toEqual( -
-

Client and server

-
, - ); - }); }); diff --git a/packages/react-dom/src/__tests__/ReactDOMFizzSuppressHydrationWarning-test.js b/packages/react-dom/src/__tests__/ReactDOMFizzSuppressHydrationWarning-test.js new file mode 100644 index 0000000000000..67c598bed5dfa --- /dev/null +++ b/packages/react-dom/src/__tests__/ReactDOMFizzSuppressHydrationWarning-test.js @@ -0,0 +1,311 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @emails react-core + */ + +'use strict'; + +let JSDOM; +let Stream; +let Scheduler; +let React; +let ReactDOMClient; +let ReactDOMFizzServer; +let document; +let writable; +let container; +let buffer = ''; +let hasErrored = false; +let fatalError = undefined; + +describe('ReactDOMFizzServer', () => { + beforeEach(() => { + jest.resetModules(); + JSDOM = require('jsdom').JSDOM; + Scheduler = require('scheduler'); + React = require('react'); + ReactDOMClient = require('react-dom/client'); + if (__EXPERIMENTAL__) { + ReactDOMFizzServer = require('react-dom/server'); + } + Stream = require('stream'); + + // Test Environment + const jsdom = new JSDOM( + '
', + { + runScripts: 'dangerously', + }, + ); + document = jsdom.window.document; + container = document.getElementById('container'); + + buffer = ''; + hasErrored = false; + + writable = new Stream.PassThrough(); + writable.setEncoding('utf8'); + writable.on('data', chunk => { + buffer += chunk; + }); + writable.on('error', error => { + hasErrored = true; + fatalError = error; + }); + }); + + async function act(callback) { + await callback(); + // Await one turn around the event loop. + // This assumes that we'll flush everything we have so far. + await new Promise(resolve => { + setImmediate(resolve); + }); + if (hasErrored) { + throw fatalError; + } + // JSDOM doesn't support stream HTML parser so we need to give it a proper fragment. + // We also want to execute any scripts that are embedded. + // We assume that we have now received a proper fragment of HTML. + const bufferedContent = buffer; + buffer = ''; + const fakeBody = document.createElement('body'); + fakeBody.innerHTML = bufferedContent; + while (fakeBody.firstChild) { + const node = fakeBody.firstChild; + if (node.nodeName === 'SCRIPT') { + const script = document.createElement('script'); + script.textContent = node.textContent; + fakeBody.removeChild(node); + container.appendChild(script); + } else { + container.appendChild(node); + } + } + } + + function getVisibleChildren(element) { + const children = []; + let node = element.firstChild; + while (node) { + if (node.nodeType === 1) { + if ( + node.tagName !== 'SCRIPT' && + node.tagName !== 'TEMPLATE' && + node.tagName !== 'template' && + !node.hasAttribute('hidden') && + !node.hasAttribute('aria-hidden') + ) { + const props = {}; + const attributes = node.attributes; + for (let i = 0; i < attributes.length; i++) { + if ( + attributes[i].name === 'id' && + attributes[i].value.includes(':') + ) { + // We assume this is a React added ID that's a non-visual implementation detail. + continue; + } + props[attributes[i].name] = attributes[i].value; + } + props.children = getVisibleChildren(node); + children.push(React.createElement(node.tagName.toLowerCase(), props)); + } + } else if (node.nodeType === 3) { + children.push(node.data); + } + node = node.nextSibling; + } + return children.length === 0 + ? undefined + : children.length === 1 + ? children[0] + : children; + } + + // @gate experimental + it('suppresses and fixes text mismatches with suppressHydrationWarning', async () => { + function App({isClient}) { + return ( +
+ + {isClient ? 'Client Text' : 'Server Text'} + + {isClient ? 2 : 1} + + hello,{isClient ? 'client' : 'server'} + +
+ ); + } + await act(async () => { + const {pipe} = ReactDOMFizzServer.renderToPipeableStream( + , + ); + pipe(writable); + }); + expect(getVisibleChildren(container)).toEqual( +
+ Server Text + 1 + + {'hello,'} + {'server'} + +
, + ); + ReactDOMClient.hydrateRoot(container, , { + onRecoverableError(error) { + // Don't miss a hydration error. There should be none. + Scheduler.unstable_yieldValue(error.message); + }, + }); + expect(Scheduler).toFlushAndYield([]); + // The text mismatch should be *silently* fixed. Even in production. + // The attribute mismatch should be ignored and not fixed. + expect(getVisibleChildren(container)).toEqual( +
+ Client Text + 2 + + {'hello,'} + {'client'} + +
, + ); + }); + + // @gate experimental + it('suppresses and does not fix html mismatches with suppressHydrationWarning', async () => { + function App({isClient}) { + return ( +
+

+

+ ); + } + await act(async () => { + const {pipe} = ReactDOMFizzServer.renderToPipeableStream( + , + ); + pipe(writable); + }); + expect(getVisibleChildren(container)).toEqual( +
+

Server HTML

+
, + ); + ReactDOMClient.hydrateRoot(container, , { + onRecoverableError(error) { + Scheduler.unstable_yieldValue(error.message); + }, + }); + expect(Scheduler).toFlushAndYield([]); + expect(getVisibleChildren(container)).toEqual( +
+

Server HTML

+
, + ); + }); + + // @gate experimental + it('does not suppress insertions with suppressHydrationWarning', async () => { + function App({isClient}) { + return ( +
+

Client and server

+ {isClient &&

Client only

} +
+ ); + } + await act(async () => { + const {pipe} = ReactDOMFizzServer.renderToPipeableStream( + , + ); + pipe(writable); + }); + expect(getVisibleChildren(container)).toEqual( +
+

Client and server

+
, + ); + ReactDOMClient.hydrateRoot(container, , { + onRecoverableError(error) { + Scheduler.unstable_yieldValue(error.message); + }, + }); + expect(() => { + expect(Scheduler).toFlushAndYield([ + 'Hydration failed because the initial UI does not match what was rendered on the server.', + 'There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.', + ]); + }).toErrorDev( + [ + 'An error occurred during hydration. The server HTML was replaced with client content in
.', + ], + {withoutStack: true}, + ); + expect(getVisibleChildren(container)).toEqual( +
+

Client and server

+

Client only

+
, + ); + }); + + // @gate experimental + it('does not suppress deletions with suppressHydrationWarning', async () => { + function App({isClient}) { + return ( +
+

Client and server

+ {!isClient &&

Server only

} +
+ ); + } + await act(async () => { + const {pipe} = ReactDOMFizzServer.renderToPipeableStream( + , + ); + pipe(writable); + }); + expect(getVisibleChildren(container)).toEqual( +
+

Client and server

+

Server only

+
, + ); + ReactDOMClient.hydrateRoot(container, , { + onRecoverableError(error) { + Scheduler.unstable_yieldValue(error.message); + }, + }); + expect(() => { + expect(Scheduler).toFlushAndYield([ + 'Hydration failed because the initial UI does not match what was rendered on the server.', + 'There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.', + ]); + }).toErrorDev( + [ + 'An error occurred during hydration. The server HTML was replaced with client content in
.', + ], + {withoutStack: true}, + ); + expect(getVisibleChildren(container)).toEqual( +
+

Client and server

+
, + ); + }); +}); From 8a7857838aca9ca919b37d18287f4f8c1324eee9 Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Mon, 4 Apr 2022 22:09:30 +0100 Subject: [PATCH 3/5] Extract more tests --- ...actDOMFizzSuppressHydrationWarning-test.js | 386 +++++++++++++++++- 1 file changed, 369 insertions(+), 17 deletions(-) diff --git a/packages/react-dom/src/__tests__/ReactDOMFizzSuppressHydrationWarning-test.js b/packages/react-dom/src/__tests__/ReactDOMFizzSuppressHydrationWarning-test.js index 67c598bed5dfa..759671f739c6d 100644 --- a/packages/react-dom/src/__tests__/ReactDOMFizzSuppressHydrationWarning-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMFizzSuppressHydrationWarning-test.js @@ -132,15 +132,10 @@ describe('ReactDOMFizzServer', () => { function App({isClient}) { return (
- + {isClient ? 'Client Text' : 'Server Text'} {isClient ? 2 : 1} - - hello,{isClient ? 'client' : 'server'} -
); } @@ -152,12 +147,8 @@ describe('ReactDOMFizzServer', () => { }); expect(getVisibleChildren(container)).toEqual(
- Server Text + Server Text 1 - - {'hello,'} - {'server'} -
, ); ReactDOMClient.hydrateRoot(container, , { @@ -168,17 +159,378 @@ describe('ReactDOMFizzServer', () => { }); expect(Scheduler).toFlushAndYield([]); // The text mismatch should be *silently* fixed. Even in production. - // The attribute mismatch should be ignored and not fixed. expect(getVisibleChildren(container)).toEqual(
- Client Text + Client Text 2 +
, + ); + }); + + // @gate experimental + it('suppresses and fixes multiple text node mismatches with suppressHydrationWarning', async () => { + function App({isClient}) { + return ( +
+ + {isClient ? 'Client1' : 'Server1'} + {isClient ? 'Client2' : 'Server2'} + +
+ ); + } + await act(async () => { + const {pipe} = ReactDOMFizzServer.renderToPipeableStream( + , + ); + pipe(writable); + }); + expect(getVisibleChildren(container)).toEqual( +
+ + {'Server1'} + {'Server2'} + +
, + ); + ReactDOMClient.hydrateRoot(container, , { + onRecoverableError(error) { + Scheduler.unstable_yieldValue(error.message); + }, + }); + expect(Scheduler).toFlushAndYield([]); + expect(getVisibleChildren(container)).toEqual( +
+ + {'Client1'} + {'Client2'} + +
, + ); + }); + + // @gate experimental + it('errors on text-to-element mismatches with suppressHydrationWarning', async () => { + function App({isClient}) { + return ( +
+ + Hello, {isClient ? Client : 'Server'}! + +
+ ); + } + await act(async () => { + const {pipe} = ReactDOMFizzServer.renderToPipeableStream( + , + ); + pipe(writable); + }); + expect(getVisibleChildren(container)).toEqual( +
- {'hello,'} - {'client'} + {'Hello, '} + {'Server'} + {'!'}
, ); + ReactDOMClient.hydrateRoot(container, , { + onRecoverableError(error) { + Scheduler.unstable_yieldValue(error.message); + }, + }); + expect(() => { + expect(Scheduler).toFlushAndYield([ + 'Hydration failed because the initial UI does not match what was rendered on the server.', + 'There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.', + ]); + }).toErrorDev( + [ + 'An error occurred during hydration. The server HTML was replaced with client content in
.', + ], + {withoutStack: true}, + ); + expect(getVisibleChildren(container)).toEqual( +
+ + Hello, Client! + +
, + ); + }); + + // @gate experimental + it('suppresses and fixes client-only single text node mismatches with suppressHydrationWarning', async () => { + function App({isClient}) { + return ( +
+ + {isClient ? 'Client' : null} + +
+ ); + } + await act(async () => { + const {pipe} = ReactDOMFizzServer.renderToPipeableStream( + , + ); + pipe(writable); + }); + expect(getVisibleChildren(container)).toEqual( +
+ +
, + ); + ReactDOMClient.hydrateRoot(container, , { + onRecoverableError(error) { + Scheduler.unstable_yieldValue(error.message); + }, + }); + expect(Scheduler).toFlushAndYield([]); + expect(getVisibleChildren(container)).toEqual( +
+ {'Client'} +
, + ); + }); + + // TODO: This behavior is not consistent with client-only single text node. + // @gate experimental + it('errors on server-only single text node mismatches with suppressHydrationWarning', async () => { + function App({isClient}) { + return ( +
+ + {isClient ? null : 'Server'} + +
+ ); + } + await act(async () => { + const {pipe} = ReactDOMFizzServer.renderToPipeableStream( + , + ); + pipe(writable); + }); + expect(getVisibleChildren(container)).toEqual( +
+ Server +
, + ); + ReactDOMClient.hydrateRoot(container, , { + onRecoverableError(error) { + Scheduler.unstable_yieldValue(error.message); + }, + }); + expect(() => { + expect(Scheduler).toFlushAndYield([ + 'Hydration failed because the initial UI does not match what was rendered on the server.', + 'There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.', + ]); + }).toErrorDev( + [ + 'An error occurred during hydration. The server HTML was replaced with client content in
.', + ], + {withoutStack: true}, + ); + expect(getVisibleChildren(container)).toEqual( +
+ +
, + ); + }); + + // @gate experimental + it('errors on client-only extra text node mismatches with suppressHydrationWarning', async () => { + function App({isClient}) { + return ( +
+ + Shared + {isClient ? 'Client' : null} + +
+ ); + } + await act(async () => { + const {pipe} = ReactDOMFizzServer.renderToPipeableStream( + , + ); + pipe(writable); + }); + expect(getVisibleChildren(container)).toEqual( +
+ + Shared + +
, + ); + ReactDOMClient.hydrateRoot(container, , { + onRecoverableError(error) { + Scheduler.unstable_yieldValue(error.message); + }, + }); + expect(() => { + expect(Scheduler).toFlushAndYield([ + 'Hydration failed because the initial UI does not match what was rendered on the server.', + 'There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.', + ]); + }).toErrorDev( + [ + 'An error occurred during hydration. The server HTML was replaced with client content in
.', + ], + {withoutStack: true}, + ); + expect(getVisibleChildren(container)).toEqual( +
+ + Shared + {'Client'} + +
, + ); + }); + + // @gate experimental + it('errors on server-only extra text node mismatches with suppressHydrationWarning', async () => { + function App({isClient}) { + return ( +
+ + Shared + {isClient ? null : 'Server'} + +
+ ); + } + await act(async () => { + const {pipe} = ReactDOMFizzServer.renderToPipeableStream( + , + ); + pipe(writable); + }); + expect(getVisibleChildren(container)).toEqual( +
+ + SharedServer + +
, + ); + ReactDOMClient.hydrateRoot(container, , { + onRecoverableError(error) { + Scheduler.unstable_yieldValue(error.message); + }, + }); + expect(() => { + expect(Scheduler).toFlushAndYield([ + 'Hydration failed because the initial UI does not match what was rendered on the server.', + 'There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.', + ]); + }).toErrorDev( + [ + 'An error occurred during hydration. The server HTML was replaced with client content in
.', + ], + {withoutStack: true}, + ); + expect(getVisibleChildren(container)).toEqual( +
+ + Shared + +
, + ); + }); + + // @gate experimental + it('errors on element-to-text mismatches with suppressHydrationWarning', async () => { + function App({isClient}) { + return ( +
+ + Hello, {isClient ? 'Client' : Server}! + +
+ ); + } + await act(async () => { + const {pipe} = ReactDOMFizzServer.renderToPipeableStream( + , + ); + pipe(writable); + }); + expect(getVisibleChildren(container)).toEqual( +
+ + Hello, Server! + +
, + ); + ReactDOMClient.hydrateRoot(container, , { + onRecoverableError(error) { + Scheduler.unstable_yieldValue(error.message); + }, + }); + expect(() => { + expect(Scheduler).toFlushAndYield([ + 'Hydration failed because the initial UI does not match what was rendered on the server.', + 'Hydration failed because the initial UI does not match what was rendered on the server.', + 'There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.', + ]); + }).toErrorDev( + [ + 'An error occurred during hydration. The server HTML was replaced with client content in
.', + ], + {withoutStack: true}, + ); + expect(getVisibleChildren(container)).toEqual( +
+ + {'Hello, '} + {'Client'} + {'!'} + +
, + ); + }); + + // @gate experimental + it('suppresses and does not fix attribute mismatches with suppressHydrationWarning', async () => { + function App({isClient}) { + return ( +
+ +
+ ); + } + await act(async () => { + const {pipe} = ReactDOMFizzServer.renderToPipeableStream( + , + ); + pipe(writable); + }); + expect(getVisibleChildren(container)).toEqual( +
+ +
, + ); + ReactDOMClient.hydrateRoot(container, , { + onRecoverableError(error) { + Scheduler.unstable_yieldValue(error.message); + }, + }); + expect(Scheduler).toFlushAndYield([]); + expect(getVisibleChildren(container)).toEqual( +
+ +
, + ); }); // @gate experimental @@ -220,7 +572,7 @@ describe('ReactDOMFizzServer', () => { }); // @gate experimental - it('does not suppress insertions with suppressHydrationWarning', async () => { + it('errors on insertions with suppressHydrationWarning', async () => { function App({isClient}) { return (
@@ -265,7 +617,7 @@ describe('ReactDOMFizzServer', () => { }); // @gate experimental - it('does not suppress deletions with suppressHydrationWarning', async () => { + it('errors on deletions with suppressHydrationWarning', async () => { function App({isClient}) { return (
From 040bc841361c28a0eb332cbf3656536639787945 Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Mon, 4 Apr 2022 23:36:07 +0100 Subject: [PATCH 4/5] Test name --- .../src/__tests__/ReactDOMFizzSuppressHydrationWarning-test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-dom/src/__tests__/ReactDOMFizzSuppressHydrationWarning-test.js b/packages/react-dom/src/__tests__/ReactDOMFizzSuppressHydrationWarning-test.js index 759671f739c6d..40b2154c73c5f 100644 --- a/packages/react-dom/src/__tests__/ReactDOMFizzSuppressHydrationWarning-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMFizzSuppressHydrationWarning-test.js @@ -22,7 +22,7 @@ let buffer = ''; let hasErrored = false; let fatalError = undefined; -describe('ReactDOMFizzServer', () => { +describe('ReactDOMFizzServerHydrationWarning', () => { beforeEach(() => { jest.resetModules(); JSDOM = require('jsdom').JSDOM; From ac628a35be2100fbc40491bc1f8d8a1945c54c8a Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Mon, 4 Apr 2022 23:46:50 +0100 Subject: [PATCH 5/5] Test legacy behavior too --- ...actDOMFizzSuppressHydrationWarning-test.js | 191 +++++++++++------- 1 file changed, 113 insertions(+), 78 deletions(-) diff --git a/packages/react-dom/src/__tests__/ReactDOMFizzSuppressHydrationWarning-test.js b/packages/react-dom/src/__tests__/ReactDOMFizzSuppressHydrationWarning-test.js index 40b2154c73c5f..f6e6d3222b90a 100644 --- a/packages/react-dom/src/__tests__/ReactDOMFizzSuppressHydrationWarning-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMFizzSuppressHydrationWarning-test.js @@ -240,17 +240,22 @@ describe('ReactDOMFizzServerHydrationWarning', () => { Scheduler.unstable_yieldValue(error.message); }, }); - expect(() => { - expect(Scheduler).toFlushAndYield([ - 'Hydration failed because the initial UI does not match what was rendered on the server.', - 'There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.', - ]); - }).toErrorDev( - [ - 'An error occurred during hydration. The server HTML was replaced with client content in
.', - ], - {withoutStack: true}, - ); + if (gate(flags => flags.enableClientRenderFallbackOnHydrationMismatch)) { + expect(() => { + expect(Scheduler).toFlushAndYield([ + 'Hydration failed because the initial UI does not match what was rendered on the server.', + 'There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.', + ]); + }).toErrorDev( + [ + 'An error occurred during hydration. The server HTML was replaced with client content in
.', + ], + {withoutStack: true}, + ); + } else { + // This used to not warn. + expect(Scheduler).toFlushAndYield([]); + } expect(getVisibleChildren(container)).toEqual(
@@ -323,17 +328,22 @@ describe('ReactDOMFizzServerHydrationWarning', () => { Scheduler.unstable_yieldValue(error.message); }, }); - expect(() => { - expect(Scheduler).toFlushAndYield([ - 'Hydration failed because the initial UI does not match what was rendered on the server.', - 'There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.', - ]); - }).toErrorDev( - [ - 'An error occurred during hydration. The server HTML was replaced with client content in
.', - ], - {withoutStack: true}, - ); + if (gate(flags => flags.enableClientRenderFallbackOnHydrationMismatch)) { + expect(() => { + expect(Scheduler).toFlushAndYield([ + 'Hydration failed because the initial UI does not match what was rendered on the server.', + 'There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.', + ]); + }).toErrorDev( + [ + 'An error occurred during hydration. The server HTML was replaced with client content in
.', + ], + {withoutStack: true}, + ); + } else { + // This used to not warn. + expect(Scheduler).toFlushAndYield([]); + } expect(getVisibleChildren(container)).toEqual(
@@ -371,17 +381,22 @@ describe('ReactDOMFizzServerHydrationWarning', () => { Scheduler.unstable_yieldValue(error.message); }, }); - expect(() => { - expect(Scheduler).toFlushAndYield([ - 'Hydration failed because the initial UI does not match what was rendered on the server.', - 'There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.', - ]); - }).toErrorDev( - [ - 'An error occurred during hydration. The server HTML was replaced with client content in
.', - ], - {withoutStack: true}, - ); + if (gate(flags => flags.enableClientRenderFallbackOnHydrationMismatch)) { + expect(() => { + expect(Scheduler).toFlushAndYield([ + 'Hydration failed because the initial UI does not match what was rendered on the server.', + 'There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.', + ]); + }).toErrorDev( + [ + 'An error occurred during hydration. The server HTML was replaced with client content in
.', + ], + {withoutStack: true}, + ); + } else { + // This used to not warn. + expect(Scheduler).toFlushAndYield([]); + } expect(getVisibleChildren(container)).toEqual(
@@ -422,17 +437,22 @@ describe('ReactDOMFizzServerHydrationWarning', () => { Scheduler.unstable_yieldValue(error.message); }, }); - expect(() => { - expect(Scheduler).toFlushAndYield([ - 'Hydration failed because the initial UI does not match what was rendered on the server.', - 'There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.', - ]); - }).toErrorDev( - [ - 'An error occurred during hydration. The server HTML was replaced with client content in
.', - ], - {withoutStack: true}, - ); + if (gate(flags => flags.enableClientRenderFallbackOnHydrationMismatch)) { + expect(() => { + expect(Scheduler).toFlushAndYield([ + 'Hydration failed because the initial UI does not match what was rendered on the server.', + 'There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.', + ]); + }).toErrorDev( + [ + 'An error occurred during hydration. The server HTML was replaced with client content in
.', + ], + {withoutStack: true}, + ); + } else { + // This used to not warn. + expect(Scheduler).toFlushAndYield([]); + } expect(getVisibleChildren(container)).toEqual(
@@ -471,18 +491,23 @@ describe('ReactDOMFizzServerHydrationWarning', () => { Scheduler.unstable_yieldValue(error.message); }, }); - expect(() => { - expect(Scheduler).toFlushAndYield([ - 'Hydration failed because the initial UI does not match what was rendered on the server.', - 'Hydration failed because the initial UI does not match what was rendered on the server.', - 'There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.', - ]); - }).toErrorDev( - [ - 'An error occurred during hydration. The server HTML was replaced with client content in
.', - ], - {withoutStack: true}, - ); + if (gate(flags => flags.enableClientRenderFallbackOnHydrationMismatch)) { + expect(() => { + expect(Scheduler).toFlushAndYield([ + 'Hydration failed because the initial UI does not match what was rendered on the server.', + 'Hydration failed because the initial UI does not match what was rendered on the server.', + 'There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.', + ]); + }).toErrorDev( + [ + 'An error occurred during hydration. The server HTML was replaced with client content in
.', + ], + {withoutStack: true}, + ); + } else { + // This used to not warn. + expect(Scheduler).toFlushAndYield([]); + } expect(getVisibleChildren(container)).toEqual(
@@ -597,17 +622,22 @@ describe('ReactDOMFizzServerHydrationWarning', () => { Scheduler.unstable_yieldValue(error.message); }, }); - expect(() => { - expect(Scheduler).toFlushAndYield([ - 'Hydration failed because the initial UI does not match what was rendered on the server.', - 'There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.', - ]); - }).toErrorDev( - [ - 'An error occurred during hydration. The server HTML was replaced with client content in
.', - ], - {withoutStack: true}, - ); + if (gate(flags => flags.enableClientRenderFallbackOnHydrationMismatch)) { + expect(() => { + expect(Scheduler).toFlushAndYield([ + 'Hydration failed because the initial UI does not match what was rendered on the server.', + 'There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.', + ]); + }).toErrorDev( + [ + 'An error occurred during hydration. The server HTML was replaced with client content in
.', + ], + {withoutStack: true}, + ); + } else { + // This used to not warn. + expect(Scheduler).toFlushAndYield([]); + } expect(getVisibleChildren(container)).toEqual(

Client and server

@@ -643,17 +673,22 @@ describe('ReactDOMFizzServerHydrationWarning', () => { Scheduler.unstable_yieldValue(error.message); }, }); - expect(() => { - expect(Scheduler).toFlushAndYield([ - 'Hydration failed because the initial UI does not match what was rendered on the server.', - 'There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.', - ]); - }).toErrorDev( - [ - 'An error occurred during hydration. The server HTML was replaced with client content in
.', - ], - {withoutStack: true}, - ); + if (gate(flags => flags.enableClientRenderFallbackOnHydrationMismatch)) { + expect(() => { + expect(Scheduler).toFlushAndYield([ + 'Hydration failed because the initial UI does not match what was rendered on the server.', + 'There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.', + ]); + }).toErrorDev( + [ + 'An error occurred during hydration. The server HTML was replaced with client content in
.', + ], + {withoutStack: true}, + ); + } else { + // This used to not warn. + expect(Scheduler).toFlushAndYield([]); + } expect(getVisibleChildren(container)).toEqual(

Client and server