diff --git a/packages/react-dom/src/__tests__/ReactDOMFiber-test.js b/packages/react-dom/src/__tests__/ReactDOMFiber-test.js index 06773b68f1f55..94172ab77b237 100644 --- a/packages/react-dom/src/__tests__/ReactDOMFiber-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMFiber-test.js @@ -9,16 +9,31 @@ 'use strict'; -const React = require('react'); -const ReactDOM = require('react-dom'); -const PropTypes = require('prop-types'); +let React; +let ReactDOM; +let PropTypes; +let ReactDOMClient; +let root; +let Scheduler; +let act; +let assertLog; describe('ReactDOMFiber', () => { let container; beforeEach(() => { + jest.resetModules(); + React = require('react'); + ReactDOM = require('react-dom'); + PropTypes = require('prop-types'); + ReactDOMClient = require('react-dom/client'); + Scheduler = require('scheduler'); + act = require('internal-test-utils').act; + assertLog = require('internal-test-utils').assertLog; + container = document.createElement('div'); document.body.appendChild(container); + root = ReactDOMClient.createRoot(container); }); afterEach(() => { @@ -27,157 +42,86 @@ describe('ReactDOMFiber', () => { jest.restoreAllMocks(); }); - it('should render strings as children', () => { + it('should render strings as children', async () => { const Box = ({value}) =>
{value}
; - - ReactDOM.render(, container); + await act(async () => { + root.render(); + }); expect(container.textContent).toEqual('foo'); }); - it('should render numbers as children', () => { + it('should render numbers as children', async () => { const Box = ({value}) =>
{value}
; - ReactDOM.render(, container); + await act(async () => { + root.render(); + }); expect(container.textContent).toEqual('10'); }); - it('should be called a callback argument', () => { + it('should call an effect after mount/update (replacing render callback pattern)', async () => { + function Component() { + React.useEffect(() => { + Scheduler.log('Callback'); + }); + return
Foo
; + } + // mounting phase - let called = false; - ReactDOM.render(
Foo
, container, () => (called = true)); - expect(called).toEqual(true); + await act(async () => { + root.render(); + }); + assertLog(['Callback']); // updating phase - called = false; - ReactDOM.render(
Foo
, container, () => (called = true)); - expect(called).toEqual(true); + await act(async () => { + root.render(); + }); + assertLog(['Callback']); }); - it('should call a callback argument when the same element is re-rendered', () => { - class Foo extends React.Component { - render() { - return
Foo
; - } + it('should call an effect when the same element is re-rendered (replacing render callback pattern)', async () => { + function Component({prop}) { + React.useEffect(() => { + Scheduler.log('Callback'); + }); + return
{prop}
; } - const element = ; // mounting phase - let called = false; - ReactDOM.render(element, container, () => (called = true)); - expect(called).toEqual(true); + await act(async () => { + root.render(); + }); + assertLog(['Callback']); // updating phase - called = false; - ReactDOM.unstable_batchedUpdates(() => { - ReactDOM.render(element, container, () => (called = true)); + await act(async () => { + root.render(); }); - expect(called).toEqual(true); + assertLog(['Callback']); }); - it('should render a component returning strings directly from render', () => { + it('should render a component returning strings directly from render', async () => { const Text = ({value}) => value; - ReactDOM.render(, container); + await act(async () => { + root.render(); + }); + expect(container.textContent).toEqual('foo'); }); - it('should render a component returning numbers directly from render', () => { + it('should render a component returning numbers directly from render', async () => { const Text = ({value}) => value; - - ReactDOM.render(, container); + await act(async () => { + root.render(); + }); 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', () => { + it('renders an empty fragment', async () => { const Div = () =>
; const EmptyFragment = () => <>; const NonEmptyFragment = () => ( @@ -186,19 +130,29 @@ describe('ReactDOMFiber', () => { ); - ReactDOM.render(, container); + await act(async () => { + root.render(); + }); expect(container.firstChild).toBe(null); - ReactDOM.render(, container); + await act(async () => { + root.render(); + }); expect(container.firstChild.tagName).toBe('DIV'); - ReactDOM.render(, container); + await act(async () => { + root.render(); + }); expect(container.firstChild).toBe(null); - ReactDOM.render(
, container); + await act(async () => { + root.render(
); + }); expect(container.firstChild.tagName).toBe('DIV'); - ReactDOM.render(, container); + await act(async () => { + root.render(); + }); expect(container.firstChild).toBe(null); }); @@ -211,13 +165,16 @@ describe('ReactDOMFiber', () => { return ReactDOM.createPortal(tree, document.createElement('div')); }; - const assertNamespacesMatch = function (tree) { + const assertNamespacesMatch = async function (tree) { const testContainer = document.createElement('div'); svgEls = []; htmlEls = []; mathEls = []; - ReactDOM.render(tree, testContainer); + const testRoot = ReactDOMClient.createRoot(testContainer); + await act(async () => { + testRoot.render(tree); + }); svgEls.forEach(el => { expect(el.namespaceURI).toBe('http://www.w3.org/2000/svg'); }); @@ -228,39 +185,39 @@ describe('ReactDOMFiber', () => { expect(el.namespaceURI).toBe('http://www.w3.org/1998/Math/MathML'); }); - ReactDOM.unmountComponentAtNode(testContainer); + testRoot.unmount(); expect(testContainer.innerHTML).toBe(''); }; - it('should render one portal', () => { + it('should render one portal', async () => { const portalContainer = document.createElement('div'); - ReactDOM.render( -
{ReactDOM.createPortal(
portal
, portalContainer)}
, - container, - ); + await act(() => { + root.render( +
{ReactDOM.createPortal(
portal
, portalContainer)}
, + ); + }); expect(portalContainer.innerHTML).toBe('
portal
'); expect(container.innerHTML).toBe('
'); - ReactDOM.unmountComponentAtNode(container); + root.unmount(); expect(portalContainer.innerHTML).toBe(''); expect(container.innerHTML).toBe(''); }); - it('should render many portals', () => { + it('should render many portals', async () => { const portalContainer1 = document.createElement('div'); const portalContainer2 = document.createElement('div'); - const ops = []; class Child extends React.Component { componentDidMount() { - ops.push(`${this.props.name} componentDidMount`); + Scheduler.log(`${this.props.name} componentDidMount`); } componentDidUpdate() { - ops.push(`${this.props.name} componentDidUpdate`); + Scheduler.log(`${this.props.name} componentDidUpdate`); } componentWillUnmount() { - ops.push(`${this.props.name} componentWillUnmount`); + Scheduler.log(`${this.props.name} componentWillUnmount`); } render() { return
{this.props.name}
; @@ -269,13 +226,13 @@ describe('ReactDOMFiber', () => { class Parent extends React.Component { componentDidMount() { - ops.push(`Parent:${this.props.step} componentDidMount`); + Scheduler.log(`Parent:${this.props.step} componentDidMount`); } componentDidUpdate() { - ops.push(`Parent:${this.props.step} componentDidUpdate`); + Scheduler.log(`Parent:${this.props.step} componentDidUpdate`); } componentWillUnmount() { - ops.push(`Parent:${this.props.step} componentWillUnmount`); + Scheduler.log(`Parent:${this.props.step} componentWillUnmount`); } render() { const {step} = this.props; @@ -297,7 +254,9 @@ describe('ReactDOMFiber', () => { } } - ReactDOM.render(, container); + await act(() => { + root.render(); + }); expect(portalContainer1.innerHTML).toBe('
portal1[0]:a
'); expect(portalContainer2.innerHTML).toBe( '
portal2[0]:a
portal2[1]:a
', @@ -305,7 +264,7 @@ describe('ReactDOMFiber', () => { expect(container.innerHTML).toBe( '
normal[0]:a
normal[1]:a
', ); - expect(ops).toEqual([ + assertLog([ 'normal[0]:a componentDidMount', 'portal1[0]:a componentDidMount', 'normal[1]:a componentDidMount', @@ -314,8 +273,9 @@ describe('ReactDOMFiber', () => { 'Parent:a componentDidMount', ]); - ops.length = 0; - ReactDOM.render(, container); + await act(() => { + root.render(); + }); expect(portalContainer1.innerHTML).toBe('
portal1[0]:b
'); expect(portalContainer2.innerHTML).toBe( '
portal2[0]:b
portal2[1]:b
', @@ -323,7 +283,7 @@ describe('ReactDOMFiber', () => { expect(container.innerHTML).toBe( '
normal[0]:b
normal[1]:b
', ); - expect(ops).toEqual([ + assertLog([ 'normal[0]:b componentDidUpdate', 'portal1[0]:b componentDidUpdate', 'normal[1]:b componentDidUpdate', @@ -332,12 +292,11 @@ describe('ReactDOMFiber', () => { 'Parent:b componentDidUpdate', ]); - ops.length = 0; - ReactDOM.unmountComponentAtNode(container); + root.unmount(); expect(portalContainer1.innerHTML).toBe(''); expect(portalContainer2.innerHTML).toBe(''); expect(container.innerHTML).toBe(''); - expect(ops).toEqual([ + assertLog([ 'Parent:b componentWillUnmount', 'normal[0]:b componentWillUnmount', 'portal1[0]:b componentWillUnmount', @@ -347,13 +306,13 @@ describe('ReactDOMFiber', () => { ]); }); - it('should render nested portals', () => { + it('should render nested portals', async () => { const portalContainer1 = document.createElement('div'); const portalContainer2 = document.createElement('div'); const portalContainer3 = document.createElement('div'); - ReactDOM.render( - [ + await act(() => { + root.render([
normal[0]
, ReactDOM.createPortal( [ @@ -371,9 +330,8 @@ describe('ReactDOMFiber', () => { portalContainer1, ),
normal[1]
, - ], - container, - ); + ]); + }); expect(portalContainer1.innerHTML).toBe( '
portal1[0]
portal1[1]
', ); @@ -383,65 +341,74 @@ describe('ReactDOMFiber', () => { '
normal[0]
normal[1]
', ); - ReactDOM.unmountComponentAtNode(container); + root.unmount(); expect(portalContainer1.innerHTML).toBe(''); expect(portalContainer2.innerHTML).toBe(''); expect(portalContainer3.innerHTML).toBe(''); expect(container.innerHTML).toBe(''); }); - it('should reconcile portal children', () => { + it('should reconcile portal children', async () => { const portalContainer = document.createElement('div'); - ReactDOM.render( -
{ReactDOM.createPortal(
portal:1
, portalContainer)}
, - container, - ); + await act(() => { + root.render( +
+ {ReactDOM.createPortal(
portal:1
, portalContainer)} +
, + ); + }); expect(portalContainer.innerHTML).toBe('
portal:1
'); expect(container.innerHTML).toBe('
'); - ReactDOM.render( -
{ReactDOM.createPortal(
portal:2
, portalContainer)}
, - container, - ); + await act(() => { + root.render( +
+ {ReactDOM.createPortal(
portal:2
, portalContainer)} +
, + ); + }); expect(portalContainer.innerHTML).toBe('
portal:2
'); expect(container.innerHTML).toBe('
'); - ReactDOM.render( -
{ReactDOM.createPortal(

portal:3

, portalContainer)}
, - container, - ); + await act(() => { + root.render( +
{ReactDOM.createPortal(

portal:3

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

portal:3

'); expect(container.innerHTML).toBe('
'); - ReactDOM.render( -
{ReactDOM.createPortal(['Hi', 'Bye'], portalContainer)}
, - container, - ); + await act(() => { + root.render( +
{ReactDOM.createPortal(['Hi', 'Bye'], portalContainer)}
, + ); + }); expect(portalContainer.innerHTML).toBe('HiBye'); expect(container.innerHTML).toBe('
'); - ReactDOM.render( -
{ReactDOM.createPortal(['Bye', 'Hi'], portalContainer)}
, - container, - ); + await act(() => { + root.render( +
{ReactDOM.createPortal(['Bye', 'Hi'], portalContainer)}
, + ); + }); expect(portalContainer.innerHTML).toBe('ByeHi'); expect(container.innerHTML).toBe('
'); - ReactDOM.render( -
{ReactDOM.createPortal(null, portalContainer)}
, - container, - ); + await act(() => { + root.render(
{ReactDOM.createPortal(null, portalContainer)}
); + }); expect(portalContainer.innerHTML).toBe(''); expect(container.innerHTML).toBe('
'); }); - it('should unmount empty portal component wherever it appears', () => { + it('should unmount empty portal component wherever it appears', async () => { const portalContainer = document.createElement('div'); - + let instance; class Wrapper extends React.Component { constructor(props) { super(props); + instance = this; this.state = { show: true, }; @@ -461,31 +428,35 @@ describe('ReactDOMFiber', () => { } } - const instance = ReactDOM.render(, container); + await act(() => { + root.render(); + }); expect(container.innerHTML).toBe( '
child
parent
', ); - instance.setState({show: false}); + await act(() => { + 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( + it('should keep track of namespace across portals (simple)', async () => { + await assertNamespacesMatch( {usePortal(
)} , ); - assertNamespacesMatch( + await assertNamespacesMatch( {usePortal(
)} , ); - assertNamespacesMatch( + await assertNamespacesMatch(

{usePortal( @@ -498,8 +469,8 @@ describe('ReactDOMFiber', () => { ); }); - it('should keep track of namespace across portals (medium)', () => { - assertNamespacesMatch( + it('should keep track of namespace across portals (medium)', async () => { + await assertNamespacesMatch( {usePortal(

)} @@ -508,7 +479,7 @@ describe('ReactDOMFiber', () => { , ); - assertNamespacesMatch( + await assertNamespacesMatch(
@@ -521,7 +492,7 @@ describe('ReactDOMFiber', () => {

, ); - assertNamespacesMatch( + await assertNamespacesMatch( {usePortal( @@ -540,7 +511,7 @@ describe('ReactDOMFiber', () => { , ); - assertNamespacesMatch( + await assertNamespacesMatch(
{usePortal( @@ -551,7 +522,7 @@ describe('ReactDOMFiber', () => {

, ); - assertNamespacesMatch( + await assertNamespacesMatch( {usePortal(
)} @@ -562,8 +533,8 @@ describe('ReactDOMFiber', () => { ); }); - it('should keep track of namespace across portals (complex)', () => { - assertNamespacesMatch( + it('should keep track of namespace across portals (complex)', async () => { + await assertNamespacesMatch(
{usePortal( @@ -583,7 +554,7 @@ describe('ReactDOMFiber', () => {

, ); - assertNamespacesMatch( + await assertNamespacesMatch(
@@ -609,7 +580,7 @@ describe('ReactDOMFiber', () => {

, ); - assertNamespacesMatch( + await assertNamespacesMatch(
@@ -636,22 +607,22 @@ describe('ReactDOMFiber', () => { ); }); - it('should unwind namespaces on uncaught errors', () => { + it('should unwind namespaces on uncaught errors', async () => { function BrokenRender() { throw new Error('Hello'); } - expect(() => { - assertNamespacesMatch( + await expect(async () => { + await assertNamespacesMatch( , ); - }).toThrow('Hello'); - assertNamespacesMatch(
); + }).rejects.toThrow('Hello'); + await assertNamespacesMatch(
); }); - it('should unwind namespaces on caught errors', () => { + it('should unwind namespaces on caught errors', async () => { function BrokenRender() { throw new Error('Hello'); } @@ -669,7 +640,7 @@ describe('ReactDOMFiber', () => { } } - assertNamespacesMatch( + await assertNamespacesMatch( @@ -681,10 +652,10 @@ describe('ReactDOMFiber', () => { , ); - assertNamespacesMatch(
); + await assertNamespacesMatch(
); }); - it('should unwind namespaces on caught errors in a portal', () => { + it('should unwind namespaces on caught errors in a portal', async () => { function BrokenRender() { throw new Error('Hello'); } @@ -702,7 +673,7 @@ describe('ReactDOMFiber', () => { } } - assertNamespacesMatch( + await assertNamespacesMatch( {usePortal( @@ -719,7 +690,7 @@ describe('ReactDOMFiber', () => { }); // @gate !disableLegacyContext - it('should pass portal context when rendering subtree elsewhere', () => { + it('should pass portal context when rendering subtree elsewhere', async () => { const portalContainer = document.createElement('div'); class Component extends React.Component { @@ -748,153 +719,50 @@ describe('ReactDOMFiber', () => { } } - ReactDOM.render(, container); + await act(async () => { + root.render(); + }); 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', () => { + it('should bubble events from the portal to the parent', async () => { 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, - ); + await act(() => { + root.render( +
Scheduler.log('parent clicked')}> + {ReactDOM.createPortal( +
Scheduler.log('portal clicked')} + ref={n => (portal = n)}> + portal +
, + portalContainer, + )} +
, + ); + }); expect(portal.tagName).toBe('DIV'); - portal.click(); + await act(() => { + portal.click(); + }); - expect(ops).toEqual(['portal clicked', 'parent clicked']); + assertLog(['portal clicked', 'parent clicked']); } finally { document.body.removeChild(portalContainer); } }); - it('should not onMouseLeave when staying in the portal', () => { + it('should not onMouseLeave when staying in the portal', async () => { const portalContainer = document.createElement('div'); document.body.appendChild(portalContainer); - let ops = []; let firstTarget = null; let secondTarget = null; let thirdTarget = null; @@ -921,42 +789,44 @@ describe('ReactDOMFiber', () => { } 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 = []; + await act(() => { + root.render( +
+
Scheduler.log('enter parent')} + onMouseLeave={() => Scheduler.log('leave parent')}> +
(firstTarget = n)} /> + {ReactDOM.createPortal( +
Scheduler.log('enter portal')} + onMouseLeave={() => Scheduler.log('leave portal')} + ref={n => (secondTarget = n)}> + portal +
, + portalContainer, + )} +
+
(thirdTarget = n)} /> +
, + ); + }); + await act(() => { + simulateMouseMove(null, firstTarget); + }); + assertLog(['enter parent']); - simulateMouseMove(firstTarget, secondTarget); - expect(ops).toEqual([ + await act(() => { + simulateMouseMove(firstTarget, secondTarget); + }); + assertLog([ // Parent did not invoke leave because we're still inside the portal. 'enter portal', ]); - ops = []; - - simulateMouseMove(secondTarget, thirdTarget); - expect(ops).toEqual([ + await act(() => { + simulateMouseMove(secondTarget, thirdTarget); + }); + assertLog([ 'leave portal', 'leave parent', // Only when we leave the portal does onMouseLeave fire. ]); @@ -966,8 +836,7 @@ describe('ReactDOMFiber', () => { }); // Regression test for https://github.com/facebook/react/issues/19562 - it('does not fire mouseEnter twice when relatedTarget is the root node', () => { - let ops = []; + it('does not fire mouseEnter twice when relatedTarget is the root node', async () => { let target = null; function simulateMouseMove(from, to) { @@ -991,45 +860,57 @@ describe('ReactDOMFiber', () => { } } - ReactDOM.render( -
(target = n)} - onMouseEnter={() => ops.push('enter')} - onMouseLeave={() => ops.push('leave')} - />, - container, - ); + await act(() => { + root.render( +
(target = n)} + onMouseEnter={() => Scheduler.log('enter')} + onMouseLeave={() => Scheduler.log('leave')} + />, + ); + }); - simulateMouseMove(null, container); - expect(ops).toEqual([]); + await act(() => { + simulateMouseMove(null, container); + }); + assertLog([]); - ops = []; - simulateMouseMove(container, target); - expect(ops).toEqual(['enter']); + await act(() => { + simulateMouseMove(container, target); + }); + assertLog(['enter']); - ops = []; - simulateMouseMove(target, container); - expect(ops).toEqual(['leave']); + await act(() => { + simulateMouseMove(target, container); + }); + assertLog(['leave']); - ops = []; - simulateMouseMove(container, null); - expect(ops).toEqual([]); + await act(() => { + simulateMouseMove(container, null); + }); + assertLog([]); }); - it('listens to events that do not exist in the Portal subtree', () => { + it('listens to events that do not exist in the Portal subtree', async () => { const onClick = jest.fn(); const ref = React.createRef(); - ReactDOM.render( -
- {ReactDOM.createPortal(, document.body)} -
, - container, - ); + await act(() => { + root.render( +
+ {ReactDOM.createPortal( + , + document.body, + )} +
, + ); + }); const event = new MouseEvent('click', { bubbles: true, }); - ref.current.dispatchEvent(event); + await act(() => { + ref.current.dispatchEvent(event); + }); expect(onClick).toHaveBeenCalledTimes(1); }); @@ -1049,7 +930,11 @@ describe('ReactDOMFiber', () => { return
; } } - expect(() => ReactDOM.render(, container)).toErrorDev( + expect(() => { + ReactDOM.flushSync(() => { + root.render(); + }); + }).toErrorDev( 'Expected `onClick` listener to be a function, instead got a value of `string` type.\n' + ' in div (at **)\n' + ' in Example (at **)', @@ -1062,7 +947,11 @@ describe('ReactDOMFiber', () => { return
; } } - expect(() => ReactDOM.render(, container)).toErrorDev( + expect(() => { + ReactDOM.flushSync(() => { + root.render(); + }); + }).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' + @@ -1071,12 +960,9 @@ describe('ReactDOMFiber', () => { ); }); - it('should not update event handlers until commit', () => { - spyOnDev(console, 'error'); - - let ops = []; - const handlerA = () => ops.push('A'); - const handlerB = () => ops.push('B'); + it('should not update event handlers until commit', async () => { + const handlerA = () => Scheduler.log('A'); + const handlerB = () => Scheduler.log('B'); function click() { const event = new MouseEvent('click', { @@ -1114,137 +1000,155 @@ describe('ReactDOMFiber', () => { } let inst; - ReactDOM.render([ (inst = n)} />], container); + await act(() => { + root.render([ (inst = n)} />]); + }); const node = container.firstChild; expect(node.tagName).toEqual('DIV'); - click(); + await act(() => { + click(); + }); - expect(ops).toEqual(['A']); - ops = []; + assertLog(['A']); // Render with the other event handler. - inst.flip(); + await act(() => { + inst.flip(); + }); - click(); + await act(() => { + click(); + }); - expect(ops).toEqual(['B']); - ops = []; + assertLog(['B']); // Rerender without changing any props. - inst.tick(); + await act(() => { + inst.tick(); + }); - click(); + await act(() => { + click(); + }); - expect(ops).toEqual(['B']); - ops = []; + assertLog(['B']); // 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, - ); + await act(() => { + root.render([, ]); + }); // Because the new click handler has not yet committed, we should still // invoke B. - expect(ops).toEqual(['B']); - ops = []; + assertLog(['B']); // Any click that happens after commit, should invoke A. - click(); - expect(ops).toEqual(['A']); + await act(() => { + click(); + }); + assertLog(['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', async () => { + await act(() => { + root.render( +