From e3b2dd64d57231e666a52551dcb428cda45ede87 Mon Sep 17 00:00:00 2001 From: Ricky Hanlon Date: Wed, 24 Jan 2024 13:25:12 -0500 Subject: [PATCH 1/2] Copy ReactDOMFiber to ReactDOMLegacyFiber --- .../src/__tests__/ReactDOMLegacyFiber-test.js | 1375 +++++++++++++++++ 1 file changed, 1375 insertions(+) create mode 100644 packages/react-dom/src/__tests__/ReactDOMLegacyFiber-test.js diff --git a/packages/react-dom/src/__tests__/ReactDOMLegacyFiber-test.js b/packages/react-dom/src/__tests__/ReactDOMLegacyFiber-test.js new file mode 100644 index 0000000000000..06773b68f1f55 --- /dev/null +++ b/packages/react-dom/src/__tests__/ReactDOMLegacyFiber-test.js @@ -0,0 +1,1375 @@ +/** + * 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'; + +const React = require('react'); +const ReactDOM = require('react-dom'); +const PropTypes = require('prop-types'); + +describe('ReactDOMFiber', () => { + let container; + + beforeEach(() => { + container = document.createElement('div'); + document.body.appendChild(container); + }); + + afterEach(() => { + document.body.removeChild(container); + container = null; + jest.restoreAllMocks(); + }); + + it('should render strings as children', () => { + const Box = ({value}) =>
{value}
; + + ReactDOM.render(, container); + expect(container.textContent).toEqual('foo'); + }); + + it('should render numbers as children', () => { + const Box = ({value}) =>
{value}
; + + ReactDOM.render(, container); + + expect(container.textContent).toEqual('10'); + }); + + it('should be called a callback argument', () => { + // mounting phase + let called = false; + ReactDOM.render(
Foo
, container, () => (called = true)); + expect(called).toEqual(true); + + // updating phase + called = false; + ReactDOM.render(
Foo
, container, () => (called = true)); + expect(called).toEqual(true); + }); + + it('should call a callback argument when the same element is re-rendered', () => { + class Foo extends React.Component { + render() { + return
Foo
; + } + } + const element = ; + + // mounting phase + let called = false; + ReactDOM.render(element, container, () => (called = true)); + expect(called).toEqual(true); + + // updating phase + called = false; + ReactDOM.unstable_batchedUpdates(() => { + ReactDOM.render(element, container, () => (called = true)); + }); + expect(called).toEqual(true); + }); + + it('should render a component returning strings directly from render', () => { + const Text = ({value}) => value; + + ReactDOM.render(, container); + expect(container.textContent).toEqual('foo'); + }); + + it('should render a component returning numbers directly from render', () => { + const Text = ({value}) => value; + + ReactDOM.render(, container); + + expect(container.textContent).toEqual('10'); + }); + + it('finds the DOM Text node of a string child', () => { + class Text extends React.Component { + render() { + return this.props.value; + } + } + + let instance = null; + ReactDOM.render( + (instance = ref)} />, + container, + ); + + const textNode = ReactDOM.findDOMNode(instance); + expect(textNode).toBe(container.firstChild); + expect(textNode.nodeType).toBe(3); + expect(textNode.nodeValue).toBe('foo'); + }); + + it('finds the first child when a component returns a fragment', () => { + class Fragment extends React.Component { + render() { + return [
, ]; + } + } + + let instance = null; + ReactDOM.render( (instance = ref)} />, container); + + expect(container.childNodes.length).toBe(2); + + const firstNode = ReactDOM.findDOMNode(instance); + expect(firstNode).toBe(container.firstChild); + expect(firstNode.tagName).toBe('DIV'); + }); + + it('finds the first child even when fragment is nested', () => { + class Wrapper extends React.Component { + render() { + return this.props.children; + } + } + + class Fragment extends React.Component { + render() { + return [ + +
+ , + , + ]; + } + } + + let instance = null; + ReactDOM.render( (instance = ref)} />, container); + + expect(container.childNodes.length).toBe(2); + + const firstNode = ReactDOM.findDOMNode(instance); + expect(firstNode).toBe(container.firstChild); + expect(firstNode.tagName).toBe('DIV'); + }); + + it('finds the first child even when first child renders null', () => { + class NullComponent extends React.Component { + render() { + return null; + } + } + + class Fragment extends React.Component { + render() { + return [,
, ]; + } + } + + let instance = null; + ReactDOM.render( (instance = ref)} />, container); + + expect(container.childNodes.length).toBe(2); + + const firstNode = ReactDOM.findDOMNode(instance); + expect(firstNode).toBe(container.firstChild); + expect(firstNode.tagName).toBe('DIV'); + }); + + it('renders an empty fragment', () => { + const Div = () =>
; + const EmptyFragment = () => <>; + const NonEmptyFragment = () => ( + <> +
+ + ); + + ReactDOM.render(, container); + expect(container.firstChild).toBe(null); + + ReactDOM.render(, container); + expect(container.firstChild.tagName).toBe('DIV'); + + ReactDOM.render(, container); + expect(container.firstChild).toBe(null); + + ReactDOM.render(
, container); + expect(container.firstChild.tagName).toBe('DIV'); + + ReactDOM.render(, container); + expect(container.firstChild).toBe(null); + }); + + let svgEls, htmlEls, mathEls; + const expectSVG = {ref: el => svgEls.push(el)}; + const expectHTML = {ref: el => htmlEls.push(el)}; + const expectMath = {ref: el => mathEls.push(el)}; + + const usePortal = function (tree) { + return ReactDOM.createPortal(tree, document.createElement('div')); + }; + + const assertNamespacesMatch = function (tree) { + const testContainer = document.createElement('div'); + svgEls = []; + htmlEls = []; + mathEls = []; + + ReactDOM.render(tree, testContainer); + svgEls.forEach(el => { + expect(el.namespaceURI).toBe('http://www.w3.org/2000/svg'); + }); + htmlEls.forEach(el => { + expect(el.namespaceURI).toBe('http://www.w3.org/1999/xhtml'); + }); + mathEls.forEach(el => { + expect(el.namespaceURI).toBe('http://www.w3.org/1998/Math/MathML'); + }); + + ReactDOM.unmountComponentAtNode(testContainer); + expect(testContainer.innerHTML).toBe(''); + }; + + it('should render one portal', () => { + const portalContainer = document.createElement('div'); + + ReactDOM.render( +
{ReactDOM.createPortal(
portal
, portalContainer)}
, + container, + ); + expect(portalContainer.innerHTML).toBe('
portal
'); + expect(container.innerHTML).toBe('
'); + + ReactDOM.unmountComponentAtNode(container); + expect(portalContainer.innerHTML).toBe(''); + expect(container.innerHTML).toBe(''); + }); + + it('should render many portals', () => { + const portalContainer1 = document.createElement('div'); + const portalContainer2 = document.createElement('div'); + + const ops = []; + class Child extends React.Component { + componentDidMount() { + ops.push(`${this.props.name} componentDidMount`); + } + componentDidUpdate() { + ops.push(`${this.props.name} componentDidUpdate`); + } + componentWillUnmount() { + ops.push(`${this.props.name} componentWillUnmount`); + } + render() { + return
{this.props.name}
; + } + } + + class Parent extends React.Component { + componentDidMount() { + ops.push(`Parent:${this.props.step} componentDidMount`); + } + componentDidUpdate() { + ops.push(`Parent:${this.props.step} componentDidUpdate`); + } + componentWillUnmount() { + ops.push(`Parent:${this.props.step} componentWillUnmount`); + } + render() { + const {step} = this.props; + return [ + , + ReactDOM.createPortal( + , + portalContainer1, + ), + , + ReactDOM.createPortal( + [ + , + , + ], + portalContainer2, + ), + ]; + } + } + + ReactDOM.render(, container); + expect(portalContainer1.innerHTML).toBe('
portal1[0]:a
'); + expect(portalContainer2.innerHTML).toBe( + '
portal2[0]:a
portal2[1]:a
', + ); + expect(container.innerHTML).toBe( + '
normal[0]:a
normal[1]:a
', + ); + expect(ops).toEqual([ + 'normal[0]:a componentDidMount', + 'portal1[0]:a componentDidMount', + 'normal[1]:a componentDidMount', + 'portal2[0]:a componentDidMount', + 'portal2[1]:a componentDidMount', + 'Parent:a componentDidMount', + ]); + + ops.length = 0; + ReactDOM.render(, container); + expect(portalContainer1.innerHTML).toBe('
portal1[0]:b
'); + expect(portalContainer2.innerHTML).toBe( + '
portal2[0]:b
portal2[1]:b
', + ); + expect(container.innerHTML).toBe( + '
normal[0]:b
normal[1]:b
', + ); + expect(ops).toEqual([ + 'normal[0]:b componentDidUpdate', + 'portal1[0]:b componentDidUpdate', + 'normal[1]:b componentDidUpdate', + 'portal2[0]:b componentDidUpdate', + 'portal2[1]:b componentDidUpdate', + 'Parent:b componentDidUpdate', + ]); + + ops.length = 0; + ReactDOM.unmountComponentAtNode(container); + expect(portalContainer1.innerHTML).toBe(''); + expect(portalContainer2.innerHTML).toBe(''); + expect(container.innerHTML).toBe(''); + expect(ops).toEqual([ + 'Parent:b componentWillUnmount', + 'normal[0]:b componentWillUnmount', + 'portal1[0]:b componentWillUnmount', + 'normal[1]:b componentWillUnmount', + 'portal2[0]:b componentWillUnmount', + 'portal2[1]:b componentWillUnmount', + ]); + }); + + it('should render nested portals', () => { + const portalContainer1 = document.createElement('div'); + const portalContainer2 = document.createElement('div'); + const portalContainer3 = document.createElement('div'); + + ReactDOM.render( + [ +
normal[0]
, + ReactDOM.createPortal( + [ +
portal1[0]
, + ReactDOM.createPortal( +
portal2[0]
, + portalContainer2, + ), + ReactDOM.createPortal( +
portal3[0]
, + portalContainer3, + ), +
portal1[1]
, + ], + portalContainer1, + ), +
normal[1]
, + ], + container, + ); + expect(portalContainer1.innerHTML).toBe( + '
portal1[0]
portal1[1]
', + ); + expect(portalContainer2.innerHTML).toBe('
portal2[0]
'); + expect(portalContainer3.innerHTML).toBe('
portal3[0]
'); + expect(container.innerHTML).toBe( + '
normal[0]
normal[1]
', + ); + + ReactDOM.unmountComponentAtNode(container); + expect(portalContainer1.innerHTML).toBe(''); + expect(portalContainer2.innerHTML).toBe(''); + expect(portalContainer3.innerHTML).toBe(''); + expect(container.innerHTML).toBe(''); + }); + + it('should reconcile portal children', () => { + const portalContainer = document.createElement('div'); + + ReactDOM.render( +
{ReactDOM.createPortal(
portal:1
, portalContainer)}
, + container, + ); + expect(portalContainer.innerHTML).toBe('
portal:1
'); + expect(container.innerHTML).toBe('
'); + + ReactDOM.render( +
{ReactDOM.createPortal(
portal:2
, portalContainer)}
, + container, + ); + expect(portalContainer.innerHTML).toBe('
portal:2
'); + expect(container.innerHTML).toBe('
'); + + ReactDOM.render( +
{ReactDOM.createPortal(

portal:3

, portalContainer)}
, + container, + ); + expect(portalContainer.innerHTML).toBe('

portal:3

'); + expect(container.innerHTML).toBe('
'); + + ReactDOM.render( +
{ReactDOM.createPortal(['Hi', 'Bye'], portalContainer)}
, + container, + ); + expect(portalContainer.innerHTML).toBe('HiBye'); + expect(container.innerHTML).toBe('
'); + + ReactDOM.render( +
{ReactDOM.createPortal(['Bye', 'Hi'], portalContainer)}
, + container, + ); + expect(portalContainer.innerHTML).toBe('ByeHi'); + expect(container.innerHTML).toBe('
'); + + ReactDOM.render( +
{ReactDOM.createPortal(null, portalContainer)}
, + container, + ); + expect(portalContainer.innerHTML).toBe(''); + expect(container.innerHTML).toBe('
'); + }); + + it('should unmount empty portal component wherever it appears', () => { + const portalContainer = document.createElement('div'); + + class Wrapper extends React.Component { + constructor(props) { + super(props); + this.state = { + show: true, + }; + } + render() { + return ( +
+ {this.state.show && ( + <> + {ReactDOM.createPortal(null, portalContainer)} +
child
+ + )} +
parent
+
+ ); + } + } + + const instance = ReactDOM.render(, container); + expect(container.innerHTML).toBe( + '
child
parent
', + ); + instance.setState({show: false}); + expect(instance.state.show).toBe(false); + expect(container.innerHTML).toBe('
parent
'); + }); + + it('should keep track of namespace across portals (simple)', () => { + assertNamespacesMatch( + + + {usePortal(
)} + + , + ); + assertNamespacesMatch( + + + {usePortal(
)} + + , + ); + assertNamespacesMatch( +
+

+ {usePortal( + + + , + )} +

+

, + ); + }); + + it('should keep track of namespace across portals (medium)', () => { + assertNamespacesMatch( + + + {usePortal(
)} + + {usePortal(
)} + + , + ); + assertNamespacesMatch( +
+ + + {usePortal( + + + , + )} + +

+

, + ); + assertNamespacesMatch( + + + {usePortal( + + + +

+ + + +

+ + + , + )} + + , + ); + assertNamespacesMatch( +

+ {usePortal( + + {usePortal(
)} + + , + )} +

+

, + ); + assertNamespacesMatch( + + + {usePortal(
)} + + + + , + ); + }); + + it('should keep track of namespace across portals (complex)', () => { + assertNamespacesMatch( +
+ {usePortal( + + + , + )} +

+ + + + + + + + + +

+

, + ); + assertNamespacesMatch( +
+ + + + {usePortal( + + + + + + + , + )} + + +

+ {usePortal(

)} +

+ + + + +

+ , + ); + assertNamespacesMatch( +

+ + +

+ {usePortal( + + + + + +

+ + {usePortal(

)} + + + , + )} +

+ + + +

+ , + ); + }); + + it('should unwind namespaces on uncaught errors', () => { + function BrokenRender() { + throw new Error('Hello'); + } + + expect(() => { + assertNamespacesMatch( + + + , + ); + }).toThrow('Hello'); + assertNamespacesMatch(

); + }); + + it('should unwind namespaces on caught errors', () => { + function BrokenRender() { + throw new Error('Hello'); + } + + class ErrorBoundary extends React.Component { + state = {error: null}; + componentDidCatch(error) { + this.setState({error}); + } + render() { + if (this.state.error) { + return

; + } + return this.props.children; + } + } + + assertNamespacesMatch( + + + + + + + + + + , + ); + assertNamespacesMatch(

); + }); + + it('should unwind namespaces on caught errors in a portal', () => { + function BrokenRender() { + throw new Error('Hello'); + } + + class ErrorBoundary extends React.Component { + state = {error: null}; + componentDidCatch(error) { + this.setState({error}); + } + render() { + if (this.state.error) { + return ; + } + return this.props.children; + } + } + + assertNamespacesMatch( + + + {usePortal( +
+ + ) + +
, + )} + + {usePortal(
)} + , + ); + }); + + // @gate !disableLegacyContext + it('should pass portal context when rendering subtree elsewhere', () => { + const portalContainer = document.createElement('div'); + + class Component extends React.Component { + static contextTypes = { + foo: PropTypes.string.isRequired, + }; + + render() { + return
{this.context.foo}
; + } + } + + class Parent extends React.Component { + static childContextTypes = { + foo: PropTypes.string.isRequired, + }; + + getChildContext() { + return { + foo: 'bar', + }; + } + + render() { + return ReactDOM.createPortal(, portalContainer); + } + } + + ReactDOM.render(, container); + expect(container.innerHTML).toBe(''); + expect(portalContainer.innerHTML).toBe('
bar
'); + }); + + // @gate !disableLegacyContext + it('should update portal context if it changes due to setState', () => { + const portalContainer = document.createElement('div'); + + class Component extends React.Component { + static contextTypes = { + foo: PropTypes.string.isRequired, + getFoo: PropTypes.func.isRequired, + }; + + render() { + return
{this.context.foo + '-' + this.context.getFoo()}
; + } + } + + class Parent extends React.Component { + static childContextTypes = { + foo: PropTypes.string.isRequired, + getFoo: PropTypes.func.isRequired, + }; + + state = { + bar: 'initial', + }; + + getChildContext() { + return { + foo: this.state.bar, + getFoo: () => this.state.bar, + }; + } + + render() { + return ReactDOM.createPortal(, portalContainer); + } + } + + const instance = ReactDOM.render(, container); + expect(portalContainer.innerHTML).toBe('
initial-initial
'); + expect(container.innerHTML).toBe(''); + instance.setState({bar: 'changed'}); + expect(portalContainer.innerHTML).toBe('
changed-changed
'); + expect(container.innerHTML).toBe(''); + }); + + // @gate !disableLegacyContext + it('should update portal context if it changes due to re-render', () => { + const portalContainer = document.createElement('div'); + + class Component extends React.Component { + static contextTypes = { + foo: PropTypes.string.isRequired, + getFoo: PropTypes.func.isRequired, + }; + + render() { + return
{this.context.foo + '-' + this.context.getFoo()}
; + } + } + + class Parent extends React.Component { + static childContextTypes = { + foo: PropTypes.string.isRequired, + getFoo: PropTypes.func.isRequired, + }; + + getChildContext() { + return { + foo: this.props.bar, + getFoo: () => this.props.bar, + }; + } + + render() { + return ReactDOM.createPortal(, portalContainer); + } + } + + ReactDOM.render(, container); + expect(portalContainer.innerHTML).toBe('
initial-initial
'); + expect(container.innerHTML).toBe(''); + ReactDOM.render(, container); + expect(portalContainer.innerHTML).toBe('
changed-changed
'); + expect(container.innerHTML).toBe(''); + }); + + it('findDOMNode should find dom element after expanding a fragment', () => { + class MyNode extends React.Component { + render() { + return !this.props.flag + ? [
] + : [,
]; + } + } + + const myNodeA = ReactDOM.render(, container); + const a = ReactDOM.findDOMNode(myNodeA); + expect(a.tagName).toBe('DIV'); + + const myNodeB = ReactDOM.render(, container); + expect(myNodeA === myNodeB).toBe(true); + + const b = ReactDOM.findDOMNode(myNodeB); + expect(b.tagName).toBe('SPAN'); + }); + + it('should bubble events from the portal to the parent', () => { + const portalContainer = document.createElement('div'); + document.body.appendChild(portalContainer); + try { + const ops = []; + let portal = null; + + ReactDOM.render( +
ops.push('parent clicked')}> + {ReactDOM.createPortal( +
ops.push('portal clicked')} + ref={n => (portal = n)}> + portal +
, + portalContainer, + )} +
, + container, + ); + + expect(portal.tagName).toBe('DIV'); + + portal.click(); + + expect(ops).toEqual(['portal clicked', 'parent clicked']); + } finally { + document.body.removeChild(portalContainer); + } + }); + + it('should not onMouseLeave when staying in the portal', () => { + const portalContainer = document.createElement('div'); + document.body.appendChild(portalContainer); + + let ops = []; + let firstTarget = null; + let secondTarget = null; + let thirdTarget = null; + + function simulateMouseMove(from, to) { + if (from) { + from.dispatchEvent( + new MouseEvent('mouseout', { + bubbles: true, + cancelable: true, + relatedTarget: to, + }), + ); + } + if (to) { + to.dispatchEvent( + new MouseEvent('mouseover', { + bubbles: true, + cancelable: true, + relatedTarget: from, + }), + ); + } + } + + try { + ReactDOM.render( +
+
ops.push('enter parent')} + onMouseLeave={() => ops.push('leave parent')}> +
(firstTarget = n)} /> + {ReactDOM.createPortal( +
ops.push('enter portal')} + onMouseLeave={() => ops.push('leave portal')} + ref={n => (secondTarget = n)}> + portal +
, + portalContainer, + )} +
+
(thirdTarget = n)} /> +
, + container, + ); + + simulateMouseMove(null, firstTarget); + expect(ops).toEqual(['enter parent']); + + ops = []; + + simulateMouseMove(firstTarget, secondTarget); + expect(ops).toEqual([ + // Parent did not invoke leave because we're still inside the portal. + 'enter portal', + ]); + + ops = []; + + simulateMouseMove(secondTarget, thirdTarget); + expect(ops).toEqual([ + 'leave portal', + 'leave parent', // Only when we leave the portal does onMouseLeave fire. + ]); + } finally { + document.body.removeChild(portalContainer); + } + }); + + // Regression test for https://github.com/facebook/react/issues/19562 + it('does not fire mouseEnter twice when relatedTarget is the root node', () => { + let ops = []; + let target = null; + + function simulateMouseMove(from, to) { + if (from) { + from.dispatchEvent( + new MouseEvent('mouseout', { + bubbles: true, + cancelable: true, + relatedTarget: to, + }), + ); + } + if (to) { + to.dispatchEvent( + new MouseEvent('mouseover', { + bubbles: true, + cancelable: true, + relatedTarget: from, + }), + ); + } + } + + ReactDOM.render( +
(target = n)} + onMouseEnter={() => ops.push('enter')} + onMouseLeave={() => ops.push('leave')} + />, + container, + ); + + simulateMouseMove(null, container); + expect(ops).toEqual([]); + + ops = []; + simulateMouseMove(container, target); + expect(ops).toEqual(['enter']); + + ops = []; + simulateMouseMove(target, container); + expect(ops).toEqual(['leave']); + + ops = []; + simulateMouseMove(container, null); + expect(ops).toEqual([]); + }); + + it('listens to events that do not exist in the Portal subtree', () => { + const onClick = jest.fn(); + + const ref = React.createRef(); + ReactDOM.render( +
+ {ReactDOM.createPortal(, document.body)} +
, + container, + ); + const event = new MouseEvent('click', { + bubbles: true, + }); + ref.current.dispatchEvent(event); + + expect(onClick).toHaveBeenCalledTimes(1); + }); + + it('should throw on bad createPortal argument', () => { + expect(() => { + ReactDOM.createPortal(
portal
, null); + }).toThrow('Target container is not a DOM element.'); + expect(() => { + ReactDOM.createPortal(
portal
, document.createTextNode('hi')); + }).toThrow('Target container is not a DOM element.'); + }); + + it('should warn for non-functional event listeners', () => { + class Example extends React.Component { + render() { + return
; + } + } + expect(() => ReactDOM.render(, container)).toErrorDev( + 'Expected `onClick` listener to be a function, instead got a value of `string` type.\n' + + ' in div (at **)\n' + + ' in Example (at **)', + ); + }); + + it('should warn with a special message for `false` event listeners', () => { + class Example extends React.Component { + render() { + return
; + } + } + expect(() => ReactDOM.render(, container)).toErrorDev( + 'Expected `onClick` listener to be a function, instead got `false`.\n\n' + + 'If you used to conditionally omit it with onClick={condition && value}, ' + + 'pass onClick={condition ? value : undefined} instead.\n' + + ' in div (at **)\n' + + ' in Example (at **)', + ); + }); + + it('should not update event handlers until commit', () => { + spyOnDev(console, 'error'); + + let ops = []; + const handlerA = () => ops.push('A'); + const handlerB = () => ops.push('B'); + + function click() { + const event = new MouseEvent('click', { + bubbles: true, + cancelable: true, + }); + Object.defineProperty(event, 'timeStamp', { + value: 0, + }); + node.dispatchEvent(event); + } + + class Example extends React.Component { + state = {flip: false, count: 0}; + flip() { + this.setState({flip: true, count: this.state.count + 1}); + } + tick() { + this.setState({count: this.state.count + 1}); + } + render() { + const useB = !this.props.forceA && this.state.flip; + return
; + } + } + + class Click extends React.Component { + constructor() { + super(); + node.click(); + } + render() { + return null; + } + } + + let inst; + ReactDOM.render([ (inst = n)} />], container); + const node = container.firstChild; + expect(node.tagName).toEqual('DIV'); + + click(); + + expect(ops).toEqual(['A']); + ops = []; + + // Render with the other event handler. + inst.flip(); + + click(); + + expect(ops).toEqual(['B']); + ops = []; + + // Rerender without changing any props. + inst.tick(); + + click(); + + expect(ops).toEqual(['B']); + ops = []; + + // Render a flip back to the A handler. The second component invokes the + // click handler during render to simulate a click during an aborted + // render. I use this hack because at current time we don't have a way to + // test aborted ReactDOM renders. + ReactDOM.render( + [, ], + container, + ); + + // Because the new click handler has not yet committed, we should still + // invoke B. + expect(ops).toEqual(['B']); + ops = []; + + // Any click that happens after commit, should invoke A. + click(); + expect(ops).toEqual(['A']); + + if (__DEV__) { + expect(console.error).toHaveBeenCalledTimes(2); + expect(console.error.mock.calls[0][0]).toMatch( + 'ReactDOM.render is no longer supported in React 18', + ); + expect(console.error.mock.calls[1][0]).toMatch( + 'ReactDOM.render is no longer supported in React 18', + ); + } + }); + + it('should not crash encountering low-priority tree', () => { + ReactDOM.render( +