From bb92ca5ba43f5143ab8f6552ccc37f2e356db43f Mon Sep 17 00:00:00 2001 From: Ricky Hanlon Date: Thu, 25 Jan 2024 22:16:41 -0500 Subject: [PATCH 1/3] cp to ReactLegacyCompositeComponent --- .../ReactLegacyCompositeComponent-test.js | 1850 +++++++++++++++++ 1 file changed, 1850 insertions(+) create mode 100644 packages/react-dom/src/__tests__/ReactLegacyCompositeComponent-test.js diff --git a/packages/react-dom/src/__tests__/ReactLegacyCompositeComponent-test.js b/packages/react-dom/src/__tests__/ReactLegacyCompositeComponent-test.js new file mode 100644 index 0000000000000..bf91fb08dda10 --- /dev/null +++ b/packages/react-dom/src/__tests__/ReactLegacyCompositeComponent-test.js @@ -0,0 +1,1850 @@ +/** + * 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 ChildUpdates; +let MorphingComponent; +let React; +let ReactDOM; +let ReactCurrentOwner; +let ReactTestUtils; +let PropTypes; + +describe('ReactCompositeComponent', () => { + const hasOwnProperty = Object.prototype.hasOwnProperty; + + /** + * Performs equality by iterating through keys on an object and returning false + * when any key has values which are not strictly equal between the arguments. + * Returns true when the values of all keys are strictly equal. + */ + function shallowEqual(objA: mixed, objB: mixed): boolean { + if (Object.is(objA, objB)) { + return true; + } + if ( + typeof objA !== 'object' || + objA === null || + typeof objB !== 'object' || + objB === null + ) { + return false; + } + const keysA = Object.keys(objA); + const keysB = Object.keys(objB); + if (keysA.length !== keysB.length) { + return false; + } + for (let i = 0; i < keysA.length; i++) { + if ( + !hasOwnProperty.call(objB, keysA[i]) || + !Object.is(objA[keysA[i]], objB[keysA[i]]) + ) { + return false; + } + } + return true; + } + + function shallowCompare(instance, nextProps, nextState) { + return ( + !shallowEqual(instance.props, nextProps) || + !shallowEqual(instance.state, nextState) + ); + } + + beforeEach(() => { + jest.resetModules(); + React = require('react'); + ReactDOM = require('react-dom'); + ReactCurrentOwner = + require('react').__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED + .ReactCurrentOwner; + ReactTestUtils = require('react-dom/test-utils'); + PropTypes = require('prop-types'); + + MorphingComponent = class extends React.Component { + state = {activated: false}; + + xRef = React.createRef(); + + _toggleActivatedState = () => { + this.setState({activated: !this.state.activated}); + }; + + render() { + const toggleActivatedState = this._toggleActivatedState; + return !this.state.activated ? ( + + ) : ( + + ); + } + }; + + /** + * We'll use this to ensure that an old version is not cached when it is + * reallocated again. + */ + ChildUpdates = class extends React.Component { + anchorRef = React.createRef(); + + getAnchor = () => { + return this.anchorRef.current; + }; + + render() { + const className = this.props.anchorClassOn ? 'anchorClass' : ''; + return this.props.renderAnchor ? ( + + ) : ( + + ); + } + }; + }); + + if (require('shared/ReactFeatureFlags').disableModulePatternComponents) { + it('should not support module pattern components', () => { + function Child({test}) { + return { + render() { + return
{test}
; + }, + }; + } + + const el = document.createElement('div'); + expect(() => { + expect(() => ReactDOM.render(, el)).toThrow( + 'Objects are not valid as a React child (found: object with keys {render}).', + ); + }).toErrorDev( + 'Warning: The component appears to be a function component that returns a class instance. ' + + 'Change Child to a class that extends React.Component instead. ' + + "If you can't use a class try assigning the prototype on the function as a workaround. " + + '`Child.prototype = React.Component.prototype`. ' + + "Don't use an arrow function since it cannot be called with `new` by React.", + ); + + expect(el.textContent).toBe(''); + }); + } else { + it('should support module pattern components', () => { + function Child({test}) { + return { + render() { + return
{test}
; + }, + }; + } + + const el = document.createElement('div'); + expect(() => ReactDOM.render(, el)).toErrorDev( + 'Warning: The component appears to be a function component that returns a class instance. ' + + 'Change Child to a class that extends React.Component instead. ' + + "If you can't use a class try assigning the prototype on the function as a workaround. " + + '`Child.prototype = React.Component.prototype`. ' + + "Don't use an arrow function since it cannot be called with `new` by React.", + ); + + expect(el.textContent).toBe('test'); + }); + } + + it('should support rendering to different child types over time', () => { + const instance = ReactTestUtils.renderIntoDocument(); + let el = ReactDOM.findDOMNode(instance); + expect(el.tagName).toBe('A'); + + instance._toggleActivatedState(); + el = ReactDOM.findDOMNode(instance); + expect(el.tagName).toBe('B'); + + instance._toggleActivatedState(); + el = ReactDOM.findDOMNode(instance); + expect(el.tagName).toBe('A'); + }); + + it('should react to state changes from callbacks', () => { + const container = document.createElement('div'); + document.body.appendChild(container); + try { + const instance = ReactDOM.render(, container); + let el = ReactDOM.findDOMNode(instance); + expect(el.tagName).toBe('A'); + el.click(); + el = ReactDOM.findDOMNode(instance); + expect(el.tagName).toBe('B'); + } finally { + document.body.removeChild(container); + } + }); + + it('should rewire refs when rendering to different child types', () => { + const instance = ReactTestUtils.renderIntoDocument(); + + expect(instance.xRef.current.tagName).toBe('A'); + instance._toggleActivatedState(); + expect(instance.xRef.current.tagName).toBe('B'); + instance._toggleActivatedState(); + expect(instance.xRef.current.tagName).toBe('A'); + }); + + it('should not cache old DOM nodes when switching constructors', () => { + const container = document.createElement('div'); + const instance = ReactDOM.render( + , + container, + ); + ReactDOM.render( + // Warm any cache + , + container, + ); + ReactDOM.render( + // Clear out the anchor + , + container, + ); + ReactDOM.render( + // rerender + , + container, + ); + expect(instance.getAnchor().className).toBe(''); + }); + + it('should use default values for undefined props', () => { + class Component extends React.Component { + static defaultProps = {prop: 'testKey'}; + + render() { + return ; + } + } + + const instance1 = ReactTestUtils.renderIntoDocument(); + expect(instance1.props).toEqual({prop: 'testKey'}); + + const instance2 = ReactTestUtils.renderIntoDocument( + , + ); + expect(instance2.props).toEqual({prop: 'testKey'}); + + const instance3 = ReactTestUtils.renderIntoDocument( + , + ); + expect(instance3.props).toEqual({prop: null}); + }); + + it('should not mutate passed-in props object', () => { + class Component extends React.Component { + static defaultProps = {prop: 'testKey'}; + + render() { + return ; + } + } + + const inputProps = {}; + let instance1 = ; + instance1 = ReactTestUtils.renderIntoDocument(instance1); + expect(instance1.props.prop).toBe('testKey'); + + // We don't mutate the input, just in case the caller wants to do something + // with it after using it to instantiate a component + expect(inputProps.prop).not.toBeDefined(); + }); + + it('should warn about `forceUpdate` on not-yet-mounted components', () => { + class MyComponent extends React.Component { + constructor(props) { + super(props); + this.forceUpdate(); + } + render() { + return
; + } + } + + const container = document.createElement('div'); + expect(() => ReactDOM.render(, container)).toErrorDev( + "Warning: Can't call forceUpdate on a component that is not yet mounted. " + + 'This is a no-op, but it might indicate a bug in your application. ' + + 'Instead, assign to `this.state` directly or define a `state = {};` ' + + 'class property with the desired state in the MyComponent component.', + ); + + // No additional warning should be recorded + const container2 = document.createElement('div'); + ReactDOM.render(, container2); + }); + + it('should warn about `setState` on not-yet-mounted components', () => { + class MyComponent extends React.Component { + constructor(props) { + super(props); + this.setState(); + } + render() { + return
; + } + } + + const container = document.createElement('div'); + expect(() => ReactDOM.render(, container)).toErrorDev( + "Warning: Can't call setState on a component that is not yet mounted. " + + 'This is a no-op, but it might indicate a bug in your application. ' + + 'Instead, assign to `this.state` directly or define a `state = {};` ' + + 'class property with the desired state in the MyComponent component.', + ); + + // No additional warning should be recorded + const container2 = document.createElement('div'); + ReactDOM.render(, container2); + }); + + it('should not warn about `forceUpdate` on unmounted components', () => { + const container = document.createElement('div'); + document.body.appendChild(container); + + class Component extends React.Component { + render() { + return
; + } + } + + let instance = ; + expect(instance.forceUpdate).not.toBeDefined(); + + instance = ReactDOM.render(instance, container); + instance.forceUpdate(); + + ReactDOM.unmountComponentAtNode(container); + + instance.forceUpdate(); + instance.forceUpdate(); + }); + + it('should not warn about `setState` on unmounted components', () => { + const container = document.createElement('div'); + document.body.appendChild(container); + + let renders = 0; + + class Component extends React.Component { + state = {value: 0}; + + render() { + renders++; + return
; + } + } + + let instance; + ReactDOM.render( +
+ + (instance = c || instance)} /> + +
, + container, + ); + + expect(renders).toBe(1); + + instance.setState({value: 1}); + expect(renders).toBe(2); + + ReactDOM.render(
, container); + instance.setState({value: 2}); + expect(renders).toBe(2); + }); + + it('should silently allow `setState`, not call cb on unmounting components', () => { + let cbCalled = false; + const container = document.createElement('div'); + document.body.appendChild(container); + + class Component extends React.Component { + state = {value: 0}; + + componentWillUnmount() { + expect(() => { + this.setState({value: 2}, function () { + cbCalled = true; + }); + }).not.toThrow(); + } + + render() { + return
; + } + } + + const instance = ReactDOM.render(, container); + instance.setState({value: 1}); + + ReactDOM.unmountComponentAtNode(container); + expect(cbCalled).toBe(false); + }); + + it('should warn when rendering a class with a render method that does not extend React.Component', () => { + const container = document.createElement('div'); + class ClassWithRenderNotExtended { + render() { + return
; + } + } + expect(() => { + expect(() => { + ReactDOM.render(, container); + }).toThrow(TypeError); + }).toErrorDev( + 'Warning: The component appears to have a render method, ' + + "but doesn't extend React.Component. This is likely to cause errors. " + + 'Change ClassWithRenderNotExtended to extend React.Component instead.', + ); + + // Test deduplication + expect(() => { + ReactDOM.render(, container); + }).toThrow(TypeError); + }); + + it('should warn about `setState` in render', () => { + const container = document.createElement('div'); + + let renderedState = -1; + let renderPasses = 0; + + class Component extends React.Component { + state = {value: 0}; + + render() { + renderPasses++; + renderedState = this.state.value; + if (this.state.value === 0) { + this.setState({value: 1}); + } + return
; + } + } + + let instance; + + expect(() => { + instance = ReactDOM.render(, container); + }).toErrorDev( + 'Cannot update during an existing state transition (such as within ' + + '`render`). Render methods should be a pure function of props and state.', + ); + + // The setState call is queued and then executed as a second pass. This + // behavior is undefined though so we're free to change it to suit the + // implementation details. + expect(renderPasses).toBe(2); + expect(renderedState).toBe(1); + expect(instance.state.value).toBe(1); + + // Forcing a rerender anywhere will cause the update to happen. + const instance2 = ReactDOM.render(, container); + expect(instance).toBe(instance2); + expect(renderedState).toBe(1); + expect(instance2.state.value).toBe(1); + + // Test deduplication; (no additional warnings are expected). + ReactDOM.unmountComponentAtNode(container); + ReactDOM.render(, container); + }); + + it('should cleanup even if render() fatals', () => { + class BadComponent extends React.Component { + render() { + throw new Error(); + } + } + + let instance = ; + + expect(ReactCurrentOwner.current).toBe(null); + + expect(() => { + instance = ReactTestUtils.renderIntoDocument(instance); + }).toThrow(); + + expect(ReactCurrentOwner.current).toBe(null); + }); + + it('should call componentWillUnmount before unmounting', () => { + const container = document.createElement('div'); + let innerUnmounted = false; + + class Component extends React.Component { + render() { + return ( +
+ + Text +
+ ); + } + } + + class Inner extends React.Component { + componentWillUnmount() { + innerUnmounted = true; + } + + render() { + return
; + } + } + + ReactDOM.render(, container); + ReactDOM.unmountComponentAtNode(container); + expect(innerUnmounted).toBe(true); + }); + + it('should warn when shouldComponentUpdate() returns undefined', () => { + class ClassComponent extends React.Component { + state = {bogus: false}; + + shouldComponentUpdate() { + return undefined; + } + + render() { + return
; + } + } + + const instance = ReactTestUtils.renderIntoDocument(); + + expect(() => instance.setState({bogus: true})).toErrorDev( + 'Warning: ClassComponent.shouldComponentUpdate(): Returned undefined instead of a ' + + 'boolean value. Make sure to return true or false.', + ); + }); + + it('should warn when componentDidUnmount method is defined', () => { + class Component extends React.Component { + componentDidUnmount = () => {}; + + render() { + return
; + } + } + + expect(() => ReactTestUtils.renderIntoDocument()).toErrorDev( + 'Warning: Component has a method called ' + + 'componentDidUnmount(). But there is no such lifecycle method. ' + + 'Did you mean componentWillUnmount()?', + ); + }); + + it('should warn when componentDidReceiveProps method is defined', () => { + class Component extends React.Component { + componentDidReceiveProps = () => {}; + + render() { + return
; + } + } + + expect(() => ReactTestUtils.renderIntoDocument()).toErrorDev( + 'Warning: Component has a method called ' + + 'componentDidReceiveProps(). But there is no such lifecycle method. ' + + 'If you meant to update the state in response to changing props, ' + + 'use componentWillReceiveProps(). If you meant to fetch data or ' + + 'run side-effects or mutations after React has updated the UI, use componentDidUpdate().', + ); + }); + + it('should warn when defaultProps was defined as an instance property', () => { + class Component extends React.Component { + constructor(props) { + super(props); + this.defaultProps = {name: 'Abhay'}; + } + + render() { + return
; + } + } + + expect(() => ReactTestUtils.renderIntoDocument()).toErrorDev( + 'Warning: Setting defaultProps as an instance property on Component is not supported ' + + 'and will be ignored. Instead, define defaultProps as a static property on Component.', + ); + }); + + // @gate !disableLegacyContext + it('should pass context to children when not owner', () => { + class Parent extends React.Component { + render() { + return ( + + + + ); + } + } + + class Child extends React.Component { + static childContextTypes = { + foo: PropTypes.string, + }; + + getChildContext() { + return { + foo: 'bar', + }; + } + + render() { + return React.Children.only(this.props.children); + } + } + + class Grandchild extends React.Component { + static contextTypes = { + foo: PropTypes.string, + }; + + render() { + return
{this.context.foo}
; + } + } + + const component = ReactTestUtils.renderIntoDocument(); + expect(ReactDOM.findDOMNode(component).innerHTML).toBe('bar'); + }); + + it('should skip update when rerendering element in container', () => { + class Parent extends React.Component { + render() { + return
{this.props.children}
; + } + } + + let childRenders = 0; + + class Child extends React.Component { + render() { + childRenders++; + return
; + } + } + + const container = document.createElement('div'); + const child = ; + + ReactDOM.render({child}, container); + ReactDOM.render({child}, container); + expect(childRenders).toBe(1); + }); + + // @gate !disableLegacyContext + it('should pass context when re-rendered for static child', () => { + let parentInstance = null; + let childInstance = null; + + class Parent extends React.Component { + static childContextTypes = { + foo: PropTypes.string, + flag: PropTypes.bool, + }; + + state = { + flag: false, + }; + + getChildContext() { + return { + foo: 'bar', + flag: this.state.flag, + }; + } + + render() { + return React.Children.only(this.props.children); + } + } + + class Middle extends React.Component { + render() { + return this.props.children; + } + } + + class Child extends React.Component { + static contextTypes = { + foo: PropTypes.string, + flag: PropTypes.bool, + }; + + render() { + childInstance = this; + return Child; + } + } + + parentInstance = ReactTestUtils.renderIntoDocument( + + + + + , + ); + + expect(parentInstance.state.flag).toBe(false); + expect(childInstance.context).toEqual({foo: 'bar', flag: false}); + + parentInstance.setState({flag: true}); + expect(parentInstance.state.flag).toBe(true); + expect(childInstance.context).toEqual({foo: 'bar', flag: true}); + }); + + // @gate !disableLegacyContext + it('should pass context when re-rendered for static child within a composite component', () => { + class Parent extends React.Component { + static childContextTypes = { + flag: PropTypes.bool, + }; + + state = { + flag: true, + }; + + getChildContext() { + return { + flag: this.state.flag, + }; + } + + render() { + return
{this.props.children}
; + } + } + + class Child extends React.Component { + static contextTypes = { + flag: PropTypes.bool, + }; + + render() { + return
; + } + } + + class Wrapper extends React.Component { + parentRef = React.createRef(); + childRef = React.createRef(); + + render() { + return ( + + + + ); + } + } + + const wrapper = ReactTestUtils.renderIntoDocument(); + + expect(wrapper.parentRef.current.state.flag).toEqual(true); + expect(wrapper.childRef.current.context).toEqual({flag: true}); + + // We update while is still a static prop relative to this update + wrapper.parentRef.current.setState({flag: false}); + + expect(wrapper.parentRef.current.state.flag).toEqual(false); + expect(wrapper.childRef.current.context).toEqual({flag: false}); + }); + + // @gate !disableLegacyContext + it('should pass context transitively', () => { + let childInstance = null; + let grandchildInstance = null; + + class Parent extends React.Component { + static childContextTypes = { + foo: PropTypes.string, + depth: PropTypes.number, + }; + + getChildContext() { + return { + foo: 'bar', + depth: 0, + }; + } + + render() { + return ; + } + } + + class Child extends React.Component { + static contextTypes = { + foo: PropTypes.string, + depth: PropTypes.number, + }; + + static childContextTypes = { + depth: PropTypes.number, + }; + + getChildContext() { + return { + depth: this.context.depth + 1, + }; + } + + render() { + childInstance = this; + return ; + } + } + + class Grandchild extends React.Component { + static contextTypes = { + foo: PropTypes.string, + depth: PropTypes.number, + }; + + render() { + grandchildInstance = this; + return
; + } + } + + ReactTestUtils.renderIntoDocument(); + expect(childInstance.context).toEqual({foo: 'bar', depth: 0}); + expect(grandchildInstance.context).toEqual({foo: 'bar', depth: 1}); + }); + + // @gate !disableLegacyContext + it('should pass context when re-rendered', () => { + let parentInstance = null; + let childInstance = null; + + class Parent extends React.Component { + static childContextTypes = { + foo: PropTypes.string, + depth: PropTypes.number, + }; + + state = { + flag: false, + }; + + getChildContext() { + return { + foo: 'bar', + depth: 0, + }; + } + + render() { + let output = ; + if (!this.state.flag) { + output = Child; + } + return output; + } + } + + class Child extends React.Component { + static contextTypes = { + foo: PropTypes.string, + depth: PropTypes.number, + }; + + render() { + childInstance = this; + return Child; + } + } + + parentInstance = ReactTestUtils.renderIntoDocument(); + expect(childInstance).toBeNull(); + + expect(parentInstance.state.flag).toBe(false); + ReactDOM.unstable_batchedUpdates(function () { + parentInstance.setState({flag: true}); + }); + expect(parentInstance.state.flag).toBe(true); + + expect(childInstance.context).toEqual({foo: 'bar', depth: 0}); + }); + + // @gate !disableLegacyContext + it('unmasked context propagates through updates', () => { + class Leaf extends React.Component { + static contextTypes = { + foo: PropTypes.string.isRequired, + }; + + UNSAFE_componentWillReceiveProps(nextProps, nextContext) { + expect('foo' in nextContext).toBe(true); + } + + shouldComponentUpdate(nextProps, nextState, nextContext) { + expect('foo' in nextContext).toBe(true); + return true; + } + + render() { + return {this.context.foo}; + } + } + + class Intermediary extends React.Component { + UNSAFE_componentWillReceiveProps(nextProps, nextContext) { + expect('foo' in nextContext).toBe(false); + } + + shouldComponentUpdate(nextProps, nextState, nextContext) { + expect('foo' in nextContext).toBe(false); + return true; + } + + render() { + return ; + } + } + + class Parent extends React.Component { + static childContextTypes = { + foo: PropTypes.string, + }; + + getChildContext() { + return { + foo: this.props.cntxt, + }; + } + + render() { + return ; + } + } + + const div = document.createElement('div'); + ReactDOM.render(, div); + expect(div.children[0].innerHTML).toBe('noise'); + div.children[0].innerHTML = 'aliens'; + div.children[0].id = 'aliens'; + expect(div.children[0].innerHTML).toBe('aliens'); + expect(div.children[0].id).toBe('aliens'); + ReactDOM.render(, div); + expect(div.children[0].innerHTML).toBe('bar'); + expect(div.children[0].id).toBe('aliens'); + }); + + // @gate !disableLegacyContext + it('should trigger componentWillReceiveProps for context changes', () => { + let contextChanges = 0; + let propChanges = 0; + + class GrandChild extends React.Component { + static contextTypes = { + foo: PropTypes.string.isRequired, + }; + + UNSAFE_componentWillReceiveProps(nextProps, nextContext) { + expect('foo' in nextContext).toBe(true); + + if (nextProps !== this.props) { + propChanges++; + } + + if (nextContext !== this.context) { + contextChanges++; + } + } + + render() { + return {this.props.children}; + } + } + + class ChildWithContext extends React.Component { + static contextTypes = { + foo: PropTypes.string.isRequired, + }; + + UNSAFE_componentWillReceiveProps(nextProps, nextContext) { + expect('foo' in nextContext).toBe(true); + + if (nextProps !== this.props) { + propChanges++; + } + + if (nextContext !== this.context) { + contextChanges++; + } + } + + render() { + return
{this.props.children}
; + } + } + + class ChildWithoutContext extends React.Component { + UNSAFE_componentWillReceiveProps(nextProps, nextContext) { + expect('foo' in nextContext).toBe(false); + + if (nextProps !== this.props) { + propChanges++; + } + + if (nextContext !== this.context) { + contextChanges++; + } + } + + render() { + return
{this.props.children}
; + } + } + + class Parent extends React.Component { + static childContextTypes = { + foo: PropTypes.string, + }; + + state = { + foo: 'abc', + }; + + getChildContext() { + return { + foo: this.state.foo, + }; + } + + render() { + return
{this.props.children}
; + } + } + + const div = document.createElement('div'); + + let parentInstance = null; + ReactDOM.render( + (parentInstance = inst)}> + + A1 + A2 + + + + B1 + B2 + + , + div, + ); + + parentInstance.setState({ + foo: 'def', + }); + + expect(propChanges).toBe(0); + expect(contextChanges).toBe(3); // ChildWithContext, GrandChild x 2 + }); + + it('should disallow nested render calls', () => { + class Inner extends React.Component { + render() { + return
; + } + } + + class Outer extends React.Component { + render() { + ReactTestUtils.renderIntoDocument(); + return
; + } + } + + expect(() => ReactTestUtils.renderIntoDocument()).toErrorDev( + 'Render methods should be a pure function of props and state; ' + + 'triggering nested component updates from render is not allowed. If ' + + 'necessary, trigger nested updates in componentDidUpdate.\n\nCheck the ' + + 'render method of Outer.', + ); + }); + + it('only renders once if updated in componentWillReceiveProps', () => { + let renders = 0; + + class Component extends React.Component { + state = {updated: false}; + + UNSAFE_componentWillReceiveProps(props) { + expect(props.update).toBe(1); + expect(renders).toBe(1); + this.setState({updated: true}); + expect(renders).toBe(1); + } + + render() { + renders++; + return
; + } + } + + const container = document.createElement('div'); + const instance = ReactDOM.render(, container); + expect(renders).toBe(1); + expect(instance.state.updated).toBe(false); + ReactDOM.render(, container); + expect(renders).toBe(2); + expect(instance.state.updated).toBe(true); + }); + + it('only renders once if updated in componentWillReceiveProps when batching', () => { + let renders = 0; + + class Component extends React.Component { + state = {updated: false}; + + UNSAFE_componentWillReceiveProps(props) { + expect(props.update).toBe(1); + expect(renders).toBe(1); + this.setState({updated: true}); + expect(renders).toBe(1); + } + + render() { + renders++; + return
; + } + } + + const container = document.createElement('div'); + const instance = ReactDOM.render(, container); + expect(renders).toBe(1); + expect(instance.state.updated).toBe(false); + ReactDOM.unstable_batchedUpdates(() => { + ReactDOM.render(, container); + }); + expect(renders).toBe(2); + expect(instance.state.updated).toBe(true); + }); + + it('should update refs if shouldComponentUpdate gives false', () => { + class Static extends React.Component { + shouldComponentUpdate() { + return false; + } + + render() { + return
{this.props.children}
; + } + } + + class Component extends React.Component { + static0Ref = React.createRef(); + static1Ref = React.createRef(); + + render() { + if (this.props.flipped) { + return ( +
+ + B (ignored) + + + A (ignored) + +
+ ); + } else { + return ( +
+ + A + + + B + +
+ ); + } + } + } + + const container = document.createElement('div'); + const comp = ReactDOM.render(, container); + expect(ReactDOM.findDOMNode(comp.static0Ref.current).textContent).toBe('A'); + expect(ReactDOM.findDOMNode(comp.static1Ref.current).textContent).toBe('B'); + + // When flipping the order, the refs should update even though the actual + // contents do not + ReactDOM.render(, container); + expect(ReactDOM.findDOMNode(comp.static0Ref.current).textContent).toBe('B'); + expect(ReactDOM.findDOMNode(comp.static1Ref.current).textContent).toBe('A'); + }); + + it('should allow access to findDOMNode in componentWillUnmount', () => { + let a = null; + let b = null; + + class Component extends React.Component { + componentDidMount() { + a = ReactDOM.findDOMNode(this); + expect(a).not.toBe(null); + } + + componentWillUnmount() { + b = ReactDOM.findDOMNode(this); + expect(b).not.toBe(null); + } + + render() { + return
; + } + } + + const container = document.createElement('div'); + expect(a).toBe(container.firstChild); + ReactDOM.render(, container); + ReactDOM.unmountComponentAtNode(container); + expect(a).toBe(b); + }); + + // @gate !disableLegacyContext || !__DEV__ + it('context should be passed down from the parent', () => { + class Parent extends React.Component { + static childContextTypes = { + foo: PropTypes.string, + }; + + getChildContext() { + return { + foo: 'bar', + }; + } + + render() { + return
{this.props.children}
; + } + } + + class Component extends React.Component { + static contextTypes = { + foo: PropTypes.string.isRequired, + }; + + render() { + return
; + } + } + + const div = document.createElement('div'); + ReactDOM.render( + + + , + div, + ); + }); + + it('should replace state', () => { + class Moo extends React.Component { + state = {x: 1}; + render() { + return
; + } + } + + const moo = ReactTestUtils.renderIntoDocument(); + // No longer a public API, but we can test that it works internally by + // reaching into the updater. + moo.updater.enqueueReplaceState(moo, {y: 2}); + expect('x' in moo.state).toBe(false); + expect(moo.state.y).toBe(2); + }); + + it('should support objects with prototypes as state', () => { + const NotActuallyImmutable = function (str) { + this.str = str; + }; + NotActuallyImmutable.prototype.amIImmutable = function () { + return true; + }; + class Moo extends React.Component { + state = new NotActuallyImmutable('first'); + // No longer a public API, but we can test that it works internally by + // reaching into the updater. + _replaceState = update => this.updater.enqueueReplaceState(this, update); + render() { + return
; + } + } + + const moo = ReactTestUtils.renderIntoDocument(); + expect(moo.state.str).toBe('first'); + expect(moo.state.amIImmutable()).toBe(true); + + const secondState = new NotActuallyImmutable('second'); + moo._replaceState(secondState); + expect(moo.state.str).toBe('second'); + expect(moo.state.amIImmutable()).toBe(true); + expect(moo.state).toBe(secondState); + + moo.setState({str: 'third'}); + expect(moo.state.str).toBe('third'); + // Here we lose the prototype. + expect(moo.state.amIImmutable).toBe(undefined); + + // When more than one state update is enqueued, we have the same behavior + const fifthState = new NotActuallyImmutable('fifth'); + ReactDOM.unstable_batchedUpdates(function () { + moo.setState({str: 'fourth'}); + moo._replaceState(fifthState); + }); + expect(moo.state).toBe(fifthState); + + // When more than one state update is enqueued, we have the same behavior + const sixthState = new NotActuallyImmutable('sixth'); + ReactDOM.unstable_batchedUpdates(function () { + moo._replaceState(sixthState); + moo.setState({str: 'seventh'}); + }); + expect(moo.state.str).toBe('seventh'); + expect(moo.state.amIImmutable).toBe(undefined); + }); + + it('should not warn about unmounting during unmounting', () => { + const container = document.createElement('div'); + const layer = document.createElement('div'); + + class Component extends React.Component { + componentDidMount() { + ReactDOM.render(
, layer); + } + + componentWillUnmount() { + ReactDOM.unmountComponentAtNode(layer); + } + + render() { + return
; + } + } + + class Outer extends React.Component { + render() { + return
{this.props.children}
; + } + } + + ReactDOM.render( + + + , + container, + ); + ReactDOM.render(, container); + }); + + it('should warn when mutated props are passed', () => { + const container = document.createElement('div'); + + class Foo extends React.Component { + constructor(props) { + const _props = {idx: props.idx + '!'}; + super(_props); + } + + render() { + return ; + } + } + + expect(() => ReactDOM.render(, container)).toErrorDev( + 'Foo(...): When calling super() in `Foo`, make sure to pass ' + + "up the same props that your component's constructor was passed.", + ); + }); + + it('should only call componentWillUnmount once', () => { + let app; + let count = 0; + + class App extends React.Component { + render() { + if (this.props.stage === 1) { + return ; + } else { + return null; + } + } + } + + class UnunmountableComponent extends React.Component { + componentWillUnmount() { + app.setState({}); + count++; + throw Error('always fails'); + } + + render() { + return
Hello {this.props.name}
; + } + } + + const container = document.createElement('div'); + + const setRef = ref => { + if (ref) { + app = ref; + } + }; + + expect(() => { + ReactDOM.render(, container); + ReactDOM.render(, container); + }).toThrow(); + expect(count).toBe(1); + }); + + it('prepares new child before unmounting old', () => { + const log = []; + + class Spy extends React.Component { + UNSAFE_componentWillMount() { + log.push(this.props.name + ' componentWillMount'); + } + render() { + log.push(this.props.name + ' render'); + return
; + } + componentDidMount() { + log.push(this.props.name + ' componentDidMount'); + } + componentWillUnmount() { + log.push(this.props.name + ' componentWillUnmount'); + } + } + + class Wrapper extends React.Component { + render() { + return ; + } + } + + const container = document.createElement('div'); + ReactDOM.render(, container); + ReactDOM.render(, container); + + expect(log).toEqual([ + 'A componentWillMount', + 'A render', + 'A componentDidMount', + + 'B componentWillMount', + 'B render', + 'A componentWillUnmount', + 'B componentDidMount', + ]); + }); + + it('respects a shallow shouldComponentUpdate implementation', () => { + let renderCalls = 0; + class PlasticWrap extends React.Component { + constructor(props, context) { + super(props, context); + this.state = { + color: 'green', + }; + this.appleRef = React.createRef(); + } + + render() { + return ; + } + } + + class Apple extends React.Component { + state = { + cut: false, + slices: 1, + }; + + shouldComponentUpdate(nextProps, nextState) { + return shallowCompare(this, nextProps, nextState); + } + + cut() { + this.setState({ + cut: true, + slices: 10, + }); + } + + eatSlice() { + this.setState({ + slices: this.state.slices - 1, + }); + } + + render() { + renderCalls++; + return
; + } + } + + const container = document.createElement('div'); + const instance = ReactDOM.render(, container); + expect(renderCalls).toBe(1); + + // Do not re-render based on props + instance.setState({color: 'green'}); + expect(renderCalls).toBe(1); + + // Re-render based on props + instance.setState({color: 'red'}); + expect(renderCalls).toBe(2); + + // Re-render base on state + instance.appleRef.current.cut(); + expect(renderCalls).toBe(3); + + // No re-render based on state + instance.appleRef.current.cut(); + expect(renderCalls).toBe(3); + + // Re-render based on state again + instance.appleRef.current.eatSlice(); + expect(renderCalls).toBe(4); + }); + + it('does not do a deep comparison for a shallow shouldComponentUpdate implementation', () => { + function getInitialState() { + return { + foo: [1, 2, 3], + bar: {a: 4, b: 5, c: 6}, + }; + } + + let renderCalls = 0; + const initialSettings = getInitialState(); + + class Component extends React.Component { + state = initialSettings; + + shouldComponentUpdate(nextProps, nextState) { + return shallowCompare(this, nextProps, nextState); + } + + render() { + renderCalls++; + return
; + } + } + + const container = document.createElement('div'); + const instance = ReactDOM.render(, container); + expect(renderCalls).toBe(1); + + // Do not re-render if state is equal + const settings = { + foo: initialSettings.foo, + bar: initialSettings.bar, + }; + instance.setState(settings); + expect(renderCalls).toBe(1); + + // Re-render because one field changed + initialSettings.foo = [1, 2, 3]; + instance.setState(initialSettings); + expect(renderCalls).toBe(2); + + // Re-render because the object changed + instance.setState(getInitialState()); + expect(renderCalls).toBe(3); + }); + + it('should call setState callback with no arguments', () => { + let mockArgs; + class Component extends React.Component { + componentDidMount() { + this.setState({}, (...args) => (mockArgs = args)); + } + render() { + return false; + } + } + + ReactTestUtils.renderIntoDocument(); + expect(mockArgs.length).toEqual(0); + }); + + it('this.state should be updated on setState callback inside componentWillMount', () => { + const div = document.createElement('div'); + let stateSuccessfullyUpdated = false; + + class Component extends React.Component { + constructor(props, context) { + super(props, context); + this.state = { + hasUpdatedState: false, + }; + } + + UNSAFE_componentWillMount() { + this.setState( + {hasUpdatedState: true}, + () => (stateSuccessfullyUpdated = this.state.hasUpdatedState), + ); + } + + render() { + return
{this.props.children}
; + } + } + + ReactDOM.render(, div); + expect(stateSuccessfullyUpdated).toBe(true); + }); + + it('should call the setState callback even if shouldComponentUpdate = false', done => { + const mockFn = jest.fn().mockReturnValue(false); + const div = document.createElement('div'); + + let instance; + + class Component extends React.Component { + constructor(props, context) { + super(props, context); + this.state = { + hasUpdatedState: false, + }; + } + + UNSAFE_componentWillMount() { + instance = this; + } + + shouldComponentUpdate() { + return mockFn(); + } + + render() { + return
{this.state.hasUpdatedState}
; + } + } + + ReactDOM.render(, div); + + expect(instance).toBeDefined(); + expect(mockFn).not.toBeCalled(); + + instance.setState({hasUpdatedState: true}, () => { + expect(mockFn).toBeCalled(); + expect(instance.state.hasUpdatedState).toBe(true); + done(); + }); + }); + + it('should return a meaningful warning when constructor is returned', () => { + class RenderTextInvalidConstructor extends React.Component { + constructor(props) { + super(props); + return {something: false}; + } + + render() { + return
; + } + } + + expect(() => { + expect(() => { + ReactTestUtils.renderIntoDocument(); + }).toThrow(); + }).toErrorDev([ + // Expect two errors because invokeGuardedCallback will dispatch an error event, + // Causing the warning to be logged again. + 'Warning: RenderTextInvalidConstructor(...): No `render` method found on the returned component instance: ' + + 'did you accidentally return an object from the constructor?', + 'Warning: RenderTextInvalidConstructor(...): No `render` method found on the returned component instance: ' + + 'did you accidentally return an object from the constructor?', + ]); + }); + + it('should warn about reassigning this.props while rendering', () => { + class Bad extends React.Component { + componentDidMount() {} + componentDidUpdate() {} + render() { + this.props = {...this.props}; + return null; + } + } + + const container = document.createElement('div'); + expect(() => { + ReactDOM.render(, container); + }).toErrorDev( + 'It looks like Bad is reassigning its own `this.props` while rendering. ' + + 'This is not supported and can lead to confusing bugs.', + ); + }); + + it('should return error if render is not defined', () => { + class RenderTestUndefinedRender extends React.Component {} + + expect(() => { + expect(() => { + ReactTestUtils.renderIntoDocument(); + }).toThrow(); + }).toErrorDev([ + // Expect two errors because invokeGuardedCallback will dispatch an error event, + // Causing the warning to be logged again. + 'Warning: RenderTestUndefinedRender(...): No `render` method found on the returned ' + + 'component instance: you may have forgotten to define `render`.', + 'Warning: RenderTestUndefinedRender(...): No `render` method found on the returned ' + + 'component instance: you may have forgotten to define `render`.', + ]); + }); + + // Regression test for accidental breaking change + // https://github.com/facebook/react/issues/13580 + it('should support classes shadowing isReactComponent', () => { + class Shadow extends React.Component { + isReactComponent() {} + render() { + return
; + } + } + const container = document.createElement('div'); + ReactDOM.render(, container); + expect(container.firstChild.tagName).toBe('DIV'); + }); + + it('should not warn on updating function component from componentWillMount', () => { + let _setState; + function A() { + _setState = React.useState()[1]; + return null; + } + class B extends React.Component { + UNSAFE_componentWillMount() { + _setState({}); + } + render() { + return null; + } + } + function Parent() { + return ( + + ); + } + const container = document.createElement('div'); + ReactDOM.render(, container); + }); + + it('should not warn on updating function component from componentWillUpdate', () => { + let _setState; + function A() { + _setState = React.useState()[1]; + return null; + } + class B extends React.Component { + UNSAFE_componentWillUpdate() { + _setState({}); + } + render() { + return null; + } + } + function Parent() { + return ( + + ); + } + const container = document.createElement('div'); + ReactDOM.render(, container); + ReactDOM.render(, container); + }); + + it('should not warn on updating function component from componentWillReceiveProps', () => { + let _setState; + function A() { + _setState = React.useState()[1]; + return null; + } + class B extends React.Component { + UNSAFE_componentWillReceiveProps() { + _setState({}); + } + render() { + return null; + } + } + function Parent() { + return ( + + ); + } + const container = document.createElement('div'); + ReactDOM.render(, container); + ReactDOM.render(, container); + }); + + it('should warn on updating function component from render', () => { + let _setState; + function A() { + _setState = React.useState()[1]; + return null; + } + class B extends React.Component { + render() { + _setState({}); + return null; + } + } + function Parent() { + return ( + + ); + } + const container = document.createElement('div'); + expect(() => { + ReactDOM.render(, container); + }).toErrorDev( + 'Cannot update a component (`A`) while rendering a different component (`B`)', + ); + // Dedupe. + ReactDOM.render(, container); + }); +}); From c43d8536b6df307e45348e95777e8e57087171e3 Mon Sep 17 00:00:00 2001 From: Ricky Hanlon Date: Thu, 25 Jan 2024 22:30:36 -0500 Subject: [PATCH 2/3] Convert ReactCompositeComponent to createRoot --- .../__tests__/ReactCompositeComponent-test.js | 1607 ++++++----------- .../ReactLegacyCompositeComponent-test.js | 1068 +---------- 2 files changed, 608 insertions(+), 2067 deletions(-) diff --git a/packages/react-dom/src/__tests__/ReactCompositeComponent-test.js b/packages/react-dom/src/__tests__/ReactCompositeComponent-test.js index bf91fb08dda10..2b423de60d9cd 100644 --- a/packages/react-dom/src/__tests__/ReactCompositeComponent-test.js +++ b/packages/react-dom/src/__tests__/ReactCompositeComponent-test.js @@ -12,10 +12,12 @@ let ChildUpdates; let MorphingComponent; let React; -let ReactDOM; +let ReactDOMX; +let ReactDOMClient; let ReactCurrentOwner; -let ReactTestUtils; -let PropTypes; +let Scheduler; +let assertLog; +let act; describe('ReactCompositeComponent', () => { const hasOwnProperty = Object.prototype.hasOwnProperty; @@ -63,56 +65,154 @@ describe('ReactCompositeComponent', () => { beforeEach(() => { jest.resetModules(); React = require('react'); - ReactDOM = require('react-dom'); + ReactDOMX = require('react-dom'); + ReactDOMClient = require('react-dom/client'); ReactCurrentOwner = require('react').__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED .ReactCurrentOwner; - ReactTestUtils = require('react-dom/test-utils'); - PropTypes = require('prop-types'); + Scheduler = require('scheduler'); + assertLog = require('internal-test-utils').assertLog; + act = require('internal-test-utils').act; + }); + + describe('MorphingComponent', () => { + let instance; + let childInstance; - MorphingComponent = class extends React.Component { - state = {activated: false}; + beforeEach(() => { + MorphingComponent = class extends React.Component { + state = {activated: false}; + xRef = React.createRef(); - xRef = React.createRef(); + componentDidMount() { + instance = this; + } + + _toggleActivatedState = () => { + this.setState({activated: !this.state.activated}); + }; - _toggleActivatedState = () => { - this.setState({activated: !this.state.activated}); + render() { + const toggleActivatedState = this._toggleActivatedState; + return !this.state.activated ? ( + + ) : ( + + ); + } }; - render() { - const toggleActivatedState = this._toggleActivatedState; - return !this.state.activated ? ( - - ) : ( - - ); - } - }; + /** + * We'll use this to ensure that an old version is not cached when it is + * reallocated again. + */ + ChildUpdates = class extends React.Component { + anchorRef = React.createRef(); - /** - * We'll use this to ensure that an old version is not cached when it is - * reallocated again. - */ - ChildUpdates = class extends React.Component { - anchorRef = React.createRef(); + componentDidMount() { + childInstance = this; + } + + getAnchor = () => { + return this.anchorRef.current; + }; - getAnchor = () => { - return this.anchorRef.current; + render() { + const className = this.props.anchorClassOn ? 'anchorClass' : ''; + return this.props.renderAnchor ? ( + + ) : ( + + ); + } }; + }); + it('should support rendering to different child types over time', async () => { + const root = ReactDOMClient.createRoot(document.createElement('div')); + await act(() => { + root.render(); + }); + expect(instance.xRef.current.tagName).toBe('A'); + + await act(() => { + instance._toggleActivatedState(); + }); + expect(instance.xRef.current.tagName).toBe('B'); + + await act(() => { + instance._toggleActivatedState(); + }); + expect(instance.xRef.current.tagName).toBe('A'); + }); - render() { - const className = this.props.anchorClassOn ? 'anchorClass' : ''; - return this.props.renderAnchor ? ( - - ) : ( - - ); + it('should react to state changes from callbacks', async () => { + const container = document.createElement('div'); + document.body.appendChild(container); + const root = ReactDOMClient.createRoot(container); + try { + await act(() => { + root.render(); + }); + expect(instance.xRef.current.tagName).toBe('A'); + await act(() => { + instance.xRef.current.click(); + }); + expect(instance.xRef.current.tagName).toBe('B'); + } finally { + document.body.removeChild(container); + root.unmount(); } - }; + }); + + it('should rewire refs when rendering to different child types', async () => { + const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); + await act(() => { + root.render(); + }); + expect(instance.xRef.current.tagName).toBe('A'); + + await act(() => { + instance._toggleActivatedState(); + }); + expect(instance.xRef.current.tagName).toBe('B'); + + await act(() => { + instance._toggleActivatedState(); + }); + expect(instance.xRef.current.tagName).toBe('A'); + }); + + it('should not cache old DOM nodes when switching constructors', async () => { + const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); + await act(() => { + root.render(); + }); + await act(() => { + root.render( + // Warm any cache + , + ); + }); + await act(() => { + root.render( + // Clear out the anchor + , + ); + }); + await act(() => { + root.render( + // rerender + , + ); + }); + expect(childInstance.getAnchor().className).toBe(''); + }); }); if (require('shared/ReactFeatureFlags').disableModulePatternComponents) { - it('should not support module pattern components', () => { + it('should not support module pattern components', async () => { function Child({test}) { return { render() { @@ -122,8 +222,13 @@ describe('ReactCompositeComponent', () => { } const el = document.createElement('div'); + const root = ReactDOMClient.createRoot(el); expect(() => { - expect(() => ReactDOM.render(, el)).toThrow( + expect(() => { + ReactDOMX.flushSync(() => { + root.render(); + }); + }).toThrow( 'Objects are not valid as a React child (found: object with keys {render}).', ); }).toErrorDev( @@ -147,7 +252,12 @@ describe('ReactCompositeComponent', () => { } const el = document.createElement('div'); - expect(() => ReactDOM.render(, el)).toErrorDev( + const root = ReactDOMClient.createRoot(el); + expect(() => { + ReactDOMX.flushSync(() => { + root.render(); + }); + }).toErrorDev( 'Warning: The component appears to be a function component that returns a class instance. ' + 'Change Child to a class that extends React.Component instead. ' + "If you can't use a class try assigning the prototype on the function as a workaround. " + @@ -159,70 +269,7 @@ describe('ReactCompositeComponent', () => { }); } - it('should support rendering to different child types over time', () => { - const instance = ReactTestUtils.renderIntoDocument(); - let el = ReactDOM.findDOMNode(instance); - expect(el.tagName).toBe('A'); - - instance._toggleActivatedState(); - el = ReactDOM.findDOMNode(instance); - expect(el.tagName).toBe('B'); - - instance._toggleActivatedState(); - el = ReactDOM.findDOMNode(instance); - expect(el.tagName).toBe('A'); - }); - - it('should react to state changes from callbacks', () => { - const container = document.createElement('div'); - document.body.appendChild(container); - try { - const instance = ReactDOM.render(, container); - let el = ReactDOM.findDOMNode(instance); - expect(el.tagName).toBe('A'); - el.click(); - el = ReactDOM.findDOMNode(instance); - expect(el.tagName).toBe('B'); - } finally { - document.body.removeChild(container); - } - }); - - it('should rewire refs when rendering to different child types', () => { - const instance = ReactTestUtils.renderIntoDocument(); - - expect(instance.xRef.current.tagName).toBe('A'); - instance._toggleActivatedState(); - expect(instance.xRef.current.tagName).toBe('B'); - instance._toggleActivatedState(); - expect(instance.xRef.current.tagName).toBe('A'); - }); - - it('should not cache old DOM nodes when switching constructors', () => { - const container = document.createElement('div'); - const instance = ReactDOM.render( - , - container, - ); - ReactDOM.render( - // Warm any cache - , - container, - ); - ReactDOM.render( - // Clear out the anchor - , - container, - ); - ReactDOM.render( - // rerender - , - container, - ); - expect(instance.getAnchor().className).toBe(''); - }); - - it('should use default values for undefined props', () => { + it('should use default values for undefined props', async () => { class Component extends React.Component { static defaultProps = {prop: 'testKey'}; @@ -231,21 +278,29 @@ describe('ReactCompositeComponent', () => { } } - const instance1 = ReactTestUtils.renderIntoDocument(); + let instance1; + let instance2; + let instance3; + const root = ReactDOMClient.createRoot(document.createElement('div')); + await act(() => { + root.render( (instance1 = ref)} />); + }); expect(instance1.props).toEqual({prop: 'testKey'}); - const instance2 = ReactTestUtils.renderIntoDocument( - , - ); + await act(() => { + root.render( + (instance2 = ref)} prop={undefined} />, + ); + }); expect(instance2.props).toEqual({prop: 'testKey'}); - const instance3 = ReactTestUtils.renderIntoDocument( - , - ); + await act(() => { + root.render( (instance3 = ref)} prop={null} />); + }); expect(instance3.props).toEqual({prop: null}); }); - it('should not mutate passed-in props object', () => { + it('should not mutate passed-in props object', async () => { class Component extends React.Component { static defaultProps = {prop: 'testKey'}; @@ -255,8 +310,11 @@ describe('ReactCompositeComponent', () => { } const inputProps = {}; - let instance1 = ; - instance1 = ReactTestUtils.renderIntoDocument(instance1); + let instance1; + const root = ReactDOMClient.createRoot(document.createElement('div')); + await act(() => { + root.render( (instance1 = ref)} />); + }); expect(instance1.props.prop).toBe('testKey'); // We don't mutate the input, just in case the caller wants to do something @@ -264,19 +322,24 @@ describe('ReactCompositeComponent', () => { expect(inputProps.prop).not.toBeDefined(); }); - it('should warn about `forceUpdate` on not-yet-mounted components', () => { + it('should warn about `forceUpdate` on not-yet-mounted components', async () => { class MyComponent extends React.Component { constructor(props) { super(props); this.forceUpdate(); } render() { - return
; + return
foo
; } } const container = document.createElement('div'); - expect(() => ReactDOM.render(, container)).toErrorDev( + const root = ReactDOMClient.createRoot(container); + expect(() => { + ReactDOMX.flushSync(() => { + root.render(); + }); + }).toErrorDev( "Warning: Can't call forceUpdate on a component that is not yet mounted. " + 'This is a no-op, but it might indicate a bug in your application. ' + 'Instead, assign to `this.state` directly or define a `state = {};` ' + @@ -285,22 +348,32 @@ describe('ReactCompositeComponent', () => { // No additional warning should be recorded const container2 = document.createElement('div'); - ReactDOM.render(, container2); + const root2 = ReactDOMClient.createRoot(container2); + await act(() => { + root2.render(); + }); + expect(container2.firstChild.textContent).toBe('foo'); }); - it('should warn about `setState` on not-yet-mounted components', () => { + it('should warn about `setState` on not-yet-mounted components', async () => { class MyComponent extends React.Component { constructor(props) { super(props); this.setState(); } render() { - return
; + return
foo
; } } const container = document.createElement('div'); - expect(() => ReactDOM.render(, container)).toErrorDev( + const root = ReactDOMClient.createRoot(container); + + expect(() => { + ReactDOMX.flushSync(() => { + root.render(); + }); + }).toErrorDev( "Warning: Can't call setState on a component that is not yet mounted. " + 'This is a no-op, but it might indicate a bug in your application. ' + 'Instead, assign to `this.state` directly or define a `state = {};` ' + @@ -309,67 +382,87 @@ describe('ReactCompositeComponent', () => { // No additional warning should be recorded const container2 = document.createElement('div'); - ReactDOM.render(, container2); + const root2 = ReactDOMClient.createRoot(container2); + await act(() => { + root2.render(); + }); + expect(container2.firstChild.textContent).toBe('foo'); }); - it('should not warn about `forceUpdate` on unmounted components', () => { + it('should not warn about `forceUpdate` on unmounted components', async () => { const container = document.createElement('div'); document.body.appendChild(container); + let instance; class Component extends React.Component { + componentDidMount() { + instance = this; + } + render() { return
; } } - let instance = ; - expect(instance.forceUpdate).not.toBeDefined(); + const component = ; + expect(component.forceUpdate).not.toBeDefined(); + const root = ReactDOMClient.createRoot(container); + await act(() => { + root.render(component); + }); - instance = ReactDOM.render(instance, container); instance.forceUpdate(); - ReactDOM.unmountComponentAtNode(container); + root.unmount(container); instance.forceUpdate(); instance.forceUpdate(); }); - it('should not warn about `setState` on unmounted components', () => { + it('should not warn about `setState` on unmounted components', async () => { const container = document.createElement('div'); document.body.appendChild(container); - let renders = 0; - class Component extends React.Component { state = {value: 0}; render() { - renders++; + Scheduler.log('render ' + this.state.value); return
; } } - let instance; - ReactDOM.render( -
- - (instance = c || instance)} /> - -
, - container, - ); + let ref; + const root = ReactDOMClient.createRoot(container); + await act(() => { + root.render( +
+ + (ref = c || ref)} /> + +
, + ); + }); - expect(renders).toBe(1); + assertLog(['render 0']); - instance.setState({value: 1}); - expect(renders).toBe(2); + await act(() => { + ref.setState({value: 1}); + }); + assertLog(['render 1']); - ReactDOM.render(
, container); - instance.setState({value: 2}); - expect(renders).toBe(2); + await act(() => { + root.render(
); + }); + + await act(() => { + ref.setState({value: 2}); + }); + // setState on an unmounted component is a noop. + assertLog([]); }); - it('should silently allow `setState`, not call cb on unmounting components', () => { + it('should silently allow `setState`, not call cb on unmounting components', async () => { let cbCalled = false; const container = document.createElement('div'); document.body.appendChild(container); @@ -389,24 +482,33 @@ describe('ReactCompositeComponent', () => { return
; } } - - const instance = ReactDOM.render(, container); + let instance; + const root = ReactDOMClient.createRoot(container); + await act(() => { + root.render( (instance = c)} />); + }); + await act(() => { + instance.setState({value: 1}); + }); instance.setState({value: 1}); - ReactDOM.unmountComponentAtNode(container); + root.unmount(); expect(cbCalled).toBe(false); }); - it('should warn when rendering a class with a render method that does not extend React.Component', () => { + it('should warn when rendering a class with a render method that does not extend React.Component', async () => { const container = document.createElement('div'); class ClassWithRenderNotExtended { render() { return
; } } + const root = ReactDOMClient.createRoot(container); expect(() => { expect(() => { - ReactDOM.render(, container); + ReactDOMX.flushSync(() => { + root.render(); + }); }).toThrow(TypeError); }).toErrorDev( 'Warning: The component appears to have a render method, ' + @@ -416,33 +518,33 @@ describe('ReactCompositeComponent', () => { // Test deduplication expect(() => { - ReactDOM.render(, container); + ReactDOMX.flushSync(() => { + root.render(); + }); }).toThrow(TypeError); }); - it('should warn about `setState` in render', () => { + it('should warn about `setState` in render', async () => { const container = document.createElement('div'); - let renderedState = -1; - let renderPasses = 0; - class Component extends React.Component { state = {value: 0}; render() { - renderPasses++; - renderedState = this.state.value; + Scheduler.log('render ' + this.state.value); if (this.state.value === 0) { this.setState({value: 1}); } - return
; + return
foo {this.state.value}
; } } let instance; - + const root = ReactDOMClient.createRoot(container); expect(() => { - instance = ReactDOM.render(, container); + ReactDOMX.flushSync(() => { + root.render( (instance = ref)} />); + }); }).toErrorDev( 'Cannot update during an existing state transition (such as within ' + '`render`). Render methods should be a pure function of props and state.', @@ -451,40 +553,37 @@ describe('ReactCompositeComponent', () => { // The setState call is queued and then executed as a second pass. This // behavior is undefined though so we're free to change it to suit the // implementation details. - expect(renderPasses).toBe(2); - expect(renderedState).toBe(1); + assertLog(['render 0', 'render 1']); expect(instance.state.value).toBe(1); // Forcing a rerender anywhere will cause the update to happen. - const instance2 = ReactDOM.render(, container); - expect(instance).toBe(instance2); - expect(renderedState).toBe(1); - expect(instance2.state.value).toBe(1); - - // Test deduplication; (no additional warnings are expected). - ReactDOM.unmountComponentAtNode(container); - ReactDOM.render(, container); + await act(() => { + root.render(); + }); + assertLog(['render 1']); }); - it('should cleanup even if render() fatals', () => { + it('should cleanup even if render() fatals', async () => { class BadComponent extends React.Component { render() { throw new Error(); } } - let instance = ; - + const instance = ; expect(ReactCurrentOwner.current).toBe(null); + const root = ReactDOMClient.createRoot(document.createElement('div')); expect(() => { - instance = ReactTestUtils.renderIntoDocument(instance); + ReactDOMX.flushSync(() => { + root.render(instance); + }); }).toThrow(); expect(ReactCurrentOwner.current).toBe(null); }); - it('should call componentWillUnmount before unmounting', () => { + it('should call componentWillUnmount before unmounting', async () => { const container = document.createElement('div'); let innerUnmounted = false; @@ -500,572 +599,149 @@ describe('ReactCompositeComponent', () => { } class Inner extends React.Component { - componentWillUnmount() { - innerUnmounted = true; - } - - render() { - return
; - } - } - - ReactDOM.render(, container); - ReactDOM.unmountComponentAtNode(container); - expect(innerUnmounted).toBe(true); - }); - - it('should warn when shouldComponentUpdate() returns undefined', () => { - class ClassComponent extends React.Component { - state = {bogus: false}; - - shouldComponentUpdate() { - return undefined; - } - - render() { - return
; - } - } - - const instance = ReactTestUtils.renderIntoDocument(); - - expect(() => instance.setState({bogus: true})).toErrorDev( - 'Warning: ClassComponent.shouldComponentUpdate(): Returned undefined instead of a ' + - 'boolean value. Make sure to return true or false.', - ); - }); - - it('should warn when componentDidUnmount method is defined', () => { - class Component extends React.Component { - componentDidUnmount = () => {}; - - render() { - return
; - } - } - - expect(() => ReactTestUtils.renderIntoDocument()).toErrorDev( - 'Warning: Component has a method called ' + - 'componentDidUnmount(). But there is no such lifecycle method. ' + - 'Did you mean componentWillUnmount()?', - ); - }); - - it('should warn when componentDidReceiveProps method is defined', () => { - class Component extends React.Component { - componentDidReceiveProps = () => {}; - - render() { - return
; - } - } - - expect(() => ReactTestUtils.renderIntoDocument()).toErrorDev( - 'Warning: Component has a method called ' + - 'componentDidReceiveProps(). But there is no such lifecycle method. ' + - 'If you meant to update the state in response to changing props, ' + - 'use componentWillReceiveProps(). If you meant to fetch data or ' + - 'run side-effects or mutations after React has updated the UI, use componentDidUpdate().', - ); - }); - - it('should warn when defaultProps was defined as an instance property', () => { - class Component extends React.Component { - constructor(props) { - super(props); - this.defaultProps = {name: 'Abhay'}; - } - - render() { - return
; - } - } - - expect(() => ReactTestUtils.renderIntoDocument()).toErrorDev( - 'Warning: Setting defaultProps as an instance property on Component is not supported ' + - 'and will be ignored. Instead, define defaultProps as a static property on Component.', - ); - }); - - // @gate !disableLegacyContext - it('should pass context to children when not owner', () => { - class Parent extends React.Component { - render() { - return ( - - - - ); - } - } - - class Child extends React.Component { - static childContextTypes = { - foo: PropTypes.string, - }; - - getChildContext() { - return { - foo: 'bar', - }; - } - - render() { - return React.Children.only(this.props.children); - } - } - - class Grandchild extends React.Component { - static contextTypes = { - foo: PropTypes.string, - }; - - render() { - return
{this.context.foo}
; - } - } - - const component = ReactTestUtils.renderIntoDocument(); - expect(ReactDOM.findDOMNode(component).innerHTML).toBe('bar'); - }); - - it('should skip update when rerendering element in container', () => { - class Parent extends React.Component { - render() { - return
{this.props.children}
; - } - } - - let childRenders = 0; - - class Child extends React.Component { - render() { - childRenders++; - return
; - } - } - - const container = document.createElement('div'); - const child = ; - - ReactDOM.render({child}, container); - ReactDOM.render({child}, container); - expect(childRenders).toBe(1); - }); - - // @gate !disableLegacyContext - it('should pass context when re-rendered for static child', () => { - let parentInstance = null; - let childInstance = null; - - class Parent extends React.Component { - static childContextTypes = { - foo: PropTypes.string, - flag: PropTypes.bool, - }; - - state = { - flag: false, - }; - - getChildContext() { - return { - foo: 'bar', - flag: this.state.flag, - }; - } - - render() { - return React.Children.only(this.props.children); - } - } - - class Middle extends React.Component { - render() { - return this.props.children; - } - } - - class Child extends React.Component { - static contextTypes = { - foo: PropTypes.string, - flag: PropTypes.bool, - }; - - render() { - childInstance = this; - return Child; - } - } - - parentInstance = ReactTestUtils.renderIntoDocument( - - - - - , - ); - - expect(parentInstance.state.flag).toBe(false); - expect(childInstance.context).toEqual({foo: 'bar', flag: false}); - - parentInstance.setState({flag: true}); - expect(parentInstance.state.flag).toBe(true); - expect(childInstance.context).toEqual({foo: 'bar', flag: true}); - }); - - // @gate !disableLegacyContext - it('should pass context when re-rendered for static child within a composite component', () => { - class Parent extends React.Component { - static childContextTypes = { - flag: PropTypes.bool, - }; - - state = { - flag: true, - }; - - getChildContext() { - return { - flag: this.state.flag, - }; - } - - render() { - return
{this.props.children}
; - } - } - - class Child extends React.Component { - static contextTypes = { - flag: PropTypes.bool, - }; - - render() { - return
; - } - } - - class Wrapper extends React.Component { - parentRef = React.createRef(); - childRef = React.createRef(); - - render() { - return ( - - - - ); - } - } - - const wrapper = ReactTestUtils.renderIntoDocument(); - - expect(wrapper.parentRef.current.state.flag).toEqual(true); - expect(wrapper.childRef.current.context).toEqual({flag: true}); - - // We update while is still a static prop relative to this update - wrapper.parentRef.current.setState({flag: false}); - - expect(wrapper.parentRef.current.state.flag).toEqual(false); - expect(wrapper.childRef.current.context).toEqual({flag: false}); - }); - - // @gate !disableLegacyContext - it('should pass context transitively', () => { - let childInstance = null; - let grandchildInstance = null; - - class Parent extends React.Component { - static childContextTypes = { - foo: PropTypes.string, - depth: PropTypes.number, - }; - - getChildContext() { - return { - foo: 'bar', - depth: 0, - }; - } - - render() { - return ; - } - } - - class Child extends React.Component { - static contextTypes = { - foo: PropTypes.string, - depth: PropTypes.number, - }; - - static childContextTypes = { - depth: PropTypes.number, - }; - - getChildContext() { - return { - depth: this.context.depth + 1, - }; - } - - render() { - childInstance = this; - return ; - } - } - - class Grandchild extends React.Component { - static contextTypes = { - foo: PropTypes.string, - depth: PropTypes.number, - }; - - render() { - grandchildInstance = this; - return
; - } - } - - ReactTestUtils.renderIntoDocument(); - expect(childInstance.context).toEqual({foo: 'bar', depth: 0}); - expect(grandchildInstance.context).toEqual({foo: 'bar', depth: 1}); - }); - - // @gate !disableLegacyContext - it('should pass context when re-rendered', () => { - let parentInstance = null; - let childInstance = null; - - class Parent extends React.Component { - static childContextTypes = { - foo: PropTypes.string, - depth: PropTypes.number, - }; - - state = { - flag: false, - }; - - getChildContext() { - return { - foo: 'bar', - depth: 0, - }; - } - - render() { - let output = ; - if (!this.state.flag) { - output = Child; - } - return output; - } - } - - class Child extends React.Component { - static contextTypes = { - foo: PropTypes.string, - depth: PropTypes.number, - }; - - render() { - childInstance = this; - return Child; - } - } - - parentInstance = ReactTestUtils.renderIntoDocument(); - expect(childInstance).toBeNull(); - - expect(parentInstance.state.flag).toBe(false); - ReactDOM.unstable_batchedUpdates(function () { - parentInstance.setState({flag: true}); - }); - expect(parentInstance.state.flag).toBe(true); - - expect(childInstance.context).toEqual({foo: 'bar', depth: 0}); - }); - - // @gate !disableLegacyContext - it('unmasked context propagates through updates', () => { - class Leaf extends React.Component { - static contextTypes = { - foo: PropTypes.string.isRequired, - }; - - UNSAFE_componentWillReceiveProps(nextProps, nextContext) { - expect('foo' in nextContext).toBe(true); - } - - shouldComponentUpdate(nextProps, nextState, nextContext) { - expect('foo' in nextContext).toBe(true); - return true; - } - - render() { - return {this.context.foo}; - } - } - - class Intermediary extends React.Component { - UNSAFE_componentWillReceiveProps(nextProps, nextContext) { - expect('foo' in nextContext).toBe(false); - } - - shouldComponentUpdate(nextProps, nextState, nextContext) { - expect('foo' in nextContext).toBe(false); - return true; - } - - render() { - return ; - } - } - - class Parent extends React.Component { - static childContextTypes = { - foo: PropTypes.string, - }; - - getChildContext() { - return { - foo: this.props.cntxt, - }; + componentWillUnmount() { + innerUnmounted = true; } render() { - return ; + return
; } } - const div = document.createElement('div'); - ReactDOM.render(, div); - expect(div.children[0].innerHTML).toBe('noise'); - div.children[0].innerHTML = 'aliens'; - div.children[0].id = 'aliens'; - expect(div.children[0].innerHTML).toBe('aliens'); - expect(div.children[0].id).toBe('aliens'); - ReactDOM.render(, div); - expect(div.children[0].innerHTML).toBe('bar'); - expect(div.children[0].id).toBe('aliens'); + const root = ReactDOMClient.createRoot(container); + await act(() => { + root.render(); + }); + root.unmount(); + expect(innerUnmounted).toBe(true); }); - // @gate !disableLegacyContext - it('should trigger componentWillReceiveProps for context changes', () => { - let contextChanges = 0; - let propChanges = 0; - - class GrandChild extends React.Component { - static contextTypes = { - foo: PropTypes.string.isRequired, - }; - - UNSAFE_componentWillReceiveProps(nextProps, nextContext) { - expect('foo' in nextContext).toBe(true); - - if (nextProps !== this.props) { - propChanges++; - } + it('should warn when shouldComponentUpdate() returns undefined', async () => { + class ClassComponent extends React.Component { + state = {bogus: false}; - if (nextContext !== this.context) { - contextChanges++; - } + shouldComponentUpdate() { + return undefined; } render() { - return {this.props.children}; + return
; } } + let instance; + const root = ReactDOMClient.createRoot(document.createElement('div')); + await act(() => { + root.render( (instance = ref)} />); + }); - class ChildWithContext extends React.Component { - static contextTypes = { - foo: PropTypes.string.isRequired, - }; - - UNSAFE_componentWillReceiveProps(nextProps, nextContext) { - expect('foo' in nextContext).toBe(true); + expect(() => { + ReactDOMX.flushSync(() => { + instance.setState({bogus: true}); + }); + }).toErrorDev( + 'Warning: ClassComponent.shouldComponentUpdate(): Returned undefined instead of a ' + + 'boolean value. Make sure to return true or false.', + ); + }); - if (nextProps !== this.props) { - propChanges++; - } + it('should warn when componentDidUnmount method is defined', async () => { + class Component extends React.Component { + componentDidUnmount = () => {}; - if (nextContext !== this.context) { - contextChanges++; - } + render() { + return
; } + } + + const root = ReactDOMClient.createRoot(document.createElement('div')); + expect(() => { + ReactDOMX.flushSync(() => { + root.render(); + }); + }).toErrorDev( + 'Warning: Component has a method called ' + + 'componentDidUnmount(). But there is no such lifecycle method. ' + + 'Did you mean componentWillUnmount()?', + ); + }); + + it('should warn when componentDidReceiveProps method is defined', () => { + class Component extends React.Component { + componentDidReceiveProps = () => {}; render() { - return
{this.props.children}
; + return
; } } - class ChildWithoutContext extends React.Component { - UNSAFE_componentWillReceiveProps(nextProps, nextContext) { - expect('foo' in nextContext).toBe(false); + const root = ReactDOMClient.createRoot(document.createElement('div')); - if (nextProps !== this.props) { - propChanges++; - } + expect(() => { + ReactDOMX.flushSync(() => { + root.render(); + }); + }).toErrorDev( + 'Warning: Component has a method called ' + + 'componentDidReceiveProps(). But there is no such lifecycle method. ' + + 'If you meant to update the state in response to changing props, ' + + 'use componentWillReceiveProps(). If you meant to fetch data or ' + + 'run side-effects or mutations after React has updated the UI, use componentDidUpdate().', + ); + }); - if (nextContext !== this.context) { - contextChanges++; - } + it('should warn when defaultProps was defined as an instance property', () => { + class Component extends React.Component { + constructor(props) { + super(props); + this.defaultProps = {name: 'Abhay'}; } render() { - return
{this.props.children}
; + return
; } } + const root = ReactDOMClient.createRoot(document.createElement('div')); - class Parent extends React.Component { - static childContextTypes = { - foo: PropTypes.string, - }; - - state = { - foo: 'abc', - }; + expect(() => { + ReactDOMX.flushSync(() => { + root.render(); + }); + }).toErrorDev( + 'Warning: Setting defaultProps as an instance property on Component is not supported ' + + 'and will be ignored. Instead, define defaultProps as a static property on Component.', + ); + }); - getChildContext() { - return { - foo: this.state.foo, - }; + it('should skip update when rerendering element in container', async () => { + class Parent extends React.Component { + render() { + return
{this.props.children}
; } + } + class Child extends React.Component { render() { - return
{this.props.children}
; + Scheduler.log('Child render'); + return
; } } - const div = document.createElement('div'); - - let parentInstance = null; - ReactDOM.render( - (parentInstance = inst)}> - - A1 - A2 - - - - B1 - B2 - - , - div, - ); - - parentInstance.setState({ - foo: 'def', + const container = document.createElement('div'); + const child = ; + const root = ReactDOMClient.createRoot(container); + await act(() => { + root.render({child}); }); + assertLog(['Child render']); - expect(propChanges).toBe(0); - expect(contextChanges).toBe(3); // ChildWithContext, GrandChild x 2 + await act(() => { + root.render({child}); + }); + assertLog([]); }); it('should disallow nested render calls', () => { + const root = ReactDOMClient.createRoot(document.createElement('div')); class Inner extends React.Component { render() { return
; @@ -1074,12 +750,16 @@ describe('ReactCompositeComponent', () => { class Outer extends React.Component { render() { - ReactTestUtils.renderIntoDocument(); + root.render(); return
; } } - expect(() => ReactTestUtils.renderIntoDocument()).toErrorDev( + expect(() => { + ReactDOMX.flushSync(() => { + root.render(); + }); + }).toErrorDev( 'Render methods should be a pure function of props and state; ' + 'triggering nested component updates from render is not allowed. If ' + 'necessary, trigger nested updates in componentDidUpdate.\n\nCheck the ' + @@ -1087,7 +767,7 @@ describe('ReactCompositeComponent', () => { ); }); - it('only renders once if updated in componentWillReceiveProps', () => { + it('only renders once if updated in componentWillReceiveProps', async () => { let renders = 0; class Component extends React.Component { @@ -1107,15 +787,23 @@ describe('ReactCompositeComponent', () => { } const container = document.createElement('div'); - const instance = ReactDOM.render(, container); + const root = ReactDOMClient.createRoot(container); + let instance; + + await act(() => { + root.render( (instance = ref)} />); + }); expect(renders).toBe(1); expect(instance.state.updated).toBe(false); - ReactDOM.render(, container); + + await act(() => { + root.render( (instance = ref)} />); + }); expect(renders).toBe(2); expect(instance.state.updated).toBe(true); }); - it('only renders once if updated in componentWillReceiveProps when batching', () => { + it('only renders once if updated in componentWillReceiveProps when batching', async () => { let renders = 0; class Component extends React.Component { @@ -1135,234 +823,21 @@ describe('ReactCompositeComponent', () => { } const container = document.createElement('div'); - const instance = ReactDOM.render(, container); + const root = ReactDOMClient.createRoot(container); + let instance; + await act(() => { + root.render( (instance = ref)} />); + }); expect(renders).toBe(1); expect(instance.state.updated).toBe(false); - ReactDOM.unstable_batchedUpdates(() => { - ReactDOM.render(, container); + await act(() => { + root.render( (instance = ref)} />); }); expect(renders).toBe(2); expect(instance.state.updated).toBe(true); }); - it('should update refs if shouldComponentUpdate gives false', () => { - class Static extends React.Component { - shouldComponentUpdate() { - return false; - } - - render() { - return
{this.props.children}
; - } - } - - class Component extends React.Component { - static0Ref = React.createRef(); - static1Ref = React.createRef(); - - render() { - if (this.props.flipped) { - return ( -
- - B (ignored) - - - A (ignored) - -
- ); - } else { - return ( -
- - A - - - B - -
- ); - } - } - } - - const container = document.createElement('div'); - const comp = ReactDOM.render(, container); - expect(ReactDOM.findDOMNode(comp.static0Ref.current).textContent).toBe('A'); - expect(ReactDOM.findDOMNode(comp.static1Ref.current).textContent).toBe('B'); - - // When flipping the order, the refs should update even though the actual - // contents do not - ReactDOM.render(, container); - expect(ReactDOM.findDOMNode(comp.static0Ref.current).textContent).toBe('B'); - expect(ReactDOM.findDOMNode(comp.static1Ref.current).textContent).toBe('A'); - }); - - it('should allow access to findDOMNode in componentWillUnmount', () => { - let a = null; - let b = null; - - class Component extends React.Component { - componentDidMount() { - a = ReactDOM.findDOMNode(this); - expect(a).not.toBe(null); - } - - componentWillUnmount() { - b = ReactDOM.findDOMNode(this); - expect(b).not.toBe(null); - } - - render() { - return
; - } - } - - const container = document.createElement('div'); - expect(a).toBe(container.firstChild); - ReactDOM.render(, container); - ReactDOM.unmountComponentAtNode(container); - expect(a).toBe(b); - }); - - // @gate !disableLegacyContext || !__DEV__ - it('context should be passed down from the parent', () => { - class Parent extends React.Component { - static childContextTypes = { - foo: PropTypes.string, - }; - - getChildContext() { - return { - foo: 'bar', - }; - } - - render() { - return
{this.props.children}
; - } - } - - class Component extends React.Component { - static contextTypes = { - foo: PropTypes.string.isRequired, - }; - - render() { - return
; - } - } - - const div = document.createElement('div'); - ReactDOM.render( - - - , - div, - ); - }); - - it('should replace state', () => { - class Moo extends React.Component { - state = {x: 1}; - render() { - return
; - } - } - - const moo = ReactTestUtils.renderIntoDocument(); - // No longer a public API, but we can test that it works internally by - // reaching into the updater. - moo.updater.enqueueReplaceState(moo, {y: 2}); - expect('x' in moo.state).toBe(false); - expect(moo.state.y).toBe(2); - }); - - it('should support objects with prototypes as state', () => { - const NotActuallyImmutable = function (str) { - this.str = str; - }; - NotActuallyImmutable.prototype.amIImmutable = function () { - return true; - }; - class Moo extends React.Component { - state = new NotActuallyImmutable('first'); - // No longer a public API, but we can test that it works internally by - // reaching into the updater. - _replaceState = update => this.updater.enqueueReplaceState(this, update); - render() { - return
; - } - } - - const moo = ReactTestUtils.renderIntoDocument(); - expect(moo.state.str).toBe('first'); - expect(moo.state.amIImmutable()).toBe(true); - - const secondState = new NotActuallyImmutable('second'); - moo._replaceState(secondState); - expect(moo.state.str).toBe('second'); - expect(moo.state.amIImmutable()).toBe(true); - expect(moo.state).toBe(secondState); - - moo.setState({str: 'third'}); - expect(moo.state.str).toBe('third'); - // Here we lose the prototype. - expect(moo.state.amIImmutable).toBe(undefined); - - // When more than one state update is enqueued, we have the same behavior - const fifthState = new NotActuallyImmutable('fifth'); - ReactDOM.unstable_batchedUpdates(function () { - moo.setState({str: 'fourth'}); - moo._replaceState(fifthState); - }); - expect(moo.state).toBe(fifthState); - - // When more than one state update is enqueued, we have the same behavior - const sixthState = new NotActuallyImmutable('sixth'); - ReactDOM.unstable_batchedUpdates(function () { - moo._replaceState(sixthState); - moo.setState({str: 'seventh'}); - }); - expect(moo.state.str).toBe('seventh'); - expect(moo.state.amIImmutable).toBe(undefined); - }); - - it('should not warn about unmounting during unmounting', () => { - const container = document.createElement('div'); - const layer = document.createElement('div'); - - class Component extends React.Component { - componentDidMount() { - ReactDOM.render(
, layer); - } - - componentWillUnmount() { - ReactDOM.unmountComponentAtNode(layer); - } - - render() { - return
; - } - } - - class Outer extends React.Component { - render() { - return
{this.props.children}
; - } - } - - ReactDOM.render( - - - , - container, - ); - ReactDOM.render(, container); - }); - - it('should warn when mutated props are passed', () => { + it('should warn when mutated props are passed', async () => { const container = document.createElement('div'); class Foo extends React.Component { @@ -1376,7 +851,12 @@ describe('ReactCompositeComponent', () => { } } - expect(() => ReactDOM.render(, container)).toErrorDev( + const root = ReactDOMClient.createRoot(container); + expect(() => { + ReactDOMX.flushSync(() => { + root.render(); + }); + }).toErrorDev( 'Foo(...): When calling super() in `Foo`, make sure to pass ' + "up the same props that your component's constructor was passed.", ); @@ -1416,29 +896,32 @@ describe('ReactCompositeComponent', () => { } }; + const root = ReactDOMClient.createRoot(container); expect(() => { - ReactDOM.render(, container); - ReactDOM.render(, container); + ReactDOMX.flushSync(() => { + root.render(); + }); + ReactDOMX.flushSync(() => { + root.render(); + }); }).toThrow(); expect(count).toBe(1); }); - it('prepares new child before unmounting old', () => { - const log = []; - + it('prepares new child before unmounting old', async () => { class Spy extends React.Component { UNSAFE_componentWillMount() { - log.push(this.props.name + ' componentWillMount'); + Scheduler.log(this.props.name + ' componentWillMount'); } render() { - log.push(this.props.name + ' render'); + Scheduler.log(this.props.name + ' render'); return
; } componentDidMount() { - log.push(this.props.name + ' componentDidMount'); + Scheduler.log(this.props.name + ' componentDidMount'); } componentWillUnmount() { - log.push(this.props.name + ' componentWillUnmount'); + Scheduler.log(this.props.name + ' componentWillUnmount'); } } @@ -1449,10 +932,15 @@ describe('ReactCompositeComponent', () => { } const container = document.createElement('div'); - ReactDOM.render(, container); - ReactDOM.render(, container); + const root = ReactDOMClient.createRoot(container); + await act(() => { + root.render(); + }); + await act(() => { + root.render(); + }); - expect(log).toEqual([ + assertLog([ 'A componentWillMount', 'A render', 'A componentDidMount', @@ -1464,8 +952,7 @@ describe('ReactCompositeComponent', () => { ]); }); - it('respects a shallow shouldComponentUpdate implementation', () => { - let renderCalls = 0; + it('respects a shallow shouldComponentUpdate implementation', async () => { class PlasticWrap extends React.Component { constructor(props, context) { super(props, context); @@ -1504,37 +991,54 @@ describe('ReactCompositeComponent', () => { } render() { - renderCalls++; + const {color} = this.props; + const {cut, slices} = this.state; + + Scheduler.log(`${color} ${cut} ${slices}`); return
; } } const container = document.createElement('div'); - const instance = ReactDOM.render(, container); - expect(renderCalls).toBe(1); + const root = ReactDOMClient.createRoot(container); + let instance; + await act(() => { + root.render( (instance = ref)} />); + }); + assertLog(['green false 1']); // Do not re-render based on props - instance.setState({color: 'green'}); - expect(renderCalls).toBe(1); + await act(() => { + instance.setState({color: 'green'}); + }); + assertLog([]); // Re-render based on props - instance.setState({color: 'red'}); - expect(renderCalls).toBe(2); + await act(() => { + instance.setState({color: 'red'}); + }); + assertLog(['red false 1']); // Re-render base on state - instance.appleRef.current.cut(); - expect(renderCalls).toBe(3); + await act(() => { + instance.appleRef.current.cut(); + }); + assertLog(['red true 10']); // No re-render based on state - instance.appleRef.current.cut(); - expect(renderCalls).toBe(3); + await act(() => { + instance.appleRef.current.cut(); + }); + assertLog([]); // Re-render based on state again - instance.appleRef.current.eatSlice(); - expect(renderCalls).toBe(4); + await act(() => { + instance.appleRef.current.eatSlice(); + }); + assertLog(['red true 9']); }); - it('does not do a deep comparison for a shallow shouldComponentUpdate implementation', () => { + it('does not do a deep comparison for a shallow shouldComponentUpdate implementation', async () => { function getInitialState() { return { foo: [1, 2, 3], @@ -1542,7 +1046,6 @@ describe('ReactCompositeComponent', () => { }; } - let renderCalls = 0; const initialSettings = getInitialState(); class Component extends React.Component { @@ -1553,34 +1056,45 @@ describe('ReactCompositeComponent', () => { } render() { - renderCalls++; + const {foo, bar} = this.state; + Scheduler.log(`{foo:[${foo}],bar:{a:${bar.a},b:${bar.b},c:${bar.c}}`); return
; } } const container = document.createElement('div'); - const instance = ReactDOM.render(, container); - expect(renderCalls).toBe(1); + const root = ReactDOMClient.createRoot(container); + let instance; + await act(() => { + root.render( (instance = ref)} />); + }); + assertLog(['{foo:[1,2,3],bar:{a:4,b:5,c:6}']); // Do not re-render if state is equal const settings = { foo: initialSettings.foo, bar: initialSettings.bar, }; - instance.setState(settings); - expect(renderCalls).toBe(1); + await act(() => { + instance.setState(settings); + }); + assertLog([]); // Re-render because one field changed initialSettings.foo = [1, 2, 3]; - instance.setState(initialSettings); - expect(renderCalls).toBe(2); + await act(() => { + instance.setState(initialSettings); + }); + assertLog(['{foo:[1,2,3],bar:{a:4,b:5,c:6}']); // Re-render because the object changed - instance.setState(getInitialState()); - expect(renderCalls).toBe(3); + await act(() => { + instance.setState(getInitialState()); + }); + assertLog(['{foo:[1,2,3],bar:{a:4,b:5,c:6}']); }); - it('should call setState callback with no arguments', () => { + it('should call setState callback with no arguments', async () => { let mockArgs; class Component extends React.Component { componentDidMount() { @@ -1590,12 +1104,15 @@ describe('ReactCompositeComponent', () => { return false; } } + const root = ReactDOMClient.createRoot(document.createElement('div')); + await act(() => { + root.render(); + }); - ReactTestUtils.renderIntoDocument(); expect(mockArgs.length).toEqual(0); }); - it('this.state should be updated on setState callback inside componentWillMount', () => { + it('this.state should be updated on setState callback inside componentWillMount', async () => { const div = document.createElement('div'); let stateSuccessfullyUpdated = false; @@ -1619,16 +1136,18 @@ describe('ReactCompositeComponent', () => { } } - ReactDOM.render(, div); + const root = ReactDOMClient.createRoot(div); + await act(() => { + root.render(); + }); + expect(stateSuccessfullyUpdated).toBe(true); }); - it('should call the setState callback even if shouldComponentUpdate = false', done => { + it('should call the setState callback even if shouldComponentUpdate = false', async () => { const mockFn = jest.fn().mockReturnValue(false); const div = document.createElement('div'); - let instance; - class Component extends React.Component { constructor(props, context) { super(props, context); @@ -1650,16 +1169,24 @@ describe('ReactCompositeComponent', () => { } } - ReactDOM.render(, div); + const root = ReactDOMClient.createRoot(div); + let instance; + await act(() => { + root.render( (instance = ref)} />); + }); expect(instance).toBeDefined(); expect(mockFn).not.toBeCalled(); - instance.setState({hasUpdatedState: true}, () => { - expect(mockFn).toBeCalled(); - expect(instance.state.hasUpdatedState).toBe(true); - done(); + await act(() => { + instance.setState({hasUpdatedState: true}, () => { + expect(mockFn).toBeCalled(); + expect(instance.state.hasUpdatedState).toBe(true); + Scheduler.log('setState callback called'); + }); }); + + assertLog(['setState callback called']); }); it('should return a meaningful warning when constructor is returned', () => { @@ -1674,9 +1201,12 @@ describe('ReactCompositeComponent', () => { } } + const root = ReactDOMClient.createRoot(document.createElement('div')); expect(() => { expect(() => { - ReactTestUtils.renderIntoDocument(); + ReactDOMX.flushSync(() => { + root.render(); + }); }).toThrow(); }).toErrorDev([ // Expect two errors because invokeGuardedCallback will dispatch an error event, @@ -1685,6 +1215,11 @@ describe('ReactCompositeComponent', () => { 'did you accidentally return an object from the constructor?', 'Warning: RenderTextInvalidConstructor(...): No `render` method found on the returned component instance: ' + 'did you accidentally return an object from the constructor?', + // And then two more because we retry errors. + 'Warning: RenderTextInvalidConstructor(...): No `render` method found on the returned component instance: ' + + 'did you accidentally return an object from the constructor?', + 'Warning: RenderTextInvalidConstructor(...): No `render` method found on the returned component instance: ' + + 'did you accidentally return an object from the constructor?', ]); }); @@ -1699,8 +1234,11 @@ describe('ReactCompositeComponent', () => { } const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); expect(() => { - ReactDOM.render(, container); + ReactDOMX.flushSync(() => { + root.render(); + }); }).toErrorDev( 'It looks like Bad is reassigning its own `this.props` while rendering. ' + 'This is not supported and can lead to confusing bugs.', @@ -1710,9 +1248,12 @@ describe('ReactCompositeComponent', () => { it('should return error if render is not defined', () => { class RenderTestUndefinedRender extends React.Component {} + const root = ReactDOMClient.createRoot(document.createElement('div')); expect(() => { expect(() => { - ReactTestUtils.renderIntoDocument(); + ReactDOMX.flushSync(() => { + root.render(); + }); }).toThrow(); }).toErrorDev([ // Expect two errors because invokeGuardedCallback will dispatch an error event, @@ -1721,12 +1262,18 @@ describe('ReactCompositeComponent', () => { 'component instance: you may have forgotten to define `render`.', 'Warning: RenderTestUndefinedRender(...): No `render` method found on the returned ' + 'component instance: you may have forgotten to define `render`.', + + // And then two more because we retry errors. + 'Warning: RenderTestUndefinedRender(...): No `render` method found on the returned ' + + 'component instance: you may have forgotten to define `render`.', + 'Warning: RenderTestUndefinedRender(...): No `render` method found on the returned ' + + 'component instance: you may have forgotten to define `render`.', ]); }); // Regression test for accidental breaking change // https://github.com/facebook/react/issues/13580 - it('should support classes shadowing isReactComponent', () => { + it('should support classes shadowing isReactComponent', async () => { class Shadow extends React.Component { isReactComponent() {} render() { @@ -1734,19 +1281,24 @@ describe('ReactCompositeComponent', () => { } } const container = document.createElement('div'); - ReactDOM.render(, container); + const root = ReactDOMClient.createRoot(container); + await act(() => { + root.render(); + }); expect(container.firstChild.tagName).toBe('DIV'); }); - it('should not warn on updating function component from componentWillMount', () => { - let _setState; + it('should not warn on updating function component from componentWillMount', async () => { + let setState; + let ref; function A() { - _setState = React.useState()[1]; - return null; + const [state, _setState] = React.useState(null); + setState = _setState; + return
(ref = r)}>{state}
; } class B extends React.Component { UNSAFE_componentWillMount() { - _setState({}); + setState(1); } render() { return null; @@ -1761,18 +1313,25 @@ describe('ReactCompositeComponent', () => { ); } const container = document.createElement('div'); - ReactDOM.render(, container); + const root = ReactDOMClient.createRoot(container); + await act(() => { + root.render(); + }); + + expect(ref.textContent).toBe('1'); }); - it('should not warn on updating function component from componentWillUpdate', () => { - let _setState; + it('should not warn on updating function component from componentWillUpdate', async () => { + let setState; + let ref; function A() { - _setState = React.useState()[1]; - return null; + const [state, _setState] = React.useState(); + setState = _setState; + return
(ref = r)}>{state}
; } class B extends React.Component { UNSAFE_componentWillUpdate() { - _setState({}); + setState(1); } render() { return null; @@ -1787,19 +1346,29 @@ describe('ReactCompositeComponent', () => { ); } const container = document.createElement('div'); - ReactDOM.render(, container); - ReactDOM.render(, container); + const root = ReactDOMClient.createRoot(container); + await act(() => { + root.render(); + }); + await act(() => { + root.render(); + }); + + expect(ref.textContent).toBe('1'); }); - it('should not warn on updating function component from componentWillReceiveProps', () => { - let _setState; + it('should not warn on updating function component from componentWillReceiveProps', async () => { + let setState; + let ref; function A() { - _setState = React.useState()[1]; - return null; + const [state, _setState] = React.useState(); + setState = _setState; + return
(ref = r)}>{state}
; } + class B extends React.Component { UNSAFE_componentWillReceiveProps() { - _setState({}); + setState(1); } render() { return null; @@ -1814,19 +1383,29 @@ describe('ReactCompositeComponent', () => { ); } const container = document.createElement('div'); - ReactDOM.render(, container); - ReactDOM.render(, container); + const root = ReactDOMClient.createRoot(container); + await act(() => { + root.render(); + }); + await act(() => { + root.render(); + }); + + expect(ref.textContent).toBe('1'); }); it('should warn on updating function component from render', () => { - let _setState; + let setState; + let ref; function A() { - _setState = React.useState()[1]; - return null; + const [state, _setState] = React.useState(0); + setState = _setState; + return
(ref = r)}>{state}
; } + class B extends React.Component { render() { - _setState({}); + setState(c => c + 1); return null; } } @@ -1839,12 +1418,24 @@ describe('ReactCompositeComponent', () => { ); } const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); expect(() => { - ReactDOM.render(, container); + ReactDOMX.flushSync(() => { + root.render(); + }); }).toErrorDev( 'Cannot update a component (`A`) while rendering a different component (`B`)', ); + + // We error, but still update the state. + expect(ref.textContent).toBe('1'); + // Dedupe. - ReactDOM.render(, container); + ReactDOMX.flushSync(() => { + root.render(); + }); + + // We error, but still update the state. + expect(ref.textContent).toBe('2'); }); }); diff --git a/packages/react-dom/src/__tests__/ReactLegacyCompositeComponent-test.js b/packages/react-dom/src/__tests__/ReactLegacyCompositeComponent-test.js index bf91fb08dda10..d1f5493e95f44 100644 --- a/packages/react-dom/src/__tests__/ReactLegacyCompositeComponent-test.js +++ b/packages/react-dom/src/__tests__/ReactLegacyCompositeComponent-test.js @@ -9,418 +9,21 @@ 'use strict'; -let ChildUpdates; -let MorphingComponent; let React; let ReactDOM; -let ReactCurrentOwner; let ReactTestUtils; let PropTypes; -describe('ReactCompositeComponent', () => { - const hasOwnProperty = Object.prototype.hasOwnProperty; - - /** - * Performs equality by iterating through keys on an object and returning false - * when any key has values which are not strictly equal between the arguments. - * Returns true when the values of all keys are strictly equal. - */ - function shallowEqual(objA: mixed, objB: mixed): boolean { - if (Object.is(objA, objB)) { - return true; - } - if ( - typeof objA !== 'object' || - objA === null || - typeof objB !== 'object' || - objB === null - ) { - return false; - } - const keysA = Object.keys(objA); - const keysB = Object.keys(objB); - if (keysA.length !== keysB.length) { - return false; - } - for (let i = 0; i < keysA.length; i++) { - if ( - !hasOwnProperty.call(objB, keysA[i]) || - !Object.is(objA[keysA[i]], objB[keysA[i]]) - ) { - return false; - } - } - return true; - } - - function shallowCompare(instance, nextProps, nextState) { - return ( - !shallowEqual(instance.props, nextProps) || - !shallowEqual(instance.state, nextState) - ); - } - +describe('ReactLegacyCompositeComponent', () => { beforeEach(() => { jest.resetModules(); React = require('react'); ReactDOM = require('react-dom'); - ReactCurrentOwner = - require('react').__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED - .ReactCurrentOwner; ReactTestUtils = require('react-dom/test-utils'); PropTypes = require('prop-types'); - - MorphingComponent = class extends React.Component { - state = {activated: false}; - - xRef = React.createRef(); - - _toggleActivatedState = () => { - this.setState({activated: !this.state.activated}); - }; - - render() { - const toggleActivatedState = this._toggleActivatedState; - return !this.state.activated ? ( - - ) : ( - - ); - } - }; - - /** - * We'll use this to ensure that an old version is not cached when it is - * reallocated again. - */ - ChildUpdates = class extends React.Component { - anchorRef = React.createRef(); - - getAnchor = () => { - return this.anchorRef.current; - }; - - render() { - const className = this.props.anchorClassOn ? 'anchorClass' : ''; - return this.props.renderAnchor ? ( - - ) : ( - - ); - } - }; - }); - - if (require('shared/ReactFeatureFlags').disableModulePatternComponents) { - it('should not support module pattern components', () => { - function Child({test}) { - return { - render() { - return
{test}
; - }, - }; - } - - const el = document.createElement('div'); - expect(() => { - expect(() => ReactDOM.render(, el)).toThrow( - 'Objects are not valid as a React child (found: object with keys {render}).', - ); - }).toErrorDev( - 'Warning: The component appears to be a function component that returns a class instance. ' + - 'Change Child to a class that extends React.Component instead. ' + - "If you can't use a class try assigning the prototype on the function as a workaround. " + - '`Child.prototype = React.Component.prototype`. ' + - "Don't use an arrow function since it cannot be called with `new` by React.", - ); - - expect(el.textContent).toBe(''); - }); - } else { - it('should support module pattern components', () => { - function Child({test}) { - return { - render() { - return
{test}
; - }, - }; - } - - const el = document.createElement('div'); - expect(() => ReactDOM.render(, el)).toErrorDev( - 'Warning: The component appears to be a function component that returns a class instance. ' + - 'Change Child to a class that extends React.Component instead. ' + - "If you can't use a class try assigning the prototype on the function as a workaround. " + - '`Child.prototype = React.Component.prototype`. ' + - "Don't use an arrow function since it cannot be called with `new` by React.", - ); - - expect(el.textContent).toBe('test'); - }); - } - - it('should support rendering to different child types over time', () => { - const instance = ReactTestUtils.renderIntoDocument(); - let el = ReactDOM.findDOMNode(instance); - expect(el.tagName).toBe('A'); - - instance._toggleActivatedState(); - el = ReactDOM.findDOMNode(instance); - expect(el.tagName).toBe('B'); - - instance._toggleActivatedState(); - el = ReactDOM.findDOMNode(instance); - expect(el.tagName).toBe('A'); - }); - - it('should react to state changes from callbacks', () => { - const container = document.createElement('div'); - document.body.appendChild(container); - try { - const instance = ReactDOM.render(, container); - let el = ReactDOM.findDOMNode(instance); - expect(el.tagName).toBe('A'); - el.click(); - el = ReactDOM.findDOMNode(instance); - expect(el.tagName).toBe('B'); - } finally { - document.body.removeChild(container); - } - }); - - it('should rewire refs when rendering to different child types', () => { - const instance = ReactTestUtils.renderIntoDocument(); - - expect(instance.xRef.current.tagName).toBe('A'); - instance._toggleActivatedState(); - expect(instance.xRef.current.tagName).toBe('B'); - instance._toggleActivatedState(); - expect(instance.xRef.current.tagName).toBe('A'); - }); - - it('should not cache old DOM nodes when switching constructors', () => { - const container = document.createElement('div'); - const instance = ReactDOM.render( - , - container, - ); - ReactDOM.render( - // Warm any cache - , - container, - ); - ReactDOM.render( - // Clear out the anchor - , - container, - ); - ReactDOM.render( - // rerender - , - container, - ); - expect(instance.getAnchor().className).toBe(''); - }); - - it('should use default values for undefined props', () => { - class Component extends React.Component { - static defaultProps = {prop: 'testKey'}; - - render() { - return ; - } - } - - const instance1 = ReactTestUtils.renderIntoDocument(); - expect(instance1.props).toEqual({prop: 'testKey'}); - - const instance2 = ReactTestUtils.renderIntoDocument( - , - ); - expect(instance2.props).toEqual({prop: 'testKey'}); - - const instance3 = ReactTestUtils.renderIntoDocument( - , - ); - expect(instance3.props).toEqual({prop: null}); - }); - - it('should not mutate passed-in props object', () => { - class Component extends React.Component { - static defaultProps = {prop: 'testKey'}; - - render() { - return ; - } - } - - const inputProps = {}; - let instance1 = ; - instance1 = ReactTestUtils.renderIntoDocument(instance1); - expect(instance1.props.prop).toBe('testKey'); - - // We don't mutate the input, just in case the caller wants to do something - // with it after using it to instantiate a component - expect(inputProps.prop).not.toBeDefined(); }); - it('should warn about `forceUpdate` on not-yet-mounted components', () => { - class MyComponent extends React.Component { - constructor(props) { - super(props); - this.forceUpdate(); - } - render() { - return
; - } - } - - const container = document.createElement('div'); - expect(() => ReactDOM.render(, container)).toErrorDev( - "Warning: Can't call forceUpdate on a component that is not yet mounted. " + - 'This is a no-op, but it might indicate a bug in your application. ' + - 'Instead, assign to `this.state` directly or define a `state = {};` ' + - 'class property with the desired state in the MyComponent component.', - ); - - // No additional warning should be recorded - const container2 = document.createElement('div'); - ReactDOM.render(, container2); - }); - - it('should warn about `setState` on not-yet-mounted components', () => { - class MyComponent extends React.Component { - constructor(props) { - super(props); - this.setState(); - } - render() { - return
; - } - } - - const container = document.createElement('div'); - expect(() => ReactDOM.render(, container)).toErrorDev( - "Warning: Can't call setState on a component that is not yet mounted. " + - 'This is a no-op, but it might indicate a bug in your application. ' + - 'Instead, assign to `this.state` directly or define a `state = {};` ' + - 'class property with the desired state in the MyComponent component.', - ); - - // No additional warning should be recorded - const container2 = document.createElement('div'); - ReactDOM.render(, container2); - }); - - it('should not warn about `forceUpdate` on unmounted components', () => { - const container = document.createElement('div'); - document.body.appendChild(container); - - class Component extends React.Component { - render() { - return
; - } - } - - let instance = ; - expect(instance.forceUpdate).not.toBeDefined(); - - instance = ReactDOM.render(instance, container); - instance.forceUpdate(); - - ReactDOM.unmountComponentAtNode(container); - - instance.forceUpdate(); - instance.forceUpdate(); - }); - - it('should not warn about `setState` on unmounted components', () => { - const container = document.createElement('div'); - document.body.appendChild(container); - - let renders = 0; - - class Component extends React.Component { - state = {value: 0}; - - render() { - renders++; - return
; - } - } - - let instance; - ReactDOM.render( -
- - (instance = c || instance)} /> - -
, - container, - ); - - expect(renders).toBe(1); - - instance.setState({value: 1}); - expect(renders).toBe(2); - - ReactDOM.render(
, container); - instance.setState({value: 2}); - expect(renders).toBe(2); - }); - - it('should silently allow `setState`, not call cb on unmounting components', () => { - let cbCalled = false; - const container = document.createElement('div'); - document.body.appendChild(container); - - class Component extends React.Component { - state = {value: 0}; - - componentWillUnmount() { - expect(() => { - this.setState({value: 2}, function () { - cbCalled = true; - }); - }).not.toThrow(); - } - - render() { - return
; - } - } - - const instance = ReactDOM.render(, container); - instance.setState({value: 1}); - - ReactDOM.unmountComponentAtNode(container); - expect(cbCalled).toBe(false); - }); - - it('should warn when rendering a class with a render method that does not extend React.Component', () => { - const container = document.createElement('div'); - class ClassWithRenderNotExtended { - render() { - return
; - } - } - expect(() => { - expect(() => { - ReactDOM.render(, container); - }).toThrow(TypeError); - }).toErrorDev( - 'Warning: The component appears to have a render method, ' + - "but doesn't extend React.Component. This is likely to cause errors. " + - 'Change ClassWithRenderNotExtended to extend React.Component instead.', - ); - - // Test deduplication - expect(() => { - ReactDOM.render(, container); - }).toThrow(TypeError); - }); - - it('should warn about `setState` in render', () => { + it('should warn about `setState` in render in legacy mode', () => { const container = document.createElement('div'); let renderedState = -1; @@ -466,127 +69,6 @@ describe('ReactCompositeComponent', () => { ReactDOM.render(, container); }); - it('should cleanup even if render() fatals', () => { - class BadComponent extends React.Component { - render() { - throw new Error(); - } - } - - let instance = ; - - expect(ReactCurrentOwner.current).toBe(null); - - expect(() => { - instance = ReactTestUtils.renderIntoDocument(instance); - }).toThrow(); - - expect(ReactCurrentOwner.current).toBe(null); - }); - - it('should call componentWillUnmount before unmounting', () => { - const container = document.createElement('div'); - let innerUnmounted = false; - - class Component extends React.Component { - render() { - return ( -
- - Text -
- ); - } - } - - class Inner extends React.Component { - componentWillUnmount() { - innerUnmounted = true; - } - - render() { - return
; - } - } - - ReactDOM.render(, container); - ReactDOM.unmountComponentAtNode(container); - expect(innerUnmounted).toBe(true); - }); - - it('should warn when shouldComponentUpdate() returns undefined', () => { - class ClassComponent extends React.Component { - state = {bogus: false}; - - shouldComponentUpdate() { - return undefined; - } - - render() { - return
; - } - } - - const instance = ReactTestUtils.renderIntoDocument(); - - expect(() => instance.setState({bogus: true})).toErrorDev( - 'Warning: ClassComponent.shouldComponentUpdate(): Returned undefined instead of a ' + - 'boolean value. Make sure to return true or false.', - ); - }); - - it('should warn when componentDidUnmount method is defined', () => { - class Component extends React.Component { - componentDidUnmount = () => {}; - - render() { - return
; - } - } - - expect(() => ReactTestUtils.renderIntoDocument()).toErrorDev( - 'Warning: Component has a method called ' + - 'componentDidUnmount(). But there is no such lifecycle method. ' + - 'Did you mean componentWillUnmount()?', - ); - }); - - it('should warn when componentDidReceiveProps method is defined', () => { - class Component extends React.Component { - componentDidReceiveProps = () => {}; - - render() { - return
; - } - } - - expect(() => ReactTestUtils.renderIntoDocument()).toErrorDev( - 'Warning: Component has a method called ' + - 'componentDidReceiveProps(). But there is no such lifecycle method. ' + - 'If you meant to update the state in response to changing props, ' + - 'use componentWillReceiveProps(). If you meant to fetch data or ' + - 'run side-effects or mutations after React has updated the UI, use componentDidUpdate().', - ); - }); - - it('should warn when defaultProps was defined as an instance property', () => { - class Component extends React.Component { - constructor(props) { - super(props); - this.defaultProps = {name: 'Abhay'}; - } - - render() { - return
; - } - } - - expect(() => ReactTestUtils.renderIntoDocument()).toErrorDev( - 'Warning: Setting defaultProps as an instance property on Component is not supported ' + - 'and will be ignored. Instead, define defaultProps as a static property on Component.', - ); - }); - // @gate !disableLegacyContext it('should pass context to children when not owner', () => { class Parent extends React.Component { @@ -629,30 +111,6 @@ describe('ReactCompositeComponent', () => { expect(ReactDOM.findDOMNode(component).innerHTML).toBe('bar'); }); - it('should skip update when rerendering element in container', () => { - class Parent extends React.Component { - render() { - return
{this.props.children}
; - } - } - - let childRenders = 0; - - class Child extends React.Component { - render() { - childRenders++; - return
; - } - } - - const container = document.createElement('div'); - const child = ; - - ReactDOM.render({child}, container); - ReactDOM.render({child}, container); - expect(childRenders).toBe(1); - }); - // @gate !disableLegacyContext it('should pass context when re-rendered for static child', () => { let parentInstance = null; @@ -1065,29 +523,7 @@ describe('ReactCompositeComponent', () => { expect(contextChanges).toBe(3); // ChildWithContext, GrandChild x 2 }); - it('should disallow nested render calls', () => { - class Inner extends React.Component { - render() { - return
; - } - } - - class Outer extends React.Component { - render() { - ReactTestUtils.renderIntoDocument(); - return
; - } - } - - expect(() => ReactTestUtils.renderIntoDocument()).toErrorDev( - 'Render methods should be a pure function of props and state; ' + - 'triggering nested component updates from render is not allowed. If ' + - 'necessary, trigger nested updates in componentDidUpdate.\n\nCheck the ' + - 'render method of Outer.', - ); - }); - - it('only renders once if updated in componentWillReceiveProps', () => { + it('only renders once if updated in componentWillReceiveProps in legacy mode', () => { let renders = 0; class Component extends React.Component { @@ -1115,7 +551,7 @@ describe('ReactCompositeComponent', () => { expect(instance.state.updated).toBe(true); }); - it('only renders once if updated in componentWillReceiveProps when batching', () => { + it('only renders once if updated in componentWillReceiveProps when batching in legacy mode', () => { let renders = 0; class Component extends React.Component { @@ -1145,7 +581,7 @@ describe('ReactCompositeComponent', () => { expect(instance.state.updated).toBe(true); }); - it('should update refs if shouldComponentUpdate gives false', () => { + it('should update refs if shouldComponentUpdate gives false in legacy mode', () => { class Static extends React.Component { shouldComponentUpdate() { return false; @@ -1199,7 +635,7 @@ describe('ReactCompositeComponent', () => { expect(ReactDOM.findDOMNode(comp.static1Ref.current).textContent).toBe('A'); }); - it('should allow access to findDOMNode in componentWillUnmount', () => { + it('should allow access to findDOMNode in componentWillUnmount in legacy mode', () => { let a = null; let b = null; @@ -1263,7 +699,7 @@ describe('ReactCompositeComponent', () => { ); }); - it('should replace state', () => { + it('should replace state in legacy mode', () => { class Moo extends React.Component { state = {x: 1}; render() { @@ -1279,7 +715,7 @@ describe('ReactCompositeComponent', () => { expect(moo.state.y).toBe(2); }); - it('should support objects with prototypes as state', () => { + it('should support objects with prototypes as state in legacy mode', () => { const NotActuallyImmutable = function (str) { this.str = str; }; @@ -1329,7 +765,7 @@ describe('ReactCompositeComponent', () => { expect(moo.state.amIImmutable).toBe(undefined); }); - it('should not warn about unmounting during unmounting', () => { + it('should not warn about unmounting during unmounting in legacy mode', () => { const container = document.createElement('div'); const layer = document.createElement('div'); @@ -1361,490 +797,4 @@ describe('ReactCompositeComponent', () => { ); ReactDOM.render(, container); }); - - it('should warn when mutated props are passed', () => { - const container = document.createElement('div'); - - class Foo extends React.Component { - constructor(props) { - const _props = {idx: props.idx + '!'}; - super(_props); - } - - render() { - return ; - } - } - - expect(() => ReactDOM.render(, container)).toErrorDev( - 'Foo(...): When calling super() in `Foo`, make sure to pass ' + - "up the same props that your component's constructor was passed.", - ); - }); - - it('should only call componentWillUnmount once', () => { - let app; - let count = 0; - - class App extends React.Component { - render() { - if (this.props.stage === 1) { - return ; - } else { - return null; - } - } - } - - class UnunmountableComponent extends React.Component { - componentWillUnmount() { - app.setState({}); - count++; - throw Error('always fails'); - } - - render() { - return
Hello {this.props.name}
; - } - } - - const container = document.createElement('div'); - - const setRef = ref => { - if (ref) { - app = ref; - } - }; - - expect(() => { - ReactDOM.render(, container); - ReactDOM.render(, container); - }).toThrow(); - expect(count).toBe(1); - }); - - it('prepares new child before unmounting old', () => { - const log = []; - - class Spy extends React.Component { - UNSAFE_componentWillMount() { - log.push(this.props.name + ' componentWillMount'); - } - render() { - log.push(this.props.name + ' render'); - return
; - } - componentDidMount() { - log.push(this.props.name + ' componentDidMount'); - } - componentWillUnmount() { - log.push(this.props.name + ' componentWillUnmount'); - } - } - - class Wrapper extends React.Component { - render() { - return ; - } - } - - const container = document.createElement('div'); - ReactDOM.render(, container); - ReactDOM.render(, container); - - expect(log).toEqual([ - 'A componentWillMount', - 'A render', - 'A componentDidMount', - - 'B componentWillMount', - 'B render', - 'A componentWillUnmount', - 'B componentDidMount', - ]); - }); - - it('respects a shallow shouldComponentUpdate implementation', () => { - let renderCalls = 0; - class PlasticWrap extends React.Component { - constructor(props, context) { - super(props, context); - this.state = { - color: 'green', - }; - this.appleRef = React.createRef(); - } - - render() { - return ; - } - } - - class Apple extends React.Component { - state = { - cut: false, - slices: 1, - }; - - shouldComponentUpdate(nextProps, nextState) { - return shallowCompare(this, nextProps, nextState); - } - - cut() { - this.setState({ - cut: true, - slices: 10, - }); - } - - eatSlice() { - this.setState({ - slices: this.state.slices - 1, - }); - } - - render() { - renderCalls++; - return
; - } - } - - const container = document.createElement('div'); - const instance = ReactDOM.render(, container); - expect(renderCalls).toBe(1); - - // Do not re-render based on props - instance.setState({color: 'green'}); - expect(renderCalls).toBe(1); - - // Re-render based on props - instance.setState({color: 'red'}); - expect(renderCalls).toBe(2); - - // Re-render base on state - instance.appleRef.current.cut(); - expect(renderCalls).toBe(3); - - // No re-render based on state - instance.appleRef.current.cut(); - expect(renderCalls).toBe(3); - - // Re-render based on state again - instance.appleRef.current.eatSlice(); - expect(renderCalls).toBe(4); - }); - - it('does not do a deep comparison for a shallow shouldComponentUpdate implementation', () => { - function getInitialState() { - return { - foo: [1, 2, 3], - bar: {a: 4, b: 5, c: 6}, - }; - } - - let renderCalls = 0; - const initialSettings = getInitialState(); - - class Component extends React.Component { - state = initialSettings; - - shouldComponentUpdate(nextProps, nextState) { - return shallowCompare(this, nextProps, nextState); - } - - render() { - renderCalls++; - return
; - } - } - - const container = document.createElement('div'); - const instance = ReactDOM.render(, container); - expect(renderCalls).toBe(1); - - // Do not re-render if state is equal - const settings = { - foo: initialSettings.foo, - bar: initialSettings.bar, - }; - instance.setState(settings); - expect(renderCalls).toBe(1); - - // Re-render because one field changed - initialSettings.foo = [1, 2, 3]; - instance.setState(initialSettings); - expect(renderCalls).toBe(2); - - // Re-render because the object changed - instance.setState(getInitialState()); - expect(renderCalls).toBe(3); - }); - - it('should call setState callback with no arguments', () => { - let mockArgs; - class Component extends React.Component { - componentDidMount() { - this.setState({}, (...args) => (mockArgs = args)); - } - render() { - return false; - } - } - - ReactTestUtils.renderIntoDocument(); - expect(mockArgs.length).toEqual(0); - }); - - it('this.state should be updated on setState callback inside componentWillMount', () => { - const div = document.createElement('div'); - let stateSuccessfullyUpdated = false; - - class Component extends React.Component { - constructor(props, context) { - super(props, context); - this.state = { - hasUpdatedState: false, - }; - } - - UNSAFE_componentWillMount() { - this.setState( - {hasUpdatedState: true}, - () => (stateSuccessfullyUpdated = this.state.hasUpdatedState), - ); - } - - render() { - return
{this.props.children}
; - } - } - - ReactDOM.render(, div); - expect(stateSuccessfullyUpdated).toBe(true); - }); - - it('should call the setState callback even if shouldComponentUpdate = false', done => { - const mockFn = jest.fn().mockReturnValue(false); - const div = document.createElement('div'); - - let instance; - - class Component extends React.Component { - constructor(props, context) { - super(props, context); - this.state = { - hasUpdatedState: false, - }; - } - - UNSAFE_componentWillMount() { - instance = this; - } - - shouldComponentUpdate() { - return mockFn(); - } - - render() { - return
{this.state.hasUpdatedState}
; - } - } - - ReactDOM.render(, div); - - expect(instance).toBeDefined(); - expect(mockFn).not.toBeCalled(); - - instance.setState({hasUpdatedState: true}, () => { - expect(mockFn).toBeCalled(); - expect(instance.state.hasUpdatedState).toBe(true); - done(); - }); - }); - - it('should return a meaningful warning when constructor is returned', () => { - class RenderTextInvalidConstructor extends React.Component { - constructor(props) { - super(props); - return {something: false}; - } - - render() { - return
; - } - } - - expect(() => { - expect(() => { - ReactTestUtils.renderIntoDocument(); - }).toThrow(); - }).toErrorDev([ - // Expect two errors because invokeGuardedCallback will dispatch an error event, - // Causing the warning to be logged again. - 'Warning: RenderTextInvalidConstructor(...): No `render` method found on the returned component instance: ' + - 'did you accidentally return an object from the constructor?', - 'Warning: RenderTextInvalidConstructor(...): No `render` method found on the returned component instance: ' + - 'did you accidentally return an object from the constructor?', - ]); - }); - - it('should warn about reassigning this.props while rendering', () => { - class Bad extends React.Component { - componentDidMount() {} - componentDidUpdate() {} - render() { - this.props = {...this.props}; - return null; - } - } - - const container = document.createElement('div'); - expect(() => { - ReactDOM.render(, container); - }).toErrorDev( - 'It looks like Bad is reassigning its own `this.props` while rendering. ' + - 'This is not supported and can lead to confusing bugs.', - ); - }); - - it('should return error if render is not defined', () => { - class RenderTestUndefinedRender extends React.Component {} - - expect(() => { - expect(() => { - ReactTestUtils.renderIntoDocument(); - }).toThrow(); - }).toErrorDev([ - // Expect two errors because invokeGuardedCallback will dispatch an error event, - // Causing the warning to be logged again. - 'Warning: RenderTestUndefinedRender(...): No `render` method found on the returned ' + - 'component instance: you may have forgotten to define `render`.', - 'Warning: RenderTestUndefinedRender(...): No `render` method found on the returned ' + - 'component instance: you may have forgotten to define `render`.', - ]); - }); - - // Regression test for accidental breaking change - // https://github.com/facebook/react/issues/13580 - it('should support classes shadowing isReactComponent', () => { - class Shadow extends React.Component { - isReactComponent() {} - render() { - return
; - } - } - const container = document.createElement('div'); - ReactDOM.render(, container); - expect(container.firstChild.tagName).toBe('DIV'); - }); - - it('should not warn on updating function component from componentWillMount', () => { - let _setState; - function A() { - _setState = React.useState()[1]; - return null; - } - class B extends React.Component { - UNSAFE_componentWillMount() { - _setState({}); - } - render() { - return null; - } - } - function Parent() { - return ( - - ); - } - const container = document.createElement('div'); - ReactDOM.render(, container); - }); - - it('should not warn on updating function component from componentWillUpdate', () => { - let _setState; - function A() { - _setState = React.useState()[1]; - return null; - } - class B extends React.Component { - UNSAFE_componentWillUpdate() { - _setState({}); - } - render() { - return null; - } - } - function Parent() { - return ( - - ); - } - const container = document.createElement('div'); - ReactDOM.render(, container); - ReactDOM.render(, container); - }); - - it('should not warn on updating function component from componentWillReceiveProps', () => { - let _setState; - function A() { - _setState = React.useState()[1]; - return null; - } - class B extends React.Component { - UNSAFE_componentWillReceiveProps() { - _setState({}); - } - render() { - return null; - } - } - function Parent() { - return ( - - ); - } - const container = document.createElement('div'); - ReactDOM.render(, container); - ReactDOM.render(, container); - }); - - it('should warn on updating function component from render', () => { - let _setState; - function A() { - _setState = React.useState()[1]; - return null; - } - class B extends React.Component { - render() { - _setState({}); - return null; - } - } - function Parent() { - return ( - - ); - } - const container = document.createElement('div'); - expect(() => { - ReactDOM.render(, container); - }).toErrorDev( - 'Cannot update a component (`A`) while rendering a different component (`B`)', - ); - // Dedupe. - ReactDOM.render(, container); - }); }); From c98cac96ae40f7ca2cef15d207690a8934058a35 Mon Sep 17 00:00:00 2001 From: Ricky Hanlon Date: Fri, 26 Jan 2024 17:01:15 -0500 Subject: [PATCH 3/3] s/ReactDOMX/ReactDOM --- .../__tests__/ReactCompositeComponent-test.js | 46 +++++++++---------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/packages/react-dom/src/__tests__/ReactCompositeComponent-test.js b/packages/react-dom/src/__tests__/ReactCompositeComponent-test.js index 2b423de60d9cd..c58e825203f8e 100644 --- a/packages/react-dom/src/__tests__/ReactCompositeComponent-test.js +++ b/packages/react-dom/src/__tests__/ReactCompositeComponent-test.js @@ -12,7 +12,7 @@ let ChildUpdates; let MorphingComponent; let React; -let ReactDOMX; +let ReactDOM; let ReactDOMClient; let ReactCurrentOwner; let Scheduler; @@ -65,7 +65,7 @@ describe('ReactCompositeComponent', () => { beforeEach(() => { jest.resetModules(); React = require('react'); - ReactDOMX = require('react-dom'); + ReactDOM = require('react-dom'); ReactDOMClient = require('react-dom/client'); ReactCurrentOwner = require('react').__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED @@ -225,7 +225,7 @@ describe('ReactCompositeComponent', () => { const root = ReactDOMClient.createRoot(el); expect(() => { expect(() => { - ReactDOMX.flushSync(() => { + ReactDOM.flushSync(() => { root.render(); }); }).toThrow( @@ -254,7 +254,7 @@ describe('ReactCompositeComponent', () => { const el = document.createElement('div'); const root = ReactDOMClient.createRoot(el); expect(() => { - ReactDOMX.flushSync(() => { + ReactDOM.flushSync(() => { root.render(); }); }).toErrorDev( @@ -336,7 +336,7 @@ describe('ReactCompositeComponent', () => { const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); expect(() => { - ReactDOMX.flushSync(() => { + ReactDOM.flushSync(() => { root.render(); }); }).toErrorDev( @@ -370,7 +370,7 @@ describe('ReactCompositeComponent', () => { const root = ReactDOMClient.createRoot(container); expect(() => { - ReactDOMX.flushSync(() => { + ReactDOM.flushSync(() => { root.render(); }); }).toErrorDev( @@ -506,7 +506,7 @@ describe('ReactCompositeComponent', () => { const root = ReactDOMClient.createRoot(container); expect(() => { expect(() => { - ReactDOMX.flushSync(() => { + ReactDOM.flushSync(() => { root.render(); }); }).toThrow(TypeError); @@ -518,7 +518,7 @@ describe('ReactCompositeComponent', () => { // Test deduplication expect(() => { - ReactDOMX.flushSync(() => { + ReactDOM.flushSync(() => { root.render(); }); }).toThrow(TypeError); @@ -542,7 +542,7 @@ describe('ReactCompositeComponent', () => { let instance; const root = ReactDOMClient.createRoot(container); expect(() => { - ReactDOMX.flushSync(() => { + ReactDOM.flushSync(() => { root.render( (instance = ref)} />); }); }).toErrorDev( @@ -575,7 +575,7 @@ describe('ReactCompositeComponent', () => { const root = ReactDOMClient.createRoot(document.createElement('div')); expect(() => { - ReactDOMX.flushSync(() => { + ReactDOM.flushSync(() => { root.render(instance); }); }).toThrow(); @@ -635,7 +635,7 @@ describe('ReactCompositeComponent', () => { }); expect(() => { - ReactDOMX.flushSync(() => { + ReactDOM.flushSync(() => { instance.setState({bogus: true}); }); }).toErrorDev( @@ -655,7 +655,7 @@ describe('ReactCompositeComponent', () => { const root = ReactDOMClient.createRoot(document.createElement('div')); expect(() => { - ReactDOMX.flushSync(() => { + ReactDOM.flushSync(() => { root.render(); }); }).toErrorDev( @@ -677,7 +677,7 @@ describe('ReactCompositeComponent', () => { const root = ReactDOMClient.createRoot(document.createElement('div')); expect(() => { - ReactDOMX.flushSync(() => { + ReactDOM.flushSync(() => { root.render(); }); }).toErrorDev( @@ -703,7 +703,7 @@ describe('ReactCompositeComponent', () => { const root = ReactDOMClient.createRoot(document.createElement('div')); expect(() => { - ReactDOMX.flushSync(() => { + ReactDOM.flushSync(() => { root.render(); }); }).toErrorDev( @@ -756,7 +756,7 @@ describe('ReactCompositeComponent', () => { } expect(() => { - ReactDOMX.flushSync(() => { + ReactDOM.flushSync(() => { root.render(); }); }).toErrorDev( @@ -853,7 +853,7 @@ describe('ReactCompositeComponent', () => { const root = ReactDOMClient.createRoot(container); expect(() => { - ReactDOMX.flushSync(() => { + ReactDOM.flushSync(() => { root.render(); }); }).toErrorDev( @@ -898,10 +898,10 @@ describe('ReactCompositeComponent', () => { const root = ReactDOMClient.createRoot(container); expect(() => { - ReactDOMX.flushSync(() => { + ReactDOM.flushSync(() => { root.render(); }); - ReactDOMX.flushSync(() => { + ReactDOM.flushSync(() => { root.render(); }); }).toThrow(); @@ -1204,7 +1204,7 @@ describe('ReactCompositeComponent', () => { const root = ReactDOMClient.createRoot(document.createElement('div')); expect(() => { expect(() => { - ReactDOMX.flushSync(() => { + ReactDOM.flushSync(() => { root.render(); }); }).toThrow(); @@ -1236,7 +1236,7 @@ describe('ReactCompositeComponent', () => { const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); expect(() => { - ReactDOMX.flushSync(() => { + ReactDOM.flushSync(() => { root.render(); }); }).toErrorDev( @@ -1251,7 +1251,7 @@ describe('ReactCompositeComponent', () => { const root = ReactDOMClient.createRoot(document.createElement('div')); expect(() => { expect(() => { - ReactDOMX.flushSync(() => { + ReactDOM.flushSync(() => { root.render(); }); }).toThrow(); @@ -1420,7 +1420,7 @@ describe('ReactCompositeComponent', () => { const container = document.createElement('div'); const root = ReactDOMClient.createRoot(container); expect(() => { - ReactDOMX.flushSync(() => { + ReactDOM.flushSync(() => { root.render(); }); }).toErrorDev( @@ -1431,7 +1431,7 @@ describe('ReactCompositeComponent', () => { expect(ref.textContent).toBe('1'); // Dedupe. - ReactDOMX.flushSync(() => { + ReactDOM.flushSync(() => { root.render(); });