From c18662405cc436646411647f8a8965c1c0594c3c Mon Sep 17 00:00:00 2001 From: Josh Story Date: Tue, 13 Jan 2026 12:48:01 -0800 Subject: [PATCH 1/2] [Fiber] Correctly handle replaying when hydrating (#35494) When hydrating if something suspends and then resolves in a microtask it is possible that React will resume the render without fully unwinding work in progress. This can cause hydration cursors to be offset and lead to hydration errors. This change adds a restore step when replaying HostComponent to ensure the hydration cursor is in the appropriate position when replaying. fixes: #35210 --- ...DOMServerPartialHydration-test.internal.js | 166 ++++++++++++++++++ .../src/ReactFiberHydrationContext.js | 39 ++++ .../src/ReactFiberWorkLoop.js | 9 +- 3 files changed, 213 insertions(+), 1 deletion(-) diff --git a/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js b/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js index d13303f006a..502ac8c326c 100644 --- a/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js +++ b/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js @@ -4063,4 +4063,170 @@ describe('ReactDOMServerPartialHydration', () => { expect(span.style.display).toBe(''); expect(ref.current).toBe(span); }); + + // Regression for https://github.com/facebook/react/issues/35210 and other issues where lazy elements created in flight + // caused hydration issues b/c the replay pathway did not correctly reset the hydration cursor + it('Can hydrate even when lazy content resumes immediately inside a HostComponent', async () => { + let resolve; + const promise = new Promise(r => { + resolve = () => r({default: 'value'}); + }); + + const lazyContent = React.lazy(() => { + Scheduler.log('Lazy initializer called'); + return promise; + }); + + function App() { + return ; + } + + // Server-rendered HTML + const container = document.createElement('div'); + container.innerHTML = ''; + + const hydrationErrors = []; + + React.startTransition(() => { + ReactDOMClient.hydrateRoot(container, , { + onRecoverableError(error) { + console.log('[DEBUG] hydration error:', error.message); + hydrationErrors.push(error.message); + }, + }); + }); + + await waitFor(['Lazy initializer called']); + resolve(); + await waitForAll([]); + + // Without the fix, hydration cursor is wrong and causes mismatch + expect(hydrationErrors).toEqual([]); + expect(container.innerHTML).toEqual(''); + }); + + it('Can hydrate even when lazy content resumes immediately inside a HostSingleton', async () => { + let resolve; + const promise = new Promise(r => { + resolve = () => r({default:
value
}); + }); + + const lazyContent = React.lazy(() => { + Scheduler.log('Lazy initializer called'); + return promise; + }); + + function App() { + return ( + + {lazyContent} + + ); + } + + // Server-rendered HTML + document.body.innerHTML = '
value
'; + + const hydrationErrors = []; + + React.startTransition(() => { + ReactDOMClient.hydrateRoot(document, , { + onRecoverableError(error) { + console.log('[DEBUG] hydration error:', error.message); + hydrationErrors.push(error.message); + }, + }); + }); + + await waitFor(['Lazy initializer called']); + resolve(); + await waitForAll([]); + + expect(hydrationErrors).toEqual([]); + expect(document.documentElement.outerHTML).toEqual( + '
value
', + ); + }); + + it('Can hydrate even when lazy content resumes immediately inside a Suspense', async () => { + let resolve; + const promise = new Promise(r => { + resolve = () => r({default: 'value'}); + }); + + const lazyContent = React.lazy(() => { + Scheduler.log('Lazy initializer called'); + return promise; + }); + + function App() { + return {lazyContent}; + } + + // Server-rendered HTML + const container = document.createElement('div'); + container.innerHTML = 'value'; + + const hydrationErrors = []; + + let root; + React.startTransition(() => { + root = ReactDOMClient.hydrateRoot(container, , { + onRecoverableError(error) { + console.log('[DEBUG] hydration error:', error.message); + hydrationErrors.push(error.message); + }, + }); + }); + + await waitFor(['Lazy initializer called']); + resolve(); + await waitForAll([]); + + expect(hydrationErrors).toEqual([]); + expect(container.innerHTML).toEqual('value'); + root.unmount(); + expect(container.innerHTML).toEqual(''); + }); + + it('Can hydrate even when lazy content resumes immediately inside an Activity', async () => { + let resolve; + const promise = new Promise(r => { + resolve = () => r({default: 'value'}); + }); + + const lazyContent = React.lazy(() => { + Scheduler.log('Lazy initializer called'); + return promise; + }); + + function App() { + return {lazyContent}; + } + + // Server-rendered HTML + const container = document.createElement('div'); + container.innerHTML = 'value'; + + const hydrationErrors = []; + + let root; + React.startTransition(() => { + root = ReactDOMClient.hydrateRoot(container, , { + onRecoverableError(error) { + console.log('[DEBUG] hydration error:', error.message); + hydrationErrors.push(error.message); + }, + }); + }); + + await waitFor(['Lazy initializer called']); + resolve(); + await waitForAll([]); + + expect(hydrationErrors).toEqual([]); + expect(container.innerHTML).toEqual('value'); + root.unmount(); + expect(container.innerHTML).toEqual(''); + }); }); diff --git a/packages/react-reconciler/src/ReactFiberHydrationContext.js b/packages/react-reconciler/src/ReactFiberHydrationContext.js index 00e2fcddfcf..0c758202b52 100644 --- a/packages/react-reconciler/src/ReactFiberHydrationContext.js +++ b/packages/react-reconciler/src/ReactFiberHydrationContext.js @@ -173,6 +173,7 @@ function enterHydrationState(fiber: Fiber): boolean { didSuspendOrErrorDEV = false; hydrationDiffRootDEV = null; rootOrSingletonContext = true; + return true; } @@ -834,6 +835,43 @@ function resetHydrationState(): void { didSuspendOrErrorDEV = false; } +// Restore the hydration cursor when unwinding a HostComponent that already +// claimed a DOM node. This is a fork of popHydrationState that does all the +// same validity checks but restores the cursor to this fiber's DOM node +// instead of advancing past it. It also does NOT clear unhydrated tail nodes +// or throw on mismatches since we're unwinding, not completing. +// +// This is needed when replaySuspendedUnitOfWork calls unwindInterruptedWork +// before re-running beginWork on the same fiber, or when throwAndUnwindWorkLoop +// calls unwindWork on ancestor fibers. +function popHydrationStateOnInterruptedWork(fiber: Fiber): void { + if (!supportsHydration) { + return; + } + if (fiber !== hydrationParentFiber) { + // We're deeper than the current hydration context, inside an inserted + // tree. Don't touch the cursor. + return; + } + if (!isHydrating) { + // If we're not currently hydrating but we're in a hydration context, then + // we were an insertion and now need to pop up to reenter hydration of our + // siblings. Same as popHydrationState. + popToNextHostParent(fiber); + isHydrating = true; + return; + } + + // We're in a valid hydration context. Restore the cursor to this fiber's + // DOM node so that when beginWork re-runs, it can claim the same node. + // Unlike popHydrationState, we do NOT check for unhydrated tail nodes + // or advance the cursor - we're restoring, not completing. + popToNextHostParent(fiber); + if (fiber.tag === HostComponent && fiber.stateNode != null) { + nextHydratableInstance = fiber.stateNode; + } +} + export function upgradeHydrationErrorsToRecoverable(): Array< CapturedValue, > | null { @@ -905,6 +943,7 @@ export { reenterHydrationStateFromDehydratedActivityInstance, reenterHydrationStateFromDehydratedSuspenseInstance, resetHydrationState, + popHydrationStateOnInterruptedWork, claimHydratableSingleton, tryToClaimNextHydratableInstance, tryToClaimNextHydratableTextInstance, diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.js b/packages/react-reconciler/src/ReactFiberWorkLoop.js index f7200458be1..9f21041e6df 100644 --- a/packages/react-reconciler/src/ReactFiberWorkLoop.js +++ b/packages/react-reconciler/src/ReactFiberWorkLoop.js @@ -120,7 +120,10 @@ import { import {createWorkInProgress, resetWorkInProgress} from './ReactFiber'; import {isRootDehydrated} from './ReactFiberShellHydration'; -import {getIsHydrating} from './ReactFiberHydrationContext'; +import { + getIsHydrating, + popHydrationStateOnInterruptedWork, +} from './ReactFiberHydrationContext'; import { NoMode, ProfileMode, @@ -3109,6 +3112,10 @@ function replayBeginWork(unitOfWork: Fiber): null | Fiber { // promises that a host component might suspend on are definitely cached // because they are controlled by us. So don't bother. resetHooksOnUnwind(unitOfWork); + // We are about to retry this host component and need to ensure the hydration + // state is appropriate for hydrating this unit. Other fiber types hydrate differently + // and aren't reliant on the cursor positioning so this function is only for HostComponent + popHydrationStateOnInterruptedWork(unitOfWork); // Fallthrough to the next branch. } default: { From 3e1abcc8d7083a13adf4774feb0d67ecbe4a2bc4 Mon Sep 17 00:00:00 2001 From: Ricky Date: Tue, 13 Jan 2026 15:52:53 -0500 Subject: [PATCH 2/2] [tests] Require exact error messages in assertConsole helpers (#35497) Requires full error message in assert helpers. Some of the error messages we asset on add a native javascript stack trace, which would be a pain to add to the messages and maintain. This PR allows you to just add `\n in ` placeholder to the error message to denote a native stack trace is present in the message. --- Note: i vibe coded this so it was a pain to backtrack this to break this into a stack, I tried and gave up, sorry. --- .github/workflows/runtime_build_and_test.yml | 3 + .../__tests__/ReactInternalTestUtils-test.js | 534 ++++++++++-------- packages/internal-test-utils/consoleMock.js | 222 +++++++- .../src/__tests__/ReactFlight-test.js | 5 +- .../__tests__/ReactHooksInspection-test.js | 11 +- .../react-dom/src/__tests__/ReactDOM-test.js | 19 +- .../src/__tests__/ReactDOMFizzServer-test.js | 45 +- .../src/__tests__/ReactDOMFloat-test.js | 19 +- .../src/__tests__/ReactDOMSrcObject-test.js | 9 +- .../src/__tests__/ReactFlushSync-test.js | 3 +- .../src/__tests__/ReactFlightDOM-test.js | 85 ++- .../src/__tests__/ReactFlightDOMForm-test.js | 6 +- .../src/__tests__/ReactFlightServer-test.js | 3 +- .../react/src/__tests__/ReactChildren-test.js | 16 +- .../createReactClassIntegration-test.js | 7 +- .../useSyncExternalStoreShared-test.js | 23 +- 16 files changed, 690 insertions(+), 320 deletions(-) diff --git a/.github/workflows/runtime_build_and_test.yml b/.github/workflows/runtime_build_and_test.yml index 25282e8400a..3eec5f90bee 100644 --- a/.github/workflows/runtime_build_and_test.yml +++ b/.github/workflows/runtime_build_and_test.yml @@ -278,6 +278,7 @@ jobs: if: steps.node_modules.outputs.cache-hit != 'true' - run: yarn --cwd compiler install --frozen-lockfile if: steps.node_modules.outputs.cache-hit != 'true' + - run: node --version - run: yarn test ${{ matrix.params }} --ci --shard=${{ matrix.shard }} # Hardcoded to improve parallelism @@ -445,6 +446,7 @@ jobs: merge-multiple: true - name: Display structure of build run: ls -R build + - run: node --version - run: yarn test --build ${{ matrix.test_params }} --shard=${{ matrix.shard }} --ci test_build_devtools: @@ -489,6 +491,7 @@ jobs: merge-multiple: true - name: Display structure of build run: ls -R build + - run: node --version - run: yarn test --build --project=devtools -r=experimental --shard=${{ matrix.shard }} --ci process_artifacts_combined: diff --git a/packages/internal-test-utils/__tests__/ReactInternalTestUtils-test.js b/packages/internal-test-utils/__tests__/ReactInternalTestUtils-test.js index b2474147786..8560c508082 100644 --- a/packages/internal-test-utils/__tests__/ReactInternalTestUtils-test.js +++ b/packages/internal-test-utils/__tests__/ReactInternalTestUtils-test.js @@ -879,7 +879,7 @@ describe('ReactInternalTestUtils console assertions', () => { if (__DEV__) { console.warn('Hello\n in div'); } - assertConsoleWarnDev(['Hello']); + assertConsoleWarnDev(['Hello\n in div']); }); it('passes if all warnings contain a stack', () => { @@ -888,7 +888,11 @@ describe('ReactInternalTestUtils console assertions', () => { console.warn('Good day\n in div'); console.warn('Bye\n in div'); } - assertConsoleWarnDev(['Hello', 'Good day', 'Bye']); + assertConsoleWarnDev([ + 'Hello\n in div', + 'Good day\n in div', + 'Bye\n in div', + ]); }); it('fails if act is called without assertConsoleWarnDev', async () => { @@ -1075,7 +1079,11 @@ describe('ReactInternalTestUtils console assertions', () => { const message = expectToThrowFailure(() => { console.warn('Hi \n in div'); console.warn('Wow \n in div'); - assertConsoleWarnDev(['Hi', 'Wow', 'Bye']); + assertConsoleWarnDev([ + 'Hi \n in div', + 'Wow \n in div', + 'Bye \n in div', + ]); }); expect(message).toMatchInlineSnapshot(` "assertConsoleWarnDev(expected) @@ -1085,9 +1093,9 @@ describe('ReactInternalTestUtils console assertions', () => { - Expected warnings + Received warnings - - Hi - - Wow - - Bye + - Hi in div + - Wow in div + - Bye in div + Hi in div (at **) + Wow in div (at **)" `); @@ -1188,16 +1196,26 @@ describe('ReactInternalTestUtils console assertions', () => { console.warn('Hello'); console.warn('Good day\n in div'); console.warn('Bye\n in div'); - assertConsoleWarnDev(['Hello', 'Good day', 'Bye']); + assertConsoleWarnDev([ + 'Hello\n in div', + 'Good day\n in div', + 'Bye\n in div', + ]); }); expect(message).toMatchInlineSnapshot(` "assertConsoleWarnDev(expected) - Missing component stack for: - "Hello" + Unexpected warning(s) recorded. - If this warning should omit a component stack, pass [log, {withoutStack: true}]. - If all warnings should omit the component stack, add {withoutStack: true} to the assertConsoleWarnDev call." + - Expected warnings + + Received warnings + + - Hello in div + - Good day in div + - Bye in div + + Hello + + Good day in div (at **) + + Bye in div (at **)" `); }); @@ -1207,16 +1225,26 @@ describe('ReactInternalTestUtils console assertions', () => { console.warn('Hello\n in div'); console.warn('Good day'); console.warn('Bye\n in div'); - assertConsoleWarnDev(['Hello', 'Good day', 'Bye']); + assertConsoleWarnDev([ + 'Hello\n in div', + 'Good day\n in div', + 'Bye\n in div', + ]); }); expect(message).toMatchInlineSnapshot(` "assertConsoleWarnDev(expected) - Missing component stack for: - "Good day" + Unexpected warning(s) recorded. - If this warning should omit a component stack, pass [log, {withoutStack: true}]. - If all warnings should omit the component stack, add {withoutStack: true} to the assertConsoleWarnDev call." + - Expected warnings + + Received warnings + + - Hello in div + - Good day in div + - Bye in div + + Hello in div (at **) + + Good day + + Bye in div (at **)" `); }); @@ -1226,41 +1254,26 @@ describe('ReactInternalTestUtils console assertions', () => { console.warn('Hello\n in div'); console.warn('Good day\n in div'); console.warn('Bye'); - assertConsoleWarnDev(['Hello', 'Good day', 'Bye']); - }); - expect(message).toMatchInlineSnapshot(` - "assertConsoleWarnDev(expected) - - Missing component stack for: - "Bye" - - If this warning should omit a component stack, pass [log, {withoutStack: true}]. - If all warnings should omit the component stack, add {withoutStack: true} to the assertConsoleWarnDev call." - `); - }); - - // @gate __DEV__ - it('fails if all warnings do not contain a stack', () => { - const message = expectToThrowFailure(() => { - console.warn('Hello'); - console.warn('Good day'); - console.warn('Bye'); - assertConsoleWarnDev(['Hello', 'Good day', 'Bye']); + assertConsoleWarnDev([ + 'Hello\n in div', + 'Good day\n in div', + 'Bye\n in div', + ]); }); expect(message).toMatchInlineSnapshot(` "assertConsoleWarnDev(expected) - Missing component stack for: - "Hello" - - Missing component stack for: - "Good day" + Unexpected warning(s) recorded. - Missing component stack for: - "Bye" + - Expected warnings + + Received warnings - If this warning should omit a component stack, pass [log, {withoutStack: true}]. - If all warnings should omit the component stack, add {withoutStack: true} to the assertConsoleWarnDev call." + - Hello in div + - Good day in div + - Bye in div + + Hello in div (at **) + + Good day in div (at **) + + Bye" `); }); @@ -1339,12 +1352,13 @@ describe('ReactInternalTestUtils console assertions', () => { expect(message).toMatchInlineSnapshot(` "assertConsoleWarnDev(expected) - Unexpected component stack for: - "Hello - in div (at **)" + Unexpected warning(s) recorded. + + - Expected warnings + + Received warnings - If this warning should include a component stack, remove {withoutStack: true} from this warning. - If all warnings should include the component stack, you may need to remove {withoutStack: true} from the assertConsoleWarnDev call." + - Hello + + Hello in div (at **)" `); }); @@ -1361,16 +1375,16 @@ describe('ReactInternalTestUtils console assertions', () => { expect(message).toMatchInlineSnapshot(` "assertConsoleWarnDev(expected) - Unexpected component stack for: - "Hello - in div (at **)" + Unexpected warning(s) recorded. - Unexpected component stack for: - "Bye - in div (at **)" + - Expected warnings + + Received warnings - If this warning should include a component stack, remove {withoutStack: true} from this warning. - If all warnings should include the component stack, you may need to remove {withoutStack: true} from the assertConsoleWarnDev call." + - Hello + + Hello in div (at **) + Good day + - Bye + + Bye in div (at **)" `); }); }); @@ -1382,9 +1396,9 @@ describe('ReactInternalTestUtils console assertions', () => { console.warn('Bye\n in div'); } assertConsoleWarnDev([ - 'Hello', + 'Hello\n in div', ['Good day', {withoutStack: true}], - 'Bye', + 'Bye\n in div', ]); }); @@ -1490,12 +1504,13 @@ describe('ReactInternalTestUtils console assertions', () => { expect(message).toMatchInlineSnapshot(` "assertConsoleWarnDev(expected) - Unexpected component stack for: - "Hello - in div (at **)" + Unexpected warning(s) recorded. - If this warning should include a component stack, remove {withoutStack: true} from this warning. - If all warnings should include the component stack, you may need to remove {withoutStack: true} from the assertConsoleWarnDev call." + - Expected warnings + + Received warnings + + - Hello + + Hello in div (at **)" `); }); @@ -1524,16 +1539,16 @@ describe('ReactInternalTestUtils console assertions', () => { expect(message).toMatchInlineSnapshot(` "assertConsoleWarnDev(expected) - Unexpected component stack for: - "Hello - in div (at **)" + Unexpected warning(s) recorded. - Unexpected component stack for: - "Bye - in div (at **)" + - Expected warnings + + Received warnings - If this warning should include a component stack, remove {withoutStack: true} from this warning. - If all warnings should include the component stack, you may need to remove {withoutStack: true} from the assertConsoleWarnDev call." + - Hello + + Hello in div (at **) + Good day + - Bye + + Bye in div (at **)" `); }); }); @@ -1606,13 +1621,18 @@ describe('ReactInternalTestUtils console assertions', () => { it('fails if component stack is passed twice', () => { const message = expectToThrowFailure(() => { console.warn('Hi %s%s', '\n in div', '\n in div'); - assertConsoleWarnDev(['Hi']); + assertConsoleWarnDev(['Hi \n in div (at **)']); }); expect(message).toMatchInlineSnapshot(` "assertConsoleWarnDev(expected) - Received more than one component stack for a warning: - "Hi %s%s"" + Unexpected warning(s) recorded. + + - Expected warnings + + Received warnings + + Hi in div (at **) + + in div (at **)" `); }); @@ -1621,16 +1641,23 @@ describe('ReactInternalTestUtils console assertions', () => { const message = expectToThrowFailure(() => { console.warn('Hi %s%s', '\n in div', '\n in div'); console.warn('Bye %s%s', '\n in div', '\n in div'); - assertConsoleWarnDev(['Hi', 'Bye']); + assertConsoleWarnDev([ + 'Hi \n in div (at **)', + 'Bye \n in div (at **)', + ]); }); expect(message).toMatchInlineSnapshot(` "assertConsoleWarnDev(expected) - Received more than one component stack for a warning: - "Hi %s%s" + Unexpected warning(s) recorded. + + - Expected warnings + + Received warnings - Received more than one component stack for a warning: - "Bye %s%s"" + Hi in div (at **) + + in div (at **) + Bye in div (at **) + + in div (at **)" `); }); @@ -1646,7 +1673,7 @@ describe('ReactInternalTestUtils console assertions', () => { Expected messages should be an array of strings but was given type "string"." `); - assertConsoleWarnDev(['Hi', 'Bye']); + assertConsoleWarnDev(['Hi \n in div', 'Bye \n in div']); }); // @gate __DEV__ @@ -1661,7 +1688,7 @@ describe('ReactInternalTestUtils console assertions', () => { Expected messages should be an array of strings but was given type "string"." `); - assertConsoleWarnDev(['Hi', 'Bye']); + assertConsoleWarnDev(['Hi \n in div', 'Bye \n in div']); }); // @gate __DEV__ @@ -1677,7 +1704,11 @@ describe('ReactInternalTestUtils console assertions', () => { Expected messages should be an array of strings but was given type "string"." `); - assertConsoleWarnDev(['Hi', 'Wow', 'Bye']); + assertConsoleWarnDev([ + 'Hi \n in div', + 'Wow \n in div', + 'Bye \n in div', + ]); }); it('should fail if waitFor is called before asserting', async () => { @@ -1884,7 +1915,7 @@ describe('ReactInternalTestUtils console assertions', () => { if (__DEV__) { console.error('Hello\n in div'); } - assertConsoleErrorDev(['Hello']); + assertConsoleErrorDev(['Hello\n in div']); }); it('passes if all errors contain a stack', () => { @@ -1893,7 +1924,11 @@ describe('ReactInternalTestUtils console assertions', () => { console.error('Good day\n in div'); console.error('Bye\n in div'); } - assertConsoleErrorDev(['Hello', 'Good day', 'Bye']); + assertConsoleErrorDev([ + 'Hello\n in div', + 'Good day\n in div', + 'Bye\n in div', + ]); }); it('fails if act is called without assertConsoleErrorDev', async () => { @@ -2080,7 +2115,11 @@ describe('ReactInternalTestUtils console assertions', () => { const message = expectToThrowFailure(() => { console.error('Hi \n in div'); console.error('Wow \n in div'); - assertConsoleErrorDev(['Hi', 'Wow', 'Bye']); + assertConsoleErrorDev([ + 'Hi \n in div', + 'Wow \n in div', + 'Bye \n in div', + ]); }); expect(message).toMatchInlineSnapshot(` "assertConsoleErrorDev(expected) @@ -2090,9 +2129,9 @@ describe('ReactInternalTestUtils console assertions', () => { - Expected errors + Received errors - - Hi - - Wow - - Bye + - Hi in div + - Wow in div + - Bye in div + Hi in div (at **) + Wow in div (at **)" `); @@ -2192,101 +2231,6 @@ describe('ReactInternalTestUtils console assertions', () => { + TypeError: Cannot read properties of undefined (reading 'stack') in Foo (at **)" `); }); - // @gate __DEV__ - it('fails if only error does not contain a stack', () => { - const message = expectToThrowFailure(() => { - console.error('Hello'); - assertConsoleErrorDev(['Hello']); - }); - expect(message).toMatchInlineSnapshot(` - "assertConsoleErrorDev(expected) - - Missing component stack for: - "Hello" - - If this error should omit a component stack, pass [log, {withoutStack: true}]. - If all errors should omit the component stack, add {withoutStack: true} to the assertConsoleErrorDev call." - `); - }); - - // @gate __DEV__ - it('fails if first error does not contain a stack', () => { - const message = expectToThrowFailure(() => { - console.error('Hello\n in div'); - console.error('Good day\n in div'); - console.error('Bye'); - assertConsoleErrorDev(['Hello', 'Good day', 'Bye']); - }); - expect(message).toMatchInlineSnapshot(` - "assertConsoleErrorDev(expected) - - Missing component stack for: - "Bye" - - If this error should omit a component stack, pass [log, {withoutStack: true}]. - If all errors should omit the component stack, add {withoutStack: true} to the assertConsoleErrorDev call." - `); - }); - // @gate __DEV__ - it('fails if last error does not contain a stack', () => { - const message = expectToThrowFailure(() => { - console.error('Hello'); - console.error('Good day\n in div'); - console.error('Bye\n in div'); - assertConsoleErrorDev(['Hello', 'Good day', 'Bye']); - }); - expect(message).toMatchInlineSnapshot(` - "assertConsoleErrorDev(expected) - - Missing component stack for: - "Hello" - - If this error should omit a component stack, pass [log, {withoutStack: true}]. - If all errors should omit the component stack, add {withoutStack: true} to the assertConsoleErrorDev call." - `); - }); - // @gate __DEV__ - it('fails if middle error does not contain a stack', () => { - const message = expectToThrowFailure(() => { - console.error('Hello\n in div'); - console.error('Good day'); - console.error('Bye\n in div'); - assertConsoleErrorDev(['Hello', 'Good day', 'Bye']); - }); - expect(message).toMatchInlineSnapshot(` - "assertConsoleErrorDev(expected) - - Missing component stack for: - "Good day" - - If this error should omit a component stack, pass [log, {withoutStack: true}]. - If all errors should omit the component stack, add {withoutStack: true} to the assertConsoleErrorDev call." - `); - }); - // @gate __DEV__ - it('fails if all errors do not contain a stack', () => { - const message = expectToThrowFailure(() => { - console.error('Hello'); - console.error('Good day'); - console.error('Bye'); - assertConsoleErrorDev(['Hello', 'Good day', 'Bye']); - }); - expect(message).toMatchInlineSnapshot(` - "assertConsoleErrorDev(expected) - - Missing component stack for: - "Hello" - - Missing component stack for: - "Good day" - - Missing component stack for: - "Bye" - - If this error should omit a component stack, pass [log, {withoutStack: true}]. - If all errors should omit the component stack, add {withoutStack: true} to the assertConsoleErrorDev call." - `); - }); // @gate __DEV__ it('regression: checks entire string, not just the first letter', async () => { @@ -2385,12 +2329,13 @@ describe('ReactInternalTestUtils console assertions', () => { expect(message).toMatchInlineSnapshot(` "assertConsoleErrorDev(expected) - Unexpected component stack for: - "Hello - in div (at **)" + Unexpected error(s) recorded. - If this error should include a component stack, remove {withoutStack: true} from this error. - If all errors should include the component stack, you may need to remove {withoutStack: true} from the assertConsoleErrorDev call." + - Expected errors + + Received errors + + - Hello + + Hello in div (at **)" `); }); @@ -2407,16 +2352,16 @@ describe('ReactInternalTestUtils console assertions', () => { expect(message).toMatchInlineSnapshot(` "assertConsoleErrorDev(expected) - Unexpected component stack for: - "Hello - in div (at **)" + Unexpected error(s) recorded. - Unexpected component stack for: - "Bye - in div (at **)" + - Expected errors + + Received errors - If this error should include a component stack, remove {withoutStack: true} from this error. - If all errors should include the component stack, you may need to remove {withoutStack: true} from the assertConsoleErrorDev call." + - Hello + + Hello in div (at **) + Good day + - Bye + + Bye in div (at **)" `); }); }); @@ -2428,9 +2373,9 @@ describe('ReactInternalTestUtils console assertions', () => { console.error('Bye\n in div'); } assertConsoleErrorDev([ - 'Hello', + 'Hello\n in div', ['Good day', {withoutStack: true}], - 'Bye', + 'Bye\n in div', ]); }); @@ -2536,12 +2481,13 @@ describe('ReactInternalTestUtils console assertions', () => { expect(message).toMatchInlineSnapshot(` "assertConsoleErrorDev(expected) - Unexpected component stack for: - "Hello - in div (at **)" + Unexpected error(s) recorded. + + - Expected errors + + Received errors - If this error should include a component stack, remove {withoutStack: true} from this error. - If all errors should include the component stack, you may need to remove {withoutStack: true} from the assertConsoleErrorDev call." + - Hello + + Hello in div (at **)" `); }); @@ -2570,16 +2516,16 @@ describe('ReactInternalTestUtils console assertions', () => { expect(message).toMatchInlineSnapshot(` "assertConsoleErrorDev(expected) - Unexpected component stack for: - "Hello - in div (at **)" + Unexpected error(s) recorded. - Unexpected component stack for: - "Bye - in div (at **)" + - Expected errors + + Received errors - If this error should include a component stack, remove {withoutStack: true} from this error. - If all errors should include the component stack, you may need to remove {withoutStack: true} from the assertConsoleErrorDev call." + - Hello + + Hello in div (at **) + Good day + - Bye + + Bye in div (at **)" `); }); @@ -2678,13 +2624,18 @@ describe('ReactInternalTestUtils console assertions', () => { it('fails if component stack is passed twice', () => { const message = expectToThrowFailure(() => { console.error('Hi %s%s', '\n in div', '\n in div'); - assertConsoleErrorDev(['Hi']); + assertConsoleErrorDev(['Hi \n in div (at **)']); }); expect(message).toMatchInlineSnapshot(` "assertConsoleErrorDev(expected) - Received more than one component stack for a warning: - "Hi %s%s"" + Unexpected error(s) recorded. + + - Expected errors + + Received errors + + Hi in div (at **) + + in div (at **)" `); }); @@ -2693,16 +2644,23 @@ describe('ReactInternalTestUtils console assertions', () => { const message = expectToThrowFailure(() => { console.error('Hi %s%s', '\n in div', '\n in div'); console.error('Bye %s%s', '\n in div', '\n in div'); - assertConsoleErrorDev(['Hi', 'Bye']); + assertConsoleErrorDev([ + 'Hi \n in div (at **)', + 'Bye \n in div (at **)', + ]); }); expect(message).toMatchInlineSnapshot(` "assertConsoleErrorDev(expected) - Received more than one component stack for a warning: - "Hi %s%s" + Unexpected error(s) recorded. + + - Expected errors + + Received errors - Received more than one component stack for a warning: - "Bye %s%s"" + Hi in div (at **) + + in div (at **) + Bye in div (at **) + + in div (at **)" `); }); @@ -2711,14 +2669,14 @@ describe('ReactInternalTestUtils console assertions', () => { const message = expectToThrowFailure(() => { console.error('Hi \n in div'); console.error('Bye \n in div'); - assertConsoleErrorDev('Hi', 'Bye'); + assertConsoleErrorDev('Hi \n in div', 'Bye \n in div'); }); expect(message).toMatchInlineSnapshot(` "assertConsoleErrorDev(expected) Expected messages should be an array of strings but was given type "string"." `); - assertConsoleErrorDev(['Hi', 'Bye']); + assertConsoleErrorDev(['Hi \n in div', 'Bye \n in div']); }); // @gate __DEV__ @@ -2733,7 +2691,7 @@ describe('ReactInternalTestUtils console assertions', () => { Expected messages should be an array of strings but was given type "string"." `); - assertConsoleErrorDev(['Hi', 'Bye']); + assertConsoleErrorDev(['Hi \n in div', 'Bye \n in div']); }); // @gate __DEV__ @@ -2749,7 +2707,133 @@ describe('ReactInternalTestUtils console assertions', () => { Expected messages should be an array of strings but was given type "string"." `); - assertConsoleErrorDev(['Hi', 'Wow', 'Bye']); + assertConsoleErrorDev([ + 'Hi \n in div', + 'Wow \n in div', + 'Bye \n in div', + ]); + }); + + describe('in placeholder', () => { + // @gate __DEV__ + it('fails if `in ` is used for a component stack instead of an error stack', () => { + const message = expectToThrowFailure(() => { + console.error('Warning message\n in div'); + assertConsoleErrorDev(['Warning message\n in ']); + }); + expect(message).toMatchInlineSnapshot(` + "assertConsoleErrorDev(expected) + + Incorrect use of \\n in placeholder. The placeholder is for JavaScript Error stack traces (messages starting with "Error:"), not for React component stacks. + + Expected: "Warning message + in " + Received: "Warning message + in div (at **)" + + If this error has a component stack, include the full component stack in your expected message (e.g., "Warning message\\n in ComponentName (at **)")." + `); + }); + + // @gate __DEV__ + it('fails if `in ` is used for multiple component stacks', () => { + const message = expectToThrowFailure(() => { + console.error('First warning\n in span'); + console.error('Second warning\n in div'); + assertConsoleErrorDev([ + 'First warning\n in ', + 'Second warning\n in ', + ]); + }); + expect(message).toMatchInlineSnapshot(` + "assertConsoleErrorDev(expected) + + Incorrect use of \\n in placeholder. The placeholder is for JavaScript Error stack traces (messages starting with "Error:"), not for React component stacks. + + Expected: "First warning + in " + Received: "First warning + in span (at **)" + + If this error has a component stack, include the full component stack in your expected message (e.g., "Warning message\\n in ComponentName (at **)"). + + Incorrect use of \\n in placeholder. The placeholder is for JavaScript Error stack traces (messages starting with "Error:"), not for React component stacks. + + Expected: "Second warning + in " + Received: "Second warning + in div (at **)" + + If this error has a component stack, include the full component stack in your expected message (e.g., "Warning message\\n in ComponentName (at **)")." + `); + }); + + it('allows `in ` for actual error stack traces', () => { + // This should pass - \n in is correctly used for an error stack + console.error(new Error('Something went wrong')); + assertConsoleErrorDev(['Error: Something went wrong\n in ']); + }); + + // @gate __DEV__ + it('fails if error stack trace is present but \\n in is not expected', () => { + const message = expectToThrowFailure(() => { + console.error(new Error('Something went wrong')); + assertConsoleErrorDev(['Error: Something went wrong']); + }); + expect(message).toMatch(`Unexpected error stack trace for:`); + expect(message).toMatch(`Error: Something went wrong`); + expect(message).toMatch( + 'If this error should include an error stack trace, add \\n in to your expected message' + ); + }); + + // @gate __DEV__ + it('fails if `in ` is expected but no stack is present', () => { + const message = expectToThrowFailure(() => { + console.error('Error: Something went wrong'); + assertConsoleErrorDev([ + 'Error: Something went wrong\n in ', + ]); + }); + expect(message).toMatchInlineSnapshot(` + "assertConsoleErrorDev(expected) + + Missing error stack trace for: + "Error: Something went wrong" + + The expected message uses \\n in but the actual error doesn't include an error stack trace. + If this error should not have an error stack trace, remove \\n in from your expected message." + `); + }); + }); + + describe('[Environment] placeholder', () => { + // @gate __DEV__ + it('expands [Server] to ANSI escape sequence for server badge', () => { + const badge = '\u001b[0m\u001b[7m Server \u001b[0m'; + console.error(badge + 'Error: something went wrong'); + assertConsoleErrorDev([ + ['[Server] Error: something went wrong', {withoutStack: true}], + ]); + }); + + // @gate __DEV__ + it('expands [Prerender] to ANSI escape sequence for server badge', () => { + const badge = '\u001b[0m\u001b[7m Prerender \u001b[0m'; + console.error(badge + 'Error: something went wrong'); + assertConsoleErrorDev([ + ['[Prerender] Error: something went wrong', {withoutStack: true}], + ]); + }); + + // @gate __DEV__ + it('expands [Cache] to ANSI escape sequence for server badge', () => { + const badge = '\u001b[0m\u001b[7m Cache \u001b[0m'; + console.error(badge + 'Error: something went wrong'); + assertConsoleErrorDev([ + ['[Cache] Error: something went wrong', {withoutStack: true}], + ]); + }); }); it('should fail if waitFor is called before asserting', async () => { diff --git a/packages/internal-test-utils/consoleMock.js b/packages/internal-test-utils/consoleMock.js index 743519590e3..8f33d807e64 100644 --- a/packages/internal-test-utils/consoleMock.js +++ b/packages/internal-test-utils/consoleMock.js @@ -168,6 +168,53 @@ function normalizeCodeLocInfo(str) { }); } +// Expands environment placeholders like [Server] into ANSI escape sequences. +// This allows test assertions to use a cleaner syntax like "[Server] Error:" +// instead of the full escape sequence "\u001b[0m\u001b[7m Server \u001b[0mError:" +function expandEnvironmentPlaceholders(str) { + if (typeof str !== 'string') { + return str; + } + // [Environment] -> ANSI escape sequence for environment badge + // The format is: reset + inverse + " Environment " + reset + return str.replace( + /^\[(\w+)] /g, + (match, env) => '\u001b[0m\u001b[7m ' + env + ' \u001b[0m', + ); +} + +// The error stack placeholder that can be used in expected messages +const ERROR_STACK_PLACEHOLDER = '\n in '; +// A marker used to protect the placeholder during normalization +const ERROR_STACK_PLACEHOLDER_MARKER = '\n in <__STACK_PLACEHOLDER__>'; + +// Normalizes expected messages, handling special placeholders +function normalizeExpectedMessage(str) { + if (typeof str !== 'string') { + return str; + } + // Protect the error stack placeholder from normalization + // (normalizeCodeLocInfo would add "(at **)" to it) + const hasStackPlaceholder = str.includes(ERROR_STACK_PLACEHOLDER); + let result = str; + if (hasStackPlaceholder) { + result = result.replace( + ERROR_STACK_PLACEHOLDER, + ERROR_STACK_PLACEHOLDER_MARKER, + ); + } + result = normalizeCodeLocInfo(result); + result = expandEnvironmentPlaceholders(result); + if (hasStackPlaceholder) { + // Restore the placeholder (remove the "(at **)" that was added) + result = result.replace( + ERROR_STACK_PLACEHOLDER_MARKER + ' (at **)', + ERROR_STACK_PLACEHOLDER, + ); + } + return result; +} + function normalizeComponentStack(entry) { if ( typeof entry[0] === 'string' && @@ -187,6 +234,15 @@ const isLikelyAComponentStack = message => message.includes('\n in ') || message.includes('\n at ')); +// Error stack traces start with "*Error:" and contain "at" frames with file paths +// Component stacks contain "in ComponentName" patterns +// This helps validate that \n in is used correctly +const isLikelyAnErrorStackTrace = message => + typeof message === 'string' && + message.includes('Error:') && + // Has "at" frames typical of error stacks (with file:line:col) + /\n\s+at .+\(.*:\d+:\d+\)/.test(message); + export function createLogAssertion( consoleMethod, matcherName, @@ -236,13 +292,11 @@ export function createLogAssertion( const withoutStack = options.withoutStack; - // Warn about invalid global withoutStack values. if (consoleMethod === 'log' && withoutStack !== undefined) { throwFormattedError( `Do not pass withoutStack to assertConsoleLogDev, console.log does not have component stacks.`, ); } else if (withoutStack !== undefined && withoutStack !== true) { - // withoutStack can only have a value true. throwFormattedError( `The second argument must be {withoutStack: true}.` + `\n\nInstead received ${JSON.stringify(options)}.`, @@ -256,8 +310,11 @@ export function createLogAssertion( const unexpectedLogs = []; const unexpectedMissingComponentStack = []; const unexpectedIncludingComponentStack = []; + const unexpectedMissingErrorStack = []; + const unexpectedIncludingErrorStack = []; const logsMismatchingFormat = []; const logsWithExtraComponentStack = []; + const stackTracePlaceholderMisuses = []; // Loop over all the observed logs to determine: // - Which expected logs are missing @@ -319,11 +376,11 @@ export function createLogAssertion( ); } - expectedMessage = normalizeCodeLocInfo(currentExpectedMessage); + expectedMessage = normalizeExpectedMessage(currentExpectedMessage); expectedWithoutStack = expectedMessageOrArray[1].withoutStack; } else if (typeof expectedMessageOrArray === 'string') { - // Should be in the form assert(['log']) or assert(['log'], {withoutStack: true}) - expectedMessage = normalizeCodeLocInfo(expectedMessageOrArray); + expectedMessage = normalizeExpectedMessage(expectedMessageOrArray); + // withoutStack: inherit from global option - simplify when withoutStack is removed. if (consoleMethod === 'log') { expectedWithoutStack = true; } else { @@ -381,19 +438,93 @@ export function createLogAssertion( } // Main logic to check if log is expected, with the component stack. - if ( - typeof expectedMessage === 'string' && - (normalizedMessage === expectedMessage || - normalizedMessage.includes(expectedMessage)) - ) { + // Check for exact match OR if the message matches with a component stack appended + let matchesExpectedMessage = false; + let expectsErrorStack = false; + const hasErrorStack = isLikelyAnErrorStackTrace(message); + + if (typeof expectedMessage === 'string') { + if (normalizedMessage === expectedMessage) { + matchesExpectedMessage = true; + } else if (expectedMessage.includes('\n in ')) { + expectsErrorStack = true; + // \n in is ONLY for JavaScript Error stack traces (e.g., "Error: message\n at fn (file.js:1:2)") + // NOT for React component stacks (e.g., "\n in ComponentName (at **)"). + // Validate that the actual message looks like an error stack trace. + if (!hasErrorStack) { + // The actual message doesn't look like an error stack trace. + // This is likely a misuse - someone used \n in for a component stack. + stackTracePlaceholderMisuses.push({ + expected: expectedMessage, + received: normalizedMessage, + }); + } + + const expectedMessageWithoutStack = expectedMessage.replace( + '\n in ', + '', + ); + if (normalizedMessage.startsWith(expectedMessageWithoutStack)) { + // Remove the stack trace + const remainder = normalizedMessage.slice( + expectedMessageWithoutStack.length, + ); + + // After normalization, both error stacks and component stacks look like + // component stacks (at frames are converted to "in ... (at **)" format). + // So we check isLikelyAComponentStack for matching purposes. + if (isLikelyAComponentStack(remainder)) { + const messageWithoutStack = normalizedMessage.replace( + remainder, + '', + ); + if (messageWithoutStack === expectedMessageWithoutStack) { + matchesExpectedMessage = true; + } + } else if (remainder === '') { + // \n in was expected but there's no stack at all + matchesExpectedMessage = true; + } + } else if (normalizedMessage === expectedMessageWithoutStack) { + // \n in was expected but actual has no stack at all (exact match without stack) + matchesExpectedMessage = true; + } + } else if ( + hasErrorStack && + !expectedMessage.includes('\n in ') && + normalizedMessage.startsWith(expectedMessage) + ) { + matchesExpectedMessage = true; + } + } + + if (matchesExpectedMessage) { + // withoutStack: Check for unexpected/missing component stacks. + // These checks can be simplified when withoutStack is removed. if (isLikelyAComponentStack(normalizedMessage)) { - if (expectedWithoutStack === true) { + if (expectedWithoutStack === true && !hasErrorStack) { + // Only report unexpected component stack if it's not an error stack + // (error stacks look like component stacks after normalization) unexpectedIncludingComponentStack.push(normalizedMessage); } - } else if (expectedWithoutStack !== true) { + } else if (expectedWithoutStack !== true && !expectsErrorStack) { unexpectedMissingComponentStack.push(normalizedMessage); } + // Check for unexpected/missing error stacks + if (hasErrorStack && !expectsErrorStack) { + // Error stack is present but \n in was not in the expected message + unexpectedIncludingErrorStack.push(normalizedMessage); + } else if ( + expectsErrorStack && + !hasErrorStack && + !isLikelyAComponentStack(normalizedMessage) + ) { + // \n in was expected but the actual message doesn't have any stack at all + // (if it has a component stack, stackTracePlaceholderMisuses already handles it) + unexpectedMissingErrorStack.push(normalizedMessage); + } + // Found expected log, remove it from missing. missingExpectedLogs.splice(0, 1); } else { @@ -422,6 +553,21 @@ export function createLogAssertion( )}`; } + // Wrong %s formatting is a failure. + // This is a common mistake when creating new warnings. + if (logsMismatchingFormat.length > 0) { + throwFormattedError( + logsMismatchingFormat + .map( + item => + `Received ${item.args.length} arguments for a message with ${ + item.expectedArgCount + } placeholders:\n ${printReceived(item.format)}`, + ) + .join('\n\n'), + ); + } + // Any unexpected warnings should be treated as a failure. if (unexpectedLogs.length > 0) { throwFormattedError( @@ -466,18 +612,33 @@ export function createLogAssertion( ); } - // Wrong %s formatting is a failure. - // This is a common mistake when creating new warnings. - if (logsMismatchingFormat.length > 0) { + // Any logs that include an error stack trace but \n in wasn't expected. + if (unexpectedIncludingErrorStack.length > 0) { throwFormattedError( - logsMismatchingFormat + `${unexpectedIncludingErrorStack .map( - item => - `Received ${item.args.length} arguments for a message with ${ - item.expectedArgCount - } placeholders:\n ${printReceived(item.format)}`, + stack => + `Unexpected error stack trace for:\n ${printReceived(stack)}`, ) - .join('\n\n'), + .join( + '\n\n', + )}\n\nIf this ${logName()} should include an error stack trace, add \\n in to your expected message ` + + `(e.g., "Error: message\\n in ").`, + ); + } + + // Any logs that are missing an error stack trace when \n in was expected. + if (unexpectedMissingErrorStack.length > 0) { + throwFormattedError( + `${unexpectedMissingErrorStack + .map( + stack => + `Missing error stack trace for:\n ${printReceived(stack)}`, + ) + .join( + '\n\n', + )}\n\nThe expected message uses \\n in but the actual ${logName()} doesn't include an error stack trace.` + + `\nIf this ${logName()} should not have an error stack trace, remove \\n in from your expected message.`, ); } @@ -496,6 +657,25 @@ export function createLogAssertion( .join('\n\n'), ); } + + // Using \n in for component stacks is a misuse. + // \n in should only be used for JavaScript Error stack traces, + // not for React component stacks. + if (stackTracePlaceholderMisuses.length > 0) { + throwFormattedError( + `${stackTracePlaceholderMisuses + .map( + item => + `Incorrect use of \\n in placeholder. The placeholder is for JavaScript Error ` + + `stack traces (messages starting with "Error:"), not for React component stacks.\n\n` + + `Expected: ${printReceived(item.expected)}\n` + + `Received: ${printReceived(item.received)}\n\n` + + `If this ${logName()} has a component stack, include the full component stack in your expected message ` + + `(e.g., "Warning message\\n in ComponentName (at **)").`, + ) + .join('\n\n')}`, + ); + } } }; } diff --git a/packages/react-client/src/__tests__/ReactFlight-test.js b/packages/react-client/src/__tests__/ReactFlight-test.js index b4a07239296..ab9bc654073 100644 --- a/packages/react-client/src/__tests__/ReactFlight-test.js +++ b/packages/react-client/src/__tests__/ReactFlight-test.js @@ -1729,7 +1729,8 @@ describe('ReactFlight', () => { 'Only plain objects can be passed to Client Components from Server Components. ' + 'Objects with symbol properties like Symbol.iterator are not supported.\n' + ' <... value={{}}>\n' + - ' ^^^^\n', + ' ^^^^\n' + + ' in (at **)', ]); }); @@ -3258,7 +3259,7 @@ describe('ReactFlight', () => { const transport = ReactNoopFlightServer.render({ root: ReactServer.createElement(App), }); - assertConsoleErrorDev(['Error: err']); + assertConsoleErrorDev(['Error: err' + '\n in ']); expect(mockConsoleLog).toHaveBeenCalledTimes(1); expect(mockConsoleLog.mock.calls[0][0]).toBe('hi'); diff --git a/packages/react-debug-tools/src/__tests__/ReactHooksInspection-test.js b/packages/react-debug-tools/src/__tests__/ReactHooksInspection-test.js index 18dab0710f2..a12abba4e63 100644 --- a/packages/react-debug-tools/src/__tests__/ReactHooksInspection-test.js +++ b/packages/react-debug-tools/src/__tests__/ReactHooksInspection-test.js @@ -734,7 +734,11 @@ describe('ReactHooksInspection', () => { }); const results = normalizeSourceLoc(tree); expect(results).toHaveLength(1); - expect(results[0]).toMatchInlineSnapshot(` + expect(results[0]).toMatchInlineSnapshot( + { + subHooks: [{value: expect.any(Promise)}], + }, + ` { "debugInfo": null, "hookSource": { @@ -759,12 +763,13 @@ describe('ReactHooksInspection', () => { "isStateEditable": false, "name": "Use", "subHooks": [], - "value": Promise {}, + "value": Any, }, ], "value": undefined, } - `); + `, + ); }); describe('useDebugValue', () => { diff --git a/packages/react-dom/src/__tests__/ReactDOM-test.js b/packages/react-dom/src/__tests__/ReactDOM-test.js index ff829654454..f87de5c2ef3 100644 --- a/packages/react-dom/src/__tests__/ReactDOM-test.js +++ b/packages/react-dom/src/__tests__/ReactDOM-test.js @@ -548,16 +548,23 @@ describe('ReactDOM', () => { ' in App (at **)', // ReactDOM(App > div > ServerEntry) >>> ReactDOMServer(Child) >>> ReactDOMServer(App2) >>> ReactDOMServer(blink) 'Invalid ARIA attribute `ariaTypo2`. ARIA attributes follow the pattern aria-* and must be lowercase.\n' + - ' in blink (at **)', + ' in blink (at **)\n' + + ' in App2 (at **)\n' + + ' in Child (at **)\n' + + ' in ServerEntry (at **)', // ReactDOM(App > div > ServerEntry) >>> ReactDOMServer(Child) >>> ReactDOMServer(App2 > Child2 > span) 'Invalid ARIA attribute `ariaTypo3`. ARIA attributes follow the pattern aria-* and must be lowercase.\n' + ' in span (at **)\n' + ' in Child2 (at **)\n' + - ' in App2 (at **)', + ' in App2 (at **)\n' + + ' in Child (at **)\n' + + ' in ServerEntry (at **)', // ReactDOM(App > div > ServerEntry) >>> ReactDOMServer(Child > span) 'Invalid ARIA attribute `ariaTypo4`. ARIA attributes follow the pattern aria-* and must be lowercase.\n' + ' in span (at **)\n' + - ' in Child (at **)', + ' in Child (at **)\n' + + ' in ServerEntry (at **)', + // ReactDOM(App > div > font) 'Invalid ARIA attribute `ariaTypo5`. ARIA attributes follow the pattern aria-* and must be lowercase.\n' + ' in font (at **)\n' + @@ -775,7 +782,11 @@ describe('ReactDOM', () => { // @TODO remove this warning check when we loosen the tag nesting restrictions to allow arbitrary tags at the // root of the application - assertConsoleErrorDev(['In HTML, cannot be a child of
']); + assertConsoleErrorDev([ + 'In HTML, cannot be a child of
.\nThis will cause a hydration error.\n' + + ' in head (at **)\n' + + ' in App (at **)', + ]); await act(() => { root.render(); diff --git a/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js b/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js index e17729c032a..32745e4b109 100644 --- a/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js @@ -6879,9 +6879,12 @@ describe('ReactDOMFizzServer', () => { }); assertConsoleErrorDev([ - 'The render was aborted by the server without a reason.', - 'The render was aborted by the server without a reason.', - 'The render was aborted by the server without a reason.', + 'Error: The render was aborted by the server without a reason.' + + '\n in ', + 'Error: The render was aborted by the server without a reason.' + + '\n in ', + 'Error: The render was aborted by the server without a reason.' + + '\n in ', ]); expect(finished).toBe(true); @@ -6943,9 +6946,12 @@ describe('ReactDOMFizzServer', () => { }); assertConsoleErrorDev([ - 'The render was aborted by the server without a reason.', - 'The render was aborted by the server without a reason.', - 'The render was aborted by the server without a reason.', + 'Error: The render was aborted by the server without a reason.' + + '\n in ', + 'Error: The render was aborted by the server without a reason.' + + '\n in ', + 'Error: The render was aborted by the server without a reason.' + + '\n in ', ]); expect(finished).toBe(true); @@ -7007,9 +7013,12 @@ describe('ReactDOMFizzServer', () => { }); assertConsoleErrorDev([ - 'The render was aborted by the server without a reason.', - 'The render was aborted by the server without a reason.', - 'The render was aborted by the server without a reason.', + 'Error: The render was aborted by the server without a reason.' + + '\n in ', + 'Error: The render was aborted by the server without a reason.' + + '\n in ', + 'Error: The render was aborted by the server without a reason.' + + '\n in ', ]); expect(finished).toBe(true); @@ -7069,9 +7078,12 @@ describe('ReactDOMFizzServer', () => { }); assertConsoleErrorDev([ - 'The render was aborted by the server without a reason.', - 'The render was aborted by the server without a reason.', - 'The render was aborted by the server without a reason.', + 'Error: The render was aborted by the server without a reason.' + + '\n in ', + 'Error: The render was aborted by the server without a reason.' + + '\n in ', + 'Error: The render was aborted by the server without a reason.' + + '\n in ', ]); expect(finished).toBe(true); @@ -9024,7 +9036,8 @@ describe('ReactDOMFizzServer', () => { pipe(writable); }); assertConsoleErrorDev([ - 'React encountered a style tag with `precedence` "default" and `nonce` "R4nd0mR4nd0m". When React manages style rules using `precedence` it will only include rules if the nonce matches the style nonce "R4nd0m" that was included with this render.', + 'React encountered a style tag with `precedence` "default" and `nonce` "R4nd0mR4nd0m". When React manages style rules using `precedence` it will only include rules if the nonce matches the style nonce "R4nd0m" that was included with this render.' + + '\n in style (at **)', ]); expect(getVisibleChildren(document)).toEqual( @@ -9054,7 +9067,8 @@ describe('ReactDOMFizzServer', () => { pipe(writable); }); assertConsoleErrorDev([ - 'React encountered a style tag with `precedence` "default" and `nonce` "R4nd0m". When React manages style rules using `precedence` it will only include a nonce attributes if you also provide the same style nonce value as a render option.', + 'React encountered a style tag with `precedence` "default" and `nonce` "R4nd0m". When React manages style rules using `precedence` it will only include a nonce attributes if you also provide the same style nonce value as a render option.' + + '\n in style (at **)', ]); expect(getVisibleChildren(document)).toEqual( @@ -9085,7 +9099,8 @@ describe('ReactDOMFizzServer', () => { pipe(writable); }); assertConsoleErrorDev([ - 'React encountered a style tag with `precedence` "default" and `nonce` "R4nd0m". When React manages style rules using `precedence` it will only include a nonce attributes if you also provide the same style nonce value as a render option.', + 'React encountered a style tag with `precedence` "default" and `nonce` "R4nd0m". When React manages style rules using `precedence` it will only include a nonce attributes if you also provide the same style nonce value as a render option.' + + '\n in style (at **)', ]); expect(getVisibleChildren(document)).toEqual( diff --git a/packages/react-dom/src/__tests__/ReactDOMFloat-test.js b/packages/react-dom/src/__tests__/ReactDOMFloat-test.js index 848fdd65219..ed3d3e08805 100644 --- a/packages/react-dom/src/__tests__/ReactDOMFloat-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMFloat-test.js @@ -3628,7 +3628,24 @@ body { assertLog(['load stylesheet: foo']); await waitForAll([]); assertConsoleErrorDev([ - "Hydration failed because the server rendered HTML didn't match the client.", + "Error: 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\n\n' + + ' \n' + + ' \n' + + '
\n' + + '
\n' + + ' \n' + + ' \n' + + ' \n' + + '+
' + + '\n in ', ]); jest.runAllTimers(); diff --git a/packages/react-dom/src/__tests__/ReactDOMSrcObject-test.js b/packages/react-dom/src/__tests__/ReactDOMSrcObject-test.js index ca56c605c7c..c77614dcfb7 100644 --- a/packages/react-dom/src/__tests__/ReactDOMSrcObject-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMSrcObject-test.js @@ -120,11 +120,14 @@ describe('ReactDOMSrcObject', () => { assertConsoleErrorDev([ 'Passing Blob, MediaSource or MediaStream to is not supported. ' + - 'Pass it directly to ,