diff --git a/packages/react-reconciler/src/__tests__/ReactLazy-test.internal.js b/packages/react-reconciler/src/__tests__/ReactLazy-test.internal.js index c592e91e94096..769679c9a7013 100644 --- a/packages/react-reconciler/src/__tests__/ReactLazy-test.internal.js +++ b/packages/react-reconciler/src/__tests__/ReactLazy-test.internal.js @@ -1631,369 +1631,4 @@ describe('ReactLazy', () => { ]); expect(root).toMatchRenderedOutput('ba'); }); - - it('does not destroy layout effects twice when hidden child is removed', async () => { - function ChildA({label}) { - React.useLayoutEffect(() => { - Scheduler.unstable_yieldValue('Did mount: ' + label); - return () => { - Scheduler.unstable_yieldValue('Will unmount: ' + label); - }; - }, []); - return ; - } - - function ChildB({label}) { - React.useLayoutEffect(() => { - Scheduler.unstable_yieldValue('Did mount: ' + label); - return () => { - Scheduler.unstable_yieldValue('Will unmount: ' + label); - }; - }, []); - return ; - } - - const LazyChildA = lazy(() => fakeImport(ChildA)); - const LazyChildB = lazy(() => fakeImport(ChildB)); - - function Parent({swap}) { - return ( - }> - {swap ? : } - - ); - } - - const root = ReactTestRenderer.create(, { - unstable_isConcurrent: true, - }); - - expect(Scheduler).toFlushAndYield(['Loading...']); - - await LazyChildA; - expect(Scheduler).toFlushAndYield(['A', 'Did mount: A']); - expect(root).toMatchRenderedOutput('A'); - - // Swap the position of A and B - root.unstable_flushSync(() => { - root.update(); - }); - expect(Scheduler).toHaveYielded(['Loading...', 'Will unmount: A']); - expect(root).toMatchRenderedOutput('Loading...'); - - await LazyChildB; - expect(Scheduler).toFlushAndYield(['B', 'Did mount: B']); - expect(root).toMatchRenderedOutput('B'); - }); - - it('does not destroy ref cleanup twice when hidden child is removed', async () => { - function ChildA({label}) { - return ( - { - if (node) { - Scheduler.unstable_yieldValue('Ref mount: ' + label); - } else { - Scheduler.unstable_yieldValue('Ref unmount: ' + label); - } - }}> - - - ); - } - - function ChildB({label}) { - return ( - { - if (node) { - Scheduler.unstable_yieldValue('Ref mount: ' + label); - } else { - Scheduler.unstable_yieldValue('Ref unmount: ' + label); - } - }}> - - - ); - } - - const LazyChildA = lazy(() => fakeImport(ChildA)); - const LazyChildB = lazy(() => fakeImport(ChildB)); - - function Parent({swap}) { - return ( - }> - {swap ? : } - - ); - } - - const root = ReactTestRenderer.create(, { - unstable_isConcurrent: true, - createNodeMock() { - return {}; - }, - }); - - expect(Scheduler).toFlushAndYield(['Loading...']); - - await LazyChildA; - expect(Scheduler).toFlushAndYield(['A', 'Ref mount: A']); - expect(root).toMatchRenderedOutput(A); - - // Swap the position of A and B - root.unstable_flushSync(() => { - root.update(); - }); - expect(Scheduler).toHaveYielded(['Loading...', 'Ref unmount: A']); - expect(root).toMatchRenderedOutput('Loading...'); - - await LazyChildB; - expect(Scheduler).toFlushAndYield(['B', 'Ref mount: B']); - expect(root).toMatchRenderedOutput(B); - }); - - it('does not call componentWillUnmount twice when hidden child is removed', async () => { - class ChildA extends React.Component { - componentDidMount() { - Scheduler.unstable_yieldValue('Did mount: ' + this.props.label); - } - componentWillUnmount() { - Scheduler.unstable_yieldValue('Will unmount: ' + this.props.label); - } - render() { - return ; - } - } - - class ChildB extends React.Component { - componentDidMount() { - Scheduler.unstable_yieldValue('Did mount: ' + this.props.label); - } - componentWillUnmount() { - Scheduler.unstable_yieldValue('Will unmount: ' + this.props.label); - } - render() { - return ; - } - } - - const LazyChildA = lazy(() => fakeImport(ChildA)); - const LazyChildB = lazy(() => fakeImport(ChildB)); - - function Parent({swap}) { - return ( - }> - {swap ? : } - - ); - } - - const root = ReactTestRenderer.create(, { - unstable_isConcurrent: true, - }); - - expect(Scheduler).toFlushAndYield(['Loading...']); - - await LazyChildA; - expect(Scheduler).toFlushAndYield(['A', 'Did mount: A']); - expect(root).toMatchRenderedOutput('A'); - - // Swap the position of A and B - root.unstable_flushSync(() => { - root.update(); - }); - expect(Scheduler).toHaveYielded(['Loading...', 'Will unmount: A']); - expect(root).toMatchRenderedOutput('Loading...'); - - await LazyChildB; - expect(Scheduler).toFlushAndYield(['B', 'Did mount: B']); - expect(root).toMatchRenderedOutput('B'); - }); - - it('does not destroy layout effects twice when parent suspense is removed', async () => { - function ChildA({label}) { - React.useLayoutEffect(() => { - Scheduler.unstable_yieldValue('Did mount: ' + label); - return () => { - Scheduler.unstable_yieldValue('Will unmount: ' + label); - }; - }, []); - return ; - } - function ChildB({label}) { - React.useLayoutEffect(() => { - Scheduler.unstable_yieldValue('Did mount: ' + label); - return () => { - Scheduler.unstable_yieldValue('Will unmount: ' + label); - }; - }, []); - return ; - } - const LazyChildA = lazy(() => fakeImport(ChildA)); - const LazyChildB = lazy(() => fakeImport(ChildB)); - - function Parent({swap}) { - return ( - }> - {swap ? : } - - ); - } - - const root = ReactTestRenderer.create(, { - unstable_isConcurrent: true, - }); - - expect(Scheduler).toFlushAndYield(['Loading...']); - - await LazyChildA; - expect(Scheduler).toFlushAndYield(['A', 'Did mount: A']); - expect(root).toMatchRenderedOutput('A'); - - // Swap the position of A and B - root.unstable_flushSync(() => { - root.update(); - }); - expect(Scheduler).toHaveYielded(['Loading...', 'Will unmount: A']); - expect(root).toMatchRenderedOutput('Loading...'); - - // Destroy the whole tree, including the hidden A - root.unstable_flushSync(() => { - root.update(

Hello

); - }); - expect(Scheduler).toFlushAndYield([]); - expect(root).toMatchRenderedOutput(

Hello

); - }); - - it('does not destroy ref cleanup twice when parent suspense is removed', async () => { - function ChildA({label}) { - return ( - { - if (node) { - Scheduler.unstable_yieldValue('Ref mount: ' + label); - } else { - Scheduler.unstable_yieldValue('Ref unmount: ' + label); - } - }}> - - - ); - } - - function ChildB({label}) { - return ( - { - if (node) { - Scheduler.unstable_yieldValue('Ref mount: ' + label); - } else { - Scheduler.unstable_yieldValue('Ref unmount: ' + label); - } - }}> - - - ); - } - - const LazyChildA = lazy(() => fakeImport(ChildA)); - const LazyChildB = lazy(() => fakeImport(ChildB)); - - function Parent({swap}) { - return ( - }> - {swap ? : } - - ); - } - - const root = ReactTestRenderer.create(, { - unstable_isConcurrent: true, - createNodeMock() { - return {}; - }, - }); - - expect(Scheduler).toFlushAndYield(['Loading...']); - - await LazyChildA; - expect(Scheduler).toFlushAndYield(['A', 'Ref mount: A']); - expect(root).toMatchRenderedOutput(A); - - // Swap the position of A and B - root.unstable_flushSync(() => { - root.update(); - }); - expect(Scheduler).toHaveYielded(['Loading...', 'Ref unmount: A']); - expect(root).toMatchRenderedOutput('Loading...'); - - // Destroy the whole tree, including the hidden A - root.unstable_flushSync(() => { - root.update(

Hello

); - }); - expect(Scheduler).toFlushAndYield([]); - expect(root).toMatchRenderedOutput(

Hello

); - }); - - it('does not call componentWillUnmount twice when parent suspense is removed', async () => { - class ChildA extends React.Component { - componentDidMount() { - Scheduler.unstable_yieldValue('Did mount: ' + this.props.label); - } - componentWillUnmount() { - Scheduler.unstable_yieldValue('Will unmount: ' + this.props.label); - } - render() { - return ; - } - } - - class ChildB extends React.Component { - componentDidMount() { - Scheduler.unstable_yieldValue('Did mount: ' + this.props.label); - } - componentWillUnmount() { - Scheduler.unstable_yieldValue('Will unmount: ' + this.props.label); - } - render() { - return ; - } - } - - const LazyChildA = lazy(() => fakeImport(ChildA)); - const LazyChildB = lazy(() => fakeImport(ChildB)); - - function Parent({swap}) { - return ( - }> - {swap ? : } - - ); - } - - const root = ReactTestRenderer.create(, { - unstable_isConcurrent: true, - }); - - expect(Scheduler).toFlushAndYield(['Loading...']); - - await LazyChildA; - expect(Scheduler).toFlushAndYield(['A', 'Did mount: A']); - expect(root).toMatchRenderedOutput('A'); - - // Swap the position of A and B - root.unstable_flushSync(() => { - root.update(); - }); - expect(Scheduler).toHaveYielded(['Loading...', 'Will unmount: A']); - expect(root).toMatchRenderedOutput('Loading...'); - - // Destroy the whole tree, including the hidden A - root.unstable_flushSync(() => { - root.update(

Hello

); - }); - expect(Scheduler).toFlushAndYield([]); - expect(root).toMatchRenderedOutput(

Hello

); - }); }); diff --git a/packages/react-reconciler/src/__tests__/ReactSuspenseEffectsSemanticsDOM-test.js b/packages/react-reconciler/src/__tests__/ReactSuspenseEffectsSemanticsDOM-test.js index cb1196baffe6a..00c126bb36450 100644 --- a/packages/react-reconciler/src/__tests__/ReactSuspenseEffectsSemanticsDOM-test.js +++ b/packages/react-reconciler/src/__tests__/ReactSuspenseEffectsSemanticsDOM-test.js @@ -10,18 +10,39 @@ 'use strict'; let React; +let ReactDOM; let ReactDOMClient; +let Scheduler; let act; +let container; describe('ReactSuspenseEffectsSemanticsDOM', () => { beforeEach(() => { jest.resetModules(); React = require('react'); + ReactDOM = require('react-dom'); ReactDOMClient = require('react-dom/client'); + Scheduler = require('scheduler'); act = require('jest-react').act; + + container = document.createElement('div'); + document.body.appendChild(container); + }); + + afterEach(() => { + document.body.removeChild(container); }); + async function fakeImport(result) { + return {default: result}; + } + + function Text(props) { + Scheduler.unstable_yieldValue(props.text); + return props.text; + } + it('should not cause a cycle when combined with a render phase update', () => { let scheduleSuspendingUpdate; @@ -63,7 +84,7 @@ describe('ReactSuspenseEffectsSemanticsDOM', () => { } act(() => { - const root = ReactDOMClient.createRoot(document.createElement('div')); + const root = ReactDOMClient.createRoot(container); root.render(); }); @@ -71,4 +92,367 @@ describe('ReactSuspenseEffectsSemanticsDOM', () => { scheduleSuspendingUpdate(); }); }); + + it('does not destroy layout effects twice when hidden child is removed', async () => { + function ChildA({label}) { + React.useLayoutEffect(() => { + Scheduler.unstable_yieldValue('Did mount: ' + label); + return () => { + Scheduler.unstable_yieldValue('Will unmount: ' + label); + }; + }, []); + return ; + } + + function ChildB({label}) { + React.useLayoutEffect(() => { + Scheduler.unstable_yieldValue('Did mount: ' + label); + return () => { + Scheduler.unstable_yieldValue('Will unmount: ' + label); + }; + }, []); + return ; + } + + const LazyChildA = React.lazy(() => fakeImport(ChildA)); + const LazyChildB = React.lazy(() => fakeImport(ChildB)); + + function Parent({swap}) { + return ( + }> + {swap ? : } + + ); + } + + const root = ReactDOMClient.createRoot(container); + act(() => { + root.render(); + }); + expect(Scheduler).toHaveYielded(['Loading...']); + + await LazyChildA; + expect(Scheduler).toFlushAndYield(['A', 'Did mount: A']); + expect(container.innerHTML).toBe('A'); + + // Swap the position of A and B + ReactDOM.flushSync(() => { + root.render(); + }); + expect(Scheduler).toHaveYielded(['Loading...', 'Will unmount: A']); + expect(container.innerHTML).toBe('Loading...'); + + await LazyChildB; + expect(Scheduler).toFlushAndYield(['B', 'Did mount: B']); + expect(container.innerHTML).toBe('B'); + }); + + it('does not destroy ref cleanup twice when hidden child is removed', async () => { + function ChildA({label}) { + return ( + { + if (node) { + Scheduler.unstable_yieldValue('Ref mount: ' + label); + } else { + Scheduler.unstable_yieldValue('Ref unmount: ' + label); + } + }}> + + + ); + } + + function ChildB({label}) { + return ( + { + if (node) { + Scheduler.unstable_yieldValue('Ref mount: ' + label); + } else { + Scheduler.unstable_yieldValue('Ref unmount: ' + label); + } + }}> + + + ); + } + + const LazyChildA = React.lazy(() => fakeImport(ChildA)); + const LazyChildB = React.lazy(() => fakeImport(ChildB)); + + function Parent({swap}) { + return ( + }> + {swap ? : } + + ); + } + + const root = ReactDOMClient.createRoot(container); + act(() => { + root.render(); + }); + expect(Scheduler).toHaveYielded(['Loading...']); + + await LazyChildA; + expect(Scheduler).toFlushAndYield(['A', 'Ref mount: A']); + expect(container.innerHTML).toBe('A'); + + // Swap the position of A and B + ReactDOM.flushSync(() => { + root.render(); + }); + expect(Scheduler).toHaveYielded(['Loading...', 'Ref unmount: A']); + expect(container.innerHTML).toBe( + 'ALoading...', + ); + + await LazyChildB; + expect(Scheduler).toFlushAndYield(['B', 'Ref mount: B']); + expect(container.innerHTML).toBe('B'); + }); + + it('does not call componentWillUnmount twice when hidden child is removed', async () => { + class ChildA extends React.Component { + componentDidMount() { + Scheduler.unstable_yieldValue('Did mount: ' + this.props.label); + } + componentWillUnmount() { + Scheduler.unstable_yieldValue('Will unmount: ' + this.props.label); + } + render() { + return ; + } + } + + class ChildB extends React.Component { + componentDidMount() { + Scheduler.unstable_yieldValue('Did mount: ' + this.props.label); + } + componentWillUnmount() { + Scheduler.unstable_yieldValue('Will unmount: ' + this.props.label); + } + render() { + return ; + } + } + + const LazyChildA = React.lazy(() => fakeImport(ChildA)); + const LazyChildB = React.lazy(() => fakeImport(ChildB)); + + function Parent({swap}) { + return ( + }> + {swap ? : } + + ); + } + + const root = ReactDOMClient.createRoot(container); + act(() => { + root.render(); + }); + expect(Scheduler).toHaveYielded(['Loading...']); + + await LazyChildA; + expect(Scheduler).toFlushAndYield(['A', 'Did mount: A']); + expect(container.innerHTML).toBe('A'); + + // Swap the position of A and B + ReactDOM.flushSync(() => { + root.render(); + }); + expect(Scheduler).toHaveYielded(['Loading...', 'Will unmount: A']); + expect(container.innerHTML).toBe('Loading...'); + + await LazyChildB; + expect(Scheduler).toFlushAndYield(['B', 'Did mount: B']); + expect(container.innerHTML).toBe('B'); + }); + + it('does not destroy layout effects twice when parent suspense is removed', async () => { + function ChildA({label}) { + React.useLayoutEffect(() => { + Scheduler.unstable_yieldValue('Did mount: ' + label); + return () => { + Scheduler.unstable_yieldValue('Will unmount: ' + label); + }; + }, []); + return ; + } + function ChildB({label}) { + React.useLayoutEffect(() => { + Scheduler.unstable_yieldValue('Did mount: ' + label); + return () => { + Scheduler.unstable_yieldValue('Will unmount: ' + label); + }; + }, []); + return ; + } + const LazyChildA = React.lazy(() => fakeImport(ChildA)); + const LazyChildB = React.lazy(() => fakeImport(ChildB)); + + function Parent({swap}) { + return ( + }> + {swap ? : } + + ); + } + + const root = ReactDOMClient.createRoot(container); + act(() => { + root.render(); + }); + expect(Scheduler).toHaveYielded(['Loading...']); + + await LazyChildA; + expect(Scheduler).toFlushAndYield(['A', 'Did mount: A']); + expect(container.innerHTML).toBe('A'); + + // Swap the position of A and B + ReactDOM.flushSync(() => { + root.render(); + }); + expect(Scheduler).toHaveYielded(['Loading...', 'Will unmount: A']); + expect(container.innerHTML).toBe('Loading...'); + + // Destroy the whole tree, including the hidden A + ReactDOM.flushSync(() => { + root.render(

Hello

); + }); + expect(Scheduler).toFlushAndYield([]); + expect(container.innerHTML).toBe('

Hello

'); + }); + + it('does not destroy ref cleanup twice when parent suspense is removed', async () => { + function ChildA({label}) { + return ( + { + if (node) { + Scheduler.unstable_yieldValue('Ref mount: ' + label); + } else { + Scheduler.unstable_yieldValue('Ref unmount: ' + label); + } + }}> + + + ); + } + + function ChildB({label}) { + return ( + { + if (node) { + Scheduler.unstable_yieldValue('Ref mount: ' + label); + } else { + Scheduler.unstable_yieldValue('Ref unmount: ' + label); + } + }}> + + + ); + } + + const LazyChildA = React.lazy(() => fakeImport(ChildA)); + const LazyChildB = React.lazy(() => fakeImport(ChildB)); + + function Parent({swap}) { + return ( + }> + {swap ? : } + + ); + } + + const root = ReactDOMClient.createRoot(container); + act(() => { + root.render(); + }); + expect(Scheduler).toHaveYielded(['Loading...']); + + await LazyChildA; + expect(Scheduler).toFlushAndYield(['A', 'Ref mount: A']); + expect(container.innerHTML).toBe('A'); + + // Swap the position of A and B + ReactDOM.flushSync(() => { + root.render(); + }); + expect(Scheduler).toHaveYielded(['Loading...', 'Ref unmount: A']); + expect(container.innerHTML).toBe( + 'ALoading...', + ); + + // Destroy the whole tree, including the hidden A + ReactDOM.flushSync(() => { + root.render(

Hello

); + }); + expect(Scheduler).toFlushAndYield([]); + expect(container.innerHTML).toBe('

Hello

'); + }); + + it('does not call componentWillUnmount twice when parent suspense is removed', async () => { + class ChildA extends React.Component { + componentDidMount() { + Scheduler.unstable_yieldValue('Did mount: ' + this.props.label); + } + componentWillUnmount() { + Scheduler.unstable_yieldValue('Will unmount: ' + this.props.label); + } + render() { + return ; + } + } + + class ChildB extends React.Component { + componentDidMount() { + Scheduler.unstable_yieldValue('Did mount: ' + this.props.label); + } + componentWillUnmount() { + Scheduler.unstable_yieldValue('Will unmount: ' + this.props.label); + } + render() { + return ; + } + } + + const LazyChildA = React.lazy(() => fakeImport(ChildA)); + const LazyChildB = React.lazy(() => fakeImport(ChildB)); + + function Parent({swap}) { + return ( + }> + {swap ? : } + + ); + } + + const root = ReactDOMClient.createRoot(container); + act(() => { + root.render(); + }); + expect(Scheduler).toHaveYielded(['Loading...']); + + await LazyChildA; + expect(Scheduler).toFlushAndYield(['A', 'Did mount: A']); + expect(container.innerHTML).toBe('A'); + + // Swap the position of A and B + ReactDOM.flushSync(() => { + root.render(); + }); + expect(Scheduler).toHaveYielded(['Loading...', 'Will unmount: A']); + expect(container.innerHTML).toBe('Loading...'); + + // Destroy the whole tree, including the hidden A + ReactDOM.flushSync(() => { + root.render(

Hello

); + }); + expect(Scheduler).toFlushAndYield([]); + expect(container.innerHTML).toBe('

Hello

'); + }); });