From bf42f639ff91aa2251d6ade56bc4505b010b2577 Mon Sep 17 00:00:00 2001 From: Josh Story Date: Tue, 19 Apr 2022 16:22:36 -0700 Subject: [PATCH] Add failing test case for #24384 If a components suspends during hydration we expect there to be mismatches with server rendered HTML but we were not always supressing warning messages related to these expected mismatches --- .../src/__tests__/ReactDOMFizzServer-test.js | 104 ++++++++++++++++++ 1 file changed, 104 insertions(+) diff --git a/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js b/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js index da9097c51d5e3..a7c2ebdb35c45 100644 --- a/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js @@ -122,6 +122,15 @@ describe('ReactDOMFizzServer', () => { } } + async function clientAct(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); + }); + } + function getVisibleChildren(element) { const children = []; let node = element.firstChild; @@ -2872,4 +2881,99 @@ describe('ReactDOMFizzServer', () => { expect(window.__test_outlet).toBe(1); }); }); + + // @gate experimental + it('#24384: Suspending should halt hydration warnings while still allowing siblings to warm up', async () => { + const makeApp = () => { + let resolve, resolved; + let promise = new Promise(r => { + resolve = () => { + resolved = true; + return r(); + }; + }); + function ComponentThatSuspends() { + if (!resolved) { + throw promise; + } + return

A

; + } + + const App = ({text}) => { + return ( +
+ Loading...}> + +

{text}

+
+
+ ); + }; + + return [App, resolve]; + }; + + const [ServerApp, serverResolve] = makeApp(); + await act(async () => { + const {pipe} = ReactDOMFizzServer.renderToPipeableStream( + , + ); + pipe(writable); + }); + await act(() => { + serverResolve(); + }); + + expect(getVisibleChildren(container)).toEqual( +
+

A

+

initial

+
, + ); + + // the client app is rendered with an intentionally incorrect text. The still Suspended component causes + // hydration to fail silently (allowing for cache warming but otherwise skipping this boundary) until it + // resolves. + const [ClientApp, clientResolve] = makeApp(); + let root; + root = ReactDOMClient.hydrateRoot( + container, + , + { + onRecoverableError(error) { + Scheduler.unstable_yieldValue( + 'Logged recoverable error: ' + error.message, + ); + }, + }, + ); + Scheduler.unstable_flushAll(); + + expect(getVisibleChildren(container)).toEqual( +
+

A

+

initial

+
, + ); + + // Now that the boundary resolves to it's children the hydration completes and discovers that there is a mismatch requiring + // client-side rendering. + await clientResolve(); + expect(() => { + expect(Scheduler).toFlushAndYield([ + 'Logged recoverable error: Text content does not match server-rendered HTML.', + 'Logged recoverable error: There was an error while hydrating this Suspense boundary. Switched to client rendering.', + ]); + }).toErrorDev( + 'Warning: Prop `name` did not match. Server: "initial" Client: "replaced"', + ); + expect(getVisibleChildren(container)).toEqual( +
+

A

+

replaced

+
, + ); + + expect(Scheduler).toFlushAndYield([]); + }); });