From b82e3b7ddd5123e0b37f3a81fbf347c60e158dde Mon Sep 17 00:00:00 2001 From: Sebastian Markbage Date: Mon, 4 Mar 2024 13:05:03 -0500 Subject: [PATCH 01/10] Build an ancestor tree of all deltas built up so far --- .../src/ReactFiberHydrationContext.js | 297 ++++-------------- .../src/ReactFiberHydrationDiffs.js | 20 ++ scripts/error-codes/codes.json | 3 +- 3 files changed, 89 insertions(+), 231 deletions(-) create mode 100644 packages/react-reconciler/src/ReactFiberHydrationDiffs.js diff --git a/packages/react-reconciler/src/ReactFiberHydrationContext.js b/packages/react-reconciler/src/ReactFiberHydrationContext.js index 49171baf1c6c2..cde12795dc656 100644 --- a/packages/react-reconciler/src/ReactFiberHydrationContext.js +++ b/packages/react-reconciler/src/ReactFiberHydrationContext.js @@ -19,11 +19,11 @@ import type { import type {SuspenseState} from './ReactFiberSuspenseComponent'; import type {TreeContext} from './ReactFiberTreeContext'; import type {CapturedValue} from './ReactCapturedValue'; +import type {HydrationDiffNode} from './ReactFiberHydrationDiffs'; import { HostComponent, HostSingleton, - HostText, HostRoot, SuspenseComponent, } from './ReactWorkTags'; @@ -72,11 +72,50 @@ let isHydrating: boolean = false; // due to earlier mismatches or a suspended fiber. let didSuspendOrErrorDEV: boolean = false; +// Hydration differences found that haven't yet been logged. +let hydrationDiffRootDEV: null | HydrationDiffNode = null; + // Hydration errors that were thrown inside this boundary let hydrationErrors: Array> | null = null; let rootOrSingletonContext = false; +// Builds a common ancestor tree from the root down for collecting diffs. +function buildHydrationDiffNode(fiber: Fiber): HydrationDiffNode { + if (fiber.return === null) { + // We're at the root. + if (hydrationDiffRootDEV === null) { + hydrationDiffRootDEV = { + fiber: fiber, + children: [], + serverProps: undefined, + serverTail: [], + }; + } else if (hydrationDiffRootDEV.fiber !== fiber) { + throw new Error( + 'Saw multiple hydration diff roots in a pass. This is a bug in React.', + ); + } + return hydrationDiffRootDEV; + } + const siblings = buildHydrationDiffNode(fiber.return).children; + // The same node may already exist in the parent. Since we currently always render depth first + // and rerender if we suspend or terminate early, if a shared ancestor was added we should still + // be inside of that shared ancestor which means it was the last one to be added. If this changes + // we may have to scan the whole set. + if (siblings.length > 0 && siblings[siblings.length - 1].fiber === fiber) { + return siblings[siblings.length - 1]; + } + const newNode: HydrationDiffNode = { + fiber: fiber, + children: [], + serverProps: undefined, + serverTail: [], + }; + siblings.push(newNode); + return newNode; +} + function warnIfHydrating() { if (__DEV__) { if (isHydrating) { @@ -105,6 +144,7 @@ function enterHydrationState(fiber: Fiber): boolean { isHydrating = true; hydrationErrors = null; didSuspendOrErrorDEV = false; + hydrationDiffRootDEV = null; rootOrSingletonContext = true; return true; } @@ -123,6 +163,7 @@ function reenterHydrationStateFromDehydratedSuspenseInstance( isHydrating = true; hydrationErrors = null; didSuspendOrErrorDEV = false; + hydrationDiffRootDEV = null; rootOrSingletonContext = false; if (treeContext !== null) { restoreSuspendedTreeContext(fiber, treeContext); @@ -130,58 +171,6 @@ function reenterHydrationStateFromDehydratedSuspenseInstance( return true; } -function warnForDeletedHydratableInstance( - parentType: string, - child: HydratableInstance, -) { - if (__DEV__) { - const description = describeHydratableInstanceForDevWarnings(child); - if (typeof description === 'string') { - console.error( - 'Did not expect server HTML to contain the text node "%s" in <%s>.', - description, - parentType, - ); - } else { - console.error( - 'Did not expect server HTML to contain a <%s> in <%s>.', - description.type, - parentType, - ); - } - } -} - -function warnForInsertedHydratedElement(parentType: string, tag: string) { - if (__DEV__) { - console.error( - 'Expected server HTML to contain a matching <%s> in <%s>.', - tag, - parentType, - ); - } -} - -function warnForInsertedHydratedText(parentType: string, text: string) { - if (__DEV__) { - console.error( - 'Expected server HTML to contain a matching text node for "%s" in <%s>.', - text, - parentType, - ); - } -} - -function warnForInsertedHydratedSuspense(parentType: string) { - if (__DEV__) { - console.error( - 'Expected server HTML to contain a matching <%s> in <%s>.', - 'Suspense', - parentType, - ); - } -} - export function errorHydratingContainer(parentContainer: Container): void { if (__DEV__) { // TODO: This gets logged by onRecoverableError, too, so we should be @@ -192,48 +181,7 @@ export function errorHydratingContainer(parentContainer: Container): void { } } -function warnUnhydratedInstance( - returnFiber: Fiber, - instance: HydratableInstance, -) { - if (__DEV__) { - if (didWarnInvalidHydration) { - return; - } - didWarnInvalidHydration = true; - - switch (returnFiber.tag) { - case HostRoot: { - const description = describeHydratableInstanceForDevWarnings(instance); - if (typeof description === 'string') { - console.error( - 'Did not expect server HTML to contain the text node "%s" in the root.', - description, - ); - } else { - console.error( - 'Did not expect server HTML to contain a <%s> in the root.', - description.type, - ); - } - break; - } - case HostSingleton: - case HostComponent: { - warnForDeletedHydratableInstance(returnFiber.type, instance); - break; - } - case SuspenseComponent: { - const suspenseState: SuspenseState = returnFiber.memoizedState; - if (suspenseState.dehydrated !== null) - warnForDeletedHydratableInstance('Suspense', instance); - break; - } - } - } -} - -function warnNonHydratedInstance(returnFiber: Fiber, fiber: Fiber) { +function warnNonHydratedInstance(fiber: Fiber) { if (__DEV__) { if (didSuspendOrErrorDEV) { // Inside a boundary that already suspended. We're currently rendering the @@ -242,84 +190,10 @@ function warnNonHydratedInstance(returnFiber: Fiber, fiber: Fiber) { return; } - if (didWarnInvalidHydration) { - return; - } - didWarnInvalidHydration = true; - - switch (returnFiber.tag) { - case HostRoot: { - // const parentContainer = returnFiber.stateNode.containerInfo; - switch (fiber.tag) { - case HostSingleton: - case HostComponent: - console.error( - 'Expected server HTML to contain a matching <%s> in the root.', - fiber.type, - ); - break; - case HostText: - const text = fiber.pendingProps; - console.error( - 'Expected server HTML to contain a matching text node for "%s" in the root.', - text, - ); - break; - case SuspenseComponent: - console.error( - 'Expected server HTML to contain a matching <%s> in the root.', - 'Suspense', - ); - break; - } - break; - } - case HostSingleton: - case HostComponent: { - const parentType = returnFiber.type; - // const parentProps = returnFiber.memoizedProps; - // const parentInstance = returnFiber.stateNode; - switch (fiber.tag) { - case HostSingleton: - case HostComponent: { - const type = fiber.type; - warnForInsertedHydratedElement(parentType, type); - break; - } - case HostText: { - const text = fiber.pendingProps; - warnForInsertedHydratedText(parentType, text); - break; - } - case SuspenseComponent: { - warnForInsertedHydratedSuspense(parentType); - break; - } - } - break; - } - case SuspenseComponent: { - // const suspenseState: SuspenseState = returnFiber.memoizedState; - // const parentInstance = suspenseState.dehydrated; - switch (fiber.tag) { - case HostSingleton: - case HostComponent: - const type = fiber.type; - warnForInsertedHydratedElement('Suspense', type); - break; - case HostText: - const text = fiber.pendingProps; - warnForInsertedHydratedText('Suspense', text); - break; - case SuspenseComponent: - warnForInsertedHydratedSuspense('Suspense'); - break; - } - break; - } - default: - return; - } + // Add this fiber to the diff tree. + const diffNode = buildHydrationDiffNode(fiber); + // We use null as a signal that there was no node to match. + diffNode.serverProps = null; } } @@ -432,7 +306,7 @@ function tryToClaimNextHydratableInstance(fiber: Fiber): void { const nextInstance = nextHydratableInstance; if (!nextInstance || !tryHydrateInstance(fiber, nextInstance)) { if (shouldKeepWarning) { - warnNonHydratedInstance((hydrationParentFiber: any), fiber); + warnNonHydratedInstance(fiber); } throwOnHydrationMismatch(fiber); } @@ -452,7 +326,7 @@ function tryToClaimNextHydratableTextInstance(fiber: Fiber): void { const nextInstance = nextHydratableInstance; if (!nextInstance || !tryHydrateText(fiber, nextInstance)) { if (shouldKeepWarning) { - warnNonHydratedInstance((hydrationParentFiber: any), fiber); + warnNonHydratedInstance(fiber); } throwOnHydrationMismatch(fiber); } @@ -464,7 +338,7 @@ function tryToClaimNextHydratableSuspenseInstance(fiber: Fiber): void { } const nextInstance = nextHydratableInstance; if (!nextInstance || !tryHydrateSuspense(fiber, nextInstance)) { - warnNonHydratedInstance((hydrationParentFiber: any), fiber); + warnNonHydratedInstance(fiber); throwOnHydrationMismatch(fiber); } } @@ -497,9 +371,6 @@ export function tryToClaimNextHydratableFormMarkerInstance( return false; } -// Temp -let didWarnInvalidHydration = false; - function prepareToHydrateHostInstance( fiber: Fiber, hostContext: HostContext, @@ -522,39 +393,8 @@ function prepareToHydrateHostInstance( hostContext, ); if (differences !== null) { - if (differences.children != null && !didWarnInvalidHydration) { - didWarnInvalidHydration = true; - const serverValue = differences.children; - const clientValue = fiber.memoizedProps.children; - console.error( - 'Text content did not match. Server: "%s" Client: "%s"', - serverValue, - clientValue, - ); - } - for (const propName in differences) { - if (!differences.hasOwnProperty(propName)) { - continue; - } - if (didWarnInvalidHydration) { - break; - } - didWarnInvalidHydration = true; - const serverValue = differences[propName]; - const clientValue = fiber.memoizedProps[propName]; - if (propName === 'children') { - // Already handled above - } else if (clientValue != null) { - console.error( - 'Prop `%s` did not match. Server: %s Client: %s', - propName, - JSON.stringify(serverValue), - JSON.stringify(clientValue), - ); - } else { - console.error('Extra attribute from the server: %s', propName); - } - } + const diffNode = buildHydrationDiffNode(fiber); + diffNode.serverProps = differences; } } } @@ -596,13 +436,9 @@ function prepareToHydrateHostTextInstance(fiber: Fiber): void { textContent, parentProps, ); - if (difference !== null && !didWarnInvalidHydration) { - didWarnInvalidHydration = true; - console.error( - 'Text content did not match. Server: "%s" Client: "%s"', - difference, - textContent, - ); + if (difference !== null) { + const diffNode = buildHydrationDiffNode(fiber); + diffNode.serverProps = difference; } } } @@ -618,13 +454,9 @@ function prepareToHydrateHostTextInstance(fiber: Fiber): void { textContent, parentProps, ); - if (difference !== null && !didWarnInvalidHydration) { - didWarnInvalidHydration = true; - console.error( - 'Text content did not match. Server: "%s" Client: "%s"', - difference, - textContent, - ); + if (difference !== null) { + const diffNode = buildHydrationDiffNode(fiber); + diffNode.serverProps = difference; } } } @@ -774,10 +606,15 @@ function popHydrationState(fiber: Fiber): boolean { } function warnIfUnhydratedTailNodes(fiber: Fiber) { - let nextInstance = nextHydratableInstance; - while (nextInstance) { - warnUnhydratedInstance(fiber, nextInstance); - nextInstance = getNextHydratableSibling(nextInstance); + if (__DEV__) { + let nextInstance = nextHydratableInstance; + while (nextInstance) { + const diffNode = buildHydrationDiffNode(fiber); + const description = + describeHydratableInstanceForDevWarnings(nextInstance); + diffNode.serverTail.push(description); + nextInstance = getNextHydratableSibling(nextInstance); + } } } diff --git a/packages/react-reconciler/src/ReactFiberHydrationDiffs.js b/packages/react-reconciler/src/ReactFiberHydrationDiffs.js new file mode 100644 index 0000000000000..7264d89179f48 --- /dev/null +++ b/packages/react-reconciler/src/ReactFiberHydrationDiffs.js @@ -0,0 +1,20 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +import type {Fiber} from './ReactInternalTypes'; + +export type HydrationDiffNode = { + fiber: Fiber, + children: Array, + serverProps: void | null | $ReadOnly<{[propName: string]: mixed}> | string, // null means no matching server node + serverTail: Array< + | $ReadOnly<{type: string, props: $ReadOnly<{[propName: string]: mixed}>}> + | string, + >, +}; diff --git a/scripts/error-codes/codes.json b/scripts/error-codes/codes.json index 4e4334e346a41..c5fc32a9cb4e6 100644 --- a/scripts/error-codes/codes.json +++ b/scripts/error-codes/codes.json @@ -502,5 +502,6 @@ "514": "Cannot access %s on the server. You cannot dot into a temporary client reference from a server component. You can only pass the value through to the client.", "515": "Cannot assign to a temporary client reference from a server module.", "516": "Attempted to call a temporary Client Reference from the server but it is on the client. It's not possible to invoke a client function from the server, it can only be rendered as a Component or passed to props of a Client Component.", - "517": "Symbols cannot be passed to a Server Function without a temporary reference set. Pass a TemporaryReferenceSet to the options.%s" + "517": "Symbols cannot be passed to a Server Function without a temporary reference set. Pass a TemporaryReferenceSet to the options.%s", + "518": "Saw multiple hydration diff roots in a pass. This is a bug in React." } From 160072986745c7e442460dc242da995ae3067f7b Mon Sep 17 00:00:00 2001 From: Sebastian Markbage Date: Mon, 4 Mar 2024 13:22:45 -0500 Subject: [PATCH 02/10] Add diff to the error message in dev We collect diffs up until the point we throw to abort. If we collect only dev-only diffs without throwing we log them once the boundary completes. --- .../src/ReactFiberCompleteWork.js | 3 + .../src/ReactFiberHydrationContext.js | 57 +++++++++++++++++-- .../src/ReactFiberHydrationDiffs.js | 4 ++ scripts/error-codes/codes.json | 2 +- 4 files changed, 61 insertions(+), 5 deletions(-) diff --git a/packages/react-reconciler/src/ReactFiberCompleteWork.js b/packages/react-reconciler/src/ReactFiberCompleteWork.js index e7462bf24aa7e..89044182672ad 100644 --- a/packages/react-reconciler/src/ReactFiberCompleteWork.js +++ b/packages/react-reconciler/src/ReactFiberCompleteWork.js @@ -144,6 +144,7 @@ import { resetHydrationState, getIsHydrating, upgradeHydrationErrorsToRecoverable, + emitPendingHydrationWarnings, } from './ReactFiberHydrationContext'; import { renderHasNotSuspendedYet, @@ -893,6 +894,7 @@ function completeDehydratedSuspenseBoundary( } return false; } else { + emitPendingHydrationWarnings(); // We might have reentered this boundary to hydrate it. If so, we need to reset the hydration // state since we're now exiting out of it. popHydrationState doesn't do that for us. resetHydrationState(); @@ -1009,6 +1011,7 @@ function completeWork( // that weren't hydrated. const wasHydrated = popHydrationState(workInProgress); if (wasHydrated) { + emitPendingHydrationWarnings(); // If we hydrated, then we'll need to schedule an update for // the commit side-effects on the root. markUpdate(workInProgress); diff --git a/packages/react-reconciler/src/ReactFiberHydrationContext.js b/packages/react-reconciler/src/ReactFiberHydrationContext.js index cde12795dc656..cd36b318d305d 100644 --- a/packages/react-reconciler/src/ReactFiberHydrationContext.js +++ b/packages/react-reconciler/src/ReactFiberHydrationContext.js @@ -61,6 +61,7 @@ import { } from './ReactFiberTreeContext'; import {queueRecoverableErrors} from './ReactFiberWorkLoop'; import {getRootHostContainer, getHostContext} from './ReactFiberHostContext'; +import {describeDiff} from './ReactFiberHydrationDiffs'; // The deepest Fiber on the stack involved in a hydration context. // This may have been an insertion or a hydration. @@ -264,9 +265,29 @@ function tryHydrateSuspense(fiber: Fiber, nextInstance: any) { } function throwOnHydrationMismatch(fiber: Fiber) { + let diff = ''; + if (__DEV__) { + // Consume the diff root for this mismatch. + // Any other errors will get their own diffs. + const diffRoot = hydrationDiffRootDEV; + if (diffRoot !== null) { + hydrationDiffRootDEV = null; + diff = describeDiff(diffRoot); + } + } throw new Error( - '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. As a result this tree will be regenerated on the client. This can happen if a SSR:ed Client Component used:\n" + + '\n' + + "- A server/client branch `if (typeof window !== 'undefined')`.\n" + + "- Variable input such as `Date.now()` or `Math.random()` which changes each time it's called.\n" + + "- Date formatting in a user's locale which doesn't match the server.\n" + + '- External changing data without sending a snapshot of it along with the HTML.\n' + + '- Invalid HTML tag nesting.\n' + + '\n' + + 'It can also happen if the client has a browser extension installed which messes with the HTML before React loaded.\n' + + '\n' + + 'https://react.dev/link/hydration-mismatch' + + diff, ); } @@ -407,7 +428,7 @@ function prepareToHydrateHostInstance( fiber, ); if (!didHydrate) { - throw new Error('Text content does not match server-rendered HTML.'); + throwOnHydrationMismatch(fiber); } } @@ -473,7 +494,7 @@ function prepareToHydrateHostTextInstance(fiber: Fiber): void { parentProps, ); if (!didHydrate) { - throw new Error('Text content does not match server-rendered HTML.'); + throwOnHydrationMismatch(fiber); } } @@ -651,6 +672,34 @@ export function queueHydrationError(error: CapturedValue): void { } } +export function emitPendingHydrationWarnings() { + if (__DEV__) { + // If we haven't yet thrown any hydration errors by the time we reach the end we've successfully + // hydrated, however, we might still have DEV-only mismatches that we log now. + const diffRoot = hydrationDiffRootDEV; + if (diffRoot !== null) { + hydrationDiffRootDEV = null; + const diff = describeDiff(diffRoot); + console.error( + "A tree hydrated but some attributes of the server rendered HTML didn't match the client properties. This won't be patched up. " + + 'This can happen if a SSR:ed Client Component used:\n' + + '\n' + + "- A server/client branch `if (typeof window !== 'undefined')`.\n" + + "- Variable input such as `Date.now()` or `Math.random()` which changes each time it's called.\n" + + "- Date formatting in a user's locale which doesn't match the server.\n" + + '- External changing data without sending a snapshot of it along with the HTML.\n' + + '- Invalid HTML tag nesting.\n' + + '\n' + + 'It can also happen if the client has a browser extension installed which messes with the HTML before React loaded.\n' + + '\n' + + '%s%s', + 'https://react.dev/link/hydration-mismatch', + diff, + ); + } + } +} + export { warnIfHydrating, enterHydrationState, diff --git a/packages/react-reconciler/src/ReactFiberHydrationDiffs.js b/packages/react-reconciler/src/ReactFiberHydrationDiffs.js index 7264d89179f48..f6b6bee117be1 100644 --- a/packages/react-reconciler/src/ReactFiberHydrationDiffs.js +++ b/packages/react-reconciler/src/ReactFiberHydrationDiffs.js @@ -18,3 +18,7 @@ export type HydrationDiffNode = { | string, >, }; + +export function describeDiff(rootNode: HydrationDiffNode): string { + return '\n'; +} diff --git a/scripts/error-codes/codes.json b/scripts/error-codes/codes.json index c5fc32a9cb4e6..e0bee7dc25399 100644 --- a/scripts/error-codes/codes.json +++ b/scripts/error-codes/codes.json @@ -403,7 +403,7 @@ "415": "Error parsing the data. It's probably an error code or network corruption.", "416": "This environment don't support binary chunks.", "417": "React currently only supports piping to one writable stream.", - "418": "Hydration failed because the initial UI does not match what was rendered on the server.", + "418": "Hydration failed because the server rendered HTML didn't match the client. As a result this tree will be regenerated on the client. This can happen if a SSR:ed Client Component used:\n\n- A server/client branch `if (typeof window !== 'undefined')`.\n- Variable input such as `Date.now()` or `Math.random()` which changes each time it's called.\n- Date formatting in a user's locale which doesn't match the server.\n- External changing data without sending a snapshot of it along with the HTML.\n- Invalid HTML tag nesting.\n\nIt can also happen if the client has a browser extension installed which messes with the HTML before React loaded.\n\nhttps://react.dev/link/hydration-mismatch%s", "419": "The server could not finish this Suspense boundary, likely due to an error during server rendering. Switched to client rendering.", "420": "ServerContext: %s already defined", "421": "This Suspense boundary received an update before it finished hydrating. This caused the boundary to switch to client rendering. The usual way to fix this is to wrap the original update in startTransition.", From 53d4929def193f3760c1405ba5dae701d917329e Mon Sep 17 00:00:00 2001 From: Sebastian Markbage Date: Mon, 4 Mar 2024 21:20:56 -0500 Subject: [PATCH 03/10] Update expected message --- .../src/__tests__/ReactDOMFizzServer-test.js | 4 +- ...actDOMFizzSuppressHydrationWarning-test.js | 14 ++--- .../__tests__/ReactDOMHydrationDiff-test.js | 56 +++++++++---------- ...DOMServerPartialHydration-test.internal.js | 10 ++-- .../ReactDOMSingletonComponents-test.js | 2 +- .../src/__tests__/ReactRenderDocument-test.js | 15 ++--- 6 files changed, 51 insertions(+), 50 deletions(-) diff --git a/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js b/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js index 33b2b322de873..417c70335e2d7 100644 --- a/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js @@ -4488,7 +4488,7 @@ describe('ReactDOMFizzServer', () => { await clientResolve(); await expect(async () => { await waitForAll([ - 'Logged recoverable error: Text content does not match server-rendered HTML.', + "Logged recoverable error: Hydration failed because the server rendered HTML didn't match the client.", 'Logged recoverable error: There was an error while hydrating this Suspense boundary. Switched to client rendering.', ]); }).toErrorDev( @@ -4543,7 +4543,7 @@ describe('ReactDOMFizzServer', () => { }, }); await waitForAll([ - 'Logged recoverable error: Text content does not match server-rendered HTML.', + "Logged recoverable error: Hydration failed because the server rendered HTML didn't match the client.", 'Logged recoverable error: There was an error while hydrating this Suspense boundary. Switched to client rendering.', ]); diff --git a/packages/react-dom/src/__tests__/ReactDOMFizzSuppressHydrationWarning-test.js b/packages/react-dom/src/__tests__/ReactDOMFizzSuppressHydrationWarning-test.js index 5fa719e3d36cc..11c6eedd22348 100644 --- a/packages/react-dom/src/__tests__/ReactDOMFizzSuppressHydrationWarning-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMFizzSuppressHydrationWarning-test.js @@ -242,7 +242,7 @@ describe('ReactDOMFizzServerHydrationWarning', () => { }); await expect(async () => { await waitForAll([ - '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. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.', ]); }).toErrorDev( @@ -331,7 +331,7 @@ describe('ReactDOMFizzServerHydrationWarning', () => { }); await expect(async () => { await waitForAll([ - '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. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.', ]); }).toErrorDev( @@ -379,7 +379,7 @@ describe('ReactDOMFizzServerHydrationWarning', () => { }); await expect(async () => { await waitForAll([ - '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. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.', ]); }).toErrorDev( @@ -430,7 +430,7 @@ describe('ReactDOMFizzServerHydrationWarning', () => { }); await expect(async () => { await waitForAll([ - '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. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.', ]); }).toErrorDev( @@ -479,7 +479,7 @@ describe('ReactDOMFizzServerHydrationWarning', () => { }); await expect(async () => { await waitForAll([ - '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. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.', ]); }).toErrorDev( @@ -602,7 +602,7 @@ describe('ReactDOMFizzServerHydrationWarning', () => { }); await expect(async () => { await waitForAll([ - '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. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.', ]); }).toErrorDev( @@ -648,7 +648,7 @@ describe('ReactDOMFizzServerHydrationWarning', () => { }); await expect(async () => { await waitForAll([ - '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. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.', ]); }).toErrorDev( diff --git a/packages/react-dom/src/__tests__/ReactDOMHydrationDiff-test.js b/packages/react-dom/src/__tests__/ReactDOMHydrationDiff-test.js index ef5507744b4ed..3e2de1f47bddd 100644 --- a/packages/react-dom/src/__tests__/ReactDOMHydrationDiff-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMHydrationDiff-test.js @@ -87,7 +87,7 @@ describe('ReactDOMServerHydration', () => { in div (at **) in Mismatch (at **)", "Warning: An error occurred during hydration. The server HTML was replaced with client content.", - "Caught [Text content does not match server-rendered HTML.]", + "Caught [Hydration failed because the server rendered HTML didn\'t match the client.]", "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]", ] `); @@ -113,7 +113,7 @@ describe('ReactDOMServerHydration', () => { in div (at **) in Mismatch (at **)", "Warning: An error occurred during hydration. The server HTML was replaced with client content.", - "Caught [Text content does not match server-rendered HTML.]", + "Caught [Hydration failed because the server rendered HTML didn\'t match the client.]", "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]", ] `); @@ -282,7 +282,7 @@ describe('ReactDOMServerHydration', () => { in div (at **) in Mismatch (at **)", "Warning: An error occurred during hydration. The server HTML was replaced with client content.", - "Caught [Hydration failed because the initial UI does not match what was rendered on the server.]", + "Caught [Hydration failed because the server rendered HTML didn\'t match the client.]", "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]", ] `); @@ -306,7 +306,7 @@ describe('ReactDOMServerHydration', () => { in div (at **) in Mismatch (at **)", "Warning: An error occurred during hydration. The server HTML was replaced with client content.", - "Caught [Hydration failed because the initial UI does not match what was rendered on the server.]", + "Caught [Hydration failed because the server rendered HTML didn\'t match the client.]", "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]", ] `); @@ -330,7 +330,7 @@ describe('ReactDOMServerHydration', () => { in div (at **) in Mismatch (at **)", "Warning: An error occurred during hydration. The server HTML was replaced with client content.", - "Caught [Hydration failed because the initial UI does not match what was rendered on the server.]", + "Caught [Hydration failed because the server rendered HTML didn\'t match the client.]", "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]", ] `); @@ -354,7 +354,7 @@ describe('ReactDOMServerHydration', () => { in div (at **) in Mismatch (at **)", "Warning: An error occurred during hydration. The server HTML was replaced with client content.", - "Caught [Hydration failed because the initial UI does not match what was rendered on the server.]", + "Caught [Hydration failed because the server rendered HTML didn\'t match the client.]", "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]", ] `); @@ -373,7 +373,7 @@ describe('ReactDOMServerHydration', () => { in div (at **) in Mismatch (at **)", "Warning: An error occurred during hydration. The server HTML was replaced with client content.", - "Caught [Text content does not match server-rendered HTML.]", + "Caught [Hydration failed because the server rendered HTML didn\'t match the client.]", "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]", ] `); @@ -396,7 +396,7 @@ describe('ReactDOMServerHydration', () => { in div (at **) in Mismatch (at **)", "Warning: An error occurred during hydration. The server HTML was replaced with client content.", - "Caught [Hydration failed because the initial UI does not match what was rendered on the server.]", + "Caught [Hydration failed because the server rendered HTML didn\'t match the client.]", "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]", ] `); @@ -419,7 +419,7 @@ describe('ReactDOMServerHydration', () => { in div (at **) in Mismatch (at **)", "Warning: An error occurred during hydration. The server HTML was replaced with client content.", - "Caught [Hydration failed because the initial UI does not match what was rendered on the server.]", + "Caught [Hydration failed because the server rendered HTML didn\'t match the client.]", "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]", ] `); @@ -442,7 +442,7 @@ describe('ReactDOMServerHydration', () => { in div (at **) in Mismatch (at **)", "Warning: An error occurred during hydration. The server HTML was replaced with client content.", - "Caught [Hydration failed because the initial UI does not match what was rendered on the server.]", + "Caught [Hydration failed because the server rendered HTML didn\'t match the client.]", "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]", ] `); @@ -467,7 +467,7 @@ describe('ReactDOMServerHydration', () => { in div (at **) in Mismatch (at **)", "Warning: An error occurred during hydration. The server HTML was replaced with client content.", - "Caught [Hydration failed because the initial UI does not match what was rendered on the server.]", + "Caught [Hydration failed because the server rendered HTML didn\'t match the client.]", "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]", ] `); @@ -491,7 +491,7 @@ describe('ReactDOMServerHydration', () => { in div (at **) in Mismatch (at **)", "Warning: An error occurred during hydration. The server HTML was replaced with client content.", - "Caught [Hydration failed because the initial UI does not match what was rendered on the server.]", + "Caught [Hydration failed because the server rendered HTML didn\'t match the client.]", "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]", ] `); @@ -515,7 +515,7 @@ describe('ReactDOMServerHydration', () => { in div (at **) in Mismatch (at **)", "Warning: An error occurred during hydration. The server HTML was replaced with client content.", - "Caught [Hydration failed because the initial UI does not match what was rendered on the server.]", + "Caught [Hydration failed because the server rendered HTML didn\'t match the client.]", "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]", ] `); @@ -538,7 +538,7 @@ describe('ReactDOMServerHydration', () => { in div (at **) in Mismatch (at **)", "Warning: An error occurred during hydration. The server HTML was replaced with client content.", - "Caught [Hydration failed because the initial UI does not match what was rendered on the server.]", + "Caught [Hydration failed because the server rendered HTML didn\'t match the client.]", "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]", ] `); @@ -557,7 +557,7 @@ describe('ReactDOMServerHydration', () => { in div (at **) in Mismatch (at **)", "Warning: An error occurred during hydration. The server HTML was replaced with client content.", - "Caught [Hydration failed because the initial UI does not match what was rendered on the server.]", + "Caught [Hydration failed because the server rendered HTML didn\'t match the client.]", "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]", ] `); @@ -581,7 +581,7 @@ describe('ReactDOMServerHydration', () => { in div (at **) in Mismatch (at **)", "Warning: An error occurred during hydration. The server HTML was replaced with client content.", - "Caught [Hydration failed because the initial UI does not match what was rendered on the server.]", + "Caught [Hydration failed because the server rendered HTML didn\'t match the client.]", "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]", ] `); @@ -605,7 +605,7 @@ describe('ReactDOMServerHydration', () => { in div (at **) in Mismatch (at **)", "Warning: An error occurred during hydration. The server HTML was replaced with client content.", - "Caught [Hydration failed because the initial UI does not match what was rendered on the server.]", + "Caught [Hydration failed because the server rendered HTML didn\'t match the client.]", "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]", ] `); @@ -628,7 +628,7 @@ describe('ReactDOMServerHydration', () => { in div (at **) in Mismatch (at **)", "Warning: An error occurred during hydration. The server HTML was replaced with client content.", - "Caught [Hydration failed because the initial UI does not match what was rendered on the server.]", + "Caught [Hydration failed because the server rendered HTML didn\'t match the client.]", "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]", ] `); @@ -662,7 +662,7 @@ describe('ReactDOMServerHydration', () => { in div (at **) in Mismatch (at **)", "Warning: An error occurred during hydration. The server HTML was replaced with client content.", - "Caught [Hydration failed because the initial UI does not match what was rendered on the server.]", + "Caught [Hydration failed because the server rendered HTML didn\'t match the client.]", "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]", ] `); @@ -687,7 +687,7 @@ describe('ReactDOMServerHydration', () => { in div (at **) in Mismatch (at **)", "Warning: An error occurred during hydration. The server HTML was replaced with client content.", - "Caught [Hydration failed because the initial UI does not match what was rendered on the server.]", + "Caught [Hydration failed because the server rendered HTML didn\'t match the client.]", "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]", ] `); @@ -715,7 +715,7 @@ describe('ReactDOMServerHydration', () => { in div (at **) in Mismatch (at **)", "Warning: An error occurred during hydration. The server HTML was replaced with client content.", - "Caught [Hydration failed because the initial UI does not match what was rendered on the server.]", + "Caught [Hydration failed because the server rendered HTML didn\'t match the client.]", "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]", ] `); @@ -746,7 +746,7 @@ describe('ReactDOMServerHydration', () => { in div (at **) in Mismatch (at **)", "Warning: An error occurred during hydration. The server HTML was replaced with client content.", - "Caught [Hydration failed because the initial UI does not match what was rendered on the server.]", + "Caught [Hydration failed because the server rendered HTML didn\'t match the client.]", "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]", ] `); @@ -772,7 +772,7 @@ describe('ReactDOMServerHydration', () => { in Suspense (at **) in div (at **) in Mismatch (at **)", - "Caught [Hydration failed because the initial UI does not match what was rendered on the server.]", + "Caught [Hydration failed because the server rendered HTML didn\'t match the client.]", "Caught [There was an error while hydrating this Suspense boundary. Switched to client rendering.]", ] `); @@ -798,7 +798,7 @@ describe('ReactDOMServerHydration', () => { in Suspense (at **) in div (at **) in Mismatch (at **)", - "Caught [Hydration failed because the initial UI does not match what was rendered on the server.]", + "Caught [Hydration failed because the server rendered HTML didn\'t match the client.]", "Caught [There was an error while hydrating this Suspense boundary. Switched to client rendering.]", ] `); @@ -880,7 +880,7 @@ describe('ReactDOMServerHydration', () => { in div (at **) in Mismatch (at **)", "Warning: An error occurred during hydration. The server HTML was replaced with client content.", - "Caught [Hydration failed because the initial UI does not match what was rendered on the server.]", + "Caught [Hydration failed because the server rendered HTML didn\'t match the client.]", "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]", ] `); @@ -907,7 +907,7 @@ describe('ReactDOMServerHydration', () => { in div (at **) in Mismatch (at **)", "Warning: An error occurred during hydration. The server HTML was replaced with client content.", - "Caught [Hydration failed because the initial UI does not match what was rendered on the server.]", + "Caught [Hydration failed because the server rendered HTML didn\'t match the client.]", "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]", ] `); @@ -959,7 +959,7 @@ describe('ReactDOMServerHydration', () => { in ProfileSettings (at **) in Mismatch (at **)", "Warning: An error occurred during hydration. The server HTML was replaced with client content.", - "Caught [Hydration failed because the initial UI does not match what was rendered on the server.]", + "Caught [Hydration failed because the server rendered HTML didn\'t match the client.]", "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]", ] `); @@ -1006,7 +1006,7 @@ describe('ReactDOMServerHydration', () => { in ProfileSettings (at **) in Mismatch (at **)", "Warning: An error occurred during hydration. The server HTML was replaced with client content.", - "Caught [Hydration failed because the initial UI does not match what was rendered on the server.]", + "Caught [Hydration failed because the server rendered HTML didn\'t match the client.]", "Caught [There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.]", ] `); diff --git a/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js b/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js index 93461a70728e9..a22f27d770bf4 100644 --- a/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js +++ b/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js @@ -339,7 +339,7 @@ describe('ReactDOMServerPartialHydration', () => { 'Component', // Hydration mismatch is logged - '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. Switched to client rendering.', ]); @@ -1427,7 +1427,7 @@ describe('ReactDOMServerPartialHydration', () => { }); }).toErrorDev('Did not expect server HTML to contain a in
'); assertLog([ - '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. Switched to client rendering.', ]); @@ -4042,7 +4042,7 @@ describe('ReactDOMServerPartialHydration', () => { {withoutStack: 1}, ); assertLog([ - 'Log recoverable error: Hydration failed because the initial UI does not match what was rendered on the server.', + "Log recoverable error: Hydration failed because the server rendered HTML didn't match the client.", '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.', ]); @@ -4086,7 +4086,7 @@ describe('ReactDOMServerPartialHydration', () => { {withoutStack: 1}, ); assertLog([ - 'Text content does not match server-rendered HTML.', + "Hydration failed because the server rendered HTML didn't match the client.", 'There was an error while hydrating. Because the error happened outside ' + 'of a Suspense boundary, the entire root will switch to client rendering.', ]); @@ -4130,7 +4130,7 @@ describe('ReactDOMServerPartialHydration', () => { {withoutStack: 1}, ); assertLog([ - 'Text content does not match server-rendered HTML.', + "Hydration failed because the server rendered HTML didn't match the client.", 'There was an error while hydrating. Because the error happened outside ' + 'of a Suspense boundary, the entire root will switch to client rendering.', ]); diff --git a/packages/react-dom/src/__tests__/ReactDOMSingletonComponents-test.js b/packages/react-dom/src/__tests__/ReactDOMSingletonComponents-test.js index b0d2f7fe77201..bb372400778e1 100644 --- a/packages/react-dom/src/__tests__/ReactDOMSingletonComponents-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMSingletonComponents-test.js @@ -476,7 +476,7 @@ describe('ReactDOM HostSingleton', () => { ); expect(hydrationErrors).toEqual([ [ - '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.", 'at div', ], [ diff --git a/packages/react-dom/src/__tests__/ReactRenderDocument-test.js b/packages/react-dom/src/__tests__/ReactRenderDocument-test.js index 746c227470c22..56cbd4527ee3a 100644 --- a/packages/react-dom/src/__tests__/ReactRenderDocument-test.js +++ b/packages/react-dom/src/__tests__/ReactRenderDocument-test.js @@ -204,7 +204,7 @@ describe('rendering React components at document', () => { ); assertLog([ - 'Log recoverable error: Hydration failed because the initial UI does not match what was rendered on the server.', + "Log recoverable error: Hydration failed because the server rendered HTML didn't match the client.", '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.', ]); @@ -240,7 +240,7 @@ describe('rendering React components at document', () => { ); assertLog([ - 'Log recoverable error: Hydration failed because the initial UI does not match what was rendered on the server.', + "Log recoverable error: Hydration failed because the server rendered HTML didn't match the client.", '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'); @@ -288,7 +288,7 @@ describe('rendering React components at document', () => { ); assertLog([ - 'Log recoverable error: Text content does not match server-rendered HTML.', + "Log recoverable error: Hydration failed because the server rendered HTML didn't match the client.", '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'); @@ -318,7 +318,9 @@ describe('rendering React components at document', () => { , { onRecoverableError: error => { - Scheduler.log('Log recoverable error: ' + error.message); + Scheduler.log( + 'Log recoverable error: ' + normalizeError(error.message), + ); }, }, ); @@ -326,13 +328,12 @@ describe('rendering React components at document', () => { }).toErrorDev( [ 'Warning: An error occurred during hydration. The server HTML was replaced with client content.', - '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.', + "Log recoverable error: Hydration failed because the server rendered HTML didn't match the client.", + 'Log recoverable error: There was an error while hydrating.', ]); expect(testDocument.body.innerHTML).toBe('Hello world'); }); From 7b065d5e4a5512d8d9d3c46578cb4f7b2e4a2a83 Mon Sep 17 00:00:00 2001 From: Sebastian Markbage Date: Mon, 4 Mar 2024 21:53:07 -0500 Subject: [PATCH 04/10] Normalize some logs so we don't have to assert every single word --- .../src/__tests__/ReactDOMFizzServer-test.js | 106 ++++++++++-------- ...actDOMFizzSuppressHydrationWarning-test.js | 47 ++++---- ...DOMServerPartialHydration-test.internal.js | 105 +++++++---------- .../ReactDOMSingletonComponents-test.js | 27 +++-- .../src/__tests__/ReactRenderDocument-test.js | 24 +++- 5 files changed, 168 insertions(+), 141 deletions(-) diff --git a/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js b/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js index 417c70335e2d7..6065137152340 100644 --- a/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js @@ -47,6 +47,15 @@ let waitForPaint; let clientAct; let streamingContainer; +function normalizeError(msg) { + // Take the first sentence to make it easier to assert on. + const idx = msg.indexOf('.'); + if (idx > -1) { + return msg.slice(0, idx + 1); + } + return msg; +} + describe('ReactDOMFizzServer', () => { beforeEach(() => { jest.resetModules(); @@ -2391,7 +2400,9 @@ describe('ReactDOMFizzServer', () => { ReactDOMClient.hydrateRoot(container, , { onRecoverableError(error) { - Scheduler.log('Log recoverable error: ' + error.message); + Scheduler.log( + 'Log recoverable error: ' + normalizeError(error.message), + ); }, }); @@ -2399,11 +2410,8 @@ describe('ReactDOMFizzServer', () => { // The first paint switches to client rendering due to mismatch await waitForPaint([ 'client', - '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.', + "Log recoverable error: Hydration failed because the server rendered HTML didn't match the client.", + 'Log recoverable error: There was an error while hydrating.', ]); }).toErrorDev( [ @@ -2474,7 +2482,9 @@ describe('ReactDOMFizzServer', () => { ReactDOMClient.hydrateRoot(container, , { onRecoverableError(error) { - Scheduler.log('Log recoverable error: ' + error.message); + Scheduler.log( + 'Log recoverable error: ' + normalizeError(error.message), + ); }, }); @@ -2483,11 +2493,8 @@ describe('ReactDOMFizzServer', () => { // The first paint switches to client rendering due to mismatch await waitForPaint([ 'client', - '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.', + "Log recoverable error: Hydration failed because the server rendered HTML didn't match the client.", + 'Log recoverable error: There was an error while hydrating.', ]); }).toErrorDev( [ @@ -2557,7 +2564,7 @@ describe('ReactDOMFizzServer', () => { isClient = true; ReactDOMClient.hydrateRoot(container, , { onRecoverableError(error) { - Scheduler.log(error.message); + Scheduler.log(normalizeError(error.message)); }, }); @@ -2567,9 +2574,7 @@ describe('ReactDOMFizzServer', () => { await waitForAll([ 'Yay!', 'Hydration error', - 'There was an error while hydrating. Because the error happened ' + - 'outside of a Suspense boundary, the entire root will switch ' + - 'to client rendering.', + 'There was an error while hydrating.', ]); }).toErrorDev( 'An error occurred during hydration. The server HTML was replaced', @@ -2739,7 +2744,7 @@ describe('ReactDOMFizzServer', () => { isClient = true; ReactDOMClient.hydrateRoot(container, , { onRecoverableError(error) { - Scheduler.log(error.message); + Scheduler.log(normalizeError(error.message)); }, }); @@ -2748,7 +2753,7 @@ describe('ReactDOMFizzServer', () => { await waitForAll([ 'Yay!', 'Hydration error', - 'There was an error while hydrating this Suspense boundary. Switched to client rendering.', + 'There was an error while hydrating this Suspense boundary.', ]); expect(getVisibleChildren(container)).toEqual(
@@ -3194,7 +3199,7 @@ describe('ReactDOMFizzServer', () => { isClient = true; ReactDOMClient.hydrateRoot(container, , { onRecoverableError(error) { - Scheduler.log(error.message); + Scheduler.log(normalizeError(error.message)); }, }); @@ -3202,8 +3207,7 @@ describe('ReactDOMFizzServer', () => { // to client rendering. await waitForAll([ 'Hydration error', - 'There was an error while hydrating this Suspense boundary. Switched ' + - 'to client rendering.', + 'There was an error while hydrating this Suspense boundary.', ]); expect(getVisibleChildren(container)).toEqual(
@@ -3263,7 +3267,9 @@ describe('ReactDOMFizzServer', () => { const root = ReactDOMClient.createRoot(container, { onRecoverableError(error) { - Scheduler.log('Logged a recoverable error: ' + error.message); + Scheduler.log( + 'Logged a recoverable error: ' + normalizeError(error.message), + ); }, }); React.startTransition(() => { @@ -3339,7 +3345,9 @@ describe('ReactDOMFizzServer', () => { isClient = true; ReactDOMClient.hydrateRoot(container, , { onRecoverableError(error) { - Scheduler.log('Logged recoverable error: ' + error.message); + Scheduler.log( + 'Logged recoverable error: ' + normalizeError(error.message), + ); }, }); @@ -3349,11 +3357,11 @@ describe('ReactDOMFizzServer', () => { 'Logged recoverable error: Hydration error', 'Logged recoverable error: There was an error while hydrating this ' + - 'Suspense boundary. Switched to client rendering.', + 'Suspense boundary.', 'Logged recoverable error: Hydration error', 'Logged recoverable error: There was an error while hydrating this ' + - 'Suspense boundary. Switched to client rendering.', + 'Suspense boundary.', ]); }); @@ -4395,7 +4403,9 @@ describe('ReactDOMFizzServer', () => { const [ClientApp, clientResolve] = makeApp(); ReactDOMClient.hydrateRoot(container, , { onRecoverableError(error) { - Scheduler.log('Logged recoverable error: ' + error.message); + Scheduler.log( + 'Logged recoverable error: ' + normalizeError(error.message), + ); }, }); await waitForAll([]); @@ -4471,7 +4481,9 @@ describe('ReactDOMFizzServer', () => { const [ClientApp, clientResolve] = makeApp(); ReactDOMClient.hydrateRoot(container, , { onRecoverableError(error) { - Scheduler.log('Logged recoverable error: ' + error.message); + Scheduler.log( + 'Logged recoverable error: ' + normalizeError(error.message), + ); }, }); await waitForAll([]); @@ -4489,7 +4501,7 @@ describe('ReactDOMFizzServer', () => { await expect(async () => { await waitForAll([ "Logged recoverable error: Hydration failed because the server rendered HTML didn't match the client.", - 'Logged recoverable error: There was an error while hydrating this Suspense boundary. Switched to client rendering.', + 'Logged recoverable error: There was an error while hydrating this Suspense boundary.', ]); }).toErrorDev( 'Warning: Text content did not match. Server: "initial" Client: "replaced', @@ -4539,12 +4551,14 @@ describe('ReactDOMFizzServer', () => { ReactDOMClient.hydrateRoot(container, , { onRecoverableError(error) { - Scheduler.log('Logged recoverable error: ' + error.message); + Scheduler.log( + 'Logged recoverable error: ' + normalizeError(error.message), + ); }, }); await waitForAll([ "Logged recoverable error: Hydration failed because the server rendered HTML didn't match the client.", - 'Logged recoverable error: There was an error while hydrating this Suspense boundary. Switched to client rendering.', + 'Logged recoverable error: There was an error while hydrating this Suspense boundary.', ]); expect(getVisibleChildren(container)).toEqual( @@ -4626,12 +4640,14 @@ describe('ReactDOMFizzServer', () => { ReactDOMClient.hydrateRoot(container, , { onRecoverableError(error) { - Scheduler.log('Logged recoverable error: ' + error.message); + Scheduler.log( + 'Logged recoverable error: ' + normalizeError(error.message), + ); }, }); await waitForAll([ 'Logged recoverable error: uh oh', - 'Logged recoverable error: There was an error while hydrating this Suspense boundary. Switched to client rendering.', + 'Logged recoverable error: There was an error while hydrating this Suspense boundary.', ]); expect(getVisibleChildren(container)).toEqual( @@ -4713,7 +4729,9 @@ describe('ReactDOMFizzServer', () => { ReactDOMClient.hydrateRoot(container, , { onRecoverableError(error) { - Scheduler.log('Logged recoverable error: ' + error.message); + Scheduler.log( + 'Logged recoverable error: ' + normalizeError(error.message), + ); }, }); await waitForAll([ @@ -4722,7 +4740,7 @@ describe('ReactDOMFizzServer', () => { // onRecoverableError because the UI recovered without surfacing the // error to the user. 'Logged recoverable error: first error', - 'Logged recoverable error: There was an error while hydrating this Suspense boundary. Switched to client rendering.', + 'Logged recoverable error: There was an error while hydrating this Suspense boundary.', ]); expect(mockError.mock.calls).toEqual([]); mockError.mockClear(); @@ -4830,7 +4848,9 @@ describe('ReactDOMFizzServer', () => { ReactDOMClient.hydrateRoot(container, , { onRecoverableError(error) { - Scheduler.log('Logged recoverable error: ' + error.message); + Scheduler.log( + 'Logged recoverable error: ' + normalizeError(error.message), + ); }, }); await waitForAll(['suspending']); @@ -4847,7 +4867,7 @@ describe('ReactDOMFizzServer', () => { await waitForAll([ 'throwing: first error', 'Logged recoverable error: first error', - 'Logged recoverable error: There was an error while hydrating this Suspense boundary. Switched to client rendering.', + 'Logged recoverable error: There was an error while hydrating this Suspense boundary.', ]); expect(getVisibleChildren(container)).toEqual(
@@ -4954,14 +4974,16 @@ describe('ReactDOMFizzServer', () => { ReactDOMClient.hydrateRoot(container, , { onRecoverableError(error) { - Scheduler.log('Logged recoverable error: ' + error.message); + Scheduler.log( + 'Logged recoverable error: ' + normalizeError(error.message), + ); }, }); await waitForAll([ 'throwing: first error', 'suspending', 'Logged recoverable error: first error', - 'Logged recoverable error: There was an error while hydrating this Suspense boundary. Switched to client rendering.', + 'Logged recoverable error: There was an error while hydrating this Suspense boundary.', ]); expect(mockError.mock.calls).toEqual([]); mockError.mockClear(); @@ -6341,13 +6363,7 @@ describe('ReactDOMFizzServer', () => { }); await expect(async () => { await waitForAll([]); - }).toErrorDev( - [ - 'Expected server HTML to contain a matching in the root', - 'An error occurred during hydration', - ], - {withoutStack: 1}, - ); + }).toErrorDev(['An error occurred during hydration'], {withoutStack: 1}); expect(errors.length).toEqual(2); expect(getVisibleChildren(container)).toEqual(); }); diff --git a/packages/react-dom/src/__tests__/ReactDOMFizzSuppressHydrationWarning-test.js b/packages/react-dom/src/__tests__/ReactDOMFizzSuppressHydrationWarning-test.js index 11c6eedd22348..4c377a9f67a94 100644 --- a/packages/react-dom/src/__tests__/ReactDOMFizzSuppressHydrationWarning-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMFizzSuppressHydrationWarning-test.js @@ -24,6 +24,15 @@ let hasErrored = false; let fatalError = undefined; let waitForAll; +function normalizeError(msg) { + // Take the first sentence to make it easier to assert on. + const idx = msg.indexOf('.'); + if (idx > -1) { + return msg.slice(0, idx + 1); + } + return msg; +} + describe('ReactDOMFizzServerHydrationWarning', () => { beforeEach(() => { jest.resetModules(); @@ -156,7 +165,7 @@ describe('ReactDOMFizzServerHydrationWarning', () => { ReactDOMClient.hydrateRoot(container, , { onRecoverableError(error) { // Don't miss a hydration error. There should be none. - Scheduler.log(error.message); + Scheduler.log(normalizeError(error.message)); }, }); await waitForAll([]); @@ -196,7 +205,7 @@ describe('ReactDOMFizzServerHydrationWarning', () => { ); ReactDOMClient.hydrateRoot(container, , { onRecoverableError(error) { - Scheduler.log(error.message); + Scheduler.log(normalizeError(error.message)); }, }); await waitForAll([]); @@ -237,13 +246,13 @@ describe('ReactDOMFizzServerHydrationWarning', () => { ); ReactDOMClient.hydrateRoot(container, , { onRecoverableError(error) { - Scheduler.log(error.message); + Scheduler.log(normalizeError(error.message)); }, }); await expect(async () => { await waitForAll([ "Hydration failed because the server rendered HTML didn't match the client.", - 'There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.', + 'There was an error while hydrating.', ]); }).toErrorDev( [ @@ -282,7 +291,7 @@ describe('ReactDOMFizzServerHydrationWarning', () => { ); const root = ReactDOMClient.hydrateRoot(container, , { onRecoverableError(error) { - Scheduler.log(error.message); + Scheduler.log(normalizeError(error.message)); }, }); await waitForAll([]); @@ -326,13 +335,13 @@ describe('ReactDOMFizzServerHydrationWarning', () => { ); ReactDOMClient.hydrateRoot(container, , { onRecoverableError(error) { - Scheduler.log(error.message); + Scheduler.log(normalizeError(error.message)); }, }); await expect(async () => { await waitForAll([ "Hydration failed because the server rendered HTML didn't match the client.", - 'There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.', + 'There was an error while hydrating.', ]); }).toErrorDev( [ @@ -374,13 +383,13 @@ describe('ReactDOMFizzServerHydrationWarning', () => { ); ReactDOMClient.hydrateRoot(container, , { onRecoverableError(error) { - Scheduler.log(error.message); + Scheduler.log(normalizeError(error.message)); }, }); await expect(async () => { await waitForAll([ "Hydration failed because the server rendered HTML didn't match the client.", - 'There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.', + 'There was an error while hydrating.', ]); }).toErrorDev( [ @@ -425,13 +434,13 @@ describe('ReactDOMFizzServerHydrationWarning', () => { ); ReactDOMClient.hydrateRoot(container, , { onRecoverableError(error) { - Scheduler.log(error.message); + Scheduler.log(normalizeError(error.message)); }, }); await expect(async () => { await waitForAll([ "Hydration failed because the server rendered HTML didn't match the client.", - 'There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.', + 'There was an error while hydrating.', ]); }).toErrorDev( [ @@ -474,13 +483,13 @@ describe('ReactDOMFizzServerHydrationWarning', () => { ); ReactDOMClient.hydrateRoot(container, , { onRecoverableError(error) { - Scheduler.log(error.message); + Scheduler.log(normalizeError(error.message)); }, }); await expect(async () => { await waitForAll([ "Hydration failed because the server rendered HTML didn't match the client.", - 'There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.', + 'There was an error while hydrating.', ]); }).toErrorDev( [ @@ -527,7 +536,7 @@ describe('ReactDOMFizzServerHydrationWarning', () => { ); ReactDOMClient.hydrateRoot(container, , { onRecoverableError(error) { - Scheduler.log(error.message); + Scheduler.log(normalizeError(error.message)); }, }); await waitForAll([]); @@ -564,7 +573,7 @@ describe('ReactDOMFizzServerHydrationWarning', () => { ); ReactDOMClient.hydrateRoot(container, , { onRecoverableError(error) { - Scheduler.log(error.message); + Scheduler.log(normalizeError(error.message)); }, }); await waitForAll([]); @@ -597,13 +606,13 @@ describe('ReactDOMFizzServerHydrationWarning', () => { ); ReactDOMClient.hydrateRoot(container, , { onRecoverableError(error) { - Scheduler.log(error.message); + Scheduler.log(normalizeError(error.message)); }, }); await expect(async () => { await waitForAll([ "Hydration failed because the server rendered HTML didn't match the client.", - 'There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.', + 'There was an error while hydrating.', ]); }).toErrorDev( [ @@ -643,13 +652,13 @@ describe('ReactDOMFizzServerHydrationWarning', () => { ); ReactDOMClient.hydrateRoot(container, , { onRecoverableError(error) { - Scheduler.log(error.message); + Scheduler.log(normalizeError(error.message)); }, }); await expect(async () => { await waitForAll([ "Hydration failed because the server rendered HTML didn't match the client.", - 'There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.', + 'There was an error while hydrating.', ]); }).toErrorDev( [ diff --git a/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js b/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js index a22f27d770bf4..d0adadded8efb 100644 --- a/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js +++ b/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js @@ -27,13 +27,13 @@ let waitFor; let waitForPaint; let assertLog; -function normalizeCodeLocInfo(strOrErr) { - if (strOrErr && strOrErr.replace) { - return strOrErr.replace(/\n +(?:at|in) ([\S]+)[^\n]*/g, function (m, name) { - return '\n in ' + name + ' (at **)'; - }); +function normalizeError(msg) { + // Take the first sentence to make it easier to assert on. + const idx = msg.indexOf('.'); + if (idx > -1) { + return msg.slice(0, idx + 1); } - return strOrErr; + return msg; } function dispatchMouseEvent(to, from) { @@ -234,7 +234,7 @@ describe('ReactDOMServerPartialHydration', () => { suspend = true; ReactDOMClient.hydrateRoot(container, , { onRecoverableError(error) { - Scheduler.log(error.message); + Scheduler.log(normalizeError(error.message)); }, }); await waitForAll([]); @@ -309,7 +309,7 @@ describe('ReactDOMServerPartialHydration', () => { ReactDOMClient.hydrateRoot(container, , { onRecoverableError(error) { - Scheduler.log(error.message); + Scheduler.log(normalizeError(error.message)); }, }); await waitForAll(['Suspend']); @@ -340,7 +340,7 @@ describe('ReactDOMServerPartialHydration', () => { // 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. Switched to client rendering.', + 'There was an error while hydrating this Suspense boundary.', ]); // Client rendered - suspense comment nodes removed @@ -1195,7 +1195,7 @@ describe('ReactDOMServerPartialHydration', () => { deleted.push(node); }, onRecoverableError(error) { - Scheduler.log(error.message); + Scheduler.log(normalizeError(error.message)); }, }); await waitForAll([]); @@ -1294,7 +1294,7 @@ describe('ReactDOMServerPartialHydration', () => { await act(() => { ReactDOMClient.hydrateRoot(container, , { onRecoverableError(error) { - Scheduler.log(error.message); + Scheduler.log(normalizeError(error.message)); }, }); }); @@ -1309,8 +1309,7 @@ describe('ReactDOMServerPartialHydration', () => { 'Server rendered', 'Client rendered', 'Hydration failed because the initial UI does not match what was rendered on the server.', - 'There was an error while hydrating this Suspense boundary. ' + - 'Switched to client rendering.', + 'There was an error while hydrating this Suspense boundary.', ]); expect(ref.current).not.toBe(span); }); @@ -1421,14 +1420,14 @@ describe('ReactDOMServerPartialHydration', () => { await act(() => { ReactDOMClient.hydrateRoot(container, , { onRecoverableError(error) { - Scheduler.log(error.message); + Scheduler.log(normalizeError(error.message)); }, }); }); }).toErrorDev('Did not expect server HTML to contain a in
'); assertLog([ "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.', + 'There was an error while hydrating this Suspense boundary.', ]); expect(container.innerHTML).toContain('A'); @@ -1787,7 +1786,7 @@ describe('ReactDOMServerPartialHydration', () => { , { onRecoverableError(error) { - Scheduler.log(error.message); + Scheduler.log(normalizeError(error.message)); }, }, ); @@ -1865,7 +1864,7 @@ describe('ReactDOMServerPartialHydration', () => { , { onRecoverableError(error) { - Scheduler.log(error.message); + Scheduler.log(normalizeError(error.message)); }, }, ); @@ -1941,7 +1940,7 @@ describe('ReactDOMServerPartialHydration', () => { , { onRecoverableError(error) { - Scheduler.log(error.message); + Scheduler.log(normalizeError(error.message)); }, }, ); @@ -2249,7 +2248,7 @@ describe('ReactDOMServerPartialHydration', () => { , { onRecoverableError(error) { - Scheduler.log(error.message); + Scheduler.log(normalizeError(error.message)); }, }, ); @@ -2324,22 +2323,18 @@ describe('ReactDOMServerPartialHydration', () => { suspend = false; ReactDOMClient.hydrateRoot(container, , { onRecoverableError(error) { - Scheduler.log(error.message); + Scheduler.log(normalizeError(error.message)); }, }); if (__DEV__) { await waitForAll([ 'The server did not finish this Suspense boundary: The server used' + - ' "renderToString" which does not support Suspense. If you intended' + - ' for this Suspense boundary to render the fallback content on the' + - ' server consider throwing an Error somewhere within the Suspense boundary.' + - ' If you intended to have the server wait for the suspended component' + - ' please switch to "renderToPipeableStream" which supports Suspense on the server', + ' "renderToString" which does not support Suspense.', ]); } else { await waitForAll([ 'The server could not finish this Suspense boundary, likely due to ' + - 'an error during server rendering. Switched to client rendering.', + 'an error during server rendering.', ]); } jest.runAllTimers(); @@ -2397,22 +2392,18 @@ describe('ReactDOMServerPartialHydration', () => { suspend = false; ReactDOMClient.hydrateRoot(container, , { onRecoverableError(error) { - Scheduler.log(error.message); + Scheduler.log(normalizeError(error.message)); }, }); if (__DEV__) { await waitForAll([ 'The server did not finish this Suspense boundary: The server used' + - ' "renderToString" which does not support Suspense. If you intended' + - ' for this Suspense boundary to render the fallback content on the' + - ' server consider throwing an Error somewhere within the Suspense boundary.' + - ' If you intended to have the server wait for the suspended component' + - ' please switch to "renderToPipeableStream" which supports Suspense on the server', + ' "renderToString" which does not support Suspense.', ]); } else { await waitForAll([ 'The server could not finish this Suspense boundary, likely due to ' + - 'an error during server rendering. Switched to client rendering.', + 'an error during server rendering.', ]); } // This will have exceeded the suspended time so we should timeout. @@ -2475,22 +2466,18 @@ describe('ReactDOMServerPartialHydration', () => { suspend = false; ReactDOMClient.hydrateRoot(container, , { onRecoverableError(error) { - Scheduler.log(error.message); + Scheduler.log(normalizeError(error.message)); }, }); if (__DEV__) { await waitForAll([ 'The server did not finish this Suspense boundary: The server used' + - ' "renderToString" which does not support Suspense. If you intended' + - ' for this Suspense boundary to render the fallback content on the' + - ' server consider throwing an Error somewhere within the Suspense boundary.' + - ' If you intended to have the server wait for the suspended component' + - ' please switch to "renderToPipeableStream" which supports Suspense on the server', + ' "renderToString" which does not support Suspense.', ]); } else { await waitForAll([ 'The server could not finish this Suspense boundary, likely due to ' + - 'an error during server rendering. Switched to client rendering.', + 'an error during server rendering.', ]); } // This will have exceeded the suspended time so we should timeout. @@ -2797,7 +2784,7 @@ describe('ReactDOMServerPartialHydration', () => { ReactDOMClient.hydrateRoot(container, , { onRecoverableError(error) { - Scheduler.log(error.message); + Scheduler.log(normalizeError(error.message)); }, }); @@ -2805,16 +2792,12 @@ describe('ReactDOMServerPartialHydration', () => { if (__DEV__) { await waitForAll([ 'The server did not finish this Suspense boundary: The server used' + - ' "renderToString" which does not support Suspense. If you intended' + - ' for this Suspense boundary to render the fallback content on the' + - ' server consider throwing an Error somewhere within the Suspense boundary.' + - ' If you intended to have the server wait for the suspended component' + - ' please switch to "renderToPipeableStream" which supports Suspense on the server', + ' "renderToString" which does not support Suspense.', ]); } else { await waitForAll([ 'The server could not finish this Suspense boundary, likely due to ' + - 'an error during server rendering. Switched to client rendering.', + 'an error during server rendering.', ]); } @@ -2873,22 +2856,18 @@ describe('ReactDOMServerPartialHydration', () => { suspend = false; ReactDOMClient.hydrateRoot(container, , { onRecoverableError(error) { - Scheduler.log(error.message); + Scheduler.log(normalizeError(error.message)); }, }); if (__DEV__) { await waitForAll([ 'The server did not finish this Suspense boundary: The server used' + - ' "renderToString" which does not support Suspense. If you intended' + - ' for this Suspense boundary to render the fallback content on the' + - ' server consider throwing an Error somewhere within the Suspense boundary.' + - ' If you intended to have the server wait for the suspended component' + - ' please switch to "renderToPipeableStream" which supports Suspense on the server', + ' "renderToString" which does not support Suspense.', ]); } else { await waitForAll([ 'The server could not finish this Suspense boundary, likely due to ' + - 'an error during server rendering. Switched to client rendering.', + 'an error during server rendering.', ]); } jest.runAllTimers(); @@ -2989,7 +2968,7 @@ describe('ReactDOMServerPartialHydration', () => { , { onRecoverableError(error) { - Scheduler.log(error.message); + Scheduler.log(normalizeError(error.message)); }, }, ); @@ -4027,7 +4006,9 @@ describe('ReactDOMServerPartialHydration', () => { await act(() => { ReactDOMClient.hydrateRoot(container, , { onRecoverableError(error) { - Scheduler.log('Log recoverable error: ' + error.message); + Scheduler.log( + 'Log recoverable error: ' + normalizeError(error.message), + ); }, }); }); @@ -4043,7 +4024,7 @@ describe('ReactDOMServerPartialHydration', () => { ); assertLog([ "Log recoverable error: Hydration failed because the server rendered HTML didn't match the client.", - '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.', + 'Log recoverable error: There was an error while hydrating.', ]); // We show fallback state when mismatch happens at root @@ -4073,7 +4054,7 @@ describe('ReactDOMServerPartialHydration', () => { await act(() => { ReactDOMClient.hydrateRoot(container, , { onRecoverableError(error) { - Scheduler.log(error.message); + Scheduler.log(normalizeError(error.message)); }, }); }); @@ -4087,8 +4068,7 @@ describe('ReactDOMServerPartialHydration', () => { ); assertLog([ "Hydration failed because the server rendered HTML didn't match the client.", - 'There was an error while hydrating. Because the error happened outside ' + - 'of a Suspense boundary, the entire root will switch to client rendering.', + 'There was an error while hydrating.', ]); }); @@ -4116,7 +4096,7 @@ describe('ReactDOMServerPartialHydration', () => { , { onRecoverableError(error) { - Scheduler.log(error.message); + Scheduler.log(normalizeError(error.message)); }, }, ); @@ -4131,8 +4111,7 @@ describe('ReactDOMServerPartialHydration', () => { ); assertLog([ "Hydration failed because the server rendered HTML didn't match the client.", - 'There was an error while hydrating. Because the error happened outside ' + - 'of a Suspense boundary, the entire root will switch to client rendering.', + 'There was an error while hydrating.', ]); }); }); diff --git a/packages/react-dom/src/__tests__/ReactDOMSingletonComponents-test.js b/packages/react-dom/src/__tests__/ReactDOMSingletonComponents-test.js index bb372400778e1..da8d2e173e4a6 100644 --- a/packages/react-dom/src/__tests__/ReactDOMSingletonComponents-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMSingletonComponents-test.js @@ -24,6 +24,15 @@ let hasErrored = false; let fatalError = undefined; let waitForAll; +function normalizeError(msg) { + // Take the first sentence to make it easier to assert on. + const idx = msg.indexOf('.'); + if (idx > -1) { + return msg.slice(0, idx + 1); + } + return msg; +} + describe('ReactDOM HostSingleton', () => { beforeEach(() => { jest.resetModules(); @@ -454,7 +463,7 @@ describe('ReactDOM HostSingleton', () => { { onRecoverableError(error, errorInfo) { hydrationErrors.push([ - error.message, + normalizeError(error.message), errorInfo.componentStack ? errorInfo.componentStack.split('\n')[1].trim() : null, @@ -466,10 +475,6 @@ describe('ReactDOM HostSingleton', () => { await waitForAll([]); }).toErrorDev( [ - `Warning: Expected server HTML to contain a matching
in . - in div (at **) - in body (at **) - in html (at **)`, `Warning: An error occurred during hydration. The server HTML was replaced with client content.`, ], {withoutStack: 1}, @@ -479,10 +484,7 @@ describe('ReactDOM HostSingleton', () => { "Hydration failed because the server rendered HTML didn't match the client.", 'at div', ], - [ - 'There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.', - null, - ], + ['There was an error while hydrating.', null], ]); expect(persistentElements).toEqual([ document.documentElement, @@ -546,7 +548,12 @@ describe('ReactDOM HostSingleton', () => { }, ); expect(hydrationErrors).toEqual([]); - await waitForAll([]); + await expect(async () => { + await waitForAll([]); + }).toErrorDev( + "A tree hydrated but some attributes of the server rendered HTML didn't match the client properties.", + {withoutStack: true}, + ); expect(persistentElements).toEqual([ document.documentElement, document.head, diff --git a/packages/react-dom/src/__tests__/ReactRenderDocument-test.js b/packages/react-dom/src/__tests__/ReactRenderDocument-test.js index 56cbd4527ee3a..50c301cb593f1 100644 --- a/packages/react-dom/src/__tests__/ReactRenderDocument-test.js +++ b/packages/react-dom/src/__tests__/ReactRenderDocument-test.js @@ -28,6 +28,15 @@ function getTestDocument(markup) { return doc; } +function normalizeError(msg) { + // Take the first sentence to make it easier to assert on. + const idx = msg.indexOf('.'); + if (idx > -1) { + return msg.slice(0, idx + 1); + } + return msg; +} + describe('rendering React components at document', () => { beforeEach(() => { jest.resetModules(); @@ -191,7 +200,9 @@ describe('rendering React components at document', () => { ReactDOM.flushSync(() => { ReactDOMClient.hydrateRoot(container,
parsnip
, { onRecoverableError: error => { - Scheduler.log('Log recoverable error: ' + error.message); + Scheduler.log( + 'Log recoverable error: ' + normalizeError(error.message), + ); }, }); }); @@ -226,7 +237,9 @@ describe('rendering React components at document', () => {
, { onRecoverableError: error => { - Scheduler.log('Log recoverable error: ' + error.message); + Scheduler.log( + 'Log recoverable error: ' + normalizeError(error.message), + ); }, }, ); @@ -272,7 +285,9 @@ describe('rendering React components at document', () => { , { onRecoverableError: error => { - Scheduler.log('Log recoverable error: ' + error.message); + Scheduler.log( + 'Log recoverable error: ' + normalizeError(error.message), + ); }, }, ); @@ -328,12 +343,13 @@ describe('rendering React components at document', () => { }).toErrorDev( [ 'Warning: An error occurred during hydration. The server HTML was replaced with client content.', + 'Expected server HTML to contain a matching text node for "Hello world" in ', ], {withoutStack: 1}, ); assertLog([ "Log recoverable error: Hydration failed because the server rendered HTML didn't match the client.", - 'Log recoverable error: There was an error while hydrating.', + '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'); }); From 6293e55d0f686927d42c3bd4bb045830fbc49faf Mon Sep 17 00:00:00 2001 From: Sebastian Markbage Date: Mon, 4 Mar 2024 22:53:27 -0500 Subject: [PATCH 05/10] Include onRecoverableErrors in the console.error logs for integration tests Previously we had a console.error + throw when something threw. We ignored the throw in tests. Now we only have the throw. --- .../utils/ReactDOMServerIntegrationTestUtils.js | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/packages/react-dom/src/__tests__/utils/ReactDOMServerIntegrationTestUtils.js b/packages/react-dom/src/__tests__/utils/ReactDOMServerIntegrationTestUtils.js index 015942131d01d..2f872492fa022 100644 --- a/packages/react-dom/src/__tests__/utils/ReactDOMServerIntegrationTestUtils.js +++ b/packages/react-dom/src/__tests__/utils/ReactDOMServerIntegrationTestUtils.js @@ -53,8 +53,17 @@ module.exports = function (initModules) { if (forceHydrate) { await act(() => { ReactDOMClient.hydrateRoot(domElement, reactElement, { - onRecoverableError: () => { - // TODO: assert on recoverable error count. + onRecoverableError(e) { + if ( + e.message.startsWith( + 'There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.', + ) + ) { + // We ignore this extra error because it shouldn't really need to be there if + // a hydration mismatch is the cause of it. + } else { + console.error(e); + } }, }); }); From 2571fa1cbf4fab6a4592f4be9e504871a14275af Mon Sep 17 00:00:00 2001 From: Sebastian Markbage Date: Tue, 5 Mar 2024 12:45:50 -0500 Subject: [PATCH 06/10] Remove extra logs now covered by onRecoverableError --- .../src/__tests__/ReactDOMFizzServer-test.js | 34 ++------ ...actDOMFizzSuppressHydrationWarning-test.js | 7 -- .../src/__tests__/ReactDOMFloat-test.js | 2 - .../src/__tests__/ReactDOMOption-test.js | 1 - ...DOMServerPartialHydration-test.internal.js | 19 ++-- .../src/__tests__/ReactRenderDocument-test.js | 86 ++++++++++++------- .../ReactServerRenderingHydration-test.js | 4 - 7 files changed, 66 insertions(+), 87 deletions(-) diff --git a/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js b/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js index 6065137152340..1caaeca68d293 100644 --- a/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js @@ -2416,9 +2416,6 @@ describe('ReactDOMFizzServer', () => { }).toErrorDev( [ 'Warning: An error occurred during hydration. The server HTML was replaced with client content.', - 'Warning: Expected server HTML to contain a matching
in the root.\n' + - ' in div (at **)\n' + - ' in App (at **)', ], {withoutStack: 1}, ); @@ -2499,9 +2496,6 @@ describe('ReactDOMFizzServer', () => { }).toErrorDev( [ 'Warning: An error occurred during hydration. The server HTML was replaced with client content', - 'Warning: Expected server HTML to contain a matching
in the root.\n' + - ' in div (at **)\n' + - ' in App (at **)', ], {withoutStack: 1}, ); @@ -4498,14 +4492,10 @@ describe('ReactDOMFizzServer', () => { // 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(); - await expect(async () => { - await waitForAll([ - "Logged recoverable error: Hydration failed because the server rendered HTML didn't match the client.", - 'Logged recoverable error: There was an error while hydrating this Suspense boundary.', - ]); - }).toErrorDev( - 'Warning: Text content did not match. Server: "initial" Client: "replaced', - ); + await waitForAll([ + "Logged recoverable error: Hydration failed because the server rendered HTML didn't match the client.", + 'Logged recoverable error: There was an error while hydrating this Suspense boundary.', + ]); expect(getVisibleChildren(container)).toEqual(

A

@@ -4570,21 +4560,7 @@ describe('ReactDOMFizzServer', () => { ); await waitForAll([]); - if (__DEV__) { - expect(mockError.mock.calls.length).toBe(1); - expect(mockError.mock.calls[0]).toEqual([ - 'Warning: Text content did not match. Server: "%s" Client: "%s"%s', - 'initial', - 'replaced', - '\n' + - ' in h2 (at **)\n' + - ' in Suspense (at **)\n' + - ' in div (at **)\n' + - ' in App (at **)', - ]); - } else { - expect(mockError.mock.calls.length).toBe(0); - } + expect(mockError.mock.calls.length).toBe(0); } finally { console.error = originalConsoleError; } diff --git a/packages/react-dom/src/__tests__/ReactDOMFizzSuppressHydrationWarning-test.js b/packages/react-dom/src/__tests__/ReactDOMFizzSuppressHydrationWarning-test.js index 4c377a9f67a94..36c3d4f804dd3 100644 --- a/packages/react-dom/src/__tests__/ReactDOMFizzSuppressHydrationWarning-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMFizzSuppressHydrationWarning-test.js @@ -256,7 +256,6 @@ describe('ReactDOMFizzServerHydrationWarning', () => { ]); }).toErrorDev( [ - 'Expected server HTML to contain a matching in ', 'An error occurred during hydration. The server HTML was replaced with client content.', ], {withoutStack: 1}, @@ -345,7 +344,6 @@ describe('ReactDOMFizzServerHydrationWarning', () => { ]); }).toErrorDev( [ - 'Did not expect server HTML to contain the text node "Server" in ', 'An error occurred during hydration. The server HTML was replaced with client content.', ], {withoutStack: 1}, @@ -393,7 +391,6 @@ describe('ReactDOMFizzServerHydrationWarning', () => { ]); }).toErrorDev( [ - 'Expected server HTML to contain a matching text node for "Client" in .', 'An error occurred during hydration. The server HTML was replaced with client content.', ], {withoutStack: 1}, @@ -444,7 +441,6 @@ describe('ReactDOMFizzServerHydrationWarning', () => { ]); }).toErrorDev( [ - 'Did not expect server HTML to contain the text node "Server" in .', 'An error occurred during hydration. The server HTML was replaced with client content.', ], {withoutStack: 1}, @@ -493,7 +489,6 @@ describe('ReactDOMFizzServerHydrationWarning', () => { ]); }).toErrorDev( [ - 'Expected server HTML to contain a matching text node for "Client" in .', 'An error occurred during hydration. The server HTML was replaced with client content.', ], {withoutStack: 1}, @@ -616,7 +611,6 @@ describe('ReactDOMFizzServerHydrationWarning', () => { ]); }).toErrorDev( [ - 'Expected server HTML to contain a matching

in

.', 'An error occurred during hydration. The server HTML was replaced with client content.', ], {withoutStack: 1}, @@ -662,7 +656,6 @@ describe('ReactDOMFizzServerHydrationWarning', () => { ]); }).toErrorDev( [ - 'Did not expect server HTML to contain a

in

.', 'An error occurred during hydration. The server HTML was replaced with client content.', ], {withoutStack: 1}, diff --git a/packages/react-dom/src/__tests__/ReactDOMFloat-test.js b/packages/react-dom/src/__tests__/ReactDOMFloat-test.js index fae926c622564..7b8e27654c6a6 100644 --- a/packages/react-dom/src/__tests__/ReactDOMFloat-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMFloat-test.js @@ -6480,7 +6480,6 @@ body { await waitForAll([]); }).toErrorDev( [ - 'Warning: Text content did not match. Server: "server" Client: "client"', 'Warning: An error occurred during hydration. The server HTML was replaced with client content.', ], {withoutStack: 1}, @@ -8270,7 +8269,6 @@ background-color: green; await waitForAll([]); }).toErrorDev( [ - 'Warning: Text content did not match. Server: "server" Client: "client"', 'Warning: An error occurred during hydration. The server HTML was replaced with client content.', ], {withoutStack: 1}, diff --git a/packages/react-dom/src/__tests__/ReactDOMOption-test.js b/packages/react-dom/src/__tests__/ReactDOMOption-test.js index 3815b413a6b5f..d5225893acfc7 100644 --- a/packages/react-dom/src/__tests__/ReactDOMOption-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMOption-test.js @@ -268,7 +268,6 @@ describe('ReactDOMOption', () => { }); }).toErrorDev( [ - 'Warning: Text content did not match. Server: "FooBaz" Client: "Foo"', 'Warning: An error occurred during hydration. The server HTML was replaced with client content.', 'Warning: In HTML,
cannot be a child of