From aa0394886900f5378508b200587b67a4e809b6b3 Mon Sep 17 00:00:00 2001 From: Sebastian Markbage Date: Tue, 5 Mar 2024 13:29:34 -0500 Subject: [PATCH] Update error message when only props mismatch --- .../src/__tests__/ReactDOMFizzForm-test.js | 10 +- .../src/__tests__/ReactDOMRoot-test.js | 3 +- ...DOMServerPartialHydration-test.internal.js | 190 +++++++----------- .../ReactServerRenderingHydration-test.js | 12 +- 4 files changed, 86 insertions(+), 129 deletions(-) diff --git a/packages/react-dom/src/__tests__/ReactDOMFizzForm-test.js b/packages/react-dom/src/__tests__/ReactDOMFizzForm-test.js index ae56c5eae4dd2..8c7589128b925 100644 --- a/packages/react-dom/src/__tests__/ReactDOMFizzForm-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMFizzForm-test.js @@ -180,7 +180,8 @@ describe('ReactDOMFizzForm', () => { ReactDOMClient.hydrateRoot(container, ); }); }).toErrorDev( - 'Prop `action` did not match. Server: "function" Client: "action"', + "A tree hydrated but some attributes of the server rendered HTML didn't match the client properties.", + {withoutStack: true}, ); }); @@ -346,7 +347,12 @@ describe('ReactDOMFizzForm', () => { await act(async () => { root = ReactDOMClient.hydrateRoot(container, ); }); - }).toErrorDev(['Prop `formTarget` did not match.']); + }).toErrorDev( + [ + "A tree hydrated but some attributes of the server rendered HTML didn't match the client properties.", + ], + {withoutStack: true}, + ); await act(async () => { root.render(); }); diff --git a/packages/react-dom/src/__tests__/ReactDOMRoot-test.js b/packages/react-dom/src/__tests__/ReactDOMRoot-test.js index f3fed8404bf5c..e1acb9827411a 100644 --- a/packages/react-dom/src/__tests__/ReactDOMRoot-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMRoot-test.js @@ -171,7 +171,8 @@ describe('ReactDOMRoot', () => { , ); await expect(async () => await waitForAll([])).toErrorDev( - 'Extra attribute', + "A tree hydrated but some attributes of the server rendered HTML didn't match the client properties.", + {withoutStack: true}, ); }); diff --git a/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js b/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js index 9d94b01d4e14d..cd5632dc0a8c0 100644 --- a/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js +++ b/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js @@ -252,12 +252,6 @@ describe('ReactDOMServerPartialHydration', () => { }); it('falls back to client rendering boundary on mismatch', async () => { - // We can't use the toErrorDev helper here because this is async. - const originalConsoleError = console.error; - const mockError = jest.fn(); - console.error = (...args) => { - mockError(...args.map(normalizeCodeLocInfo)); - }; let client = false; let suspend = false; let resolve; @@ -294,77 +288,58 @@ describe('ReactDOMServerPartialHydration', () => { ); } - try { - const finalHTML = ReactDOMServer.renderToString(); - const container = document.createElement('section'); - container.innerHTML = finalHTML; - assertLog(['Hello', 'Component', 'Component', 'Component', 'Component']); - - expect(container.innerHTML).toBe( - 'Hello
Component
Component
Component
Component
', - ); + const finalHTML = ReactDOMServer.renderToString(); + const container = document.createElement('section'); + container.innerHTML = finalHTML; + assertLog(['Hello', 'Component', 'Component', 'Component', 'Component']); - suspend = true; - client = true; + expect(container.innerHTML).toBe( + 'Hello
Component
Component
Component
Component
', + ); - ReactDOMClient.hydrateRoot(container, , { - onRecoverableError(error) { - Scheduler.log(normalizeError(error.message)); - }, - }); - await waitForAll(['Suspend']); - jest.runAllTimers(); + suspend = true; + client = true; - // Unchanged - expect(container.innerHTML).toBe( - 'Hello
Component
Component
Component
Component
', - ); + ReactDOMClient.hydrateRoot(container, , { + onRecoverableError(error) { + Scheduler.log(normalizeError(error.message)); + }, + }); + await waitForAll(['Suspend']); + jest.runAllTimers(); - suspend = false; - resolve(); - await promise; - await waitForAll([ - // first pass, mismatches at end - 'Hello', - 'Component', - 'Component', - 'Component', - 'Component', - - // second pass as client render - 'Hello', - 'Component', - 'Component', - 'Component', - 'Component', - - // Hydration mismatch is logged - "Hydration failed because the server rendered HTML didn't match the client.", - 'There was an error while hydrating this Suspense boundary.', - ]); + // Unchanged + expect(container.innerHTML).toBe( + 'Hello
Component
Component
Component
Component
', + ); - // Client rendered - suspense comment nodes removed - expect(container.innerHTML).toBe( - 'Hello
Component
Component
Component
Mismatch
', - ); + suspend = false; + resolve(); + await promise; + await waitForAll([ + // first pass, mismatches at end + 'Hello', + 'Component', + 'Component', + 'Component', + 'Component', + + // second pass as client render + 'Hello', + 'Component', + 'Component', + 'Component', + 'Component', + + // Hydration mismatch is logged + "Hydration failed because the server rendered HTML didn't match the client.", + 'There was an error while hydrating this Suspense boundary.', + ]); - if (__DEV__) { - const secondToLastCall = - mockError.mock.calls[mockError.mock.calls.length - 2]; - expect(secondToLastCall).toEqual([ - 'Warning: Expected server HTML to contain a matching <%s> in <%s>.%s', - 'article', - 'Suspense', - '\n' + - ' in article (at **)\n' + - ' in Component (at **)\n' + - ' in Suspense (at **)\n' + - ' in App (at **)', - ]); - } - } finally { - console.error = originalConsoleError; - } + // Client rendered - suspense comment nodes removed + expect(container.innerHTML).toBe( + 'Hello
Component
Component
Component
Mismatch
', + ); }); it('calls the hydration callbacks after hydration or deletion', async () => { @@ -522,17 +497,13 @@ describe('ReactDOMServerPartialHydration', () => { expect(container.innerHTML).toContain('B'); expect(ref.current).toBe(null); - await expect(async () => { - await act(() => { - ReactDOMClient.hydrateRoot(container, , { - onRecoverableError(error) { - Scheduler.log(normalizeError(error.message)); - }, - }); + await act(() => { + ReactDOMClient.hydrateRoot(container, , { + onRecoverableError(error) { + Scheduler.log(normalizeError(error.message)); + }, }); - }).toErrorDev( - 'Did not expect server HTML to contain a in ', - ); + }); expect(container.innerHTML).toContain('A'); expect(container.innerHTML).not.toContain('B'); @@ -540,20 +511,13 @@ describe('ReactDOMServerPartialHydration', () => { assertLog([ 'Server rendered', 'Client rendered', - 'Hydration failed because the initial UI does not match what was rendered on the server.', + "Hydration failed because the server rendered HTML didn't match the client.", 'There was an error while hydrating this Suspense boundary.', ]); expect(ref.current).not.toBe(span); }); it('recovers with client render when server rendered additional nodes at suspense root after unsuspending', async () => { - // We can't use the toErrorDev helper here because this is async. - const originalConsoleError = console.error; - const mockError = jest.fn(); - console.error = (...args) => { - mockError(...args.map(normalizeCodeLocInfo)); - }; - const ref = React.createRef(); let shouldSuspend = false; let resolve; @@ -581,44 +545,34 @@ describe('ReactDOMServerPartialHydration', () => { ); } - try { - const finalHTML = ReactDOMServer.renderToString(); + const finalHTML = ReactDOMServer.renderToString(); - const container = document.createElement('div'); - container.innerHTML = finalHTML; + const container = document.createElement('div'); + container.innerHTML = finalHTML; - const span = container.getElementsByTagName('span')[0]; + const span = container.getElementsByTagName('span')[0]; - expect(container.innerHTML).toContain('A'); - expect(container.innerHTML).toContain('B'); - expect(ref.current).toBe(null); + expect(container.innerHTML).toContain('A'); + expect(container.innerHTML).toContain('B'); + expect(ref.current).toBe(null); - shouldSuspend = true; - await act(() => { - ReactDOMClient.hydrateRoot(container, ); - }); + shouldSuspend = true; + await act(() => { + ReactDOMClient.hydrateRoot(container, ); + }); + await expect(async () => { await act(() => { resolve(); }); + }).toErrorDev([ + "Hydration failed because the server rendered HTML didn't match the client.", + 'There was an error while hydrating this Suspense boundary. Switched to client rendering.', + ]); - expect(container.innerHTML).toContain('A'); - expect(container.innerHTML).not.toContain('B'); - expect(ref.current).not.toBe(span); - if (__DEV__) { - expect(mockError).toHaveBeenCalledWith( - 'Warning: Did not expect server HTML to contain a <%s> in <%s>.%s', - 'span', - 'Suspense', - '\n' + - ' in Suspense (at **)\n' + - ' in div (at **)\n' + - ' in App (at **)', - ); - } - } finally { - console.error = originalConsoleError; - } + expect(container.innerHTML).toContain('A'); + expect(container.innerHTML).not.toContain('B'); + expect(ref.current).not.toBe(span); }); it('recovers with client render when server rendered additional nodes deep inside suspense root', async () => { diff --git a/packages/react-dom/src/__tests__/ReactServerRenderingHydration-test.js b/packages/react-dom/src/__tests__/ReactServerRenderingHydration-test.js index 2ac86769f7f3d..c1989691109ab 100644 --- a/packages/react-dom/src/__tests__/ReactServerRenderingHydration-test.js +++ b/packages/react-dom/src/__tests__/ReactServerRenderingHydration-test.js @@ -251,10 +251,8 @@ describe('ReactDOMServerHydration', () => { ); }); }).toErrorDev( - 'Warning: Prop `style` did not match. Server: ' + - '{"text-decoration":"none","color":"black","height":"10px"}' + - ' Client: ' + - '{"textDecoration":"none","color":"white","height":"10px"}', + "A tree hydrated but some attributes of the server rendered HTML didn't match the client properties.", + {withoutStack: true}, ); }); @@ -301,10 +299,8 @@ describe('ReactDOMServerHydration', () => { ); }); }).toErrorDev( - 'Warning: Prop `style` did not match. Server: ' + - '{"text-decoration":"none","color":"black","height":"10px"}' + - ' Client: ' + - '{"textDecoration":"none","color":"black","height":"10px"}', // note that this is no difference + "A tree hydrated but some attributes of the server rendered HTML didn't match the client properties.", + {withoutStack: true}, ); });