/
githubmr
/
facebook-react
Обзор
Документация
Войти
/
githubmr
/
facebook-react
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
Аналитика
main
packages/react/src/__tests__/createReactClassIntegration-test.js
923 строки
30 KB
Ricky
[flags] remove enableOwnerStacks (#32426)
04 мар 2025, 20:34
Не верифицирован
04 мар 2025, 20:34
e0fe347
Код
Авторство
О чём код?
/** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @emails react-core */ 'use strict'; let act; let assertConsoleErrorDev; let assertConsoleWarnDev; let PropTypes; let React; let ReactDOMClient; let createReactClass; describe('create-react-class-integration', () => { beforeEach(() => { jest.resetModules(); ({ act, assertConsoleErrorDev, assertConsoleWarnDev, } = require('internal-test-utils')); PropTypes = require('prop-types'); React = require('react'); ReactDOMClient = require('react-dom/client'); createReactClass = require('create-react-class/factory')( React.Component, React.isValidElement, new React.Component().updater, ); }); it('should throw when `render` is not specified', () => { expect(function () { createReactClass({}); }).toThrowError('Class specification must implement a `render` method.'); }); it('should copy prop types onto the Constructor', () => { const propValidator = jest.fn(); const TestComponent = createReactClass({ propTypes: { value: propValidator, }, render: function () { return <div />; }, }); expect(TestComponent.propTypes).toBeDefined(); expect(TestComponent.propTypes.value).toBe(propValidator); }); it('should warn on invalid prop types', () => { createReactClass({ displayName: 'Component', propTypes: { prop: null, }, render: function () { return <span>{this.props.prop}</span>; }, }); 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', () => { createReactClass({ displayName: 'Component', contextTypes: { prop: null, }, render: function () { return <span>{this.props.prop}</span>; }, }); 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', () => { createReactClass({ displayName: 'Component', childContextTypes: { prop: null, }, render: function () { return <span>{this.props.prop}</span>; }, }); 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', () => { createReactClass({ componentShouldUpdate: function () { return false; }, render: function () { return <div />; }, }); 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}, ); createReactClass({ displayName: 'NamedComponent', componentShouldUpdate: function () { return false; }, render: function () { return <div />; }, }); 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', () => { createReactClass({ componentWillRecieveProps: function () { return false; }, render: function () { return <div />; }, }); assertConsoleErrorDev( [ 'Warning: A component has a method called componentWillRecieveProps(). Did you ' + 'mean componentWillReceiveProps()?', ], {withoutStack: true}, ); }); it('should warn when misspelling UNSAFE_componentWillReceiveProps', () => { createReactClass({ UNSAFE_componentWillRecieveProps: function () { return false; }, render: function () { return <div />; }, }); assertConsoleErrorDev( [ 'Warning: A component has a method called UNSAFE_componentWillRecieveProps(). ' + 'Did you mean UNSAFE_componentWillReceiveProps()?', ], {withoutStack: true}, ); }); it('should throw if a reserved property is in statics', () => { expect(function () { createReactClass({ statics: { getDefaultProps: function () { return { foo: 0, }; }, }, render: function () { return <span />; }, }); }).toThrowError( 'ReactClass: You are attempting to define a reserved property, ' + '`getDefaultProps`, that shouldn\'t be on the "statics" key. Define ' + 'it as an instance property instead; it will still be accessible on ' + 'the constructor.', ); }); // 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', () => { createReactClass({ mixins: [{}], propTypes: { foo: PropTypes.string, }, contextTypes: { foo: PropTypes.string, }, childContextTypes: { foo: PropTypes.string, }, render: function () { return <div />; }, }); assertConsoleErrorDev([ '`mixins` is now a static property and should ' + 'be defined inside "statics".', '`propTypes` is now a static property and should ' + 'be defined inside "statics".', '`contextTypes` is now a static property and ' + 'should be defined inside "statics".', '`childContextTypes` is now a static property and ' + 'should be defined inside "statics".', ]); }); it('should support statics', async () => { const Component = createReactClass({ statics: { abc: 'def', def: 0, ghi: null, jkl: 'mno', pqr: function () { return this; }, }, render: function () { return <span />; }, }); const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); let instance; await act(() => { root.render(<Component ref={current => (instance = current)} />); }); expect(instance.constructor.abc).toBe('def'); expect(Component.abc).toBe('def'); expect(instance.constructor.def).toBe(0); expect(Component.def).toBe(0); expect(instance.constructor.ghi).toBe(null); expect(Component.ghi).toBe(null); expect(instance.constructor.jkl).toBe('mno'); expect(Component.jkl).toBe('mno'); expect(instance.constructor.pqr()).toBe(Component); expect(Component.pqr()).toBe(Component); }); it('should work with object getInitialState() return values', async () => { const Component = createReactClass({ getInitialState: function () { return { occupation: 'clown', }; }, render: function () { return <span />; }, }); const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); let instance; await act(() => { root.render(<Component ref={current => (instance = current)} />); }); expect(instance.state.occupation).toEqual('clown'); }); it('should work with getDerivedStateFromProps() return values', async () => { const Component = createReactClass({ getInitialState() { return {}; }, render: function () { return <span />; }, }); Component.getDerivedStateFromProps = () => { return {occupation: 'clown'}; }; let instance; const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); await act(() => { root.render(<Component ref={current => (instance = current)} />); }); expect(instance.state.occupation).toEqual('clown'); }); // @gate !disableLegacyContext it('renders based on context getInitialState', async () => { const Foo = createReactClass({ contextTypes: { className: PropTypes.string, }, getInitialState() { return {className: this.context.className}; }, render() { return <span className={this.state.className} />; }, }); const Outer = createReactClass({ childContextTypes: { className: PropTypes.string, }, getChildContext() { return {className: 'foo'}; }, render() { return <Foo />; }, }); const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); await act(() => { root.render(<Outer />); }); assertConsoleErrorDev([ [ 'Component uses the legacy childContextTypes API which will soon be removed. Use React.createContext() instead.', {withoutStack: true}, ], 'Component uses the legacy contextTypes API which will soon be removed. Use React.createContext() with static contextType instead.', ]); expect(container.firstChild.className).toBe('foo'); }); it('should throw with non-object getInitialState() return values', async () => { // eslint-disable-next-line no-for-of-loops/no-for-of-loops for (const state of [['an array'], 'a string', 1234]) { const Component = createReactClass({ getInitialState: function () { return state; }, render: function () { return <span />; }, }); const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); await expect( act(() => { root.render(<Component />); }), ).rejects.toThrowError( 'Component.getInitialState(): must return an object or null', ); } }); it('should work with a null getInitialState() return value', async () => { const Component = createReactClass({ getInitialState: function () { return null; }, render: function () { return <span />; }, }); const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); await expect( act(() => { root.render(<Component />); }), ).resolves.not.toThrow(); }); it('should throw when using legacy factories', () => { const Component = createReactClass({ render() { return <div />; }, }); 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}, ); }); it('replaceState and callback works', async () => { const ops = []; const Component = createReactClass({ getInitialState() { return {step: 0}; }, render() { ops.push('Render: ' + this.state.step); return <div />; }, }); const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); let instance; await act(() => { root.render(<Component ref={current => (instance = current)} />); }); await act(() => { instance.replaceState({step: 1}, () => { ops.push('Callback: ' + instance.state.step); }); }); expect(ops).toEqual(['Render: 0', 'Render: 1', 'Callback: 1']); }); it('getDerivedStateFromProps updates state when props change', async () => { const Component = createReactClass({ getInitialState() { return { count: 1, }; }, render() { return <div>count:{this.state.count}</div>; }, }); Component.getDerivedStateFromProps = (nextProps, prevState) => ({ count: prevState.count + nextProps.incrementBy, }); const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); await act(() => { root.render( <div> <Component incrementBy={0} /> </div>, ); }); expect(container.firstChild.textContent).toEqual('count:1'); await act(() => { root.render( <div> <Component incrementBy={2} /> </div>, ); }); expect(container.firstChild.textContent).toEqual('count:3'); }); it('should support the new static getDerivedStateFromProps method', async () => { let instance; const Component = createReactClass({ statics: { getDerivedStateFromProps: function () { return {foo: 'bar'}; }, }, getInitialState() { return {}; }, render: function () { instance = this; return null; }, }); const root = ReactDOMClient.createRoot(document.createElement('div')); await act(() => { root.render(<Component />); }); expect(instance.state.foo).toBe('bar'); }); it('warns if getDerivedStateFromProps is not static', async () => { const Foo = createReactClass({ displayName: 'Foo', getDerivedStateFromProps() { return {}; }, render() { return <div />; }, }); const root = ReactDOMClient.createRoot(document.createElement('div')); await act(() => { root.render(<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', async () => { const Foo = createReactClass({ displayName: 'Foo', getDerivedStateFromError() { return {}; }, render() { return <div />; }, }); const root = ReactDOMClient.createRoot(document.createElement('div')); await act(() => { root.render(<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', async () => { const Foo = createReactClass({ displayName: 'Foo', statics: { getSnapshotBeforeUpdate: function () { return null; }, }, render() { return <div />; }, }); const root = ReactDOMClient.createRoot(document.createElement('div')); await act(() => { root.render(<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('should warn if state is not properly initialized before getDerivedStateFromProps', async () => { const Component = createReactClass({ displayName: 'Component', statics: { getDerivedStateFromProps: function () { return null; }, }, render: function () { return null; }, }); const root = ReactDOMClient.createRoot(document.createElement('div')); await act(() => { root.render(<Component />); }); 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.\n' + ' in Component (at **)', ]); }); it('should not invoke deprecated lifecycles (cWM/cWRP/cWU) if new static gDSFP is present', async () => { const Component = createReactClass({ statics: { getDerivedStateFromProps: function () { return null; }, }, componentWillMount: function () { throw Error('unexpected'); }, componentWillReceiveProps: function () { throw Error('unexpected'); }, componentWillUpdate: function () { throw Error('unexpected'); }, getInitialState: function () { return {}; }, render: function () { return null; }, }); Component.displayName = 'Component'; const root = ReactDOMClient.createRoot(document.createElement('div')); await act(() => { root.render(<Component />); }); 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, 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}, ); await act(() => { root.render(<Component foo={1} />); }); }); it('should not invoke deprecated lifecycles (cWM/cWRP/cWU) if new getSnapshotBeforeUpdate is present', async () => { const Component = createReactClass({ getSnapshotBeforeUpdate: function () { return null; }, componentWillMount: function () { throw Error('unexpected'); }, componentWillReceiveProps: function () { throw Error('unexpected'); }, componentWillUpdate: function () { throw Error('unexpected'); }, componentDidUpdate: function () {}, render: function () { return null; }, }); Component.displayName = 'Component'; const root = ReactDOMClient.createRoot(document.createElement('div')); await act(() => { root.render(<Component />); }); 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, 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}, ); await act(() => { const root2 = ReactDOMClient.createRoot(document.createElement('div')); root2.render(<Component foo={1} />); }); }); it('should invoke both deprecated and new lifecycles if both are present', async () => { const log = []; const Component = createReactClass({ mixins: [ { componentWillMount: function () { log.push('componentWillMount'); }, componentWillReceiveProps: function () { log.push('componentWillReceiveProps'); }, componentWillUpdate: function () { log.push('componentWillUpdate'); }, }, ], UNSAFE_componentWillMount: function () { log.push('UNSAFE_componentWillMount'); }, UNSAFE_componentWillReceiveProps: function () { log.push('UNSAFE_componentWillReceiveProps'); }, UNSAFE_componentWillUpdate: function () { log.push('UNSAFE_componentWillUpdate'); }, render: function () { return null; }, }); const root = ReactDOMClient.createRoot(document.createElement('div')); await act(() => { root.render(<Component foo="bar" />); }); assertConsoleWarnDev( [ '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}, ); expect(log).toEqual(['componentWillMount', 'UNSAFE_componentWillMount']); log.length = 0; await act(() => { root.render(<Component foo="baz" />); }); expect(log).toEqual([ 'componentWillReceiveProps', 'UNSAFE_componentWillReceiveProps', 'componentWillUpdate', 'UNSAFE_componentWillUpdate', ]); }); it('isMounted works', async () => { const ops = []; let instance; const Component = createReactClass({ displayName: 'MyComponent', mixins: [ { UNSAFE_componentWillMount() { this.log('mixin.componentWillMount'); }, componentDidMount() { this.log('mixin.componentDidMount'); }, UNSAFE_componentWillUpdate() { this.log('mixin.componentWillUpdate'); }, componentDidUpdate() { this.log('mixin.componentDidUpdate'); }, componentWillUnmount() { this.log('mixin.componentWillUnmount'); }, }, ], log(name) { ops.push(`${name}: ${this.isMounted()}`); }, getInitialState() { this.log('getInitialState'); return {}; }, UNSAFE_componentWillMount() { this.log('componentWillMount'); }, componentDidMount() { this.log('componentDidMount'); }, UNSAFE_componentWillUpdate() { this.log('componentWillUpdate'); }, componentDidUpdate() { this.log('componentDidUpdate'); }, componentWillUnmount() { this.log('componentWillUnmount'); }, render() { instance = this; this.log('render'); return <div />; }, }); const root = ReactDOMClient.createRoot(document.createElement('div')); await act(() => { root.render(<Component />); }); 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. ); // Dedupe await act(() => { root.render(<Component />); }); await act(() => { root.unmount(); }); instance.log('after unmount'); expect(ops).toEqual([ 'getInitialState: false', 'mixin.componentWillMount: false', 'componentWillMount: false', 'render: false', 'mixin.componentDidMount: true', 'componentDidMount: true', 'mixin.componentWillUpdate: true', 'componentWillUpdate: true', 'render: true', 'mixin.componentDidUpdate: true', 'componentDidUpdate: true', 'mixin.componentWillUnmount: true', 'componentWillUnmount: true', 'after unmount: false', ]); }); });