diff --git a/packages/react-reconciler/src/__tests__/ReactLazy-test.internal.js b/packages/react-reconciler/src/__tests__/ReactLazy-test.internal.js index ddcd751957100..1b665b2b8bc7f 100644 --- a/packages/react-reconciler/src/__tests__/ReactLazy-test.internal.js +++ b/packages/react-reconciler/src/__tests__/ReactLazy-test.internal.js @@ -233,8 +233,28 @@ describe('ReactLazy', () => { expect(error.message).toMatch('Element type is invalid'); assertLog(['Loading...']); assertConsoleErrorDev([ - 'Expected the result of a dynamic import() call', - 'Expected the result of a dynamic import() call', + 'lazy: Expected the result of a dynamic import() call. ' + + 'Instead received: function Text(props) {\n' + + ' Scheduler.log(props.text);\n' + + ' return props.text;\n' + + ' }\n\n' + + 'Your code should look like: \n ' + + "const MyComponent = lazy(() => import('./MyComponent'))\n" + + (gate('enableOwnerStacks') + ? '' + : ' in Lazy (at **)\n' + ' in Suspense (at **)\n') + + ' in App (at **)', + 'lazy: Expected the result of a dynamic import() call. ' + + 'Instead received: function Text(props) {\n' + + ' Scheduler.log(props.text);\n' + + ' return props.text;\n' + + ' }\n\n' + + 'Your code should look like: \n ' + + "const MyComponent = lazy(() => import('./MyComponent'))\n" + + (gate('enableOwnerStacks') + ? '' + : ' in Lazy (at **)\n' + ' in Suspense (at **)\n') + + ' in App (at **)', ]); expect(root).not.toMatchRenderedOutput('Hi'); }); @@ -852,19 +872,21 @@ describe('ReactLazy', () => { expect(root).not.toMatchRenderedOutput('22'); // Mount - await expect(async () => { - await act(() => resolveFakeImport(Add)); - }).toErrorDev( - shouldWarnAboutFunctionDefaultProps - ? [ - 'Add: Support for defaultProps will be removed from function components in a future major release. Use JavaScript default parameters instead.', - ] - : shouldWarnAboutMemoDefaultProps - ? [ - 'Add: Support for defaultProps will be removed from memo components in a future major release. Use JavaScript default parameters instead.', - ] - : [], - ); + await act(() => resolveFakeImport(Add)); + + if (shouldWarnAboutFunctionDefaultProps) { + assertConsoleErrorDev([ + 'Add: Support for defaultProps will be removed from function components in a future major release. Use JavaScript default parameters instead.\n' + + ' in Add (at **)\n' + + ' in Suspense (at **)', + ]); + } else if (shouldWarnAboutMemoDefaultProps) { + assertConsoleErrorDev([ + 'Add: Support for defaultProps will be removed from memo components in a future major release. Use JavaScript default parameters instead.\n' + + ' in Suspense (at **)', + ]); + } + expect(root).toMatchRenderedOutput('22'); // Update diff --git a/packages/react/src/__tests__/ReactCoffeeScriptClass-test.coffee b/packages/react/src/__tests__/ReactCoffeeScriptClass-test.coffee index 3ad6aa0235188..5fab9d7d0bbde 100644 --- a/packages/react/src/__tests__/ReactCoffeeScriptClass-test.coffee +++ b/packages/react/src/__tests__/ReactCoffeeScriptClass-test.coffee @@ -9,6 +9,8 @@ PropTypes = null React = null ReactDOM = null ReactDOMClient = null +assertConsoleErrorDev = null +assertConsoleWarnDev = null featureFlags = require 'shared/ReactFeatureFlags' @@ -28,6 +30,9 @@ describe 'ReactCoffeeScriptClass', -> root = ReactDOMClient.createRoot container attachedListener = null renderedName = null + TestUtils = require 'internal-test-utils' + assertConsoleErrorDev = TestUtils.assertConsoleErrorDev + assertConsoleWarnDev = TestUtils.assertConsoleWarnDev InnerComponent = class extends React.Component getName: -> this.props.name render: -> @@ -53,14 +58,15 @@ describe 'ReactCoffeeScriptClass', -> event.preventDefault() caughtErrors.push(event.error) window.addEventListener 'error', errorHandler; - expect(-> - ReactDOM.flushSync -> - root.render React.createElement(Foo) - ).toErrorDev([ - # A failed component renders twice in DEV in concurrent mode - 'No `render` method found on the Foo instance', - 'No `render` method found on the Foo instance', - ]) + ReactDOM.flushSync -> + root.render React.createElement(Foo) + assertConsoleErrorDev [ +# A failed component renders twice in DEV in concurrent mode + 'No `render` method found on the Foo instance: you may have forgotten to define `render`.\n' + + ' in Foo (at **)', + 'No `render` method found on the Foo instance: you may have forgotten to define `render`.\n' + + ' in Foo (at **)', + ] window.removeEventListener 'error', errorHandler; expect(caughtErrors).toEqual([ expect.objectContaining( @@ -136,11 +142,11 @@ describe 'ReactCoffeeScriptClass', -> React.createElement('div') getDerivedStateFromProps: -> {} - expect(-> - ReactDOM.flushSync -> - root.render React.createElement(Foo, foo: 'foo') - return - ).toErrorDev 'Foo: getDerivedStateFromProps() is defined as an instance method and will be ignored. Instead, declare it as a static method.' + ReactDOM.flushSync -> + root.render React.createElement(Foo, foo: 'foo') + assertConsoleErrorDev [ + 'Foo: getDerivedStateFromProps() is defined as an instance method and will be ignored. Instead, declare it as a static method.\n' + + ' in Foo (at **)'] it 'warns if getDerivedStateFromError is not static', -> class Foo extends React.Component @@ -148,11 +154,13 @@ describe 'ReactCoffeeScriptClass', -> React.createElement('div') getDerivedStateFromError: -> {} - expect(-> - ReactDOM.flushSync -> - root.render React.createElement(Foo, foo: 'foo') - return - ).toErrorDev 'Foo: getDerivedStateFromError() is defined as an instance method and will be ignored. Instead, declare it as a static method.' + ReactDOM.flushSync -> + root.render React.createElement(Foo, foo: 'foo') + + assertConsoleErrorDev [ + 'Foo: getDerivedStateFromError() is defined as an instance method and will be ignored. Instead, declare it as a static method.\n' + + ' in Foo (at **)' + ] it 'warns if getSnapshotBeforeUpdate is static', -> class Foo extends React.Component @@ -160,11 +168,13 @@ describe 'ReactCoffeeScriptClass', -> React.createElement('div') Foo.getSnapshotBeforeUpdate = () -> {} - expect(-> - ReactDOM.flushSync -> - root.render React.createElement(Foo, foo: 'foo') - return - ).toErrorDev 'Foo: getSnapshotBeforeUpdate() is defined as a static method and will be ignored. Instead, declare it as an instance method.' + ReactDOM.flushSync -> + root.render React.createElement(Foo, foo: 'foo') + + assertConsoleErrorDev [ + 'Foo: getSnapshotBeforeUpdate() is defined as a static method and will be ignored. Instead, declare it as an instance method.\n' + + ' in Foo (at **)' + ] it 'warns if state not initialized before static getDerivedStateFromProps', -> class Foo extends React.Component @@ -177,16 +187,16 @@ describe 'ReactCoffeeScriptClass', -> foo: nextProps.foo bar: 'bar' } - expect(-> - ReactDOM.flushSync -> - root.render React.createElement(Foo, foo: 'foo') - return - ).toErrorDev ( - '`Foo` uses `getDerivedStateFromProps` but its initial state is ' + - 'undefined. This is not recommended. Instead, define the initial state by ' + - 'assigning an object to `this.state` in the constructor of `Foo`. ' + - 'This ensures that `getDerivedStateFromProps` arguments have a consistent shape.' - ) + ReactDOM.flushSync -> + root.render React.createElement(Foo, foo: 'foo') + + assertConsoleErrorDev [ + '`Foo` uses `getDerivedStateFromProps` but its initial state is + undefined. This is not recommended. Instead, define the initial state by + assigning an object to `this.state` in the constructor of `Foo`. + This ensures that `getDerivedStateFromProps` arguments have a consistent shape.\n' + + ' in Foo (at **)' + ] it 'updates initial state with values returned by static getDerivedStateFromProps', -> class Foo extends React.Component @@ -254,12 +264,28 @@ describe 'ReactCoffeeScriptClass', -> render: -> React.createElement Foo - expect(-> - test React.createElement(Outer), 'SPAN', 'foo' - ).toErrorDev([ - 'Outer uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead.', - 'Foo uses the legacy contextTypes API which will soon be removed. Use React.createContext() with static contextType instead.', - ]) + test React.createElement(Outer), 'SPAN', 'foo' + + if featureFlags.enableOwnerStacks + assertConsoleErrorDev([ + 'Outer uses the legacy childContextTypes API which will soon be removed. + Use React.createContext() instead. (https://react.dev/link/legacy-context)\n' + + ' in Outer (at **)', + 'Foo uses the legacy contextTypes API which will soon be removed. + Use React.createContext() with static contextType instead. (https://react.dev/link/legacy-context)\n' + + ' in Outer (at **)', + ]); + else + assertConsoleErrorDev([ + 'Outer uses the legacy childContextTypes API which will soon be removed. + Use React.createContext() instead. (https://react.dev/link/legacy-context)\n' + + ' in Outer (at **)', + 'Foo uses the legacy contextTypes API which will soon be removed. + Use React.createContext() with static contextType instead. (https://react.dev/link/legacy-context)\n' + + ' in Foo (at **)\n' + + ' in Outer (at **)', + ]); + it 'renders only once when setting state in componentWillMount', -> renderCount = 0 @@ -286,9 +312,11 @@ describe 'ReactCoffeeScriptClass', -> render: -> React.createElement('span') - expect(-> - test React.createElement(Foo), 'SPAN', '' - ).toErrorDev('Foo.state: must be set to an object or null') + test React.createElement(Foo), 'SPAN', '' + assertConsoleErrorDev [ + 'Foo.state: must be set to an object or null\n' + + ' in Foo (at **)' + ] it 'should render with null in the initial state property', -> class Foo extends React.Component @@ -430,14 +458,21 @@ describe 'ReactCoffeeScriptClass', -> className: 'foo' ) - expect(-> - test React.createElement(Foo), 'SPAN', 'foo' - ).toErrorDev([ - 'getInitialState was defined on Foo, a plain JavaScript class.', - 'getDefaultProps was defined on Foo, a plain JavaScript class.', - 'contextTypes was defined as an instance property on Foo.', - 'contextType was defined as an instance property on Foo.', - ]) + test React.createElement(Foo), 'SPAN', 'foo' + assertConsoleErrorDev [ + 'getInitialState was defined on Foo, a plain JavaScript class. + This is only supported for classes created using React.createClass. + Did you mean to define a state property instead?\n' + + ' in Foo (at **)', + 'getDefaultProps was defined on Foo, a plain JavaScript class. + This is only supported for classes created using React.createClass. + Use a static property to define defaultProps instead.\n' + + ' in Foo (at **)', + 'contextType was defined as an instance property on Foo. Use a static property to define contextType instead.\n' + + ' in Foo (at **)', + 'contextTypes was defined as an instance property on Foo. Use a static property to define contextTypes instead.\n' + + ' in Foo (at **)', + ] expect(getInitialStateWasCalled).toBe false expect(getDefaultPropsWasCalled).toBe false @@ -468,13 +503,13 @@ describe 'ReactCoffeeScriptClass', -> className: 'foo' ) - expect(-> - test React.createElement(NamedComponent), 'SPAN', 'foo' - ).toErrorDev( + test React.createElement(NamedComponent), 'SPAN', 'foo' + assertConsoleErrorDev [ 'NamedComponent has a method called componentShouldUpdate(). Did you mean shouldComponentUpdate()? The name is phrased as a - question because the function is expected to return a value.' - ) + question because the function is expected to return a value.\n' + + ' in NamedComponent (at **)' + ] it 'should warn when misspelling componentWillReceiveProps', -> class NamedComponent extends React.Component @@ -486,12 +521,12 @@ describe 'ReactCoffeeScriptClass', -> className: 'foo' ) - expect(-> - test React.createElement(NamedComponent), 'SPAN', 'foo' - ).toErrorDev( + test React.createElement(NamedComponent), 'SPAN', 'foo' + assertConsoleErrorDev [ 'NamedComponent has a method called componentWillRecieveProps(). - Did you mean componentWillReceiveProps()?' - ) + Did you mean componentWillReceiveProps()?\n' + + ' in NamedComponent (at **)' + ] it 'should warn when misspelling UNSAFE_componentWillReceiveProps', -> class NamedComponent extends React.Component @@ -503,28 +538,28 @@ describe 'ReactCoffeeScriptClass', -> className: 'foo' ) - expect(-> - test React.createElement(NamedComponent), 'SPAN', 'foo' - ).toErrorDev( + test React.createElement(NamedComponent), 'SPAN', 'foo' + assertConsoleErrorDev [ 'NamedComponent has a method called UNSAFE_componentWillRecieveProps(). - Did you mean UNSAFE_componentWillReceiveProps()?' - ) + Did you mean UNSAFE_componentWillReceiveProps()?\n' + + ' in NamedComponent (at **)' + ] it 'should throw AND warn when trying to access classic APIs', -> ref = React.createRef() test React.createElement(InnerComponent, name: 'foo', ref: ref), 'DIV', 'foo' - expect(-> - expect(-> ref.current.replaceState {}).toThrow() - ).toWarnDev( - 'replaceState(...) is deprecated in plain JavaScript React classes', - {withoutStack: true} - ) - expect(-> - expect(-> ref.current.isMounted()).toThrow() - ).toWarnDev( - 'isMounted(...) is deprecated in plain JavaScript React classes', - {withoutStack: true} - ) + + expect(-> ref.current.replaceState {}).toThrow() + assertConsoleWarnDev([ + 'replaceState(...) is deprecated in plain JavaScript React classes. + Refactor your code to use setState instead (see https://github.com/facebook/react/issues/3236).' + ], {withoutStack: true}) + + expect(-> ref.current.isMounted()).toThrow() + assertConsoleWarnDev([ + 'isMounted(...) is deprecated in plain JavaScript React classes. + Instead, make sure to clean up subscriptions and pending requests in componentWillUnmount to prevent memory leaks.', + ], {withoutStack: true}) if !featureFlags.disableLegacyContext it 'supports this.context passed via getChildContext', -> @@ -542,13 +577,25 @@ describe 'ReactCoffeeScriptClass', -> render: -> React.createElement Bar - expect(-> - test React.createElement(Foo), 'DIV', 'bar-through-context' - ).toErrorDev( - [ - 'Foo uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead.', - 'Bar uses the legacy contextTypes API which will soon be removed. Use React.createContext() with static contextType instead.', - ], - ) + test React.createElement(Foo), 'DIV', 'bar-through-context' + if featureFlags.enableOwnerStacks + assertConsoleErrorDev [ + 'Foo uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead. + (https://react.dev/link/legacy-context)\n' + + ' in Foo (at **)', + 'Bar uses the legacy contextTypes API which will soon be removed. Use React.createContext() with static contextType instead. + (https://react.dev/link/legacy-context)\n' + + ' in Foo (at **)' + ] + else + assertConsoleErrorDev [ + 'Foo uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead. + (https://react.dev/link/legacy-context)\n' + + ' in Foo (at **)', + 'Bar uses the legacy contextTypes API which will soon be removed. Use React.createContext() with static contextType instead. + (https://react.dev/link/legacy-context)\n' + + ' in Bar (at **)\n' + + ' in Foo (at **)' + ] undefined diff --git a/packages/react/src/__tests__/ReactContextValidator-test.js b/packages/react/src/__tests__/ReactContextValidator-test.js index 359857af15c61..66597f0dd52d0 100644 --- a/packages/react/src/__tests__/ReactContextValidator-test.js +++ b/packages/react/src/__tests__/ReactContextValidator-test.js @@ -19,6 +19,7 @@ let PropTypes; let React; let ReactDOMClient; let act; +let assertConsoleErrorDev; describe('ReactContextValidator', () => { beforeEach(() => { @@ -27,7 +28,7 @@ describe('ReactContextValidator', () => { PropTypes = require('prop-types'); React = require('react'); ReactDOMClient = require('react-dom/client'); - act = require('internal-test-utils').act; + ({act, assertConsoleErrorDev} = require('internal-test-utils')); }); // TODO: This behavior creates a runtime dependency on propTypes. We should @@ -66,15 +67,20 @@ describe('ReactContextValidator', () => { let instance; const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); - await expect(async () => { - await act(() => { - root.render( - (instance = current)} />, - ); - }); - }).toErrorDev([ - 'ComponentInFooBarContext uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead.', - 'Component uses the legacy contextTypes API which will soon be removed. Use React.createContext() with static contextType instead.', + await act(() => { + root.render( + (instance = current)} />, + ); + }); + assertConsoleErrorDev([ + 'ComponentInFooBarContext uses the legacy childContextTypes API which will soon be removed. ' + + 'Use React.createContext() instead. (https://react.dev/link/legacy-context)\n' + + ' in ComponentInFooBarContext (at **)', + 'Component uses the legacy contextTypes API which will soon be removed. ' + + 'Use React.createContext() with static contextType instead. (https://react.dev/link/legacy-context)\n' + + (gate(flags => flags.enableOwnerStacks) + ? ' in ComponentInFooBarContext (at **)' + : ' in Component (at **)'), ]); expect(instance.childRef.current.context).toEqual({foo: 'abc'}); }); @@ -144,13 +150,18 @@ describe('ReactContextValidator', () => { const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); - await expect(async () => { - await act(() => { - root.render(); - }); - }).toErrorDev([ - 'Parent uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead.', - 'Component uses the legacy contextTypes API which will soon be removed. Use React.createContext() with static contextType instead.', + await act(() => { + root.render(); + }); + assertConsoleErrorDev([ + 'Parent uses the legacy childContextTypes API which will soon be removed. ' + + 'Use React.createContext() instead. (https://react.dev/link/legacy-context)\n' + + ' in Parent (at **)', + 'Component uses the legacy contextTypes API which will soon be removed. ' + + 'Use React.createContext() with static contextType instead. (https://react.dev/link/legacy-context)\n' + + (gate(flags => flags.enableOwnerStacks) + ? ' in Parent (at **)' + : ' in Component (at **)'), ]); expect(constructorContext).toEqual({foo: 'abc'}); @@ -191,33 +202,33 @@ describe('ReactContextValidator', () => { } } - await expect(async () => { - const container = document.createElement('div'); - const root = ReactDOMClient.createRoot(container); - await act(() => { - root.render(); - }); - }).toErrorDev([ - 'ComponentA uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead.', - 'ComponentA.childContextTypes is specified but there is no getChildContext() method on the instance. You can either define getChildContext() on ComponentA or remove childContextTypes from it.', + const root = ReactDOMClient.createRoot(document.createElement('div')); + await act(() => { + root.render(); + }); + assertConsoleErrorDev([ + 'ComponentA uses the legacy childContextTypes API which will soon be removed. ' + + 'Use React.createContext() instead. (https://react.dev/link/legacy-context)\n' + + ' in ComponentA (at **)', + 'ComponentA.childContextTypes is specified but there is no getChildContext() method on the instance. ' + + 'You can either define getChildContext() on ComponentA or remove childContextTypes from it.\n' + + ' in ComponentA (at **)', ]); // Warnings should be deduped by component type - let container = document.createElement('div'); - let root = ReactDOMClient.createRoot(container); await act(() => { root.render(); }); - - await expect(async () => { - container = document.createElement('div'); - root = ReactDOMClient.createRoot(container); - await act(() => { - root.render(); - }); - }).toErrorDev([ - 'ComponentB uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead.', - 'ComponentB.childContextTypes is specified but there is no getChildContext() method on the instance. You can either define getChildContext() on ComponentB or remove childContextTypes from it.', + await act(() => { + root.render(); + }); + assertConsoleErrorDev([ + 'ComponentB uses the legacy childContextTypes API which will soon be removed. ' + + 'Use React.createContext() instead. (https://react.dev/link/legacy-context)\n' + + ' in ComponentB (at **)', + 'ComponentB.childContextTypes is specified but there is no getChildContext() method on the instance. ' + + 'You can either define getChildContext() on ComponentB or remove childContextTypes from it.\n' + + ' in ComponentB (at **)', ]); }); @@ -260,17 +271,34 @@ describe('ReactContextValidator', () => { foo: PropTypes.string.isRequired, }; - await expect(async () => { - const container = document.createElement('div'); - const root = ReactDOMClient.createRoot(container); - await act(() => { - root.render(); - }); - }).toErrorDev([ - 'ParentContextProvider uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead.', - 'MiddleMissingContext uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead.', - 'MiddleMissingContext.childContextTypes is specified but there is no getChildContext() method on the instance. You can either define getChildContext() on MiddleMissingContext or remove childContextTypes from it.', - 'ChildContextConsumer uses the legacy contextTypes API which will soon be removed. Use React.createContext() with static contextType instead.', + const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); + await act(() => { + root.render(); + }); + assertConsoleErrorDev([ + 'ParentContextProvider uses the legacy childContextTypes API which will soon be removed. ' + + 'Use React.createContext() instead. (https://react.dev/link/legacy-context)\n' + + ' in ParentContextProvider (at **)', + 'MiddleMissingContext uses the legacy childContextTypes API which will soon be removed. ' + + 'Use React.createContext() instead. (https://react.dev/link/legacy-context)\n' + + (gate(flags => flags.enableOwnerStacks) + ? '' + : ' in MiddleMissingContext (at **)\n') + + ' in ParentContextProvider (at **)', + 'MiddleMissingContext.childContextTypes is specified but there is no getChildContext() method on the instance. ' + + 'You can either define getChildContext() on MiddleMissingContext or remove childContextTypes from it.\n' + + (gate(flags => flags.enableOwnerStacks) + ? '' + : ' in MiddleMissingContext (at **)\n') + + ' in ParentContextProvider (at **)', + 'ChildContextConsumer uses the legacy contextTypes API which will soon be removed. ' + + 'Use React.createContext() with static contextType instead. (https://react.dev/link/legacy-context)\n' + + (gate(flags => flags.enableOwnerStacks) + ? '' + : ' in ChildContextConsumer (at **)\n') + + ' in MiddleMissingContext (at **)\n' + + ' in ParentContextProvider (at **)', ]); expect(childContext.bar).toBeUndefined(); expect(childContext.foo).toBe('FOO'); @@ -428,25 +456,28 @@ describe('ReactContextValidator', () => { } } - await expect(async () => { - const container = document.createElement('div'); - const root = ReactDOMClient.createRoot(container); - await act(() => { - root.render( - - - , - ); - }); - }).toErrorDev([ - 'ParentContextProvider uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead', - 'ComponentA uses the legacy contextTypes API which will soon be removed. Use React.createContext() with static contextType instead.', - 'ComponentA declares both contextTypes and contextType static properties. The legacy contextTypes property will be ignored.', + const root = ReactDOMClient.createRoot(document.createElement('div')); + await act(() => { + root.render( + + + , + ); + }); + + assertConsoleErrorDev([ + 'ParentContextProvider uses the legacy childContextTypes API which will soon be removed. ' + + 'Use React.createContext() instead. (https://react.dev/link/legacy-context)\n' + + ' in ParentContextProvider (at **)', + 'ComponentA declares both contextTypes and contextType static properties. ' + + 'The legacy contextTypes property will be ignored.\n' + + ' in ComponentA (at **)', + 'ComponentA uses the legacy contextTypes API which will soon be removed. ' + + 'Use React.createContext() with static contextType instead. (https://react.dev/link/legacy-context)\n' + + ' in ComponentA (at **)', ]); // Warnings should be deduped by component type - let container = document.createElement('div'); - let root = ReactDOMClient.createRoot(container); await act(() => { root.render( @@ -455,19 +486,20 @@ describe('ReactContextValidator', () => { ); }); - await expect(async () => { - container = document.createElement('div'); - root = ReactDOMClient.createRoot(container); - await act(() => { - root.render( - - - , - ); - }); - }).toErrorDev([ - 'ComponentB declares both contextTypes and contextType static properties. The legacy contextTypes property will be ignored.', - 'ComponentB uses the legacy contextTypes API which will soon be removed. Use React.createContext() with static contextType instead.', + await act(() => { + root.render( + + + , + ); + }); + assertConsoleErrorDev([ + 'ComponentB declares both contextTypes and contextType static properties. ' + + 'The legacy contextTypes property will be ignored.\n' + + ' in ComponentB (at **)', + 'ComponentB uses the legacy contextTypes API which will soon be removed. ' + + 'Use React.createContext() with static contextType instead. (https://react.dev/link/legacy-context)\n' + + ' in ComponentB (at **)', ]); }); @@ -481,20 +513,17 @@ describe('ReactContextValidator', () => { } } - await expect(async () => { - const container = document.createElement('div'); - const root = ReactDOMClient.createRoot(container); - await act(() => { - root.render(); - }); - }).toErrorDev( + const root = ReactDOMClient.createRoot(document.createElement('div')); + await act(() => { + root.render(); + }); + assertConsoleErrorDev([ 'ComponentA defines an invalid contextType. ' + 'contextType should point to the Context object returned by React.createContext(). ' + - 'Did you accidentally pass the Context.Consumer instead?', - ); + 'Did you accidentally pass the Context.Consumer instead?\n' + + ' in ComponentA (at **)', + ]); - let container = document.createElement('div'); - let root = ReactDOMClient.createRoot(container); await act(() => { root.render(); }); @@ -505,8 +534,6 @@ describe('ReactContextValidator', () => { return
; } } - container = document.createElement('div'); - root = ReactDOMClient.createRoot(container); await act(() => { root.render(); }); @@ -539,23 +566,22 @@ describe('ReactContextValidator', () => { } await expect(async () => { - await expect(async () => { - const container = document.createElement('div'); - const root = ReactDOMClient.createRoot(container); - await act(() => { - root.render(); - }); - }).rejects.toThrow( - "Cannot read properties of undefined (reading 'world')", - ); - }).toErrorDev( + const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); + await act(() => { + root.render(); + }); + }).rejects.toThrow("Cannot read properties of undefined (reading 'world')"); + + assertConsoleErrorDev([ 'Foo defines an invalid contextType. ' + 'contextType should point to the Context object returned by React.createContext(). ' + 'However, it is set to undefined. ' + 'This can be caused by a typo or by mixing up named and default imports. ' + 'This can also happen due to a circular dependency, ' + - 'so try moving the createContext() call to a separate file.', - ); + 'so try moving the createContext() call to a separate file.\n' + + ' in Foo (at **)', + ]); }); it('should warn when class contextType is an object', async () => { @@ -571,20 +597,19 @@ describe('ReactContextValidator', () => { } await expect(async () => { - await expect(async () => { - const container = document.createElement('div'); - const root = ReactDOMClient.createRoot(container); - await act(() => { - root.render(); - }); - }).rejects.toThrow( - "Cannot read properties of undefined (reading 'hello')", - ); - }).toErrorDev( + const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); + await act(() => { + root.render(); + }); + }).rejects.toThrow("Cannot read properties of undefined (reading 'hello')"); + + assertConsoleErrorDev([ 'Foo defines an invalid contextType. ' + 'contextType should point to the Context object returned by React.createContext(). ' + - 'However, it is set to an object with keys {x, y}.', - ); + 'However, it is set to an object with keys {x, y}.\n' + + ' in Foo (at **)', + ]); }); it('should warn when class contextType is a primitive', async () => { @@ -596,20 +621,19 @@ describe('ReactContextValidator', () => { } await expect(async () => { - await expect(async () => { - const container = document.createElement('div'); - const root = ReactDOMClient.createRoot(container); - await act(() => { - root.render(); - }); - }).rejects.toThrow( - "Cannot read properties of undefined (reading 'world')", - ); - }).toErrorDev( + const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); + await act(() => { + root.render(); + }); + }).rejects.toThrow("Cannot read properties of undefined (reading 'world')"); + + assertConsoleErrorDev([ 'Foo defines an invalid contextType. ' + 'contextType should point to the Context object returned by React.createContext(). ' + - 'However, it is set to a string.', - ); + 'However, it is set to a string.\n' + + ' in Foo (at **)', + ]); }); it('should warn if you define contextType on a function component', async () => { @@ -625,31 +649,26 @@ describe('ReactContextValidator', () => { } ComponentB.contextType = Context; - await expect(async () => { - const container = document.createElement('div'); - const root = ReactDOMClient.createRoot(container); - await act(() => { - root.render(); - }); - }).toErrorDev( - 'ComponentA: Function components do not support contextType.', - ); + const root = ReactDOMClient.createRoot(document.createElement('div')); + await act(() => { + root.render(); + }); + assertConsoleErrorDev([ + 'ComponentA: Function components do not support contextType.\n' + + ' in ComponentA (at **)', + ]); // Warnings should be deduped by component type - let container = document.createElement('div'); - let root = ReactDOMClient.createRoot(container); await act(() => { root.render(); }); - await expect(async () => { - container = document.createElement('div'); - root = ReactDOMClient.createRoot(container); - await act(() => { - root.render(); - }); - }).toErrorDev( - 'ComponentB: Function components do not support contextType.', - ); + await act(() => { + root.render(); + }); + assertConsoleErrorDev([ + 'ComponentB: Function components do not support contextType.\n' + + ' in ComponentB (at **)', + ]); }); }); diff --git a/packages/react/src/__tests__/ReactCreateElement-test.js b/packages/react/src/__tests__/ReactCreateElement-test.js index 392f89979e769..44952536ac470 100644 --- a/packages/react/src/__tests__/ReactCreateElement-test.js +++ b/packages/react/src/__tests__/ReactCreateElement-test.js @@ -13,6 +13,8 @@ let act; let React; let ReactDOMClient; +let assertConsoleErrorDev; +let assertConsoleWarnDev; // NOTE: This module tests the old, "classic" JSX runtime, React.createElement. // Do not use JSX syntax in this module; call React.createElement directly. @@ -22,7 +24,11 @@ describe('ReactCreateElement', () => { beforeEach(() => { jest.resetModules(); - act = require('internal-test-utils').act; + ({ + act, + assertConsoleErrorDev, + assertConsoleWarnDev, + } = require('internal-test-utils')); React = require('react'); ReactDOMClient = require('react-dom/client'); @@ -63,25 +69,34 @@ describe('ReactCreateElement', () => { } } const root = ReactDOMClient.createRoot(document.createElement('div')); - await expect(async () => { - await act(() => { - root.render(React.createElement(Parent)); - }); - }).toErrorDev( + await act(() => { + root.render(React.createElement(Parent)); + }); + assertConsoleErrorDev([ 'Child: `key` is not a prop. Trying to access it will result ' + 'in `undefined` being returned. If you need to access the same ' + 'value within the child component, you should pass it as a different ' + - 'prop. (https://react.dev/link/special-props)', - ); + 'prop. (https://react.dev/link/special-props)\n' + + (gate(flags => flags.enableOwnerStacks) + ? [' in Parent (at **)'] + : [ + ' in Child (at **)\n' + + ' in div (at **)\n' + + ' in Parent (at **)', + ]), + ]); }); it('should warn when `key` is being accessed on a host element', () => { const element = React.createElement('div', {key: '3'}); - expect(() => void element.props.key).toErrorDev( - 'div: `key` is not a prop. Trying to access it will result ' + - 'in `undefined` being returned. If you need to access the same ' + - 'value within the child component, you should pass it as a different ' + - 'prop. (https://react.dev/link/special-props)', + void element.props.key; + assertConsoleErrorDev( + [ + 'div: `key` is not a prop. Trying to access it will result ' + + 'in `undefined` being returned. If you need to access the same ' + + 'value within the child component, you should pass it as a different ' + + 'prop. (https://react.dev/link/special-props)', + ], {withoutStack: true}, ); }); @@ -141,9 +156,16 @@ describe('ReactCreateElement', () => { foo: '56', }); expect(element.type).toBe(ComponentClass); - expect(() => expect(element.ref).toBe(ref)).toErrorDev( - 'Accessing element.ref was removed in React 19', - {withoutStack: true}, + expect(element.ref).toBe(ref); + assertConsoleErrorDev( + [ + 'Accessing element.ref was removed in React 19. ref is now a ' + + 'regular prop. It will be removed from the JSX Element ' + + 'type in a future release.', + ], + { + withoutStack: true, + }, ); const expectation = {foo: '56', ref}; Object.freeze(expectation); @@ -412,11 +434,13 @@ describe('ReactCreateElement', () => { it('warns if outdated JSX transform is detected', async () => { // Warns if __self is detected, because that's only passed by a compiler - expect(() => { - React.createElement('div', {className: 'foo', __self: this}); - }).toWarnDev( - 'Your app (or one of its dependencies) is using an outdated ' + - 'JSX transform.', + React.createElement('div', {className: 'foo', __self: this}); + assertConsoleWarnDev( + [ + 'Your app (or one of its dependencies) is using an outdated JSX ' + + 'transform. Update to the modern JSX transform for ' + + 'faster performance: https://react.dev/link/new-jsx-transform', + ], { withoutStack: true, }, diff --git a/packages/react/src/__tests__/ReactCreateRef-test.js b/packages/react/src/__tests__/ReactCreateRef-test.js index 616c62e00c88f..bf7166d8cf80a 100644 --- a/packages/react/src/__tests__/ReactCreateRef-test.js +++ b/packages/react/src/__tests__/ReactCreateRef-test.js @@ -12,6 +12,7 @@ let React; let ReactDOM; let ReactDOMClient; +let assertConsoleErrorDev; describe('ReactCreateRef', () => { beforeEach(() => { @@ -20,6 +21,7 @@ describe('ReactCreateRef', () => { React = require('react'); ReactDOM = require('react-dom'); ReactDOMClient = require('react-dom/client'); + ({assertConsoleErrorDev} = require('internal-test-utils')); }); it('should warn in dev if an invalid ref object is provided', () => { @@ -34,38 +36,36 @@ describe('ReactCreateRef', () => { } const root = ReactDOMClient.createRoot(document.createElement('div')); - expect(() => - ReactDOM.flushSync(() => { - root.render( - -
- , - ); - }), - ).toErrorDev( + ReactDOM.flushSync(() => { + root.render( + +
+ , + ); + }); + assertConsoleErrorDev([ 'Unexpected ref object provided for div. ' + 'Use either a ref-setter function or React.createRef().\n' + ' in div (at **)' + (gate(flags => flags.enableOwnerStacks) ? '' : '\n in Wrapper (at **)'), - ); + ]); - expect(() => - ReactDOM.flushSync(() => { - root.render( - - - , - ); - }), - ).toErrorDev( + ReactDOM.flushSync(() => { + root.render( + + + , + ); + }); + assertConsoleErrorDev([ 'Unexpected ref object provided for ExampleComponent. ' + 'Use either a ref-setter function or React.createRef().\n' + ' in ExampleComponent (at **)' + (gate(flags => flags.enableOwnerStacks) ? '' : '\n in Wrapper (at **)'), - ); + ]); }); }); diff --git a/packages/react/src/__tests__/ReactES6Class-test.js b/packages/react/src/__tests__/ReactES6Class-test.js index eceeab18c5bb0..640ae7ef37542 100644 --- a/packages/react/src/__tests__/ReactES6Class-test.js +++ b/packages/react/src/__tests__/ReactES6Class-test.js @@ -14,6 +14,7 @@ let React; let ReactDOM; let ReactDOMClient; let assertConsoleErrorDev; +let assertConsoleWarnDev; describe('ReactES6Class', () => { let container; @@ -31,7 +32,10 @@ describe('ReactES6Class', () => { React = require('react'); ReactDOM = require('react-dom'); ReactDOMClient = require('react-dom/client'); - ({assertConsoleErrorDev} = require('internal-test-utils')); + ({ + assertConsoleErrorDev, + assertConsoleWarnDev, + } = require('internal-test-utils')); container = document.createElement('div'); root = ReactDOMClient.createRoot(container); attachedListener = null; @@ -69,14 +73,15 @@ describe('ReactES6Class', () => { } window.addEventListener('error', errorHandler); try { - expect(() => { - ReactDOM.flushSync(() => root.render()); - }).toErrorDev([ + ReactDOM.flushSync(() => root.render()); + assertConsoleErrorDev([ // A failed component renders twice in DEV in concurrent mode 'No `render` method found on the Foo instance: ' + - 'you may have forgotten to define `render`.', + 'you may have forgotten to define `render`.\n' + + ' in Foo (at **)', 'No `render` method found on the Foo instance: ' + - 'you may have forgotten to define `render`.', + 'you may have forgotten to define `render`.\n' + + ' in Foo (at **)', ]); } finally { window.removeEventListener('error', errorHandler); @@ -158,12 +163,12 @@ describe('ReactES6Class', () => { return
; } } - expect(() => { - ReactDOM.flushSync(() => root.render()); - }).toErrorDev( + ReactDOM.flushSync(() => root.render()); + assertConsoleErrorDev([ 'Foo: getDerivedStateFromProps() is defined as an instance method ' + - 'and will be ignored. Instead, declare it as a static method.', - ); + 'and will be ignored. Instead, declare it as a static method.\n' + + ' in Foo (at **)', + ]); }); it('warns if getDerivedStateFromError is not static', () => { @@ -175,12 +180,12 @@ describe('ReactES6Class', () => { return
; } } - expect(() => { - ReactDOM.flushSync(() => root.render()); - }).toErrorDev( + ReactDOM.flushSync(() => root.render()); + assertConsoleErrorDev([ 'Foo: getDerivedStateFromError() is defined as an instance method ' + - 'and will be ignored. Instead, declare it as a static method.', - ); + 'and will be ignored. Instead, declare it as a static method.\n' + + ' in Foo (at **)', + ]); }); it('warns if getSnapshotBeforeUpdate is static', () => { @@ -190,12 +195,12 @@ describe('ReactES6Class', () => { return
; } } - expect(() => { - ReactDOM.flushSync(() => root.render()); - }).toErrorDev( + ReactDOM.flushSync(() => root.render()); + assertConsoleErrorDev([ 'Foo: getSnapshotBeforeUpdate() is defined as a static method ' + - 'and will be ignored. Instead, declare it as an instance method.', - ); + 'and will be ignored. Instead, declare it as an instance method.\n' + + ' in Foo (at **)', + ]); }); it('warns if state not initialized before static getDerivedStateFromProps', () => { @@ -210,14 +215,14 @@ describe('ReactES6Class', () => { return
; } } - expect(() => { - ReactDOM.flushSync(() => root.render()); - }).toErrorDev( + ReactDOM.flushSync(() => root.render()); + assertConsoleErrorDev([ '`Foo` uses `getDerivedStateFromProps` but its initial state is ' + 'undefined. This is not recommended. Instead, define the initial state by ' + 'assigning an object to `this.state` in the constructor of `Foo`. ' + - 'This ensures that `getDerivedStateFromProps` arguments have a consistent shape.', - ); + 'This ensures that `getDerivedStateFromProps` arguments have a consistent shape.\n' + + ' in Foo (at **)', + ]); }); it('updates initial state with values returned by static getDerivedStateFromProps', () => { @@ -266,11 +271,13 @@ describe('ReactES6Class', () => { super(props, context); this.state = {tag: context.tag, className: this.context.className}; } + render() { const Tag = this.state.tag; return ; } } + Foo.contextTypes = { tag: PropTypes.string, className: PropTypes.string, @@ -280,10 +287,12 @@ describe('ReactES6Class', () => { getChildContext() { return {tag: 'span', className: 'foo'}; } + render() { return ; } } + Outer.childContextTypes = { tag: PropTypes.string, className: PropTypes.string, @@ -291,8 +300,15 @@ describe('ReactES6Class', () => { runTest(, 'SPAN', 'foo'); assertConsoleErrorDev([ - 'Outer uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead.', - 'Foo uses the legacy contextTypes API which will soon be removed. Use React.createContext() with static contextType instead.', + 'Outer uses the legacy childContextTypes API which will soon be removed. ' + + 'Use React.createContext() instead. (https://react.dev/link/legacy-context)\n' + + ' in Outer (at **)', + 'Foo uses the legacy contextTypes API which will soon be removed. ' + + 'Use React.createContext() with static contextType instead. (https://react.dev/link/legacy-context)\n' + + (gate(flags => flags.enableOwnerStacks) + ? '' + : ' in Foo (at **)\n') + + ' in Outer (at **)', ]); }); } @@ -327,9 +343,10 @@ describe('ReactES6Class', () => { return ; } } - expect(() => runTest(, 'SPAN', '')).toErrorDev( - 'Foo.state: must be set to an object or null', - ); + runTest(, 'SPAN', ''); + assertConsoleErrorDev([ + 'Foo.state: must be set to an object or null\n in Foo (at **)', + ]); }); }); @@ -480,11 +497,22 @@ describe('ReactES6Class', () => { } } - expect(() => runTest(, 'SPAN', 'foo')).toErrorDev([ - 'getInitialState was defined on Foo, a plain JavaScript class.', - 'getDefaultProps was defined on Foo, a plain JavaScript class.', - 'contextType was defined as an instance property on Foo.', - 'contextTypes was defined as an instance property on Foo.', + runTest(, 'SPAN', 'foo'); + assertConsoleErrorDev([ + 'getInitialState was defined on Foo, a plain JavaScript class. ' + + 'This is only supported for classes created using React.createClass. ' + + 'Did you mean to define a state property instead?\n' + + ' in Foo (at **)', + 'getDefaultProps was defined on Foo, a plain JavaScript class. ' + + 'This is only supported for classes created using React.createClass. ' + + 'Use a static property to define defaultProps instead.\n' + + ' in Foo (at **)', + 'contextType was defined as an instance property on Foo. ' + + 'Use a static property to define contextType instead.\n' + + ' in Foo (at **)', + 'contextTypes was defined as an instance property on Foo. ' + + 'Use a static property to define contextTypes instead.\n' + + ' in Foo (at **)', ]); expect(getInitialStateWasCalled).toBe(false); expect(getDefaultPropsWasCalled).toBe(false); @@ -514,11 +542,13 @@ describe('ReactES6Class', () => { } } - expect(() => runTest(, 'SPAN', 'foo')).toErrorDev( + runTest(, 'SPAN', 'foo'); + assertConsoleErrorDev([ 'NamedComponent has a method called componentShouldUpdate(). Did you ' + 'mean shouldComponentUpdate()? The name is phrased as a question ' + - 'because the function is expected to return a value.', - ); + 'because the function is expected to return a value.\n' + + ' in NamedComponent (at **)', + ]); }); it('should warn when misspelling componentWillReceiveProps', () => { @@ -531,10 +561,12 @@ describe('ReactES6Class', () => { } } - expect(() => runTest(, 'SPAN', 'foo')).toErrorDev( + runTest(, 'SPAN', 'foo'); + assertConsoleErrorDev([ 'NamedComponent has a method called componentWillRecieveProps(). Did ' + - 'you mean componentWillReceiveProps()?', - ); + 'you mean componentWillReceiveProps()?\n' + + ' in NamedComponent (at **)', + ]); }); it('should warn when misspelling UNSAFE_componentWillReceiveProps', () => { @@ -547,23 +579,33 @@ describe('ReactES6Class', () => { } } - expect(() => runTest(, 'SPAN', 'foo')).toErrorDev( + runTest(, 'SPAN', 'foo'); + assertConsoleErrorDev([ 'NamedComponent has a method called UNSAFE_componentWillRecieveProps(). ' + - 'Did you mean UNSAFE_componentWillReceiveProps()?', - ); + 'Did you mean UNSAFE_componentWillReceiveProps()?\n' + + ' in NamedComponent (at **)', + ]); }); it('should throw AND warn when trying to access classic APIs', () => { const ref = React.createRef(); runTest(, 'DIV', 'foo'); - expect(() => - expect(() => ref.current.replaceState({})).toThrow(), - ).toWarnDev( - 'replaceState(...) is deprecated in plain JavaScript React classes', + + expect(() => ref.current.replaceState({})).toThrow(); + assertConsoleWarnDev( + [ + 'replaceState(...) is deprecated in plain JavaScript React classes. ' + + 'Refactor your code to use setState instead (see https://github.com/facebook/react/issues/3236).', + ], {withoutStack: true}, ); - expect(() => expect(() => ref.current.isMounted()).toThrow()).toWarnDev( - 'isMounted(...) is deprecated in plain JavaScript React classes', + expect(() => ref.current.isMounted()).toThrow(); + assertConsoleWarnDev( + [ + 'isMounted(...) is deprecated in plain JavaScript React classes. ' + + 'Instead, make sure to clean up subscriptions and pending requests in ' + + 'componentWillUnmount to prevent memory leaks.', + ], {withoutStack: true}, ); }); @@ -575,20 +617,31 @@ describe('ReactES6Class', () => { return
; } } + Bar.contextTypes = {bar: PropTypes.string}; + class Foo extends React.Component { getChildContext() { return {bar: 'bar-through-context'}; } + render() { return ; } } + Foo.childContextTypes = {bar: PropTypes.string}; runTest(, 'DIV', 'bar-through-context'); assertConsoleErrorDev([ - 'Foo uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead.', - 'Bar uses the legacy contextTypes API which will soon be removed. Use React.createContext() with static contextType instead.', + 'Foo uses the legacy childContextTypes API which will soon be removed. ' + + 'Use React.createContext() instead. (https://react.dev/link/legacy-context)\n' + + ' in Foo (at **)', + 'Bar uses the legacy contextTypes API which will soon be removed. ' + + 'Use React.createContext() with static contextType instead. (https://react.dev/link/legacy-context)\n' + + (gate(flags => flags.enableOwnerStacks) + ? '' + : ' in Bar (at **)\n') + + ' in Foo (at **)', ]); }); } diff --git a/packages/react/src/__tests__/ReactElementClone-test.js b/packages/react/src/__tests__/ReactElementClone-test.js index fb0bfe2df8d6b..b2e791f8268c9 100644 --- a/packages/react/src/__tests__/ReactElementClone-test.js +++ b/packages/react/src/__tests__/ReactElementClone-test.js @@ -12,6 +12,7 @@ let act; let React; let ReactDOMClient; +let assertConsoleErrorDev; describe('ReactElementClone', () => { let ComponentClass; @@ -19,7 +20,7 @@ describe('ReactElementClone', () => { beforeEach(() => { jest.resetModules(); - act = require('internal-test-utils').act; + ({act, assertConsoleErrorDev} = require('internal-test-utils')); React = require('react'); ReactDOMClient = require('react-dom/client'); @@ -314,11 +315,14 @@ describe('ReactElementClone', () => { it('warns for keys for arrays of elements in rest args', async () => { const root = ReactDOMClient.createRoot(document.createElement('div')); - await expect(async () => { - await act(() => { - root.render(React.cloneElement(
, null, [
,
])); - }); - }).toErrorDev('Each child in a list should have a unique "key" prop.'); + await act(() => { + root.render(React.cloneElement(
, null, [
,
])); + }); + assertConsoleErrorDev([ + 'Each child in a list should have a unique "key" prop.\n\n' + + 'Check the top-level render call using
. See https://react.dev/link/warning-keys for more information.\n' + + ' in div (at **)', + ]); }); it('does not warns for arrays of elements with keys', async () => { @@ -363,9 +367,16 @@ describe('ReactElementClone', () => { expect(clone.type).toBe(ComponentClass); expect(clone.key).toBe('12'); expect(clone.props.ref).toBe('34'); - expect(() => expect(clone.ref).toBe('34')).toErrorDev( - 'Accessing element.ref was removed in React 19', - {withoutStack: true}, + expect(clone.ref).toBe('34'); + assertConsoleErrorDev( + [ + 'Accessing element.ref was removed in React 19. ref is now a ' + + 'regular prop. It will be removed from the JSX Element ' + + 'type in a future release.', + ], + { + withoutStack: true, + }, ); expect(clone.props).toEqual({foo: 'ef', ref: '34'}); if (__DEV__) { diff --git a/packages/react/src/__tests__/ReactElementValidator-test.internal.js b/packages/react/src/__tests__/ReactElementValidator-test.internal.js index 4383a6472d350..1191aa7ec2743 100644 --- a/packages/react/src/__tests__/ReactElementValidator-test.internal.js +++ b/packages/react/src/__tests__/ReactElementValidator-test.internal.js @@ -18,6 +18,7 @@ let React; let ReactDOMClient; let act; +let assertConsoleErrorDev; describe('ReactElementValidator', () => { let ComponentClass; @@ -27,7 +28,7 @@ describe('ReactElementValidator', () => { React = require('react'); ReactDOMClient = require('react-dom/client'); - act = require('internal-test-utils').act; + ({act, assertConsoleErrorDev} = require('internal-test-utils')); ComponentClass = class extends React.Component { render() { return React.createElement('div', null, this.props.children); @@ -37,16 +38,27 @@ describe('ReactElementValidator', () => { it('warns for keys for arrays of elements in rest args', async () => { const root = ReactDOMClient.createRoot(document.createElement('div')); - await expect(async () => { - await act(() => - root.render( - React.createElement(ComponentClass, null, [ - React.createElement(ComponentClass), - React.createElement(ComponentClass), - ]), - ), - ); - }).toErrorDev('Each child in a list should have a unique "key" prop.'); + await act(() => + root.render( + React.createElement(ComponentClass, null, [ + React.createElement(ComponentClass), + React.createElement(ComponentClass), + ]), + ), + ); + assertConsoleErrorDev( + gate(flags => flags.enableOwnerStacks) + ? [ + 'Each child in a list should have a unique "key" prop.\n\n' + + 'Check the render method of `ComponentClass`. See https://react.dev/link/warning-keys for more information.\n' + + ' in ComponentClass (at **)', + ] + : [ + 'Each child in a list should have a unique "key" prop.\n\n' + + 'Check the top-level render call using . See https://react.dev/link/warning-keys for more information.\n' + + ' in ComponentClass (at **)', + ], + ); }); it('warns for keys for arrays of elements with owner info', async () => { @@ -67,18 +79,23 @@ describe('ReactElementValidator', () => { } } - await expect(async () => { - const root = ReactDOMClient.createRoot(document.createElement('div')); - await act(() => root.render(React.createElement(ComponentWrapper))); - }).toErrorDev( + const root = ReactDOMClient.createRoot(document.createElement('div')); + await act(() => root.render(React.createElement(ComponentWrapper))); + assertConsoleErrorDev([ 'Each child in a list should have a unique "key" prop.' + '\n\nCheck the render method of `' + (gate(flags => flags.enableOwnerStacks) ? 'ComponentClass' : 'InnerClass') + '`. ' + - 'It was passed a child from ComponentWrapper. ', - ); + 'It was passed a child from ComponentWrapper. ' + + 'See https://react.dev/link/warning-keys for more information.\n' + + (gate(flags => flags.enableOwnerStacks) + ? ' in ComponentWrapper (at **)' + : ' in ComponentClass (at **)\n' + + ' in InnerClass (at **)\n' + + ' in ComponentWrapper (at **)'), + ]); }); it('warns for keys for arrays with no owner or parent info', async () => { @@ -89,36 +106,35 @@ describe('ReactElementValidator', () => { const divs = [
,
]; - await expect(async () => { - const root = ReactDOMClient.createRoot(document.createElement('div')); - await act(() => root.render({divs})); - }).toErrorDev( + const root = ReactDOMClient.createRoot(document.createElement('div')); + await act(() => root.render({divs})); + assertConsoleErrorDev([ gate(flags => flags.enableOwnerStacks) ? // For owner stacks the parent being validated is the div. 'Each child in a list should have a unique ' + - '"key" prop.' + - '\n\nCheck the top-level render call using
. ' + - 'See https://react.dev/link/warning-keys for more information.\n' + - ' in div (at **)' + '"key" prop.' + + '\n\nCheck the top-level render call using
. ' + + 'See https://react.dev/link/warning-keys for more information.\n' + + ' in div (at **)' : 'Each child in a list should have a unique ' + - '"key" prop. See https://react.dev/link/warning-keys for more information.\n' + - ' in div (at **)', - ); + '"key" prop. See https://react.dev/link/warning-keys for more information.\n' + + ' in div (at **)', + ]); }); it('warns for keys for arrays of elements with no owner info', async () => { const divs = [
,
]; - await expect(async () => { - const root = ReactDOMClient.createRoot(document.createElement('div')); + const root = ReactDOMClient.createRoot(document.createElement('div')); - await act(() => root.render(
{divs}
)); - }).toErrorDev( + await act(() => root.render(
{divs}
)); + assertConsoleErrorDev([ 'Each child in a list should have a unique ' + - '"key" prop.\n\nCheck the top-level render call using
. See ' + - 'https://react.dev/link/warning-keys for more information.\n' + + '"key" prop.' + + '\n\nCheck the top-level render call using
. ' + + 'See https://react.dev/link/warning-keys for more information.\n' + ' in div (at **)', - ); + ]); }); it('warns for keys with component stack info', async () => { @@ -134,10 +150,9 @@ describe('ReactElementValidator', () => { return } />; } - await expect(async () => { - const root = ReactDOMClient.createRoot(document.createElement('div')); - await act(() => root.render()); - }).toErrorDev( + const root = ReactDOMClient.createRoot(document.createElement('div')); + await act(() => root.render()); + assertConsoleErrorDev([ 'Each child in a list should have a unique ' + '"key" prop.\n\nCheck the render method of `Component`. See ' + 'https://react.dev/link/warning-keys for more information.\n' + @@ -147,7 +162,7 @@ describe('ReactElementValidator', () => { ? '' : ' in Parent (at **)\n') + ' in GrandParent (at **)', - ); + ]); }); it('does not warn for keys when passing children down', async () => { @@ -187,21 +202,37 @@ describe('ReactElementValidator', () => { }, }; - await expect(async () => { - const root = ReactDOMClient.createRoot(document.createElement('div')); - await act(() => - root.render(React.createElement(ComponentClass, null, iterable)), - ); - }).toErrorDev( + const root = ReactDOMClient.createRoot(document.createElement('div')); + await act(() => + root.render(React.createElement(ComponentClass, null, iterable)), + ); + assertConsoleErrorDev( gate(flag => flag.enableOwnerStacks) - ? 'Each child in a list should have a unique "key" prop.' + ? [ + 'Each child in a list should have a unique "key" prop.\n\n' + + 'Check the render method of `ComponentClass`. It was passed a child from div. ' + + 'See https://react.dev/link/warning-keys for more information.\n' + + ' in ComponentClass (at **)', + ] : // Since each pass generates a new element, it doesn't get marked as // validated and it gets rechecked each time. - [ - 'Each child in a list should have a unique "key" prop.', - 'Each child in a list should have a unique "key" prop.', - 'Each child in a list should have a unique "key" prop.', + 'Each child in a list should have a unique "key" prop.\n\n' + + 'Check the top-level render call using . ' + + 'See https://react.dev/link/warning-keys for more information.\n' + + ' in ComponentClass (at **)', + + 'Each child in a list should have a unique "key" prop.\n\n' + + 'Check the render method of `ComponentClass`. ' + + 'See https://react.dev/link/warning-keys for more information.\n' + + ' in ComponentClass (at **)\n' + + ' in ComponentClass (at **)', + 'Each child in a list should have a unique "key" prop.\n\n' + + 'Check the render method of `ComponentClass`. It was passed a child from div. ' + + 'See https://react.dev/link/warning-keys for more information.\n' + + ' in ComponentClass (at **)\n' + + ' in div (at **)\n' + + ' in ComponentClass (at **)', ], ); }); @@ -254,86 +285,102 @@ describe('ReactElementValidator', () => { function ParentComp() { return React.createElement(MyComp); } - await expect(async () => { - const root = ReactDOMClient.createRoot(document.createElement('div')); - await act(() => root.render(React.createElement(ParentComp))); - }).toErrorDev( + const root = ReactDOMClient.createRoot(document.createElement('div')); + await act(() => root.render(React.createElement(ParentComp))); + assertConsoleErrorDev([ 'Each child in a list should have a unique "key" prop.' + '\n\nCheck the render method of `ParentComp`. It was passed a child from MyComp. ' + 'See https://react.dev/link/warning-keys for more information.\n' + ' in div (at **)\n' + ' in MyComp (at **)\n' + ' in ParentComp (at **)', - ); + ]); }); it('gives a helpful error when passing invalid types', async () => { function Foo() {} const errors = []; - await expect(async () => { - const root = ReactDOMClient.createRoot(document.createElement('div'), { - onUncaughtError(error) { - errors.push(error.message); - }, - }); - const cases = [ - React.createElement(undefined), - React.createElement(null), - React.createElement(true), - React.createElement({x: 17}), - React.createElement({}), - React.createElement(React.createElement('div')), - React.createElement(React.createElement(Foo)), - React.createElement( - React.createElement(React.createContext().Consumer), - ), - React.createElement({$$typeof: 'non-react-thing'}), - ]; - for (let i = 0; i < cases.length; i++) { - await act(() => root.render(cases[i])); - } - }).toErrorDev( - gate(flag => flag.enableOwnerStacks) - ? // We don't need these extra warnings because we already have the errors. - [] - : [ - 'React.createElement: type is invalid -- expected a string ' + - '(for built-in components) or a class/function (for composite ' + - 'components) but got: undefined. You likely forgot to export your ' + - "component from the file it's defined in, or you might have mixed up " + - 'default and named imports.', - 'React.createElement: type is invalid -- expected a string ' + - '(for built-in components) or a class/function (for composite ' + - 'components) but got: null.', - 'React.createElement: type is invalid -- expected a string ' + - '(for built-in components) or a class/function (for composite ' + - 'components) but got: boolean.', - 'React.createElement: type is invalid -- expected a string ' + - '(for built-in components) or a class/function (for composite ' + - 'components) but got: object.', - 'React.createElement: type is invalid -- expected a string ' + - '(for built-in components) or a class/function (for composite ' + - 'components) but got: object. You likely forgot to export your ' + - "component from the file it's defined in, or you might have mixed up " + - 'default and named imports.', - 'React.createElement: type is invalid -- expected a string ' + - '(for built-in components) or a class/function (for composite ' + - 'components) but got:
. Did you accidentally export a JSX literal ' + - 'instead of a component?', - 'React.createElement: type is invalid -- expected a string ' + - '(for built-in components) or a class/function (for composite ' + - 'components) but got: . Did you accidentally export a JSX literal ' + - 'instead of a component?', - 'React.createElement: type is invalid -- expected a string ' + - '(for built-in components) or a class/function (for composite ' + - 'components) but got: . Did you accidentally ' + - 'export a JSX literal instead of a component?', - 'React.createElement: type is invalid -- expected a string ' + - '(for built-in components) or a class/function (for composite ' + - 'components) but got: object.', - ], - {withoutStack: true}, - ); + const root = ReactDOMClient.createRoot(document.createElement('div'), { + onUncaughtError(error) { + errors.push(error.message); + }, + }); + const cases = [ + [ + () => React.createElement(undefined), + 'React.createElement: type is invalid -- expected a string ' + + '(for built-in components) or a class/function (for composite ' + + 'components) but got: undefined. You likely forgot to export your ' + + "component from the file it's defined in, or you might have mixed up " + + 'default and named imports.', + ], + [ + () => React.createElement(null), + 'React.createElement: type is invalid -- expected a string ' + + '(for built-in components) or a class/function (for composite ' + + 'components) but got: null.', + ], + [ + () => React.createElement(true), + 'React.createElement: type is invalid -- expected a string ' + + '(for built-in components) or a class/function (for composite ' + + 'components) but got: boolean.', + ], + [ + () => React.createElement({x: 17}), + 'React.createElement: type is invalid -- expected a string ' + + '(for built-in components) or a class/function (for composite ' + + 'components) but got: object.', + ], + [ + () => React.createElement({}), + 'React.createElement: type is invalid -- expected a string ' + + '(for built-in components) or a class/function (for composite ' + + 'components) but got: object. You likely forgot to export your ' + + "component from the file it's defined in, or you might have mixed up " + + 'default and named imports.', + ], + [ + () => React.createElement(React.createElement('div')), + 'React.createElement: type is invalid -- expected a string ' + + '(for built-in components) or a class/function (for composite ' + + 'components) but got:
. Did you accidentally export a JSX literal ' + + 'instead of a component?', + ], + [ + () => React.createElement(React.createElement(Foo)), + 'React.createElement: type is invalid -- expected a string ' + + '(for built-in components) or a class/function (for composite ' + + 'components) but got: . Did you accidentally export a JSX literal ' + + 'instead of a component?', + ], + [ + () => + React.createElement( + React.createElement(React.createContext().Consumer), + ), + 'React.createElement: type is invalid -- expected a string ' + + '(for built-in components) or a class/function (for composite ' + + 'components) but got: . Did you accidentally ' + + 'export a JSX literal instead of a component?', + ], + [ + () => React.createElement({$$typeof: 'non-react-thing'}), + 'React.createElement: type is invalid -- expected a string ' + + '(for built-in components) or a class/function (for composite ' + + 'components) but got: object.', + ], + ]; + for (let i = 0; i < cases.length; i++) { + await act(async () => root.render(cases[i][0]())); + assertConsoleErrorDev( + gate(flag => flag.enableOwnerStacks) + ? // We don't need these extra warnings because we already have the errors. + [] + : [cases[i][1]], + {withoutStack: true}, + ); + } expect(errors).toEqual( __DEV__ @@ -414,15 +461,14 @@ describe('ReactElementValidator', () => { } await expect(async () => { - await expect(async () => { - const root = ReactDOMClient.createRoot(document.createElement('div')); - await act(() => root.render(React.createElement(ParentComp))); - }).rejects.toThrowError( - 'Element type is invalid: expected a string (for built-in components) ' + - 'or a class/function (for composite components) but got: null.' + - (__DEV__ ? '\n\nCheck the render method of `ParentComp`.' : ''), - ); - }).toErrorDev( + const root = ReactDOMClient.createRoot(document.createElement('div')); + await act(() => root.render(React.createElement(ParentComp))); + }).rejects.toThrowError( + 'Element type is invalid: expected a string (for built-in components) ' + + 'or a class/function (for composite components) but got: null.' + + (__DEV__ ? '\n\nCheck the render method of `ParentComp`.' : ''), + ); + assertConsoleErrorDev( gate(flag => flag.enableOwnerStacks) ? // We don't need these extra warnings because we already have the errors. [] @@ -446,13 +492,13 @@ describe('ReactElementValidator', () => { } } - await expect(async () => { - const root = ReactDOMClient.createRoot(document.createElement('div')); - await act(() => root.render(React.createElement(Foo))); - }).toErrorDev( + const root = ReactDOMClient.createRoot(document.createElement('div')); + await act(() => root.render(React.createElement(Foo))); + assertConsoleErrorDev([ 'Invalid prop `a` supplied to `React.Fragment`. React.Fragment ' + - 'can only have `key` and `children` props.', - ); + 'can only have `key` and `children` props.\n' + + ' in Foo (at **)', + ]); }); it('does not warn when using DOM node as children', async () => { @@ -512,9 +558,8 @@ describe('ReactElementValidator', () => { it('does not blow up on key warning with undefined type', () => { const Foo = undefined; - expect(() => { - void ({[
]}); - }).toErrorDev( + void ({[
]}); + assertConsoleErrorDev( gate(flags => flags.enableOwnerStacks) ? [] : [ diff --git a/packages/react/src/__tests__/ReactJSXElementValidator-test.js b/packages/react/src/__tests__/ReactJSXElementValidator-test.js index 12e54e5c969cc..d4e78f1b1cd46 100644 --- a/packages/react/src/__tests__/ReactJSXElementValidator-test.js +++ b/packages/react/src/__tests__/ReactJSXElementValidator-test.js @@ -14,6 +14,7 @@ let act; let React; let ReactDOMClient; +let assertConsoleErrorDev; describe('ReactJSXElementValidator', () => { let Component; @@ -22,7 +23,7 @@ describe('ReactJSXElementValidator', () => { beforeEach(() => { jest.resetModules(); - act = require('internal-test-utils').act; + ({act, assertConsoleErrorDev} = require('internal-test-utils')); React = require('react'); ReactDOMClient = require('react-dom/client'); @@ -41,14 +42,21 @@ describe('ReactJSXElementValidator', () => { }); it('warns for keys for arrays of elements in children position', async () => { - await expect(async () => { - const container = document.createElement('div'); - const root = ReactDOMClient.createRoot(container); - - await act(() => { - root.render({[, ]}); - }); - }).toErrorDev('Each child in a list should have a unique "key" prop.'); + const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); + + await act(() => { + root.render({[, ]}); + }); + assertConsoleErrorDev([ + gate(flags => flags.enableOwnerStacks) + ? 'Each child in a list should have a unique "key" prop.\n\n' + + 'Check the render method of `Component`. See https://react.dev/link/warning-keys for more information.\n' + + ' in Component (at **)' + : 'Each child in a list should have a unique "key" prop.\n\n' + + 'Check the top-level render call using . See https://react.dev/link/warning-keys for more information.\n' + + ' in Component (at **)', + ]); }); it('warns for keys for arrays of elements with owner info', async () => { @@ -64,20 +72,24 @@ describe('ReactJSXElementValidator', () => { } } - await expect(async () => { - const container = document.createElement('div'); - const root = ReactDOMClient.createRoot(container); - await act(() => { - root.render(); - }); - }).toErrorDev([ + const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); + await act(() => { + root.render(); + }); + assertConsoleErrorDev([ 'Each child in a list should have a unique "key" prop.' + '\n\nCheck the render method of `' + (gate(flag => flag.enableOwnerStacks) ? 'Component' : 'InnerComponent') + '`. ' + - 'It was passed a child from ComponentWrapper. ', + 'It was passed a child from ComponentWrapper. See https://react.dev/link/warning-keys for more information.\n' + + (gate(flag => flag.enableOwnerStacks) + ? ' in ComponentWrapper (at **)' + : ' in Component (at **)\n' + + ' in InnerComponent (at **)\n' + + ' in ComponentWrapper (at **)'), ]); }); @@ -94,22 +106,38 @@ describe('ReactJSXElementValidator', () => { }, }; - await expect(async () => { - const container = document.createElement('div'); - const root = ReactDOMClient.createRoot(container); + const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); - await act(() => { - root.render({iterable}); - }); - }).toErrorDev( + await act(() => { + root.render({iterable}); + }); + assertConsoleErrorDev( gate(flag => flag.enableOwnerStacks) - ? ['Each child in a list should have a unique "key" prop.'] + ? [ + 'Each child in a list should have a unique "key" prop.\n\n' + + 'Check the render method of `Component`. It was passed a child from div. ' + + 'See https://react.dev/link/warning-keys for more information.\n' + + ' in Component (at **)', + ] : // Since each pass generates a new element, it doesn't get marked as // validated and it gets rechecked each time. [ - 'Each child in a list should have a unique "key" prop.', - 'Each child in a list should have a unique "key" prop.', - 'Each child in a list should have a unique "key" prop.', + 'Each child in a list should have a unique "key" prop.\n\n' + + 'Check the top-level render call using . ' + + 'See https://react.dev/link/warning-keys for more information.\n' + + ' in Component (at **)', + 'Each child in a list should have a unique "key" prop.\n\n' + + 'Check the render method of `Component`. ' + + 'See https://react.dev/link/warning-keys for more information.\n' + + ' in Component (at **)\n' + + ' in Component (at **)', + 'Each child in a list should have a unique "key" prop.\n\n' + + 'Check the render method of `Component`. It was passed a child from div. ' + + 'See https://react.dev/link/warning-keys for more information.\n' + + ' in Component (at **)\n' + + ' in div (at **)\n' + + ' in Component (at **)', ], ); }); @@ -198,21 +226,20 @@ describe('ReactJSXElementValidator', () => { return ; } } - await expect(async () => { - const container = document.createElement('div'); - const root = ReactDOMClient.createRoot(container); - - await act(() => { - root.render(); - }); - }).toErrorDev( + const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); + + await act(() => { + root.render(); + }); + assertConsoleErrorDev([ 'Each child in a list should have a unique "key" prop.' + '\n\nCheck the render method of `ParentComp`. It was passed a child from MyComp. ' + 'See https://react.dev/link/warning-keys for more information.\n' + ' in div (at **)\n' + ' in MyComp (at **)\n' + ' in ParentComp (at **)', - ); + ]); }); it('warns for fragments with illegal attributes', async () => { @@ -222,16 +249,16 @@ describe('ReactJSXElementValidator', () => { } } - await expect(async () => { - const container = document.createElement('div'); - const root = ReactDOMClient.createRoot(container); - await act(() => { - root.render(); - }); - }).toErrorDev( + const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); + await act(() => { + root.render(); + }); + assertConsoleErrorDev([ 'Invalid prop `a` supplied to `React.Fragment`. React.Fragment ' + - 'can only have `key` and `children` props.', - ); + 'can only have `key` and `children` props.\n' + + ' in Foo (at **)', + ]); }); it('warns for fragments with refs', async () => { @@ -248,13 +275,16 @@ describe('ReactJSXElementValidator', () => { } } - await expect(async () => { - const container = document.createElement('div'); - const root = ReactDOMClient.createRoot(container); - await act(() => { - root.render(); - }); - }).toErrorDev('Invalid prop `ref` supplied to `React.Fragment`.'); + const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); + await act(() => { + root.render(); + }); + assertConsoleErrorDev([ + 'Invalid prop `ref` supplied to `React.Fragment`.' + + ' React.Fragment can only have `key` and `children` props.\n' + + ' in Foo (at **)', + ]); }); it('does not warn for fragments of multiple elements without keys', async () => { @@ -271,19 +301,24 @@ describe('ReactJSXElementValidator', () => { }); it('warns for fragments of multiple elements with same key', async () => { - await expect(async () => { - const container = document.createElement('div'); - const root = ReactDOMClient.createRoot(container); - await act(() => { - root.render( - <> - 1 - 2 - 3 - , - ); - }); - }).toErrorDev('Encountered two children with the same key, `a`.'); + const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); + await act(() => { + root.render( + <> + 1 + 2 + 3 + , + ); + }); + assertConsoleErrorDev([ + 'Encountered two children with the same key, `a`. ' + + 'Keys should be unique so that components maintain their identity across updates. ' + + 'Non-unique keys may cause children to be duplicated and/or omitted — ' + + 'the behavior is unsupported and could change in a future version.\n' + + ' in span (at **)', + ]); }); it('does not call lazy initializers eagerly', () => { diff --git a/packages/react/src/__tests__/ReactJSXRuntime-test.js b/packages/react/src/__tests__/ReactJSXRuntime-test.js index e3de4dbf5ead5..663c935caf1da 100644 --- a/packages/react/src/__tests__/ReactJSXRuntime-test.js +++ b/packages/react/src/__tests__/ReactJSXRuntime-test.js @@ -14,6 +14,7 @@ let ReactDOMClient; let JSXRuntime; let JSXDEVRuntime; let act; +let assertConsoleErrorDev; // NOTE: Prefer to call the JSXRuntime directly in these tests so we can be // certain that we are testing the runtime behavior, as opposed to the Babel @@ -26,7 +27,7 @@ describe('ReactJSXRuntime', () => { JSXRuntime = require('react/jsx-runtime'); JSXDEVRuntime = require('react/jsx-dev-runtime'); ReactDOMClient = require('react-dom/client'); - act = require('internal-test-utils').act; + ({act, assertConsoleErrorDev} = require('internal-test-utils')); }); it('allows static methods to be called using the type property', () => { @@ -205,41 +206,49 @@ describe('ReactJSXRuntime', () => { }); } } - await expect(async () => { - const root = ReactDOMClient.createRoot(container); - await act(() => { - root.render(JSXRuntime.jsx(Parent, {})); - }); - }).toErrorDev( + const root = ReactDOMClient.createRoot(container); + await act(() => { + root.render(JSXRuntime.jsx(Parent, {})); + }); + assertConsoleErrorDev([ 'Child: `key` is not a prop. Trying to access it will result ' + 'in `undefined` being returned. If you need to access the same ' + 'value within the child component, you should pass it as a different ' + - 'prop. (https://react.dev/link/special-props)', - ); + 'prop. (https://react.dev/link/special-props)\n' + + (gate(flags => flags.enableOwnerStacks) + ? ' in Parent (at **)' + : ' in Child (at **)\n' + + ' in div (at **)\n' + + ' in Parent (at **)'), + ]); }); it('warns when a jsxs is passed something that is not an array', async () => { const container = document.createElement('div'); - await expect(async () => { - const root = ReactDOMClient.createRoot(container); - await act(() => { - root.render(JSXRuntime.jsxs('div', {children: 'foo'}, null)); - }); - }).toErrorDev( - 'React.jsx: Static children should always be an array. ' + - 'You are likely explicitly calling React.jsxs or React.jsxDEV. ' + - 'Use the Babel transform instead.', + const root = ReactDOMClient.createRoot(container); + await act(() => { + root.render(JSXRuntime.jsxs('div', {children: 'foo'}, null)); + }); + assertConsoleErrorDev( + [ + 'React.jsx: Static children should always be an array. ' + + 'You are likely explicitly calling React.jsxs or React.jsxDEV. ' + + 'Use the Babel transform instead.', + ], {withoutStack: true}, ); }); it('should warn when `key` is being accessed on a host element', () => { const element = JSXRuntime.jsxs('div', {}, '3'); - expect(() => void element.props.key).toErrorDev( - 'div: `key` is not a prop. Trying to access it will result ' + - 'in `undefined` being returned. If you need to access the same ' + - 'value within the child component, you should pass it as a different ' + - 'prop. (https://react.dev/link/special-props)', + void element.props.key; + assertConsoleErrorDev( + [ + 'div: `key` is not a prop. Trying to access it will result ' + + 'in `undefined` being returned. If you need to access the same ' + + 'value within the child component, you should pass it as a different ' + + 'prop. (https://react.dev/link/special-props)', + ], {withoutStack: true}, ); }); @@ -263,19 +272,18 @@ describe('ReactJSXRuntime', () => { }); } } - await expect(async () => { - const root = ReactDOMClient.createRoot(container); - await act(() => { - root.render(JSXRuntime.jsx(Parent, {})); - }); - }).toErrorDev( + const root = ReactDOMClient.createRoot(container); + await act(() => { + root.render(JSXRuntime.jsx(Parent, {})); + }); + assertConsoleErrorDev([ 'Each child in a list should have a unique "key" prop.\n\n' + 'Check the render method of `Parent`. See https://react.dev/link/warning-keys for more information.\n' + (gate(flags => flags.enableOwnerStacks) ? '' : ' in Child (at **)\n') + ' in Parent (at **)', - ); + ]); }); it('should warn when keys are passed as part of props', async () => { @@ -292,19 +300,19 @@ describe('ReactJSXRuntime', () => { }); } } - await expect(async () => { - const root = ReactDOMClient.createRoot(container); - await act(() => { - root.render(JSXRuntime.jsx(Parent, {})); - }); - }).toErrorDev( + const root = ReactDOMClient.createRoot(container); + await act(() => { + root.render(JSXRuntime.jsx(Parent, {})); + }); + assertConsoleErrorDev([ 'A props object containing a "key" prop is being spread into JSX:\n' + ' let props = {key: someKey, prop: ...};\n' + ' \n' + 'React keys must be passed directly to JSX without using spread:\n' + ' let props = {prop: ...};\n' + - ' ', - ); + ' \n' + + ' in Parent (at **)', + ]); }); it('should not warn when unkeyed children are passed to jsxs', async () => { @@ -368,13 +376,18 @@ describe('ReactJSXRuntime', () => { key: 'key', }; - let elementWithSpreadKey; - expect(() => { - elementWithSpreadKey = __DEV__ - ? JSXDEVRuntime.jsxDEV('div', configWithKey) - : JSXRuntime.jsx('div', configWithKey); - }).toErrorDev( - 'A props object containing a "key" prop is being spread into JSX', + const elementWithSpreadKey = __DEV__ + ? JSXDEVRuntime.jsxDEV('div', configWithKey) + : JSXRuntime.jsx('div', configWithKey); + assertConsoleErrorDev( + [ + 'A props object containing a "key" prop is being spread into JSX:\n' + + ' let props = {key: someKey, foo: ..., bar: ...};\n' + + '
\n' + + 'React keys must be passed directly to JSX without using spread:\n' + + ' let props = {foo: ..., bar: ...};\n' + + '
', + ], {withoutStack: true}, ); expect(elementWithSpreadKey.props).not.toBe(configWithKey); diff --git a/packages/react/src/__tests__/ReactJSXTransformIntegration-test.js b/packages/react/src/__tests__/ReactJSXTransformIntegration-test.js index f0caf6b494ae1..5075c4a9a0bc5 100644 --- a/packages/react/src/__tests__/ReactJSXTransformIntegration-test.js +++ b/packages/react/src/__tests__/ReactJSXTransformIntegration-test.js @@ -12,6 +12,7 @@ let React; let ReactDOMClient; let act; +let assertConsoleErrorDev; // TODO: Historically this module was used to confirm that the JSX transform // produces the correct output. However, most users (and indeed our own test @@ -29,7 +30,7 @@ describe('ReactJSXTransformIntegration', () => { React = require('react'); ReactDOMClient = require('react-dom/client'); - act = require('internal-test-utils').act; + ({act, assertConsoleErrorDev} = require('internal-test-utils')); Component = class extends React.Component { render() { @@ -112,8 +113,13 @@ describe('ReactJSXTransformIntegration', () => { const ref = React.createRef(); const element = ; expect(element.type).toBe(Component); - expect(() => expect(element.ref).toBe(ref)).toErrorDev( - 'Accessing element.ref was removed in React 19', + expect(element.ref).toBe(ref); + assertConsoleErrorDev( + [ + 'Accessing element.ref was removed in React 19. ref is now a ' + + 'regular prop. It will be removed from the JSX Element ' + + 'type in a future release.', + ], {withoutStack: true}, ); const expectation = {foo: '56', ref}; diff --git a/packages/react/src/__tests__/ReactProfilerComponent-test.internal.js b/packages/react/src/__tests__/ReactProfilerComponent-test.internal.js index d8fc623a3dafc..20395118c5c60 100644 --- a/packages/react/src/__tests__/ReactProfilerComponent-test.internal.js +++ b/packages/react/src/__tests__/ReactProfilerComponent-test.internal.js @@ -14,6 +14,7 @@ let ReactDOMClient; let ReactFeatureFlags; let act; let container; +let assertConsoleErrorDev; function loadModules({ enableProfilerTimer = true, @@ -31,6 +32,7 @@ function loadModules({ ReactDOMClient = require('react-dom/client'); const InternalTestUtils = require('internal-test-utils'); act = InternalTestUtils.act; + assertConsoleErrorDev = InternalTestUtils.assertConsoleErrorDev; } describe('Profiler', () => { @@ -54,12 +56,13 @@ describe('Profiler', () => { if (__DEV__ && enableProfilerTimer) { it('should warn if required params are missing', async () => { const root = ReactDOMClient.createRoot(container); - await expect(async () => { - await act(() => { - root.render(); - }); - }).toErrorDev( - 'Profiler must specify an "id" of type `string` as a prop. Received the type `undefined` instead.', + await act(() => { + root.render(); + }); + assertConsoleErrorDev( + [ + 'Profiler must specify an "id" of type `string` as a prop. Received the type `undefined` instead.', + ], { withoutStack: true, }, diff --git a/packages/react/src/__tests__/ReactPureComponent-test.js b/packages/react/src/__tests__/ReactPureComponent-test.js index 9efdf183ee375..c54945ff250c2 100644 --- a/packages/react/src/__tests__/ReactPureComponent-test.js +++ b/packages/react/src/__tests__/ReactPureComponent-test.js @@ -10,13 +10,13 @@ 'use strict'; let act; - +let assertConsoleErrorDev; let React; let ReactDOMClient; describe('ReactPureComponent', () => { beforeEach(() => { - act = require('internal-test-utils').act; + ({act, assertConsoleErrorDev} = require('internal-test-utils')); React = require('react'); ReactDOMClient = require('react-dom/client'); @@ -90,16 +90,15 @@ describe('ReactPureComponent', () => { const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); - await expect(async () => { - await act(() => { - root.render(); - }); - }).toErrorDev( - '' + - 'Component has a method called shouldComponentUpdate(). ' + + await act(() => { + root.render(); + }); + assertConsoleErrorDev([ + 'Component has a method called shouldComponentUpdate(). ' + 'shouldComponentUpdate should not be used when extending React.PureComponent. ' + - 'Please extend React.Component if shouldComponentUpdate is used.', - ); + 'Please extend React.Component if shouldComponentUpdate is used.\n' + + ' in Component (at **)', + ]); await act(() => { root.render(); }); @@ -133,15 +132,14 @@ describe('ReactPureComponent', () => { } } const root = ReactDOMClient.createRoot(document.createElement('div')); - await expect(async () => { - await act(() => { - root.render(); - }); - }).toErrorDev( - '' + - 'PureComponent has a method called shouldComponentUpdate(). ' + + await act(() => { + root.render(); + }); + assertConsoleErrorDev([ + 'PureComponent has a method called shouldComponentUpdate(). ' + 'shouldComponentUpdate should not be used when extending React.PureComponent. ' + - 'Please extend React.Component if shouldComponentUpdate is used.', - ); + 'Please extend React.Component if shouldComponentUpdate is used.\n' + + ' in PureComponent (at **)', + ]); }); }); diff --git a/packages/react/src/__tests__/ReactStartTransition-test.js b/packages/react/src/__tests__/ReactStartTransition-test.js index 9e689ac6e7105..00387dbb267ac 100644 --- a/packages/react/src/__tests__/ReactStartTransition-test.js +++ b/packages/react/src/__tests__/ReactStartTransition-test.js @@ -12,6 +12,7 @@ let React; let ReactTestRenderer; let act; +let assertConsoleWarnDev; let useState; let useTransition; @@ -22,7 +23,7 @@ describe('ReactStartTransition', () => { jest.resetModules(); React = require('react'); ReactTestRenderer = require('react-test-renderer'); - act = require('internal-test-utils').act; + ({act, assertConsoleWarnDev} = require('internal-test-utils')); useState = React.useState; useTransition = React.useTransition; }); @@ -53,15 +54,14 @@ describe('ReactStartTransition', () => { }); }); - await expect(async () => { - await act(() => { - React.startTransition(() => { - subs.forEach(setState => { - setState(state => state + 1); - }); + await act(() => { + React.startTransition(() => { + subs.forEach(setState => { + setState(state => state + 1); }); }); - }).toWarnDev( + }); + assertConsoleWarnDev( [ 'Detected a large number of updates inside startTransition. ' + 'If this is due to a subscription please re-write it to use React provided hooks. ' + @@ -70,15 +70,14 @@ describe('ReactStartTransition', () => { {withoutStack: true}, ); - await expect(async () => { - await act(() => { - triggerHookTransition(() => { - subs.forEach(setState => { - setState(state => state + 1); - }); + await act(() => { + triggerHookTransition(() => { + subs.forEach(setState => { + setState(state => state + 1); }); }); - }).toWarnDev( + }); + assertConsoleWarnDev( [ 'Detected a large number of updates inside startTransition. ' + 'If this is due to a subscription please re-write it to use React provided hooks. ' + diff --git a/packages/react/src/__tests__/ReactStrictMode-test.js b/packages/react/src/__tests__/ReactStrictMode-test.js index f28c70a871665..6d44a28881fad 100644 --- a/packages/react/src/__tests__/ReactStrictMode-test.js +++ b/packages/react/src/__tests__/ReactStrictMode-test.js @@ -19,6 +19,7 @@ let useMemo; let useState; let useReducer; let assertConsoleErrorDev; +let assertConsoleWarnDev; describe('ReactStrictMode', () => { beforeEach(() => { @@ -27,7 +28,11 @@ describe('ReactStrictMode', () => { ReactDOM = require('react-dom'); ReactDOMClient = require('react-dom/client'); ReactDOMServer = require('react-dom/server'); - ({act, assertConsoleErrorDev} = require('internal-test-utils')); + ({ + act, + assertConsoleErrorDev, + assertConsoleWarnDev, + } = require('internal-test-utils')); useMemo = React.useMemo; useState = React.useState; useReducer = React.useReducer; @@ -40,20 +45,19 @@ describe('ReactStrictMode', () => { const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); - await expect(async () => { - await act(() => { - root.render( - - - , - ); - }); - }).toErrorDev( + await act(() => { + root.render( + + + , + ); + }); + assertConsoleErrorDev([ 'Invalid ARIA attribute `ariaTypo`. ' + 'ARIA attributes follow the pattern aria-* and must be lowercase.\n' + ' in div (at **)\n' + ' in Foo (at **)', - ); + ]); }); it('should appear in the SSR component stack', () => { @@ -61,18 +65,17 @@ describe('ReactStrictMode', () => { return
; } - expect(() => { - ReactDOMServer.renderToString( - - - , - ); - }).toErrorDev( + ReactDOMServer.renderToString( + + + , + ); + assertConsoleErrorDev([ 'Invalid ARIA attribute `ariaTypo`. ' + 'ARIA attributes follow the pattern aria-* and must be lowercase.\n' + ' in div (at **)\n' + ' in Foo (at **)', - ); + ]); }); // @gate __DEV__ @@ -620,9 +623,8 @@ describe('Concurrent Mode', () => { const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); - await expect( - async () => await act(() => root.render()), - ).toErrorDev( + await act(() => root.render()); + assertConsoleErrorDev( [ `Using UNSAFE_componentWillMount in strict mode is not recommended and may indicate bugs in your code. See https://react.dev/link/unsafe-component-lifecycles for details. @@ -681,31 +683,29 @@ Please update the following components: App`, const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); - await expect(async () => { - await expect( - async () => await act(() => root.render()), - ).toErrorDev( - [ - `Using UNSAFE_componentWillMount in strict mode is not recommended and may indicate bugs in your code. See https://react.dev/link/unsafe-component-lifecycles for details. + await act(() => root.render()); + assertConsoleErrorDev( + [ + `Using UNSAFE_componentWillMount in strict mode is not recommended and may indicate bugs in your code. See https://react.dev/link/unsafe-component-lifecycles for details. * Move code with side effects to componentDidMount, and set initial state in the constructor. Please update the following components: App`, - `Using UNSAFE_componentWillReceiveProps in strict mode is not recommended and may indicate bugs in your code. See https://react.dev/link/unsafe-component-lifecycles for details. + `Using UNSAFE_componentWillReceiveProps in strict mode is not recommended and may indicate bugs in your code. See https://react.dev/link/unsafe-component-lifecycles for details. * Move data fetching code or side effects to componentDidUpdate. * If you're updating state whenever props change, refactor your code to use memoization techniques or move it to static getDerivedStateFromProps. Learn more at: https://react.dev/link/derived-state Please update the following components: Child`, - `Using UNSAFE_componentWillUpdate in strict mode is not recommended and may indicate bugs in your code. See https://react.dev/link/unsafe-component-lifecycles for details. + `Using UNSAFE_componentWillUpdate in strict mode is not recommended and may indicate bugs in your code. See https://react.dev/link/unsafe-component-lifecycles for details. * Move data fetching code or side effects to componentDidUpdate. Please update the following components: App`, - ], - {withoutStack: true}, - ); - }).toWarnDev( + ], + {withoutStack: true}, + ); + assertConsoleWarnDev( [ `componentWillMount has been renamed, and is not recommended for use. See https://react.dev/link/unsafe-component-lifecycles for details. @@ -752,17 +752,25 @@ Please update the following components: Parent`, const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); - await expect(async () => { - await act(() => root.render()); - }).toErrorDev( - 'Using UNSAFE_componentWillMount in strict mode is not recommended', + await act(() => root.render()); + assertConsoleErrorDev( + [ + 'Using UNSAFE_componentWillMount in strict mode is not recommended and may indicate bugs in your code. ' + + 'See https://react.dev/link/unsafe-component-lifecycles for details.\n\n' + + '* Move code with side effects to componentDidMount, and set initial state in the constructor.\n\n' + + 'Please update the following components: Foo', + ], {withoutStack: true}, ); - await expect(async () => { - await act(() => root.render()); - }).toErrorDev( - 'Using UNSAFE_componentWillMount in strict mode is not recommended', + await act(() => root.render()); + assertConsoleErrorDev( + [ + 'Using UNSAFE_componentWillMount in strict mode is not recommended and may indicate bugs in your code. ' + + 'See https://react.dev/link/unsafe-component-lifecycles for details.\n\n' + + '* Move code with side effects to componentDidMount, and set initial state in the constructor.\n\n' + + 'Please update the following components: Bar', + ], {withoutStack: true}, ); @@ -810,12 +818,20 @@ Please update the following components: Parent`, const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); - await expect(async () => { - await act(() => { - root.render(); - }); - }).toErrorDev( - 'Using UNSAFE_componentWillReceiveProps in strict mode is not recommended', + await act(() => { + root.render(); + }); + assertConsoleErrorDev( + [ + 'Using UNSAFE_componentWillReceiveProps in strict mode is not recommended ' + + 'and may indicate bugs in your code. ' + + 'See https://react.dev/link/unsafe-component-lifecycles for details.\n\n' + + '* Move data fetching code or side effects to componentDidUpdate.\n' + + "* If you're updating state whenever props change, " + + 'refactor your code to use memoization techniques or move it to ' + + 'static getDerivedStateFromProps. Learn more at: https://react.dev/link/derived-state\n\n' + + 'Please update the following components: Bar, Foo', + ], {withoutStack: true}, ); diff --git a/packages/react/src/__tests__/ReactTypeScriptClass-test.ts b/packages/react/src/__tests__/ReactTypeScriptClass-test.ts index 00e3a8b3ff8a8..5f51cc6f38c77 100644 --- a/packages/react/src/__tests__/ReactTypeScriptClass-test.ts +++ b/packages/react/src/__tests__/ReactTypeScriptClass-test.ts @@ -16,9 +16,11 @@ import ReactDOM = require('react-dom'); import ReactDOMClient = require('react-dom/client'); import PropTypes = require('prop-types'); import ReactFeatureFlags = require('shared/ReactFeatureFlags'); +import TestUtils = require('internal-test-utils'); // Before Each - +const assertConsoleErrorDev = TestUtils.assertConsoleErrorDev; +const assertConsoleWarnDev = TestUtils.assertConsoleWarnDev; let container; let root; let attachedListener = null; @@ -313,19 +315,19 @@ class ClassicRefs extends React.Component { // Describe the actual test cases. -describe('ReactTypeScriptClass', function() { - beforeEach(function() { +describe('ReactTypeScriptClass', function () { + beforeEach(function () { container = document.createElement('div'); root = ReactDOMClient.createRoot(container); attachedListener = null; renderedName = null; }); - it('preserves the name of the class for use in error messages', function() { + it('preserves the name of the class for use in error messages', function () { expect(Empty.name).toBe('Empty'); }); - it('throws if no render function is defined', function() { + it('throws if no render function is defined', function () { class Foo extends React.Component {} const caughtErrors = []; function errorHandler(event) { @@ -334,14 +336,15 @@ describe('ReactTypeScriptClass', function() { } window.addEventListener('error', errorHandler); try { - expect(() => { - ReactDOM.flushSync(() => root.render(React.createElement(Empty))) - }).toErrorDev([ + ReactDOM.flushSync(() => root.render(React.createElement(Empty))); + assertConsoleErrorDev([ // A failed component renders twice in DEV in concurrent mode 'No `render` method found on the Empty instance: ' + - 'you may have forgotten to define `render`.', + 'you may have forgotten to define `render`.\n' + + ' in Empty (at **)', 'No `render` method found on the Empty instance: ' + - 'you may have forgotten to define `render`.', + 'you may have forgotten to define `render`.\n' + + ' in Empty (at **)', ]); } finally { window.removeEventListener('error', errorHandler); @@ -349,31 +352,31 @@ describe('ReactTypeScriptClass', function() { expect(caughtErrors.length).toBe(1); }); - it('renders a simple stateless component with prop', function() { + it('renders a simple stateless component with prop', function () { test(React.createElement(SimpleStateless, {bar: 'foo'}), 'DIV', 'foo'); test(React.createElement(SimpleStateless, {bar: 'bar'}), 'DIV', 'bar'); }); - it('renders based on state using initial values in this.props', function() { + it('renders based on state using initial values in this.props', function () { test( React.createElement(InitialState, {initialValue: 'foo'}), 'SPAN', - 'foo' + 'foo', ); }); - it('renders based on state using props in the constructor', function() { + it('renders based on state using props in the constructor', function () { const ref = React.createRef(); test( React.createElement(StateBasedOnProps, {initialValue: 'foo', ref: ref}), 'DIV', - 'foo' + 'foo', ); ReactDOM.flushSync(() => ref.current.changeState()); test(React.createElement(StateBasedOnProps), 'SPAN', 'bar'); }); - it('sets initial state with value returned by static getDerivedStateFromProps', function() { + it('sets initial state with value returned by static getDerivedStateFromProps', function () { class Foo extends React.Component { state = { foo: null, @@ -394,7 +397,7 @@ describe('ReactTypeScriptClass', function() { test(React.createElement(Foo, {foo: 'foo'}), 'DIV', 'foo bar'); }); - it('warns if getDerivedStateFromProps is not static', function() { + it('warns if getDerivedStateFromProps is not static', function () { class Foo extends React.Component { getDerivedStateFromProps() { return {}; @@ -403,17 +406,17 @@ describe('ReactTypeScriptClass', function() { return React.createElement('div', {}); } } - expect(function() { - ReactDOM.flushSync(() => - root.render(React.createElement(Foo, {foo: 'foo'})) - ); - }).toErrorDev( - 'Foo: getDerivedStateFromProps() is defined as an instance method ' + - 'and will be ignored. Instead, declare it as a static method.' + ReactDOM.flushSync(() => + root.render(React.createElement(Foo, {foo: 'foo'})), ); + assertConsoleErrorDev([ + 'Foo: getDerivedStateFromProps() is defined as an instance method ' + + 'and will be ignored. Instead, declare it as a static method.\n' + + ' in Foo (at **)', + ]); }); - it('warns if getDerivedStateFromError is not static', function() { + it('warns if getDerivedStateFromError is not static', function () { class Foo extends React.Component { getDerivedStateFromError() { return {}; @@ -422,34 +425,34 @@ describe('ReactTypeScriptClass', function() { return React.createElement('div'); } } - expect(function() { - ReactDOM.flushSync(() => - root.render(React.createElement(Foo, {foo: 'foo'})) - ); - }).toErrorDev( - 'Foo: getDerivedStateFromError() is defined as an instance method ' + - 'and will be ignored. Instead, declare it as a static method.' + ReactDOM.flushSync(() => + root.render(React.createElement(Foo, {foo: 'foo'})), ); + assertConsoleErrorDev([ + 'Foo: getDerivedStateFromError() is defined as an instance method ' + + 'and will be ignored. Instead, declare it as a static method.\n' + + ' in Foo (at **)', + ]); }); - it('warns if getSnapshotBeforeUpdate is static', function() { + it('warns if getSnapshotBeforeUpdate is static', function () { class Foo extends React.Component { static getSnapshotBeforeUpdate() {} render() { return React.createElement('div', {}); } } - expect(function() { - ReactDOM.flushSync(() => - root.render(React.createElement(Foo, {foo: 'foo'})) - ); - }).toErrorDev( - 'Foo: getSnapshotBeforeUpdate() is defined as a static method ' + - 'and will be ignored. Instead, declare it as an instance method.' + ReactDOM.flushSync(() => + root.render(React.createElement(Foo, {foo: 'foo'})), ); + assertConsoleErrorDev([ + 'Foo: getSnapshotBeforeUpdate() is defined as a static method ' + + 'and will be ignored. Instead, declare it as an instance method.\n' + + ' in Foo (at **)', + ]); }); - it('warns if state not initialized before static getDerivedStateFromProps', function() { + it('warns if state not initialized before static getDerivedStateFromProps', function () { class Foo extends React.Component { static getDerivedStateFromProps(nextProps, prevState) { return { @@ -463,19 +466,19 @@ describe('ReactTypeScriptClass', function() { }); } } - expect(function() { - ReactDOM.flushSync(() => - root.render(React.createElement(Foo, {foo: 'foo'})) - ); - }).toErrorDev( + ReactDOM.flushSync(() => + root.render(React.createElement(Foo, {foo: 'foo'})), + ); + assertConsoleErrorDev([ '`Foo` uses `getDerivedStateFromProps` but its initial state is ' + 'undefined. This is not recommended. Instead, define the initial state by ' + 'assigning an object to `this.state` in the constructor of `Foo`. ' + - 'This ensures that `getDerivedStateFromProps` arguments have a consistent shape.' - ); + 'This ensures that `getDerivedStateFromProps` arguments have a consistent shape.\n' + + ' in Foo (at **)', + ]); }); - it('updates initial state with values returned by static getDerivedStateFromProps', function() { + it('updates initial state with values returned by static getDerivedStateFromProps', function () { class Foo extends React.Component { state = { foo: 'foo', @@ -495,7 +498,7 @@ describe('ReactTypeScriptClass', function() { test(React.createElement(Foo), 'DIV', 'not-foo bar'); }); - it('renders updated state with values returned by static getDerivedStateFromProps', function() { + it('renders updated state with values returned by static getDerivedStateFromProps', function () { class Foo extends React.Component { state = { value: 'initial', @@ -517,66 +520,80 @@ describe('ReactTypeScriptClass', function() { }); if (!ReactFeatureFlags.disableLegacyContext) { - it('renders based on context in the constructor', function() { - expect(() => test(React.createElement(ProvideChildContextTypes), 'SPAN', 'foo')).toErrorDev([ - 'ProvideChildContextTypes uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead.', - 'StateBasedOnContext uses the legacy contextTypes API which will soon be removed. Use React.createContext() with static contextType instead.' + it('renders based on context in the constructor', function () { + test(React.createElement(ProvideChildContextTypes), 'SPAN', 'foo'); + assertConsoleErrorDev([ + 'ProvideChildContextTypes uses the legacy childContextTypes API which will soon be removed. ' + + 'Use React.createContext() instead. (https://react.dev/link/legacy-context)\n' + + ' in ProvideChildContextTypes (at **)', + 'StateBasedOnContext uses the legacy contextTypes API which will soon be removed. ' + + 'Use React.createContext() with static contextType instead. (https://react.dev/link/legacy-context)\n' + + (ReactFeatureFlags.enableOwnerStacks + ? ' in ProvideChildContextTypes.Object..ProvideChildContextTypes (at **)' + : ' in StateBasedOnContext (at **)\n') + + ' in ProvideChildContextTypes (at **)', ]); }); } - it('renders only once when setting state in componentWillMount', function() { + it('renders only once when setting state in componentWillMount', function () { renderCount = 0; test(React.createElement(RenderOnce, {initialValue: 'foo'}), 'SPAN', 'bar'); expect(renderCount).toBe(1); }); - it('should warn with non-object in the initial state property', function() { - expect(() => test(React.createElement(ArrayState), 'SPAN', '')).toErrorDev( - 'ArrayState.state: must be set to an object or null' - ); - expect(() => test(React.createElement(StringState), 'SPAN', '')).toErrorDev( - 'StringState.state: must be set to an object or null' - ); - expect(() => test(React.createElement(NumberState), 'SPAN', '')).toErrorDev( - 'NumberState.state: must be set to an object or null' - ); + it('should warn with non-object in the initial state property', function () { + test(React.createElement(ArrayState), 'SPAN', ''); + assertConsoleErrorDev([ + 'ArrayState.state: must be set to an object or null\n' + + ' in ArrayState (at **)', + ]); + test(React.createElement(StringState), 'SPAN', ''); + assertConsoleErrorDev([ + 'StringState.state: must be set to an object or null\n' + + ' in StringState (at **)', + ]); + test(React.createElement(NumberState), 'SPAN', ''); + assertConsoleErrorDev([ + 'NumberState.state: must be set to an object or null\n' + + ' in NumberState (at **)', + ]); }); - it('should render with null in the initial state property', function() { + it('should render with null in the initial state property', function () { test(React.createElement(NullState), 'SPAN', ''); }); - it('setState through an event handler', function() { + it('setState through an event handler', function () { test( React.createElement(BoundEventHandler, {initialValue: 'foo'}), 'DIV', - 'foo' + 'foo', ); ReactDOM.flushSync(() => attachedListener()); expect(renderedName).toBe('bar'); }); - it('should not implicitly bind event handlers', function() { + it('should not implicitly bind event handlers', function () { test( React.createElement(UnboundEventHandler, {initialValue: 'foo'}), 'DIV', - 'foo' + 'foo', ); expect(attachedListener).toThrow(); }); - it('renders using forceUpdate even when there is no state', function() { + it('renders using forceUpdate even when there is no state', function () { test( React.createElement(ForceUpdateWithNoState, {initialValue: 'foo'}), 'DIV', - 'foo' + 'foo', ); ReactDOM.flushSync(() => attachedListener()); expect(renderedName).toBe('bar'); }); - it('will call all the normal life cycle methods', function() { + it('will call all the normal life cycle methods', function () { lifeCycles = []; test(React.createElement(NormalLifeCycles, {value: 'foo'}), 'SPAN', 'foo'); expect(lifeCycles).toEqual(['will-mount', 'did-mount']); @@ -604,22 +621,29 @@ describe('ReactTypeScriptClass', function() { it( 'warns when classic properties are defined on the instance, ' + 'but does not invoke them.', - function() { + function () { getInitialStateWasCalled = false; getDefaultPropsWasCalled = false; - expect(() => - test(React.createElement(ClassicProperties), 'SPAN', 'foo') - ).toErrorDev([ - 'getInitialState was defined on ClassicProperties, ' + - 'a plain JavaScript class.', - 'getDefaultProps was defined on ClassicProperties, ' + - 'a plain JavaScript class.', - 'contextTypes was defined as an instance property on ClassicProperties.', - 'contextType was defined as an instance property on ClassicProperties.', + test(React.createElement(ClassicProperties), 'SPAN', 'foo'); + assertConsoleErrorDev([ + 'getInitialState was defined on ClassicProperties, a plain JavaScript class. ' + + 'This is only supported for classes created using React.createClass. ' + + 'Did you mean to define a state property instead?\n' + + ' in ClassicProperties (at **)', + 'getDefaultProps was defined on ClassicProperties, a plain JavaScript class. ' + + 'This is only supported for classes created using React.createClass. ' + + 'Use a static property to define defaultProps instead.\n' + + ' in ClassicProperties (at **)', + 'contextType was defined as an instance property on ClassicProperties. ' + + 'Use a static property to define contextType instead.\n' + + ' in ClassicProperties (at **)', + 'contextTypes was defined as an instance property on ClassicProperties. ' + + 'Use a static property to define contextTypes instead.\n' + + ' in ClassicProperties (at **)', ]); expect(getInitialStateWasCalled).toBe(false); expect(getDefaultPropsWasCalled).toBe(false); - } + }, ); } @@ -638,63 +662,73 @@ describe('ReactTypeScriptClass', function() { } test(React.createElement(Example), 'SPAN', 'foo'); - } + }, ); - it('should warn when misspelling shouldComponentUpdate', function() { - expect(() => - test(React.createElement(MisspelledComponent1), 'SPAN', 'foo') - ).toErrorDev( - '' + - 'MisspelledComponent1 has a method called componentShouldUpdate(). Did ' + + it('should warn when misspelling shouldComponentUpdate', function () { + test(React.createElement(MisspelledComponent1), 'SPAN', 'foo'); + assertConsoleErrorDev([ + 'MisspelledComponent1 has a method called componentShouldUpdate(). Did ' + 'you mean shouldComponentUpdate()? The name is phrased as a question ' + - 'because the function is expected to return a value.' - ); + 'because the function is expected to return a value.\n' + + ' in MisspelledComponent1 (at **)', + ]); }); - it('should warn when misspelling componentWillReceiveProps', function() { - expect(() => - test(React.createElement(MisspelledComponent2), 'SPAN', 'foo') - ).toErrorDev( - '' + - 'MisspelledComponent2 has a method called componentWillRecieveProps(). ' + - 'Did you mean componentWillReceiveProps()?' - ); + it('should warn when misspelling componentWillReceiveProps', function () { + test(React.createElement(MisspelledComponent2), 'SPAN', 'foo'); + assertConsoleErrorDev([ + 'MisspelledComponent2 has a method called componentWillRecieveProps(). ' + + 'Did you mean componentWillReceiveProps()?\n' + + ' in MisspelledComponent2 (at **)', + ]); }); - it('should warn when misspelling UNSAFE_componentWillReceiveProps', function() { - expect(() => - test(React.createElement(MisspelledComponent3), 'SPAN', 'foo') - ).toErrorDev( - '' + - 'MisspelledComponent3 has a method called UNSAFE_componentWillRecieveProps(). ' + - 'Did you mean UNSAFE_componentWillReceiveProps()?' - ); + it('should warn when misspelling UNSAFE_componentWillReceiveProps', function () { + test(React.createElement(MisspelledComponent3), 'SPAN', 'foo'); + assertConsoleErrorDev([ + 'MisspelledComponent3 has a method called UNSAFE_componentWillRecieveProps(). ' + + 'Did you mean UNSAFE_componentWillReceiveProps()?\n' + + ' in MisspelledComponent3 (at **)', + ]); }); - it('should throw AND warn when trying to access classic APIs', function() { + it('should throw AND warn when trying to access classic APIs', function () { const ref = React.createRef(); test(React.createElement(Inner, {name: 'foo', ref: ref}), 'DIV', 'foo'); - expect(() => - expect(() => ref.current.replaceState({})).toThrow() - ).toWarnDev( - 'replaceState(...) is deprecated in plain JavaScript React classes', - {withoutStack: true} + expect(() => ref.current.replaceState({})).toThrow(); + assertConsoleWarnDev( + [ + 'replaceState(...) is deprecated in plain JavaScript React classes. ' + + 'Refactor your code to use setState instead (see https://github.com/facebook/react/issues/3236).', + ], + {withoutStack: true}, ); - expect(() => - expect(() => ref.current.isMounted()).toThrow() - ).toWarnDev( - 'isMounted(...) is deprecated in plain JavaScript React classes', - {withoutStack: true} + expect(() => ref.current.isMounted()).toThrow(); + assertConsoleWarnDev( + [ + 'isMounted(...) is deprecated in plain JavaScript React classes. ' + + 'Instead, make sure to clean up subscriptions and pending requests in ' + + 'componentWillUnmount to prevent memory leaks.', + ], + {withoutStack: true}, ); }); if (!ReactFeatureFlags.disableLegacyContext) { it('supports this.context passed via getChildContext', () => { - expect(() => test(React.createElement(ProvideContext), 'DIV', 'bar-through-context')).toErrorDev([ - 'ProvideContext uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead.', - 'ReadContext uses the legacy contextTypes API which will soon be removed. Use React.createContext() with static contextType instead.', -] ); + test(React.createElement(ProvideContext), 'DIV', 'bar-through-context'); + assertConsoleErrorDev([ + 'ProvideContext uses the legacy childContextTypes API which will soon be removed. ' + + 'Use React.createContext() instead. (https://react.dev/link/legacy-context)\n' + + ' in ProvideContext (at **)', + 'ReadContext uses the legacy contextTypes API which will soon be removed. ' + + 'Use React.createContext() with static contextType instead. (https://react.dev/link/legacy-context)\n' + + (ReactFeatureFlags.enableOwnerStacks + ? ' in ProvideContext.Object..ProvideContext (at **)' + : ' in ReadContext (at **)\n') + + ' in ProvideContext (at **)', + ]); }); } }); diff --git a/packages/react/src/__tests__/createReactClassIntegration-test.js b/packages/react/src/__tests__/createReactClassIntegration-test.js index 4aabc11b52e8d..6bd3f0692168c 100644 --- a/packages/react/src/__tests__/createReactClassIntegration-test.js +++ b/packages/react/src/__tests__/createReactClassIntegration-test.js @@ -11,6 +11,7 @@ let act; let assertConsoleErrorDev; +let assertConsoleWarnDev; let PropTypes; let React; @@ -20,7 +21,11 @@ let createReactClass; describe('create-react-class-integration', () => { beforeEach(() => { jest.resetModules(); - ({act, assertConsoleErrorDev} = require('internal-test-utils')); + ({ + act, + assertConsoleErrorDev, + assertConsoleWarnDev, + } = require('internal-test-utils')); PropTypes = require('prop-types'); React = require('react'); ReactDOMClient = require('react-dom/client'); @@ -53,124 +58,130 @@ describe('create-react-class-integration', () => { }); it('should warn on invalid prop types', () => { - expect(() => - createReactClass({ - displayName: 'Component', - propTypes: { - prop: null, - }, - render: function () { - return {this.props.prop}; - }, - }), - ).toErrorDev( - 'Component: prop type `prop` is invalid; ' + - 'it must be a function, usually from React.PropTypes.', + createReactClass({ + displayName: 'Component', + propTypes: { + prop: null, + }, + render: function () { + return {this.props.prop}; + }, + }); + assertConsoleErrorDev( + [ + 'Warning: Component: prop type `prop` is invalid; ' + + 'it must be a function, usually from React.PropTypes.', + ], {withoutStack: true}, ); }); it('should warn on invalid context types', () => { - expect(() => - createReactClass({ - displayName: 'Component', - contextTypes: { - prop: null, - }, - render: function () { - return {this.props.prop}; - }, - }), - ).toErrorDev( - 'Component: context type `prop` is invalid; ' + - 'it must be a function, usually from React.PropTypes.', + createReactClass({ + displayName: 'Component', + contextTypes: { + prop: null, + }, + render: function () { + return {this.props.prop}; + }, + }); + assertConsoleErrorDev( + [ + 'Warning: Component: context type `prop` is invalid; ' + + 'it must be a function, usually from React.PropTypes.', + ], {withoutStack: true}, ); }); it('should throw on invalid child context types', () => { - expect(() => - createReactClass({ - displayName: 'Component', - childContextTypes: { - prop: null, - }, - render: function () { - return {this.props.prop}; - }, - }), - ).toErrorDev( - 'Component: child context type `prop` is invalid; ' + - 'it must be a function, usually from React.PropTypes.', + createReactClass({ + displayName: 'Component', + childContextTypes: { + prop: null, + }, + render: function () { + return {this.props.prop}; + }, + }); + assertConsoleErrorDev( + [ + 'Warning: Component: child context type `prop` is invalid; it must be a function, usually from React.PropTypes.', + ], {withoutStack: true}, ); }); it('should warn when misspelling shouldComponentUpdate', () => { - expect(() => - createReactClass({ - componentShouldUpdate: function () { - return false; - }, - render: function () { - return
; - }, - }), - ).toErrorDev( - 'A component has a method called componentShouldUpdate(). Did you ' + - 'mean shouldComponentUpdate()? The name is phrased as a question ' + - 'because the function is expected to return a value.', + createReactClass({ + componentShouldUpdate: function () { + return false; + }, + render: function () { + return
; + }, + }); + assertConsoleErrorDev( + [ + 'Warning: A component has a method called componentShouldUpdate(). Did you ' + + 'mean shouldComponentUpdate()? The name is phrased as a question ' + + 'because the function is expected to return a value.', + ], {withoutStack: true}, ); - expect(() => - createReactClass({ - displayName: 'NamedComponent', - componentShouldUpdate: function () { - return false; - }, - render: function () { - return
; - }, - }), - ).toErrorDev( - 'NamedComponent has a method called componentShouldUpdate(). Did you ' + - 'mean shouldComponentUpdate()? The name is phrased as a question ' + - 'because the function is expected to return a value.', + createReactClass({ + displayName: 'NamedComponent', + componentShouldUpdate: function () { + return false; + }, + render: function () { + return
; + }, + }); + assertConsoleErrorDev( + [ + 'Warning: NamedComponent has a method called componentShouldUpdate(). Did you ' + + 'mean shouldComponentUpdate()? The name is phrased as a question ' + + 'because the function is expected to return a value.', + ], {withoutStack: true}, ); }); it('should warn when misspelling componentWillReceiveProps', () => { - expect(() => - createReactClass({ - componentWillRecieveProps: function () { - return false; - }, - render: function () { - return
; - }, - }), - ).toErrorDev( - 'A component has a method called componentWillRecieveProps(). Did you ' + - 'mean componentWillReceiveProps()?', + createReactClass({ + componentWillRecieveProps: function () { + return false; + }, + render: function () { + return
; + }, + }); + assertConsoleErrorDev( + [ + 'Warning: A component has a method called componentWillRecieveProps(). Did you ' + + 'mean componentWillReceiveProps()?', + ], {withoutStack: true}, ); }); it('should warn when misspelling UNSAFE_componentWillReceiveProps', () => { - expect(() => - createReactClass({ - UNSAFE_componentWillRecieveProps: function () { - return false; - }, - render: function () { - return
; - }, - }), - ).toErrorDev( - 'A component has a method called UNSAFE_componentWillRecieveProps(). ' + - 'Did you mean UNSAFE_componentWillReceiveProps()?', + createReactClass({ + UNSAFE_componentWillRecieveProps: function () { + return false; + }, + render: function () { + return
; + }, + }); + assertConsoleErrorDev( + [ + 'Warning: A component has a method called UNSAFE_componentWillRecieveProps(). ' + + 'Did you mean UNSAFE_componentWillReceiveProps()?', + ], {withoutStack: true}, ); }); @@ -201,23 +212,22 @@ describe('create-react-class-integration', () => { // TODO: Consider actually moving these to statics or drop this unit test. // eslint-disable-next-line jest/no-disabled-tests it.skip('should warn when using deprecated non-static spec keys', () => { - expect(() => - createReactClass({ - mixins: [{}], - propTypes: { - foo: PropTypes.string, - }, - contextTypes: { - foo: PropTypes.string, - }, - childContextTypes: { - foo: PropTypes.string, - }, - render: function () { - return
; - }, - }), - ).toErrorDev([ + createReactClass({ + mixins: [{}], + propTypes: { + foo: PropTypes.string, + }, + contextTypes: { + foo: PropTypes.string, + }, + childContextTypes: { + foo: PropTypes.string, + }, + render: function () { + return
; + }, + }); + assertConsoleErrorDev([ '`mixins` is now a static property and should ' + 'be defined inside "statics".', '`propTypes` is now a static property and should ' + @@ -399,9 +409,12 @@ describe('create-react-class-integration', () => { }, }); - expect(() => expect(() => Component()).toThrow()).toErrorDev( - 'Something is calling a React component directly. Use a ' + - 'factory or JSX instead. See: https://fb.me/react-legacyfactory', + expect(() => Component()).toThrow(); + assertConsoleErrorDev( + [ + 'Warning: Something is calling a React component directly. Use a ' + + 'factory or JSX instead. See: https://fb.me/react-legacyfactory', + ], {withoutStack: true}, ); }); @@ -504,15 +517,15 @@ describe('create-react-class-integration', () => { return
; }, }); - await expect(async () => { - const root = ReactDOMClient.createRoot(document.createElement('div')); - await act(() => { - root.render(); - }); - }).toErrorDev( + const root = ReactDOMClient.createRoot(document.createElement('div')); + await act(() => { + root.render(); + }); + assertConsoleErrorDev([ 'Foo: getDerivedStateFromProps() is defined as an instance method ' + - 'and will be ignored. Instead, declare it as a static method.', - ); + 'and will be ignored. Instead, declare it as a static method.\n' + + ' in Foo (at **)', + ]); }); it('warns if getDerivedStateFromError is not static', async () => { @@ -525,15 +538,15 @@ describe('create-react-class-integration', () => { return
; }, }); - await expect(async () => { - const root = ReactDOMClient.createRoot(document.createElement('div')); - await act(() => { - root.render(); - }); - }).toErrorDev( + const root = ReactDOMClient.createRoot(document.createElement('div')); + await act(() => { + root.render(); + }); + assertConsoleErrorDev([ 'Foo: getDerivedStateFromError() is defined as an instance method ' + - 'and will be ignored. Instead, declare it as a static method.', - ); + 'and will be ignored. Instead, declare it as a static method.\n' + + ' in Foo (at **)', + ]); }); it('warns if getSnapshotBeforeUpdate is static', async () => { @@ -548,15 +561,15 @@ describe('create-react-class-integration', () => { return
; }, }); - await expect(async () => { - const root = ReactDOMClient.createRoot(document.createElement('div')); - await act(() => { - root.render(); - }); - }).toErrorDev( + const root = ReactDOMClient.createRoot(document.createElement('div')); + await act(() => { + root.render(); + }); + assertConsoleErrorDev([ 'Foo: getSnapshotBeforeUpdate() is defined as a static method ' + - 'and will be ignored. Instead, declare it as an instance method.', - ); + 'and will be ignored. Instead, declare it as an instance method.\n' + + ' in Foo (at **)', + ]); }); it('should warn if state is not properly initialized before getDerivedStateFromProps', async () => { @@ -571,17 +584,17 @@ describe('create-react-class-integration', () => { return null; }, }); - await expect(async () => { - const root = ReactDOMClient.createRoot(document.createElement('div')); - await act(() => { - root.render(); - }); - }).toErrorDev( + const root = ReactDOMClient.createRoot(document.createElement('div')); + await act(() => { + root.render(); + }); + assertConsoleErrorDev([ '`Component` uses `getDerivedStateFromProps` but its initial state is ' + 'null. This is not recommended. Instead, define the initial state by ' + 'assigning an object to `this.state` in the constructor of `Component`. ' + - 'This ensures that `getDerivedStateFromProps` arguments have a consistent shape.', - ); + 'This ensures that `getDerivedStateFromProps` arguments have a consistent shape.\n' + + ' in Component (at **)', + ]); }); it('should not invoke deprecated lifecycles (cWM/cWRP/cWU) if new static gDSFP is present', async () => { @@ -609,30 +622,52 @@ describe('create-react-class-integration', () => { }); Component.displayName = 'Component'; - await expect(async () => { - await expect(async () => { - const root = ReactDOMClient.createRoot(document.createElement('div')); - await act(() => { - root.render(); - }); - }).toErrorDev( - 'Unsafe legacy lifecycles will not be called for components using new component APIs.\n\n' + - 'Component uses getDerivedStateFromProps() but also contains the following legacy lifecycles:\n' + - ' componentWillMount\n' + - ' componentWillReceiveProps\n' + - ' componentWillUpdate\n\n' + - 'The above lifecycles should be removed. Learn more about this warning here:\n' + - 'https://react.dev/link/unsafe-component-lifecycles', - ); - }).toWarnDev( + const root = ReactDOMClient.createRoot(document.createElement('div')); + await act(() => { + root.render(); + }); + assertConsoleErrorDev([ + 'Unsafe legacy lifecycles will not be called for components using new component APIs.\n\n' + + 'Component uses getDerivedStateFromProps() but also contains the following legacy lifecycles:\n' + + ' componentWillMount\n' + + ' componentWillReceiveProps\n' + + ' componentWillUpdate\n\n' + + 'The above lifecycles should be removed. Learn more about this warning here:\n' + + 'https://react.dev/link/unsafe-component-lifecycles\n' + + ' in Component (at **)', + ]); + assertConsoleWarnDev( [ - 'componentWillMount has been renamed', - 'componentWillReceiveProps has been renamed', - 'componentWillUpdate has been renamed', + 'componentWillMount has been renamed, and is not recommended for use. ' + + 'See https://react.dev/link/unsafe-component-lifecycles for details.\n\n' + + '* Move code with side effects to componentDidMount, and set initial state in the constructor.\n' + + '* Rename componentWillMount to UNSAFE_componentWillMount to suppress ' + + 'this warning in non-strict mode. In React 18.x, only the UNSAFE_ name will work. ' + + 'To rename all deprecated lifecycles to their new names, you can run ' + + '`npx react-codemod rename-unsafe-lifecycles` in your project source folder.\n' + + '\nPlease update the following components: Component', + 'componentWillReceiveProps has been renamed, and is not recommended for use. ' + + 'See https://react.dev/link/unsafe-component-lifecycles for details.\n\n' + + '* Move data fetching code or side effects to componentDidUpdate.\n' + + "* If you're updating state whenever props change, refactor your " + + 'code to use memoization techniques or move it to ' + + 'static getDerivedStateFromProps. Learn more at: https://react.dev/link/derived-state\n' + + '* Rename componentWillReceiveProps to UNSAFE_componentWillReceiveProps to suppress ' + + 'this warning in non-strict mode. In React 18.x, only the UNSAFE_ name will work. ' + + 'To rename all deprecated lifecycles to their new names, you can run ' + + '`npx react-codemod rename-unsafe-lifecycles` in your project source folder.\n' + + '\nPlease update the following components: Component', + 'componentWillUpdate has been renamed, and is not recommended for use. ' + + 'See https://react.dev/link/unsafe-component-lifecycles for details.\n\n' + + '* Move data fetching code or side effects to componentDidUpdate.\n' + + '* Rename componentWillUpdate to UNSAFE_componentWillUpdate to suppress ' + + 'this warning in non-strict mode. In React 18.x, only the UNSAFE_ name will work. ' + + 'To rename all deprecated lifecycles to their new names, you can run ' + + '`npx react-codemod rename-unsafe-lifecycles` in your project source folder.\n' + + '\nPlease update the following components: Component', ], {withoutStack: true}, ); - const root = ReactDOMClient.createRoot(document.createElement('div')); await act(() => { root.render(); }); @@ -659,26 +694,49 @@ describe('create-react-class-integration', () => { }); Component.displayName = 'Component'; - await expect(async () => { - await expect(async () => { - const root = ReactDOMClient.createRoot(document.createElement('div')); - await act(() => { - root.render(); - }); - }).toErrorDev( - 'Unsafe legacy lifecycles will not be called for components using new component APIs.\n\n' + - 'Component uses getSnapshotBeforeUpdate() but also contains the following legacy lifecycles:\n' + - ' componentWillMount\n' + - ' componentWillReceiveProps\n' + - ' componentWillUpdate\n\n' + - 'The above lifecycles should be removed. Learn more about this warning here:\n' + - 'https://react.dev/link/unsafe-component-lifecycles', - ); - }).toWarnDev( + const root = ReactDOMClient.createRoot(document.createElement('div')); + await act(() => { + root.render(); + }); + assertConsoleErrorDev([ + 'Unsafe legacy lifecycles will not be called for components using new component APIs.\n\n' + + 'Component uses getSnapshotBeforeUpdate() but also contains the following legacy lifecycles:\n' + + ' componentWillMount\n' + + ' componentWillReceiveProps\n' + + ' componentWillUpdate\n\n' + + 'The above lifecycles should be removed. Learn more about this warning here:\n' + + 'https://react.dev/link/unsafe-component-lifecycles\n' + + ' in Component (at **)', + ]); + assertConsoleWarnDev( [ - 'componentWillMount has been renamed', - 'componentWillReceiveProps has been renamed', - 'componentWillUpdate has been renamed', + 'componentWillMount has been renamed, and is not recommended for use. ' + + 'See https://react.dev/link/unsafe-component-lifecycles for details.\n\n' + + '* Move code with side effects to componentDidMount, and set initial state in the constructor.\n' + + '* Rename componentWillMount to UNSAFE_componentWillMount to suppress ' + + 'this warning in non-strict mode. In React 18.x, only the UNSAFE_ name will work. ' + + 'To rename all deprecated lifecycles to their new names, you can run ' + + '`npx react-codemod rename-unsafe-lifecycles` in your project source folder.\n' + + '\nPlease update the following components: Component', + 'componentWillReceiveProps has been renamed, and is not recommended for use. ' + + 'See https://react.dev/link/unsafe-component-lifecycles for details.\n\n' + + '* Move data fetching code or side effects to componentDidUpdate.\n' + + "* If you're updating state whenever props change, refactor your " + + 'code to use memoization techniques or move it to ' + + 'static getDerivedStateFromProps. Learn more at: https://react.dev/link/derived-state\n' + + '* Rename componentWillReceiveProps to UNSAFE_componentWillReceiveProps to suppress ' + + 'this warning in non-strict mode. In React 18.x, only the UNSAFE_ name will work. ' + + 'To rename all deprecated lifecycles to their new names, you can run ' + + '`npx react-codemod rename-unsafe-lifecycles` in your project source folder.\n' + + '\nPlease update the following components: Component', + 'componentWillUpdate has been renamed, and is not recommended for use. ' + + 'See https://react.dev/link/unsafe-component-lifecycles for details.\n\n' + + '* Move data fetching code or side effects to componentDidUpdate.\n' + + '* Rename componentWillUpdate to UNSAFE_componentWillUpdate to suppress ' + + 'this warning in non-strict mode. In React 18.x, only the UNSAFE_ name will work. ' + + 'To rename all deprecated lifecycles to their new names, you can run ' + + '`npx react-codemod rename-unsafe-lifecycles` in your project source folder.\n' + + '\nPlease update the following components: Component', ], {withoutStack: true}, ); @@ -721,15 +779,38 @@ describe('create-react-class-integration', () => { const root = ReactDOMClient.createRoot(document.createElement('div')); - await expect(async () => { - await act(() => { - root.render(); - }); - }).toWarnDev( + await act(() => { + root.render(); + }); + assertConsoleWarnDev( [ - 'componentWillMount has been renamed', - 'componentWillReceiveProps has been renamed', - 'componentWillUpdate has been renamed', + 'componentWillMount has been renamed, and is not recommended for use. ' + + 'See https://react.dev/link/unsafe-component-lifecycles for details.\n\n' + + '* Move code with side effects to componentDidMount, and set initial state in the constructor.\n' + + '* Rename componentWillMount to UNSAFE_componentWillMount to suppress ' + + 'this warning in non-strict mode. In React 18.x, only the UNSAFE_ name will work. ' + + 'To rename all deprecated lifecycles to their new names, you can run ' + + '`npx react-codemod rename-unsafe-lifecycles` in your project source folder.\n' + + '\nPlease update the following components: Component', + 'componentWillReceiveProps has been renamed, and is not recommended for use. ' + + 'See https://react.dev/link/unsafe-component-lifecycles for details.\n\n' + + '* Move data fetching code or side effects to componentDidUpdate.\n' + + "* If you're updating state whenever props change, refactor your " + + 'code to use memoization techniques or move it to ' + + 'static getDerivedStateFromProps. Learn more at: https://react.dev/link/derived-state\n' + + '* Rename componentWillReceiveProps to UNSAFE_componentWillReceiveProps to suppress ' + + 'this warning in non-strict mode. In React 18.x, only the UNSAFE_ name will work. ' + + 'To rename all deprecated lifecycles to their new names, you can run ' + + '`npx react-codemod rename-unsafe-lifecycles` in your project source folder.\n' + + '\nPlease update the following components: Component', + 'componentWillUpdate has been renamed, and is not recommended for use. ' + + 'See https://react.dev/link/unsafe-component-lifecycles for details.\n\n' + + '* Move data fetching code or side effects to componentDidUpdate.\n' + + '* Rename componentWillUpdate to UNSAFE_componentWillUpdate to suppress ' + + 'this warning in non-strict mode. In React 18.x, only the UNSAFE_ name will work. ' + + 'To rename all deprecated lifecycles to their new names, you can run ' + + '`npx react-codemod rename-unsafe-lifecycles` in your project source folder.\n' + + '\nPlease update the following components: Component', ], {withoutStack: true}, ); @@ -803,14 +884,16 @@ describe('create-react-class-integration', () => { const root = ReactDOMClient.createRoot(document.createElement('div')); - await expect(async () => { - await act(() => { - root.render(); - }); - }).toErrorDev( - 'MyComponent: isMounted is deprecated. Instead, make sure to ' + - 'clean up subscriptions and pending requests in componentWillUnmount ' + - 'to prevent memory leaks.', + await act(() => { + root.render(); + }); + assertConsoleErrorDev( + [ + 'Warning: MyComponent: isMounted is deprecated. Instead, make sure to ' + + 'clean up subscriptions and pending requests in componentWillUnmount ' + + 'to prevent memory leaks.\n' + + ' in MyComponent (at **)', + ], // This now has a component stack even though it's part of a third-party library. ); diff --git a/scripts/jest/spec-equivalence-reporter/setupTests.js b/scripts/jest/spec-equivalence-reporter/setupTests.js index 487e0d3003078..c3eaf5690afcb 100644 --- a/scripts/jest/spec-equivalence-reporter/setupTests.js +++ b/scripts/jest/spec-equivalence-reporter/setupTests.js @@ -7,6 +7,11 @@ 'use strict'; +const { + patchConsoleMethods, + resetAllUnexpectedConsoleCalls, + flushAllUnexpectedConsoleCalls, +} = require('internal-test-utils/consoleMock'); const spyOn = jest.spyOn; // Spying on console methods in production builds can mask errors. @@ -36,6 +41,11 @@ global.spyOnProd = function (...args) { } }; +// Patch the console to assert that all console error/warn/log calls assert. +patchConsoleMethods({includeLog: !!process.env.CI}); +beforeEach(resetAllUnexpectedConsoleCalls); +afterEach(flushAllUnexpectedConsoleCalls); + expect.extend({ ...require('../matchers/reactTestMatchers'), ...require('../matchers/toThrow'),