diff --git a/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js b/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js index 0320272b9cbb4..f895bbd55e71b 100644 --- a/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js @@ -1812,83 +1812,59 @@ describe('ReactDOMFizzServer', () => { ); } - // We can't use the toErrorDev helper here because this is an async act. - const originalConsoleError = console.error; - const mockError = jest.fn(); - console.error = (...args) => { - mockError(...args.map(normalizeCodeLocInfo)); - }; - - try { - await act(() => { - const {pipe} = renderToPipeableStream(); - pipe(writable); - }); + await act(() => { + const {pipe} = renderToPipeableStream(); + pipe(writable); + }); - expect(getVisibleChildren(container)).toEqual( -
- Loading -
, - ); + expect(getVisibleChildren(container)).toEqual( +
+ Loading +
, + ); - if (__DEV__) { - expect(mockError).toHaveBeenCalledWith( - '<%s /> is using incorrect casing. Use PascalCase for React components, or lowercase for HTML elements.%s', - 'inCorrectTag', - '\n' + - (gate(flags => flags.enableOwnerStacks) - ? ' in inCorrectTag (at **)\n' + - ' in C (at **)\n' + - ' in A (at **)' - : ' in inCorrectTag (at **)\n' + - ' in C (at **)\n' + - ' in Suspense (at **)\n' + - ' in div (at **)\n' + - ' in A (at **)'), - ); - mockError.mockClear(); - } else { - expect(mockError).not.toHaveBeenCalled(); - } + assertConsoleErrorDev([ + ' is using incorrect casing. Use PascalCase for React components, or lowercase for HTML elements.' + + '\n' + + (gate(flags => flags.enableOwnerStacks) + ? ' in inCorrectTag (at **)\n' + + ' in C (at **)\n' + + ' in A (at **)' + : ' in inCorrectTag (at **)\n' + + ' in C (at **)\n' + + ' in Suspense (at **)\n' + + ' in div (at **)\n' + + ' in A (at **)'), + ]); - await act(() => { - resolveText('Hello'); - resolveText('World'); - }); + await act(() => { + resolveText('Hello'); + resolveText('World'); + }); - if (__DEV__) { - expect(mockError).toHaveBeenCalledWith( - 'Each child in a list should have a unique "key" prop.%s%s' + - ' See https://react.dev/link/warning-keys for more information.%s', - '\n\nCheck the render method of `B`.', - '', - '\n' + - (gate(flags => flags.enableOwnerStacks) - ? ' in span (at **)\n' + - ' in mapper (at **)\n' + - ' in B (at **)\n' + - ' in A (at **)' - : ' in span (at **)\n' + - ' in B (at **)\n' + - ' in Suspense (at **)\n' + - ' in div (at **)\n' + - ' in A (at **)'), - ); - } else { - expect(mockError).not.toHaveBeenCalled(); - } + assertConsoleErrorDev([ + 'Each child in a list should have a unique "key" prop.\n\nCheck the render method of `B`.' + + ' See https://react.dev/link/warning-keys for more information.\n' + + (gate(flags => flags.enableOwnerStacks) + ? ' in span (at **)\n' + + ' in mapper (at **)\n' + + ' in B (at **)\n' + + ' in A (at **)' + : ' in span (at **)\n' + + ' in B (at **)\n' + + ' in Suspense (at **)\n' + + ' in div (at **)\n' + + ' in A (at **)'), + ]); - expect(getVisibleChildren(container)).toEqual( + expect(getVisibleChildren(container)).toEqual( +
-
- Hello - World -
-
, - ); - } finally { - console.error = originalConsoleError; - } + Hello + World +
+ , + ); }); // @gate !disableLegacyContext @@ -4683,12 +4659,6 @@ describe('ReactDOMFizzServer', () => { // @gate favorSafetyOverHydrationPerf it('only warns once on hydration mismatch while within a suspense boundary', async () => { - const originalConsoleError = console.error; - const mockError = jest.fn(); - console.error = (...args) => { - mockError(...args.map(normalizeCodeLocInfo)); - }; - const App = ({text}) => { return (
@@ -4701,45 +4671,40 @@ describe('ReactDOMFizzServer', () => { ); }; - try { - await act(() => { - const {pipe} = renderToPipeableStream(); - pipe(writable); - }); + await act(() => { + const {pipe} = renderToPipeableStream(); + pipe(writable); + }); - expect(getVisibleChildren(container)).toEqual( -
-

initial

-

initial

-

initial

-
, - ); + expect(getVisibleChildren(container)).toEqual( +
+

initial

+

initial

+

initial

+
, + ); - ReactDOMClient.hydrateRoot(container, , { - onRecoverableError(error) { - Scheduler.log('onRecoverableError: ' + normalizeError(error.message)); - if (error.cause) { - Scheduler.log('Cause: ' + normalizeError(error.cause.message)); - } - }, - }); - await waitForAll([ - "onRecoverableError: Hydration failed because the server rendered HTML didn't match the client.", - ]); + ReactDOMClient.hydrateRoot(container, , { + onRecoverableError(error) { + Scheduler.log('onRecoverableError: ' + normalizeError(error.message)); + if (error.cause) { + Scheduler.log('Cause: ' + normalizeError(error.cause.message)); + } + }, + }); + await waitForAll([ + "onRecoverableError: Hydration failed because the server rendered HTML didn't match the client.", + ]); - expect(getVisibleChildren(container)).toEqual( -
-

replaced

-

replaced

-

replaced

-
, - ); + expect(getVisibleChildren(container)).toEqual( +
+

replaced

+

replaced

+

replaced

+
, + ); - await waitForAll([]); - expect(mockError.mock.calls.length).toBe(0); - } finally { - console.error = originalConsoleError; - } + await waitForAll([]); }); it('supresses hydration warnings when an error occurs within a Suspense boundary', async () => { @@ -4815,18 +4780,6 @@ describe('ReactDOMFizzServer', () => { }); it('does not log for errors after the first hydration error', async () => { - // We can't use the toErrorDev helper here because this is async. - const originalConsoleError = console.error; - const mockError = jest.fn(); - console.error = (...args) => { - if (args.length > 1) { - if (typeof args[1] === 'object') { - mockError(args[0].split('\n')[0]); - return; - } - } - mockError(...args.map(normalizeCodeLocInfo)); - }; let isClient = false; function ThrowWhenHydrating({children, message}) { @@ -4864,69 +4817,50 @@ describe('ReactDOMFizzServer', () => { ); }; - try { - await act(() => { - const {pipe} = renderToPipeableStream(); - pipe(writable); - }); + await act(() => { + const {pipe} = renderToPipeableStream(); + pipe(writable); + }); - expect(getVisibleChildren(container)).toEqual( -
-

one

-

two

-

three

-
, - ); + expect(getVisibleChildren(container)).toEqual( +
+

one

+

two

+

three

+
, + ); - isClient = true; + isClient = true; - ReactDOMClient.hydrateRoot(container, , { - onRecoverableError(error) { - Scheduler.log('onRecoverableError: ' + normalizeError(error.message)); - if (error.cause) { - Scheduler.log('Cause: ' + normalizeError(error.cause.message)); - } - }, - }); - await waitForAll([ - 'throwing: first error', + ReactDOMClient.hydrateRoot(container, , { + onRecoverableError(error) { + Scheduler.log('onRecoverableError: ' + normalizeError(error.message)); + if (error.cause) { + Scheduler.log('Cause: ' + normalizeError(error.cause.message)); + } + }, + }); + await waitForAll([ + 'throwing: first error', - // onRecoverableError because the UI recovered without surfacing the - // error to the user. - 'onRecoverableError: There was an error while hydrating but React was able to recover by instead client rendering from the nearest Suspense boundary.', - 'Cause: first error', - ]); - expect(mockError.mock.calls).toEqual([]); - mockError.mockClear(); + // onRecoverableError because the UI recovered without surfacing the + // error to the user. + 'onRecoverableError: There was an error while hydrating but React was able to recover by instead client rendering from the nearest Suspense boundary.', + 'Cause: first error', + ]); - expect(getVisibleChildren(container)).toEqual( -
-

one

-

two

-

three

-
, - ); + expect(getVisibleChildren(container)).toEqual( +
+

one

+

two

+

three

+
, + ); - await waitForAll([]); - expect(mockError.mock.calls).toEqual([]); - } finally { - console.error = originalConsoleError; - } + await waitForAll([]); }); it('does not log for errors after a preceding fiber suspends', async () => { - // We can't use the toErrorDev helper here because this is async. - const originalConsoleError = console.error; - const mockError = jest.fn(); - console.error = (...args) => { - if (args.length > 1) { - if (typeof args[1] === 'object') { - mockError(args[0].split('\n')[0]); - return; - } - } - mockError(...args.map(normalizeCodeLocInfo)); - }; let isClient = false; let promise = null; let unsuspend = null; @@ -4984,56 +4918,51 @@ describe('ReactDOMFizzServer', () => { ); }; - try { - await act(() => { - const {pipe} = renderToPipeableStream(); - pipe(writable); - }); + await act(() => { + const {pipe} = renderToPipeableStream(); + pipe(writable); + }); - expect(getVisibleChildren(container)).toEqual( -
-

one

-

two

-

three

-
, - ); + expect(getVisibleChildren(container)).toEqual( +
+

one

+

two

+

three

+
, + ); - isClient = true; + isClient = true; - ReactDOMClient.hydrateRoot(container, , { - onRecoverableError(error) { - Scheduler.log('onRecoverableError: ' + normalizeError(error.message)); - if (error.cause) { - Scheduler.log('Cause: ' + normalizeError(error.cause.message)); - } - }, - }); - await waitForAll(['suspending']); - expect(mockError.mock.calls).toEqual([]); + ReactDOMClient.hydrateRoot(container, , { + onRecoverableError(error) { + Scheduler.log('onRecoverableError: ' + normalizeError(error.message)); + if (error.cause) { + Scheduler.log('Cause: ' + normalizeError(error.cause.message)); + } + }, + }); + await waitForAll(['suspending']); - expect(getVisibleChildren(container)).toEqual( -
-

one

-

two

-

three

-
, - ); - await unsuspend(); - await waitForAll([ - 'throwing: first error', - 'onRecoverableError: There was an error while hydrating but React was able to recover by instead client rendering from the nearest Suspense boundary.', - 'Cause: first error', - ]); - expect(getVisibleChildren(container)).toEqual( -
-

one

-

two

-

three

-
, - ); - } finally { - console.error = originalConsoleError; - } + expect(getVisibleChildren(container)).toEqual( +
+

one

+

two

+

three

+
, + ); + await unsuspend(); + await waitForAll([ + 'throwing: first error', + 'onRecoverableError: There was an error while hydrating but React was able to recover by instead client rendering from the nearest Suspense boundary.', + 'Cause: first error', + ]); + expect(getVisibleChildren(container)).toEqual( +
+

one

+

two

+

three

+
, + ); }); it('(outdated behavior) suspending after erroring will cause errors previously queued to be silenced until the boundary resolves', async () => { @@ -5042,18 +4971,6 @@ describe('ReactDOMFizzServer', () => { // stack and revert to client rendering. I've kept the test around just to // demonstrate what actually happens in this sequence of events. - // We can't use the toErrorDev helper here because this is async. - const originalConsoleError = console.error; - const mockError = jest.fn(); - console.error = (...args) => { - if (args.length > 1) { - if (typeof args[1] === 'object') { - mockError(args[0].split('\n')[0]); - return; - } - } - mockError(...args.map(normalizeCodeLocInfo)); - }; let isClient = false; let promise = null; let unsuspend = null; @@ -5111,60 +5028,53 @@ describe('ReactDOMFizzServer', () => { ); }; - try { - await act(() => { - const {pipe} = renderToPipeableStream(); - pipe(writable); - }); + await act(() => { + const {pipe} = renderToPipeableStream(); + pipe(writable); + }); - expect(getVisibleChildren(container)).toEqual( -
-

one

-

two

-

three

-
, - ); + expect(getVisibleChildren(container)).toEqual( +
+

one

+

two

+

three

+
, + ); - isClient = true; + isClient = true; - ReactDOMClient.hydrateRoot(container, , { - onRecoverableError(error) { - Scheduler.log('onRecoverableError: ' + normalizeError(error.message)); - if (error.cause) { - Scheduler.log('Cause: ' + normalizeError(error.cause.message)); - } - }, - }); - await waitForAll([ - 'throwing: first error', - 'suspending', - 'onRecoverableError: There was an error while hydrating but React was able to recover by instead client rendering from the nearest Suspense boundary.', - 'Cause: first error', - ]); - expect(mockError.mock.calls).toEqual([]); - mockError.mockClear(); + ReactDOMClient.hydrateRoot(container, , { + onRecoverableError(error) { + Scheduler.log('onRecoverableError: ' + normalizeError(error.message)); + if (error.cause) { + Scheduler.log('Cause: ' + normalizeError(error.cause.message)); + } + }, + }); + await waitForAll([ + 'throwing: first error', + 'suspending', + 'onRecoverableError: There was an error while hydrating but React was able to recover by instead client rendering from the nearest Suspense boundary.', + 'Cause: first error', + ]); - expect(getVisibleChildren(container)).toEqual( -
-

Loading...

-
, - ); - await clientAct(() => unsuspend()); - // Since our client components only throw on the very first render there are no - // new throws in this pass - assertLog([]); - expect(mockError.mock.calls).toEqual([]); + expect(getVisibleChildren(container)).toEqual( +
+

Loading...

+
, + ); + await clientAct(() => unsuspend()); + // Since our client components only throw on the very first render there are no + // new throws in this pass + assertLog([]); - expect(getVisibleChildren(container)).toEqual( -
-

one

-

two

-

three

-
, - ); - } finally { - console.error = originalConsoleError; - } + expect(getVisibleChildren(container)).toEqual( +
+

one

+

two

+

three

+
, + ); }); it('#24578 Hydration errors caused by a suspending component should not become recoverable when nested in an ancestor Suspense that is showing primary content', async () => { @@ -6541,11 +6451,6 @@ describe('ReactDOMFizzServer', () => { function MyScript() { return 'bar();'; } - const originalConsoleError = console.error; - const mockError = jest.fn(); - console.error = (...args) => { - mockError(...args.map(normalizeCodeLocInfo)); - }; function App() { return ( @@ -6563,47 +6468,31 @@ describe('ReactDOMFizzServer', () => { ); } - try { - await act(async () => { - const {pipe} = renderToPipeableStream(); - pipe(writable); - }); + await act(async () => { + const {pipe} = renderToPipeableStream(); + pipe(writable); + }); - if (__DEV__) { - expect(mockError.mock.calls.length).toBe(3); - expect(mockError.mock.calls[0]).toEqual([ - 'A script element was rendered with %s. If script element has children it must be a single string. Consider using dangerouslySetInnerHTML or passing a plain string as children.%s', - 'a number for children', - componentStack( - gate(flags => flags.enableOwnerStacks) - ? ['script', 'App'] - : ['script', 'body', 'html', 'App'], - ), - ]); - expect(mockError.mock.calls[1]).toEqual([ - 'A script element was rendered with %s. If script element has children it must be a single string. Consider using dangerouslySetInnerHTML or passing a plain string as children.%s', - 'an array for children', - componentStack( - gate(flags => flags.enableOwnerStacks) - ? ['script', 'App'] - : ['script', 'body', 'html', 'App'], - ), - ]); - expect(mockError.mock.calls[2]).toEqual([ - 'A script element was rendered with %s. If script element has children it must be a single string. Consider using dangerouslySetInnerHTML or passing a plain string as children.%s', - 'something unexpected for children', - componentStack( - gate(flags => flags.enableOwnerStacks) - ? ['script', 'App'] - : ['script', 'body', 'html', 'App'], - ), - ]); - } else { - expect(mockError.mock.calls.length).toBe(0); - } - } finally { - console.error = originalConsoleError; - } + assertConsoleErrorDev([ + 'A script element was rendered with a number for children. If script element has children it must be a single string. Consider using dangerouslySetInnerHTML or passing a plain string as children.' + + componentStack( + gate(flags => flags.enableOwnerStacks) + ? ['script', 'App'] + : ['script', 'body', 'html', 'App'], + ), + 'A script element was rendered with an array for children. If script element has children it must be a single string. Consider using dangerouslySetInnerHTML or passing a plain string as children.' + + componentStack( + gate(flags => flags.enableOwnerStacks) + ? ['script', 'App'] + : ['script', 'body', 'html', 'App'], + ), + 'A script element was rendered with something unexpected for children. If script element has children it must be a single string. Consider using dangerouslySetInnerHTML or passing a plain string as children.' + + componentStack( + gate(flags => flags.enableOwnerStacks) + ? ['script', 'App'] + : ['script', 'body', 'html', 'App'], + ), + ]); }); // @gate enablePostpone diff --git a/packages/react-reconciler/src/__tests__/ReactFlushSync-test.js b/packages/react-reconciler/src/__tests__/ReactFlushSync-test.js index c96c381a617a0..98303dc4603fd 100644 --- a/packages/react-reconciler/src/__tests__/ReactFlushSync-test.js +++ b/packages/react-reconciler/src/__tests__/ReactFlushSync-test.js @@ -7,6 +7,7 @@ let useState; let useEffect; let startTransition; let assertLog; +let assertConsoleErrorDev; let waitForPaint; // TODO: Migrate tests to React DOM instead of React Noop @@ -26,6 +27,7 @@ describe('ReactFlushSync', () => { const InternalTestUtils = require('internal-test-utils'); assertLog = InternalTestUtils.assertLog; + assertConsoleErrorDev = InternalTestUtils.assertConsoleErrorDev; waitForPaint = InternalTestUtils.waitForPaint; }); @@ -77,8 +79,6 @@ describe('ReactFlushSync', () => { } it('changes priority of updates in useEffect', async () => { - spyOnDev(console, 'error').mockImplementation(() => {}); - function App() { const [syncState, setSyncState] = useState(0); const [state, setState] = useState(0); @@ -107,18 +107,15 @@ describe('ReactFlushSync', () => { // The remaining update is not sync ReactDOM.flushSync(); assertLog([]); + assertConsoleErrorDev([ + 'flushSync was called from inside a lifecycle method. React ' + + 'cannot flush when React is already rendering. Consider moving this ' + + 'call to a scheduler task or micro task.', + ]); await waitForPaint([]); }); expect(getVisibleChildren(container)).toEqual('1, 1'); - - if (__DEV__) { - expect(console.error.mock.calls[0][0]).toContain( - 'flushSync was called from inside a lifecycle method. React ' + - 'cannot flush when React is already rendering. Consider moving this ' + - 'call to a scheduler task or micro task.%s', - ); - } }); it('supports nested flushSync with startTransition', async () => {