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__/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-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 ,