diff --git a/packages/react-dom/src/__tests__/ReactDOMActivity-test.js b/packages/react-dom/src/__tests__/ReactDOMActivity-test.js index e849ddc501d..ec2048ec434 100644 --- a/packages/react-dom/src/__tests__/ReactDOMActivity-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMActivity-test.js @@ -10,11 +10,17 @@ 'use strict'; let React; -let Activity; -let useState; let ReactDOM; let ReactDOMClient; +let Scheduler; let act; +let Activity; +let useState; +let useLayoutEffect; +let useEffect; +let LegacyHidden; +let assertLog; +let Suspense; describe('ReactDOMActivity', () => { let container; @@ -22,11 +28,19 @@ describe('ReactDOMActivity', () => { beforeEach(() => { jest.resetModules(); React = require('react'); + Scheduler = require('scheduler/unstable_mock'); Activity = React.Activity; useState = React.useState; + Suspense = React.Suspense; + useState = React.useState; + LegacyHidden = React.unstable_LegacyHidden; + useLayoutEffect = React.useLayoutEffect; + useEffect = React.useEffect; ReactDOM = require('react-dom'); ReactDOMClient = require('react-dom/client'); - act = require('internal-test-utils').act; + const InternalTestUtils = require('internal-test-utils'); + act = InternalTestUtils.act; + assertLog = InternalTestUtils.assertLog; container = document.createElement('div'); document.body.appendChild(container); }); @@ -35,6 +49,11 @@ describe('ReactDOMActivity', () => { document.body.removeChild(container); }); + function Text(props) { + Scheduler.log(props.text); + return {props.children}; + } + // @gate enableActivity it( 'hiding an Activity boundary also hides the direct children of any ' + @@ -53,7 +72,7 @@ describe('ReactDOMActivity', () => { ); } - function App({portalContents}) { + function App() { return (
@@ -99,7 +118,7 @@ describe('ReactDOMActivity', () => { ); } - function App({portalContents}) { + function App() { return (
@@ -131,4 +150,416 @@ describe('ReactDOMActivity', () => { ); }, ); + + // @gate enableActivity + it('hides new portals added to an already hidden tree', async () => { + function Child() { + return ; + } + + const portalContainer = document.createElement('div'); + + function Portal({children}) { + return
{ReactDOM.createPortal(children, portalContainer)}
; + } + + const root = ReactDOMClient.createRoot(container); + // Mount hidden tree. + await act(() => { + root.render( + + + , + ); + }); + assertLog(['Parent']); + expect(container.innerHTML).toBe( + '', + ); + expect(portalContainer.innerHTML).toBe(''); + + // Add a portal inside the hidden tree. + await act(() => { + root.render( + + + + + + , + ); + }); + assertLog(['Parent', 'Child']); + expect(container.innerHTML).toBe( + '
', + ); + expect(portalContainer.innerHTML).toBe( + '', + ); + + // Now reveal it. + await act(() => { + root.render( + + + + + + , + ); + }); + + assertLog(['Parent', 'Child']); + expect(container.innerHTML).toBe( + '
', + ); + expect(portalContainer.innerHTML).toBe( + '', + ); + }); + + // @gate enableActivity + it('hides new insertions inside an already hidden portal', async () => { + function Child({text}) { + useLayoutEffect(() => { + Scheduler.log(`Mount layout ${text}`); + return () => { + Scheduler.log(`Unmount layout ${text}`); + }; + }, [text]); + return ; + } + + const portalContainer = document.createElement('div'); + + function Portal({children}) { + return
{ReactDOM.createPortal(children, portalContainer)}
; + } + + const root = ReactDOMClient.createRoot(container); + // Mount hidden tree. + await act(() => { + root.render( + + + + + , + ); + }); + assertLog(['A']); + expect(container.innerHTML).toBe('
'); + expect(portalContainer.innerHTML).toBe( + '', + ); + + // Add a node inside the hidden portal. + await act(() => { + root.render( + + + + + + , + ); + }); + assertLog(['A', 'B']); + expect(container.innerHTML).toBe('
'); + expect(portalContainer.innerHTML).toBe( + '', + ); + + // Now reveal it. + await act(() => { + root.render( + + + + + + , + ); + }); + + assertLog(['A', 'B', 'Mount layout A', 'Mount layout B']); + expect(container.innerHTML).toBe('
'); + expect(portalContainer.innerHTML).toBe( + '', + ); + }); + + // @gate enableActivity + it('reveal an inner Suspense boundary without revealing an outer Activity on the same host child', async () => { + const promise = new Promise(() => {}); + + function Child({showInner}) { + useLayoutEffect(() => { + Scheduler.log('Mount layout'); + return () => { + Scheduler.log('Unmount layout'); + }; + }, []); + return ( + <> + {showInner ? null : promise} + + + ); + } + + const portalContainer = document.createElement('div'); + + function Portal({children}) { + return
{ReactDOM.createPortal(children, portalContainer)}
; + } + + const root = ReactDOMClient.createRoot(container); + + // Prerender the whole tree. + await act(() => { + root.render( + + + Loading}> + + + + , + ); + }); + + assertLog(['Child']); + expect(container.innerHTML).toBe('
'); + expect(portalContainer.innerHTML).toBe( + '', + ); + + // Re-suspend the inner. + await act(() => { + root.render( + + + Loading}> + + + + , + ); + }); + assertLog([]); + expect(container.innerHTML).toBe('
'); + expect(portalContainer.innerHTML).toBe( + 'Loading', + ); + + // Toggle to visible while suspended. + await act(() => { + root.render( + + + Loading}> + + + + , + ); + }); + assertLog([]); + expect(container.innerHTML).toBe('
'); + expect(portalContainer.innerHTML).toBe( + 'Loading', + ); + + // Now reveal. + await act(() => { + root.render( + + + Loading}> + + + + , + ); + }); + assertLog(['Child', 'Mount layout']); + expect(container.innerHTML).toBe('
'); + expect(portalContainer.innerHTML).toBe( + '', + ); + }); + + // @gate enableActivity + it('mounts/unmounts layout effects in portal when visibility changes (starting visible)', async () => { + function Child() { + useLayoutEffect(() => { + Scheduler.log('Mount layout'); + return () => { + Scheduler.log('Unmount layout'); + }; + }, []); + return ; + } + + const portalContainer = document.createElement('div'); + + function Portal({children}) { + return
{ReactDOM.createPortal(children, portalContainer)}
; + } + + const root = ReactDOMClient.createRoot(container); + // Mount visible tree. + await act(() => { + root.render( + + + + + , + ); + }); + assertLog(['Child', 'Mount layout']); + expect(container.innerHTML).toBe('
'); + expect(portalContainer.innerHTML).toBe(''); + + // Hide the tree. The layout effect is unmounted. + await act(() => { + root.render( + + + + + , + ); + }); + assertLog(['Unmount layout', 'Child']); + expect(container.innerHTML).toBe('
'); + expect(portalContainer.innerHTML).toBe( + '', + ); + }); + + // @gate enableActivity + it('mounts/unmounts layout effects in portal when visibility changes (starting hidden)', async () => { + function Child() { + useLayoutEffect(() => { + Scheduler.log('Mount layout'); + return () => { + Scheduler.log('Unmount layout'); + }; + }, []); + return ; + } + + const portalContainer = document.createElement('div'); + + function Portal({children}) { + return
{ReactDOM.createPortal(children, portalContainer)}
; + } + + const root = ReactDOMClient.createRoot(container); + // Mount hidden tree. + await act(() => { + root.render( + + + + + , + ); + }); + // No layout effect. + assertLog(['Child']); + expect(container.innerHTML).toBe('
'); + expect(portalContainer.innerHTML).toBe( + '', + ); + + // Unhide the tree. The layout effect is mounted. + await act(() => { + root.render( + + + + + , + ); + }); + assertLog(['Child', 'Mount layout']); + expect(container.innerHTML).toBe('
'); + expect(portalContainer.innerHTML).toBe( + '', + ); + }); + + // @gate enableLegacyHidden + it('does not toggle effects or hide nodes for LegacyHidden component inside portal', async () => { + function Child() { + useLayoutEffect(() => { + Scheduler.log('Mount layout'); + return () => { + Scheduler.log('Unmount layout'); + }; + }, []); + useEffect(() => { + Scheduler.log('Mount passive'); + return () => { + Scheduler.log('Unmount passive'); + }; + }, []); + return ; + } + + const portalContainer = document.createElement('div'); + + function Portal({children}) { + return
{ReactDOM.createPortal(children, portalContainer)}
; + } + + const root = ReactDOMClient.createRoot(container); + // Mount visible tree. + await act(() => { + root.render( + + + + + , + ); + }); + assertLog(['Child', 'Mount layout', 'Mount passive']); + expect(container.innerHTML).toBe('
'); + expect(portalContainer.innerHTML).toBe(''); + + // Hide the tree. + await act(() => { + root.render( + + + + + , + ); + }); + // Effects not unmounted. + assertLog(['Child']); + expect(container.innerHTML).toBe('
'); + expect(portalContainer.innerHTML).toBe(''); + + // Unhide the tree. + await act(() => { + root.render( + + + + + , + ); + }); + // Effects already mounted. + assertLog(['Child']); + expect(container.innerHTML).toBe('
'); + expect(portalContainer.innerHTML).toBe(''); + }); });