1. let React;
    
  2. let ReactNoop;
    
  3. let Scheduler;
    
  4. let act;
    
  5. let LegacyHidden;
    
  6. let Activity;
    
  7. let useState;
    
  8. let useLayoutEffect;
    
  9. let useEffect;
    
  10. let useMemo;
    
  11. let useRef;
    
  12. let startTransition;
    
  13. let waitForPaint;
    
  14. let waitFor;
    
  15. let assertLog;
    
  16. 
    
  17. describe('Activity', () => {
    
  18.   beforeEach(() => {
    
  19.     jest.resetModules();
    
  20. 
    
  21.     React = require('react');
    
  22.     ReactNoop = require('react-noop-renderer');
    
  23.     Scheduler = require('scheduler');
    
  24.     act = require('internal-test-utils').act;
    
  25.     LegacyHidden = React.unstable_LegacyHidden;
    
  26.     Activity = React.unstable_Activity;
    
  27.     useState = React.useState;
    
  28.     useLayoutEffect = React.useLayoutEffect;
    
  29.     useEffect = React.useEffect;
    
  30.     useMemo = React.useMemo;
    
  31.     useRef = React.useRef;
    
  32.     startTransition = React.startTransition;
    
  33. 
    
  34.     const InternalTestUtils = require('internal-test-utils');
    
  35.     waitForPaint = InternalTestUtils.waitForPaint;
    
  36.     waitFor = InternalTestUtils.waitFor;
    
  37.     assertLog = InternalTestUtils.assertLog;
    
  38.   });
    
  39. 
    
  40.   function Text(props) {
    
  41.     Scheduler.log(props.text);
    
  42.     return <span prop={props.text}>{props.children}</span>;
    
  43.   }
    
  44. 
    
  45.   function LoggedText({text, children}) {
    
  46.     useEffect(() => {
    
  47.       Scheduler.log(`mount ${text}`);
    
  48.       return () => {
    
  49.         Scheduler.log(`unmount ${text}`);
    
  50.       };
    
  51.     });
    
  52. 
    
  53.     useLayoutEffect(() => {
    
  54.       Scheduler.log(`mount layout ${text}`);
    
  55.       return () => {
    
  56.         Scheduler.log(`unmount layout ${text}`);
    
  57.       };
    
  58.     });
    
  59.     return <Text text={text}>{children}</Text>;
    
  60.   }
    
  61. 
    
  62.   // @gate enableLegacyHidden
    
  63.   it('unstable-defer-without-hiding should never toggle the visibility of its children', async () => {
    
  64.     function App({mode}) {
    
  65.       return (
    
  66.         <>
    
  67.           <Text text="Normal" />
    
  68.           <LegacyHidden mode={mode}>
    
  69.             <Text text="Deferred" />
    
  70.           </LegacyHidden>
    
  71.         </>
    
  72.       );
    
  73.     }
    
  74. 
    
  75.     // Test the initial mount
    
  76.     const root = ReactNoop.createRoot();
    
  77.     await act(async () => {
    
  78.       root.render(<App mode="unstable-defer-without-hiding" />);
    
  79.       await waitForPaint(['Normal']);
    
  80.       expect(root).toMatchRenderedOutput(<span prop="Normal" />);
    
  81.     });
    
  82.     assertLog(['Deferred']);
    
  83.     expect(root).toMatchRenderedOutput(
    
  84.       <>
    
  85.         <span prop="Normal" />
    
  86.         <span prop="Deferred" />
    
  87.       </>,
    
  88.     );
    
  89. 
    
  90.     // Now try after an update
    
  91.     await act(() => {
    
  92.       root.render(<App mode="visible" />);
    
  93.     });
    
  94.     assertLog(['Normal', 'Deferred']);
    
  95.     expect(root).toMatchRenderedOutput(
    
  96.       <>
    
  97.         <span prop="Normal" />
    
  98.         <span prop="Deferred" />
    
  99.       </>,
    
  100.     );
    
  101. 
    
  102.     await act(async () => {
    
  103.       root.render(<App mode="unstable-defer-without-hiding" />);
    
  104.       await waitForPaint(['Normal']);
    
  105.       expect(root).toMatchRenderedOutput(
    
  106.         <>
    
  107.           <span prop="Normal" />
    
  108.           <span prop="Deferred" />
    
  109.         </>,
    
  110.       );
    
  111.     });
    
  112.     assertLog(['Deferred']);
    
  113.     expect(root).toMatchRenderedOutput(
    
  114.       <>
    
  115.         <span prop="Normal" />
    
  116.         <span prop="Deferred" />
    
  117.       </>,
    
  118.     );
    
  119.   });
    
  120. 
    
  121.   // @gate www
    
  122.   it('does not defer in legacy mode', async () => {
    
  123.     let setState;
    
  124.     function Foo() {
    
  125.       const [state, _setState] = useState('A');
    
  126.       setState = _setState;
    
  127.       return <Text text={state} />;
    
  128.     }
    
  129. 
    
  130.     const root = ReactNoop.createLegacyRoot();
    
  131.     await act(() => {
    
  132.       root.render(
    
  133.         <>
    
  134.           <LegacyHidden mode="hidden">
    
  135.             <Foo />
    
  136.           </LegacyHidden>
    
  137.           <Text text="Outside" />
    
  138.         </>,
    
  139.       );
    
  140. 
    
  141.       ReactNoop.flushSync();
    
  142. 
    
  143.       // Should not defer the hidden tree
    
  144.       assertLog(['A', 'Outside']);
    
  145.     });
    
  146.     expect(root).toMatchRenderedOutput(
    
  147.       <>
    
  148.         <span prop="A" />
    
  149.         <span prop="Outside" />
    
  150.       </>,
    
  151.     );
    
  152. 
    
  153.     // Test that the children can be updated
    
  154.     await act(() => {
    
  155.       setState('B');
    
  156.     });
    
  157.     assertLog(['B']);
    
  158.     expect(root).toMatchRenderedOutput(
    
  159.       <>
    
  160.         <span prop="B" />
    
  161.         <span prop="Outside" />
    
  162.       </>,
    
  163.     );
    
  164.   });
    
  165. 
    
  166.   // @gate www
    
  167.   it('does defer in concurrent mode', async () => {
    
  168.     let setState;
    
  169.     function Foo() {
    
  170.       const [state, _setState] = useState('A');
    
  171.       setState = _setState;
    
  172.       return <Text text={state} />;
    
  173.     }
    
  174. 
    
  175.     const root = ReactNoop.createRoot();
    
  176.     await act(async () => {
    
  177.       root.render(
    
  178.         <>
    
  179.           <LegacyHidden mode="hidden">
    
  180.             <Foo />
    
  181.           </LegacyHidden>
    
  182.           <Text text="Outside" />
    
  183.         </>,
    
  184.       );
    
  185.       // Should defer the hidden tree.
    
  186.       await waitForPaint(['Outside']);
    
  187.     });
    
  188. 
    
  189.     // The hidden tree was rendered at lower priority.
    
  190.     assertLog(['A']);
    
  191. 
    
  192.     expect(root).toMatchRenderedOutput(
    
  193.       <>
    
  194.         <span prop="A" />
    
  195.         <span prop="Outside" />
    
  196.       </>,
    
  197.     );
    
  198. 
    
  199.     // Test that the children can be updated
    
  200.     await act(() => {
    
  201.       setState('B');
    
  202.     });
    
  203.     assertLog(['B']);
    
  204.     expect(root).toMatchRenderedOutput(
    
  205.       <>
    
  206.         <span prop="B" />
    
  207.         <span prop="Outside" />
    
  208.       </>,
    
  209.     );
    
  210.   });
    
  211. 
    
  212.   // @gate enableActivity
    
  213.   it('mounts without layout effects when hidden', async () => {
    
  214.     function Child({text}) {
    
  215.       useLayoutEffect(() => {
    
  216.         Scheduler.log('Mount layout');
    
  217.         return () => {
    
  218.           Scheduler.log('Unmount layout');
    
  219.         };
    
  220.       }, []);
    
  221.       return <Text text="Child" />;
    
  222.     }
    
  223. 
    
  224.     const root = ReactNoop.createRoot();
    
  225. 
    
  226.     // Mount hidden tree.
    
  227.     await act(() => {
    
  228.       root.render(
    
  229.         <Activity mode="hidden">
    
  230.           <Child />
    
  231.         </Activity>,
    
  232.       );
    
  233.     });
    
  234.     // No layout effect.
    
  235.     assertLog(['Child']);
    
  236.     expect(root).toMatchRenderedOutput(<span hidden={true} prop="Child" />);
    
  237. 
    
  238.     // Unhide the tree. The layout effect is mounted.
    
  239.     await act(() => {
    
  240.       root.render(
    
  241.         <Activity mode="visible">
    
  242.           <Child />
    
  243.         </Activity>,
    
  244.       );
    
  245.     });
    
  246.     assertLog(['Child', 'Mount layout']);
    
  247.     expect(root).toMatchRenderedOutput(<span prop="Child" />);
    
  248.   });
    
  249. 
    
  250.   // @gate enableActivity
    
  251.   it('mounts/unmounts layout effects when visibility changes (starting visible)', async () => {
    
  252.     function Child({text}) {
    
  253.       useLayoutEffect(() => {
    
  254.         Scheduler.log('Mount layout');
    
  255.         return () => {
    
  256.           Scheduler.log('Unmount layout');
    
  257.         };
    
  258.       }, []);
    
  259.       return <Text text="Child" />;
    
  260.     }
    
  261. 
    
  262.     const root = ReactNoop.createRoot();
    
  263.     await act(() => {
    
  264.       root.render(
    
  265.         <Activity mode="visible">
    
  266.           <Child />
    
  267.         </Activity>,
    
  268.       );
    
  269.     });
    
  270.     assertLog(['Child', 'Mount layout']);
    
  271.     expect(root).toMatchRenderedOutput(<span prop="Child" />);
    
  272. 
    
  273.     // Hide the tree. The layout effect is unmounted.
    
  274.     await act(() => {
    
  275.       root.render(
    
  276.         <Activity mode="hidden">
    
  277.           <Child />
    
  278.         </Activity>,
    
  279.       );
    
  280.     });
    
  281.     assertLog(['Unmount layout', 'Child']);
    
  282.     expect(root).toMatchRenderedOutput(<span hidden={true} prop="Child" />);
    
  283. 
    
  284.     // Unhide the tree. The layout effect is re-mounted.
    
  285.     await act(() => {
    
  286.       root.render(
    
  287.         <Activity mode="visible">
    
  288.           <Child />
    
  289.         </Activity>,
    
  290.       );
    
  291.     });
    
  292.     assertLog(['Child', 'Mount layout']);
    
  293.     expect(root).toMatchRenderedOutput(<span prop="Child" />);
    
  294.   });
    
  295. 
    
  296.   // @gate enableActivity
    
  297.   it('nested offscreen does not call componentWillUnmount when hidden', async () => {
    
  298.     // This is a bug that appeared during production test of <unstable_Activity />.
    
  299.     // It is a very specific scenario with nested Offscreens. The inner offscreen
    
  300.     // goes from visible to hidden in synchronous update.
    
  301.     class ClassComponent extends React.Component {
    
  302.       render() {
    
  303.         return <Text text="child" />;
    
  304.       }
    
  305. 
    
  306.       componentWillUnmount() {
    
  307.         Scheduler.log('componentWillUnmount');
    
  308.       }
    
  309. 
    
  310.       componentDidMount() {
    
  311.         Scheduler.log('componentDidMount');
    
  312.       }
    
  313.     }
    
  314. 
    
  315.     const root = ReactNoop.createRoot();
    
  316.     await act(() => {
    
  317.       // Outer and inner offscreen are hidden.
    
  318.       root.render(
    
  319.         <Activity mode={'hidden'}>
    
  320.           <Activity mode={'hidden'}>
    
  321.             <ClassComponent />
    
  322.           </Activity>
    
  323.         </Activity>,
    
  324.       );
    
  325.     });
    
  326. 
    
  327.     assertLog(['child']);
    
  328.     expect(root).toMatchRenderedOutput(<span hidden={true} prop="child" />);
    
  329. 
    
  330.     await act(() => {
    
  331.       // Inner offscreen is visible.
    
  332.       root.render(
    
  333.         <Activity mode={'hidden'}>
    
  334.           <Activity mode={'visible'}>
    
  335.             <ClassComponent />
    
  336.           </Activity>
    
  337.         </Activity>,
    
  338.       );
    
  339.     });
    
  340. 
    
  341.     assertLog(['child']);
    
  342.     expect(root).toMatchRenderedOutput(<span hidden={true} prop="child" />);
    
  343. 
    
  344.     await act(() => {
    
  345.       // Inner offscreen is hidden.
    
  346.       root.render(
    
  347.         <Activity mode={'hidden'}>
    
  348.           <Activity mode={'hidden'}>
    
  349.             <ClassComponent />
    
  350.           </Activity>
    
  351.         </Activity>,
    
  352.       );
    
  353.     });
    
  354. 
    
  355.     assertLog(['child']);
    
  356.     expect(root).toMatchRenderedOutput(<span hidden={true} prop="child" />);
    
  357. 
    
  358.     await act(() => {
    
  359.       // Inner offscreen is visible.
    
  360.       root.render(
    
  361.         <Activity mode={'hidden'}>
    
  362.           <Activity mode={'visible'}>
    
  363.             <ClassComponent />
    
  364.           </Activity>
    
  365.         </Activity>,
    
  366.       );
    
  367.     });
    
  368. 
    
  369.     Scheduler.unstable_clearLog();
    
  370. 
    
  371.     await act(() => {
    
  372.       // Outer offscreen is visible.
    
  373.       // Inner offscreen is hidden.
    
  374.       root.render(
    
  375.         <Activity mode={'visible'}>
    
  376.           <Activity mode={'hidden'}>
    
  377.             <ClassComponent />
    
  378.           </Activity>
    
  379.         </Activity>,
    
  380.       );
    
  381.     });
    
  382. 
    
  383.     assertLog(['child']);
    
  384. 
    
  385.     await act(() => {
    
  386.       // Outer offscreen is hidden.
    
  387.       // Inner offscreen is visible.
    
  388.       root.render(
    
  389.         <Activity mode={'hidden'}>
    
  390.           <Activity mode={'visible'}>
    
  391.             <ClassComponent />
    
  392.           </Activity>
    
  393.         </Activity>,
    
  394.       );
    
  395.     });
    
  396. 
    
  397.     assertLog(['child']);
    
  398.   });
    
  399. 
    
  400.   // @gate enableActivity
    
  401.   it('mounts/unmounts layout effects when visibility changes (starting hidden)', async () => {
    
  402.     function Child({text}) {
    
  403.       useLayoutEffect(() => {
    
  404.         Scheduler.log('Mount layout');
    
  405.         return () => {
    
  406.           Scheduler.log('Unmount layout');
    
  407.         };
    
  408.       }, []);
    
  409.       return <Text text="Child" />;
    
  410.     }
    
  411. 
    
  412.     const root = ReactNoop.createRoot();
    
  413.     await act(() => {
    
  414.       // Start the tree hidden. The layout effect is not mounted.
    
  415.       root.render(
    
  416.         <Activity mode="hidden">
    
  417.           <Child />
    
  418.         </Activity>,
    
  419.       );
    
  420.     });
    
  421.     assertLog(['Child']);
    
  422.     expect(root).toMatchRenderedOutput(<span hidden={true} prop="Child" />);
    
  423. 
    
  424.     // Show the tree. The layout effect is mounted.
    
  425.     await act(() => {
    
  426.       root.render(
    
  427.         <Activity mode="visible">
    
  428.           <Child />
    
  429.         </Activity>,
    
  430.       );
    
  431.     });
    
  432.     assertLog(['Child', 'Mount layout']);
    
  433.     expect(root).toMatchRenderedOutput(<span prop="Child" />);
    
  434. 
    
  435.     // Hide the tree again. The layout effect is un-mounted.
    
  436.     await act(() => {
    
  437.       root.render(
    
  438.         <Activity mode="hidden">
    
  439.           <Child />
    
  440.         </Activity>,
    
  441.       );
    
  442.     });
    
  443.     assertLog(['Unmount layout', 'Child']);
    
  444.     expect(root).toMatchRenderedOutput(<span hidden={true} prop="Child" />);
    
  445.   });
    
  446. 
    
  447.   // @gate enableActivity
    
  448.   it('hides children of offscreen after layout effects are destroyed', async () => {
    
  449.     const root = ReactNoop.createRoot();
    
  450.     function Child({text}) {
    
  451.       useLayoutEffect(() => {
    
  452.         Scheduler.log('Mount layout');
    
  453.         return () => {
    
  454.           // The child should not be hidden yet.
    
  455.           expect(root).toMatchRenderedOutput(<span prop="Child" />);
    
  456.           Scheduler.log('Unmount layout');
    
  457.         };
    
  458.       }, []);
    
  459.       return <Text text="Child" />;
    
  460.     }
    
  461. 
    
  462.     await act(() => {
    
  463.       root.render(
    
  464.         <Activity mode="visible">
    
  465.           <Child />
    
  466.         </Activity>,
    
  467.       );
    
  468.     });
    
  469.     assertLog(['Child', 'Mount layout']);
    
  470.     expect(root).toMatchRenderedOutput(<span prop="Child" />);
    
  471. 
    
  472.     // Hide the tree. The layout effect is unmounted.
    
  473.     await act(() => {
    
  474.       root.render(
    
  475.         <Activity mode="hidden">
    
  476.           <Child />
    
  477.         </Activity>,
    
  478.       );
    
  479.     });
    
  480.     assertLog(['Unmount layout', 'Child']);
    
  481. 
    
  482.     // After the layout effect is unmounted, the child is hidden.
    
  483.     expect(root).toMatchRenderedOutput(<span hidden={true} prop="Child" />);
    
  484.   });
    
  485. 
    
  486.   // @gate enableLegacyHidden
    
  487.   it('does not toggle effects for LegacyHidden component', async () => {
    
  488.     // LegacyHidden is meant to be the same as offscreen except it doesn't
    
  489.     // do anything to effects. Only used by www, as a temporary migration step.
    
  490.     function Child({text}) {
    
  491.       useLayoutEffect(() => {
    
  492.         Scheduler.log('Mount layout');
    
  493.         return () => {
    
  494.           Scheduler.log('Unmount layout');
    
  495.         };
    
  496.       }, []);
    
  497.       return <Text text="Child" />;
    
  498.     }
    
  499. 
    
  500.     const root = ReactNoop.createRoot();
    
  501.     await act(() => {
    
  502.       root.render(
    
  503.         <LegacyHidden mode="visible">
    
  504.           <Child />
    
  505.         </LegacyHidden>,
    
  506.       );
    
  507.     });
    
  508.     assertLog(['Child', 'Mount layout']);
    
  509. 
    
  510.     await act(() => {
    
  511.       root.render(
    
  512.         <LegacyHidden mode="hidden">
    
  513.           <Child />
    
  514.         </LegacyHidden>,
    
  515.       );
    
  516.     });
    
  517.     assertLog(['Child']);
    
  518. 
    
  519.     await act(() => {
    
  520.       root.render(
    
  521.         <LegacyHidden mode="visible">
    
  522.           <Child />
    
  523.         </LegacyHidden>,
    
  524.       );
    
  525.     });
    
  526.     assertLog(['Child']);
    
  527. 
    
  528.     await act(() => {
    
  529.       root.render(null);
    
  530.     });
    
  531.     assertLog(['Unmount layout']);
    
  532.   });
    
  533. 
    
  534.   // @gate enableActivity
    
  535.   it('hides new insertions into an already hidden tree', async () => {
    
  536.     const root = ReactNoop.createRoot();
    
  537.     await act(() => {
    
  538.       root.render(
    
  539.         <Activity mode="hidden">
    
  540.           <span>Hi</span>
    
  541.         </Activity>,
    
  542.       );
    
  543.     });
    
  544.     expect(root).toMatchRenderedOutput(<span hidden={true}>Hi</span>);
    
  545. 
    
  546.     // Insert a new node into the hidden tree
    
  547.     await act(() => {
    
  548.       root.render(
    
  549.         <Activity mode="hidden">
    
  550.           <span>Hi</span>
    
  551.           <span>Something new</span>
    
  552.         </Activity>,
    
  553.       );
    
  554.     });
    
  555.     expect(root).toMatchRenderedOutput(
    
  556.       <>
    
  557.         <span hidden={true}>Hi</span>
    
  558.         {/* This new node should also be hidden */}
    
  559.         <span hidden={true}>Something new</span>
    
  560.       </>,
    
  561.     );
    
  562.   });
    
  563. 
    
  564.   // @gate enableActivity
    
  565.   it('hides updated nodes inside an already hidden tree', async () => {
    
  566.     const root = ReactNoop.createRoot();
    
  567.     await act(() => {
    
  568.       root.render(
    
  569.         <Activity mode="hidden">
    
  570.           <span>Hi</span>
    
  571.         </Activity>,
    
  572.       );
    
  573.     });
    
  574.     expect(root).toMatchRenderedOutput(<span hidden={true}>Hi</span>);
    
  575. 
    
  576.     // Set the `hidden` prop to on an already hidden node
    
  577.     await act(() => {
    
  578.       root.render(
    
  579.         <Activity mode="hidden">
    
  580.           <span hidden={false}>Hi</span>
    
  581.         </Activity>,
    
  582.       );
    
  583.     });
    
  584.     // It should still be hidden, because the Activity container overrides it
    
  585.     expect(root).toMatchRenderedOutput(<span hidden={true}>Hi</span>);
    
  586. 
    
  587.     // Unhide the boundary
    
  588.     await act(() => {
    
  589.       root.render(
    
  590.         <Activity mode="visible">
    
  591.           <span hidden={true}>Hi</span>
    
  592.         </Activity>,
    
  593.       );
    
  594.     });
    
  595.     // It should still be hidden, because of the prop
    
  596.     expect(root).toMatchRenderedOutput(<span hidden={true}>Hi</span>);
    
  597. 
    
  598.     // Remove the `hidden` prop
    
  599.     await act(() => {
    
  600.       root.render(
    
  601.         <Activity mode="visible">
    
  602.           <span>Hi</span>
    
  603.         </Activity>,
    
  604.       );
    
  605.     });
    
  606.     // Now it's visible
    
  607.     expect(root).toMatchRenderedOutput(<span>Hi</span>);
    
  608.   });
    
  609. 
    
  610.   // @gate enableActivity
    
  611.   it('revealing a hidden tree at high priority does not cause tearing', async () => {
    
  612.     // When revealing an offscreen tree, we need to include updates that were
    
  613.     // previously deferred because the tree was hidden, even if they are lower
    
  614.     // priority than the current render. However, we should *not* include low
    
  615.     // priority updates that are entangled with updates outside of the hidden
    
  616.     // tree, because that can cause tearing.
    
  617.     //
    
  618.     // This test covers a scenario where an update multiple updates inside a
    
  619.     // hidden tree share the same lane, but are processed at different times
    
  620.     // because of the timing of when they were scheduled.
    
  621. 
    
  622.     // This functions checks whether the "outer" and "inner" states are
    
  623.     // consistent in the rendered output.
    
  624.     let currentOuter = null;
    
  625.     let currentInner = null;
    
  626.     function areOuterAndInnerConsistent() {
    
  627.       return (
    
  628.         currentOuter === null ||
    
  629.         currentInner === null ||
    
  630.         currentOuter === currentInner
    
  631.       );
    
  632.     }
    
  633. 
    
  634.     let setInner;
    
  635.     function Child() {
    
  636.       const [inner, _setInner] = useState(0);
    
  637.       setInner = _setInner;
    
  638. 
    
  639.       useEffect(() => {
    
  640.         currentInner = inner;
    
  641.         return () => {
    
  642.           currentInner = null;
    
  643.         };
    
  644.       }, [inner]);
    
  645. 
    
  646.       return <Text text={'Inner: ' + inner} />;
    
  647.     }
    
  648. 
    
  649.     let setOuter;
    
  650.     function App({show}) {
    
  651.       const [outer, _setOuter] = useState(0);
    
  652.       setOuter = _setOuter;
    
  653. 
    
  654.       useEffect(() => {
    
  655.         currentOuter = outer;
    
  656.         return () => {
    
  657.           currentOuter = null;
    
  658.         };
    
  659.       }, [outer]);
    
  660. 
    
  661.       return (
    
  662.         <>
    
  663.           <Text text={'Outer: ' + outer} />
    
  664.           <Activity mode={show ? 'visible' : 'hidden'}>
    
  665.             <Child />
    
  666.           </Activity>
    
  667.         </>
    
  668.       );
    
  669.     }
    
  670. 
    
  671.     // Render a hidden tree
    
  672.     const root = ReactNoop.createRoot();
    
  673.     await act(() => {
    
  674.       root.render(<App show={false} />);
    
  675.     });
    
  676.     assertLog(['Outer: 0', 'Inner: 0']);
    
  677.     expect(root).toMatchRenderedOutput(
    
  678.       <>
    
  679.         <span prop="Outer: 0" />
    
  680.         <span hidden={true} prop="Inner: 0" />
    
  681.       </>,
    
  682.     );
    
  683.     expect(areOuterAndInnerConsistent()).toBe(true);
    
  684. 
    
  685.     await act(async () => {
    
  686.       // Update a value both inside and outside the hidden tree. These values
    
  687.       // must always be consistent.
    
  688.       setOuter(1);
    
  689.       setInner(1);
    
  690.       // Only the outer updates finishes because the inner update is inside a
    
  691.       // hidden tree. The outer update is deferred to a later render.
    
  692.       await waitForPaint(['Outer: 1']);
    
  693.       expect(root).toMatchRenderedOutput(
    
  694.         <>
    
  695.           <span prop="Outer: 1" />
    
  696.           <span hidden={true} prop="Inner: 0" />
    
  697.         </>,
    
  698.       );
    
  699. 
    
  700.       // Before the inner update can finish, we receive another pair of updates.
    
  701.       if (gate(flags => flags.enableUnifiedSyncLane)) {
    
  702.         React.startTransition(() => {
    
  703.           setOuter(2);
    
  704.           setInner(2);
    
  705.         });
    
  706.       } else {
    
  707.         setOuter(2);
    
  708.         setInner(2);
    
  709.       }
    
  710. 
    
  711.       // Also, before either of these new updates are processed, the hidden
    
  712.       // tree is revealed at high priority.
    
  713.       ReactNoop.flushSync(() => {
    
  714.         root.render(<App show={true} />);
    
  715.       });
    
  716. 
    
  717.       assertLog([
    
  718.         'Outer: 1',
    
  719. 
    
  720.         // There are two pending updates on Inner, but only the first one
    
  721.         // is processed, even though they share the same lane. If the second
    
  722.         // update were erroneously processed, then Inner would be inconsistent
    
  723.         // with Outer.
    
  724.         'Inner: 1',
    
  725.       ]);
    
  726.       expect(root).toMatchRenderedOutput(
    
  727.         <>
    
  728.           <span prop="Outer: 1" />
    
  729.           <span prop="Inner: 1" />
    
  730.         </>,
    
  731.       );
    
  732.       expect(areOuterAndInnerConsistent()).toBe(true);
    
  733.     });
    
  734.     assertLog(['Outer: 2', 'Inner: 2']);
    
  735.     expect(root).toMatchRenderedOutput(
    
  736.       <>
    
  737.         <span prop="Outer: 2" />
    
  738.         <span prop="Inner: 2" />
    
  739.       </>,
    
  740.     );
    
  741.     expect(areOuterAndInnerConsistent()).toBe(true);
    
  742.   });
    
  743. 
    
  744.   // @gate enableActivity
    
  745.   it('regression: Activity instance is sometimes null during setState', async () => {
    
  746.     let setState;
    
  747.     function Child() {
    
  748.       const [state, _setState] = useState('Initial');
    
  749.       setState = _setState;
    
  750.       return <Text text={state} />;
    
  751.     }
    
  752. 
    
  753.     const root = ReactNoop.createRoot();
    
  754.     await act(() => {
    
  755.       root.render(<Activity hidden={false} />);
    
  756.     });
    
  757.     assertLog([]);
    
  758.     expect(root).toMatchRenderedOutput(null);
    
  759. 
    
  760.     await act(async () => {
    
  761.       // Partially render a component
    
  762.       startTransition(() => {
    
  763.         root.render(
    
  764.           <Activity hidden={false}>
    
  765.             <Child />
    
  766.             <Text text="Sibling" />
    
  767.           </Activity>,
    
  768.         );
    
  769.       });
    
  770.       await waitFor(['Initial']);
    
  771. 
    
  772.       // Before it finishes rendering, the whole tree gets deleted
    
  773.       ReactNoop.flushSync(() => {
    
  774.         root.render(null);
    
  775.       });
    
  776. 
    
  777.       // Something attempts to update the never-mounted component. When this
    
  778.       // regression test was written, we would walk up the component's return
    
  779.       // path and reach an unmounted Activity component fiber. Its `stateNode`
    
  780.       // would be null because it was nulled out when it was deleted, but there
    
  781.       // was no null check before we accessed it. A weird edge case but we must
    
  782.       // account for it.
    
  783.       expect(() => {
    
  784.         setState('Updated');
    
  785.       }).toErrorDev(
    
  786.         "Can't perform a React state update on a component that hasn't mounted yet",
    
  787.       );
    
  788.     });
    
  789.     expect(root).toMatchRenderedOutput(null);
    
  790.   });
    
  791. 
    
  792.   // @gate enableActivity
    
  793.   it('class component setState callbacks do not fire until tree is visible', async () => {
    
  794.     const root = ReactNoop.createRoot();
    
  795. 
    
  796.     let child;
    
  797.     class Child extends React.Component {
    
  798.       state = {text: 'A'};
    
  799.       render() {
    
  800.         child = this;
    
  801.         return <Text text={this.state.text} />;
    
  802.       }
    
  803.     }
    
  804. 
    
  805.     // Initial render
    
  806.     await act(() => {
    
  807.       root.render(
    
  808.         <Activity mode="hidden">
    
  809.           <Child />
    
  810.         </Activity>,
    
  811.       );
    
  812.     });
    
  813.     assertLog(['A']);
    
  814.     expect(root).toMatchRenderedOutput(<span hidden={true} prop="A" />);
    
  815. 
    
  816.     // Schedule an update to a hidden class component. The update will finish
    
  817.     // rendering in the background, but the callback shouldn't fire yet, because
    
  818.     // the component isn't visible.
    
  819.     await act(() => {
    
  820.       child.setState({text: 'B'}, () => {
    
  821.         Scheduler.log('B update finished');
    
  822.       });
    
  823.     });
    
  824.     assertLog(['B']);
    
  825.     expect(root).toMatchRenderedOutput(<span hidden={true} prop="B" />);
    
  826. 
    
  827.     // Now reveal the hidden component. Simultaneously, schedule another
    
  828.     // update with a callback to the same component. When the component is
    
  829.     // revealed, both the B callback and C callback should fire, in that order.
    
  830.     await act(() => {
    
  831.       root.render(
    
  832.         <Activity mode="visible">
    
  833.           <Child />
    
  834.         </Activity>,
    
  835.       );
    
  836.       child.setState({text: 'C'}, () => {
    
  837.         Scheduler.log('C update finished');
    
  838.       });
    
  839.     });
    
  840.     assertLog(['C', 'B update finished', 'C update finished']);
    
  841.     expect(root).toMatchRenderedOutput(<span prop="C" />);
    
  842.   });
    
  843. 
    
  844.   // @gate enableActivity
    
  845.   it('does not call componentDidUpdate when reappearing a hidden class component', async () => {
    
  846.     class Child extends React.Component {
    
  847.       componentDidMount() {
    
  848.         Scheduler.log('componentDidMount');
    
  849.       }
    
  850.       componentDidUpdate() {
    
  851.         Scheduler.log('componentDidUpdate');
    
  852.       }
    
  853.       componentWillUnmount() {
    
  854.         Scheduler.log('componentWillUnmount');
    
  855.       }
    
  856.       render() {
    
  857.         return 'Child';
    
  858.       }
    
  859.     }
    
  860. 
    
  861.     // Initial mount
    
  862.     const root = ReactNoop.createRoot();
    
  863.     await act(() => {
    
  864.       root.render(
    
  865.         <Activity mode="visible">
    
  866.           <Child />
    
  867.         </Activity>,
    
  868.       );
    
  869.     });
    
  870.     assertLog(['componentDidMount']);
    
  871. 
    
  872.     // Hide the class component
    
  873.     await act(() => {
    
  874.       root.render(
    
  875.         <Activity mode="hidden">
    
  876.           <Child />
    
  877.         </Activity>,
    
  878.       );
    
  879.     });
    
  880.     assertLog(['componentWillUnmount']);
    
  881. 
    
  882.     // Reappear the class component. componentDidMount should fire, not
    
  883.     // componentDidUpdate.
    
  884.     await act(() => {
    
  885.       root.render(
    
  886.         <Activity mode="visible">
    
  887.           <Child />
    
  888.         </Activity>,
    
  889.       );
    
  890.     });
    
  891.     assertLog(['componentDidMount']);
    
  892.   });
    
  893. 
    
  894.   // @gate enableActivity
    
  895.   it(
    
  896.     'when reusing old components (hidden -> visible), layout effects fire ' +
    
  897.       'with same timing as if it were brand new',
    
  898.     async () => {
    
  899.       function Child({label}) {
    
  900.         useLayoutEffect(() => {
    
  901.           Scheduler.log('Mount ' + label);
    
  902.           return () => {
    
  903.             Scheduler.log('Unmount ' + label);
    
  904.           };
    
  905.         }, [label]);
    
  906.         return label;
    
  907.       }
    
  908. 
    
  909.       // Initial mount
    
  910.       const root = ReactNoop.createRoot();
    
  911.       await act(() => {
    
  912.         root.render(
    
  913.           <Activity mode="visible">
    
  914.             <Child key="B" label="B" />
    
  915.           </Activity>,
    
  916.         );
    
  917.       });
    
  918.       assertLog(['Mount B']);
    
  919. 
    
  920.       // Hide the component
    
  921.       await act(() => {
    
  922.         root.render(
    
  923.           <Activity mode="hidden">
    
  924.             <Child key="B" label="B" />
    
  925.           </Activity>,
    
  926.         );
    
  927.       });
    
  928.       assertLog(['Unmount B']);
    
  929. 
    
  930.       // Reappear the component and also add some new siblings.
    
  931.       await act(() => {
    
  932.         root.render(
    
  933.           <Activity mode="visible">
    
  934.             <Child key="A" label="A" />
    
  935.             <Child key="B" label="B" />
    
  936.             <Child key="C" label="C" />
    
  937.           </Activity>,
    
  938.         );
    
  939.       });
    
  940.       // B's effect should fire in between A and C even though it's been reused
    
  941.       // from a previous render. In other words, it's the same order as if all
    
  942.       // three siblings were brand new.
    
  943.       assertLog(['Mount A', 'Mount B', 'Mount C']);
    
  944.     },
    
  945.   );
    
  946. 
    
  947.   // @gate enableActivity
    
  948.   it(
    
  949.     'when reusing old components (hidden -> visible), layout effects fire ' +
    
  950.       'with same timing as if it were brand new (includes setState callback)',
    
  951.     async () => {
    
  952.       class Child extends React.Component {
    
  953.         componentDidMount() {
    
  954.           Scheduler.log('Mount ' + this.props.label);
    
  955.         }
    
  956.         componentWillUnmount() {
    
  957.           Scheduler.log('Unmount ' + this.props.label);
    
  958.         }
    
  959.         render() {
    
  960.           return this.props.label;
    
  961.         }
    
  962.       }
    
  963. 
    
  964.       // Initial mount
    
  965.       const bRef = React.createRef();
    
  966.       const root = ReactNoop.createRoot();
    
  967.       await act(() => {
    
  968.         root.render(
    
  969.           <Activity mode="visible">
    
  970.             <Child key="B" ref={bRef} label="B" />
    
  971.           </Activity>,
    
  972.         );
    
  973.       });
    
  974.       assertLog(['Mount B']);
    
  975. 
    
  976.       // We're going to schedule an update on a hidden component, so stash a
    
  977.       // reference to its setState before the ref gets detached
    
  978.       const setStateB = bRef.current.setState.bind(bRef.current);
    
  979. 
    
  980.       // Hide the component
    
  981.       await act(() => {
    
  982.         root.render(
    
  983.           <Activity mode="hidden">
    
  984.             <Child key="B" ref={bRef} label="B" />
    
  985.           </Activity>,
    
  986.         );
    
  987.       });
    
  988.       assertLog(['Unmount B']);
    
  989. 
    
  990.       // Reappear the component and also add some new siblings.
    
  991.       await act(() => {
    
  992.         setStateB(null, () => {
    
  993.           Scheduler.log('setState callback B');
    
  994.         });
    
  995.         root.render(
    
  996.           <Activity mode="visible">
    
  997.             <Child key="A" label="A" />
    
  998.             <Child key="B" ref={bRef} label="B" />
    
  999.             <Child key="C" label="C" />
    
  1000.           </Activity>,
    
  1001.         );
    
  1002.       });
    
  1003.       // B's effect should fire in between A and C even though it's been reused
    
  1004.       // from a previous render. In other words, it's the same order as if all
    
  1005.       // three siblings were brand new.
    
  1006.       assertLog(['Mount A', 'Mount B', 'setState callback B', 'Mount C']);
    
  1007.     },
    
  1008.   );
    
  1009. 
    
  1010.   // @gate enableActivity
    
  1011.   it('defer passive effects when prerendering a new Activity tree', async () => {
    
  1012.     function Child({label}) {
    
  1013.       useEffect(() => {
    
  1014.         Scheduler.log('Mount ' + label);
    
  1015.         return () => {
    
  1016.           Scheduler.log('Unmount ' + label);
    
  1017.         };
    
  1018.       }, [label]);
    
  1019.       return <Text text={label} />;
    
  1020.     }
    
  1021. 
    
  1022.     function App({showMore}) {
    
  1023.       return (
    
  1024.         <>
    
  1025.           <Child label="Shell" />
    
  1026.           <Activity mode={showMore ? 'visible' : 'hidden'}>
    
  1027.             <Child label="More" />
    
  1028.           </Activity>
    
  1029.         </>
    
  1030.       );
    
  1031.     }
    
  1032. 
    
  1033.     const root = ReactNoop.createRoot();
    
  1034. 
    
  1035.     // Mount the app without showing the extra content
    
  1036.     await act(() => {
    
  1037.       root.render(<App showMore={false} />);
    
  1038.     });
    
  1039.     assertLog([
    
  1040.       // First mount the outer visible shell
    
  1041.       'Shell',
    
  1042.       'Mount Shell',
    
  1043. 
    
  1044.       // Then prerender the hidden extra context. The passive effects in the
    
  1045.       // hidden tree should not fire
    
  1046.       'More',
    
  1047.       // Does not fire
    
  1048.       // 'Mount More',
    
  1049.     ]);
    
  1050.     // The hidden content has been prerendered
    
  1051.     expect(root).toMatchRenderedOutput(
    
  1052.       <>
    
  1053.         <span prop="Shell" />
    
  1054.         <span hidden={true} prop="More" />
    
  1055.       </>,
    
  1056.     );
    
  1057. 
    
  1058.     // Reveal the prerendered tree
    
  1059.     await act(() => {
    
  1060.       root.render(<App showMore={true} />);
    
  1061.     });
    
  1062.     assertLog([
    
  1063.       'Shell',
    
  1064.       'More',
    
  1065. 
    
  1066.       // Mount the passive effects in the newly revealed tree, the ones that
    
  1067.       // were skipped during pre-rendering.
    
  1068.       'Mount More',
    
  1069.     ]);
    
  1070.   });
    
  1071. 
    
  1072.   // @gate enableLegacyHidden
    
  1073.   it('do not defer passive effects when prerendering a new LegacyHidden tree', async () => {
    
  1074.     function Child({label}) {
    
  1075.       useEffect(() => {
    
  1076.         Scheduler.log('Mount ' + label);
    
  1077.         return () => {
    
  1078.           Scheduler.log('Unmount ' + label);
    
  1079.         };
    
  1080.       }, [label]);
    
  1081.       return <Text text={label} />;
    
  1082.     }
    
  1083. 
    
  1084.     function App({showMore}) {
    
  1085.       return (
    
  1086.         <>
    
  1087.           <Child label="Shell" />
    
  1088.           <LegacyHidden
    
  1089.             mode={showMore ? 'visible' : 'unstable-defer-without-hiding'}>
    
  1090.             <Child label="More" />
    
  1091.           </LegacyHidden>
    
  1092.         </>
    
  1093.       );
    
  1094.     }
    
  1095. 
    
  1096.     const root = ReactNoop.createRoot();
    
  1097. 
    
  1098.     // Mount the app without showing the extra content
    
  1099.     await act(() => {
    
  1100.       root.render(<App showMore={false} />);
    
  1101.     });
    
  1102.     assertLog([
    
  1103.       // First mount the outer visible shell
    
  1104.       'Shell',
    
  1105.       'Mount Shell',
    
  1106. 
    
  1107.       // Then prerender the hidden extra context. Unlike Activity, the passive
    
  1108.       // effects in the hidden tree *should* fire
    
  1109.       'More',
    
  1110.       'Mount More',
    
  1111.     ]);
    
  1112. 
    
  1113.     // The hidden content has been prerendered
    
  1114.     expect(root).toMatchRenderedOutput(
    
  1115.       <>
    
  1116.         <span prop="Shell" />
    
  1117.         <span prop="More" />
    
  1118.       </>,
    
  1119.     );
    
  1120. 
    
  1121.     // Reveal the prerendered tree
    
  1122.     await act(() => {
    
  1123.       root.render(<App showMore={true} />);
    
  1124.     });
    
  1125.     assertLog(['Shell', 'More']);
    
  1126.   });
    
  1127. 
    
  1128.   // @gate enableActivity
    
  1129.   it('passive effects are connected and disconnected when the visibility changes', async () => {
    
  1130.     function Child({step}) {
    
  1131.       useEffect(() => {
    
  1132.         Scheduler.log(`Commit mount [${step}]`);
    
  1133.         return () => {
    
  1134.           Scheduler.log(`Commit unmount [${step}]`);
    
  1135.         };
    
  1136.       }, [step]);
    
  1137.       return <Text text={step} />;
    
  1138.     }
    
  1139. 
    
  1140.     function App({show, step}) {
    
  1141.       return (
    
  1142.         <Activity mode={show ? 'visible' : 'hidden'}>
    
  1143.           {useMemo(
    
  1144.             () => (
    
  1145.               <Child step={step} />
    
  1146.             ),
    
  1147.             [step],
    
  1148.           )}
    
  1149.         </Activity>
    
  1150.       );
    
  1151.     }
    
  1152. 
    
  1153.     const root = ReactNoop.createRoot();
    
  1154.     await act(() => {
    
  1155.       root.render(<App show={true} step={1} />);
    
  1156.     });
    
  1157.     assertLog([1, 'Commit mount [1]']);
    
  1158.     expect(root).toMatchRenderedOutput(<span prop={1} />);
    
  1159. 
    
  1160.     // Hide the tree. This will unmount the effect.
    
  1161.     await act(() => {
    
  1162.       root.render(<App show={false} step={1} />);
    
  1163.     });
    
  1164.     assertLog(['Commit unmount [1]']);
    
  1165.     expect(root).toMatchRenderedOutput(<span hidden={true} prop={1} />);
    
  1166. 
    
  1167.     // Update.
    
  1168.     await act(() => {
    
  1169.       root.render(<App show={false} step={2} />);
    
  1170.     });
    
  1171.     // The update is prerendered but no effects are fired
    
  1172.     assertLog([2]);
    
  1173.     expect(root).toMatchRenderedOutput(<span hidden={true} prop={2} />);
    
  1174. 
    
  1175.     // Reveal the tree.
    
  1176.     await act(() => {
    
  1177.       root.render(<App show={true} step={2} />);
    
  1178.     });
    
  1179.     // The update doesn't render because it was already prerendered, but we do
    
  1180.     // fire the effect.
    
  1181.     assertLog(['Commit mount [2]']);
    
  1182.     expect(root).toMatchRenderedOutput(<span prop={2} />);
    
  1183.   });
    
  1184. 
    
  1185.   // @gate enableActivity
    
  1186.   it('passive effects are unmounted on hide in the same order as during a deletion: parent before child', async () => {
    
  1187.     function Child({label}) {
    
  1188.       useEffect(() => {
    
  1189.         Scheduler.log('Mount Child');
    
  1190.         return () => {
    
  1191.           Scheduler.log('Unmount Child');
    
  1192.         };
    
  1193.       }, []);
    
  1194.       return <div>Hi</div>;
    
  1195.     }
    
  1196.     function Parent() {
    
  1197.       useEffect(() => {
    
  1198.         Scheduler.log('Mount Parent');
    
  1199.         return () => {
    
  1200.           Scheduler.log('Unmount Parent');
    
  1201.         };
    
  1202.       }, []);
    
  1203.       return <Child />;
    
  1204.     }
    
  1205. 
    
  1206.     function App({show}) {
    
  1207.       return (
    
  1208.         <Activity mode={show ? 'visible' : 'hidden'}>
    
  1209.           <Parent />
    
  1210.         </Activity>
    
  1211.       );
    
  1212.     }
    
  1213. 
    
  1214.     const root = ReactNoop.createRoot();
    
  1215.     await act(() => {
    
  1216.       root.render(<App show={true} />);
    
  1217.     });
    
  1218.     assertLog(['Mount Child', 'Mount Parent']);
    
  1219. 
    
  1220.     // First demonstrate what happens during a normal deletion
    
  1221.     await act(() => {
    
  1222.       root.render(null);
    
  1223.     });
    
  1224.     assertLog(['Unmount Parent', 'Unmount Child']);
    
  1225. 
    
  1226.     // Now redo the same thing but hide instead of deleting
    
  1227.     await act(() => {
    
  1228.       root.render(<App show={true} />);
    
  1229.     });
    
  1230.     assertLog(['Mount Child', 'Mount Parent']);
    
  1231.     await act(() => {
    
  1232.       root.render(<App show={false} />);
    
  1233.     });
    
  1234.     // The order is the same as during a deletion: parent before child
    
  1235.     assertLog(['Unmount Parent', 'Unmount Child']);
    
  1236.   });
    
  1237. 
    
  1238.   // TODO: As of now, there's no way to hide a tree without also unmounting its
    
  1239.   // effects. (Except for Suspense, which has its own tests associated with it.)
    
  1240.   // Re-enable this test once we add this ability. For example, we'll likely add
    
  1241.   // either an option or a heuristic to mount passive effects inside a hidden
    
  1242.   // tree after a delay.
    
  1243.   // @gate enableActivity
    
  1244.   it.skip("don't defer passive effects when prerendering in a tree whose effects are already connected", async () => {
    
  1245.     function Child({label}) {
    
  1246.       useEffect(() => {
    
  1247.         Scheduler.log('Mount ' + label);
    
  1248.         return () => {
    
  1249.           Scheduler.log('Unmount ' + label);
    
  1250.         };
    
  1251.       }, [label]);
    
  1252.       return <Text text={label} />;
    
  1253.     }
    
  1254. 
    
  1255.     function App({showMore, step}) {
    
  1256.       return (
    
  1257.         <>
    
  1258.           <Child label={'Shell ' + step} />
    
  1259.           <Activity mode={showMore ? 'visible' : 'hidden'}>
    
  1260.             <Child label={'More ' + step} />
    
  1261.           </Activity>
    
  1262.         </>
    
  1263.       );
    
  1264.     }
    
  1265. 
    
  1266.     const root = ReactNoop.createRoot();
    
  1267. 
    
  1268.     // Mount the app, including the extra content
    
  1269.     await act(() => {
    
  1270.       root.render(<App showMore={true} step={1} />);
    
  1271.     });
    
  1272.     assertLog(['Shell 1', 'More 1', 'Mount Shell 1', 'Mount More 1']);
    
  1273.     expect(root).toMatchRenderedOutput(
    
  1274.       <>
    
  1275.         <span prop="Shell 1" />
    
  1276.         <span prop="More 1" />
    
  1277.       </>,
    
  1278.     );
    
  1279. 
    
  1280.     // Hide the extra content. while also updating one of its props
    
  1281.     await act(() => {
    
  1282.       root.render(<App showMore={false} step={2} />);
    
  1283.     });
    
  1284.     assertLog([
    
  1285.       // First update the outer visible shell
    
  1286.       'Shell 2',
    
  1287.       'Unmount Shell 1',
    
  1288.       'Mount Shell 2',
    
  1289. 
    
  1290.       // Then prerender the update to the hidden content. Since the effects
    
  1291.       // are already connected inside the hidden tree, we don't defer updates
    
  1292.       // to them.
    
  1293.       'More 2',
    
  1294.       'Unmount More 1',
    
  1295.       'Mount More 2',
    
  1296.     ]);
    
  1297.   });
    
  1298. 
    
  1299.   // @gate enableActivity
    
  1300.   it('does not mount effects when prerendering a nested Activity boundary', async () => {
    
  1301.     function Child({label}) {
    
  1302.       useEffect(() => {
    
  1303.         Scheduler.log('Mount ' + label);
    
  1304.         return () => {
    
  1305.           Scheduler.log('Unmount ' + label);
    
  1306.         };
    
  1307.       }, [label]);
    
  1308.       return <Text text={label} />;
    
  1309.     }
    
  1310. 
    
  1311.     function App({showOuter, showInner}) {
    
  1312.       return (
    
  1313.         <Activity mode={showOuter ? 'visible' : 'hidden'}>
    
  1314.           {useMemo(
    
  1315.             () => (
    
  1316.               <div>
    
  1317.                 <Child label="Outer" />
    
  1318.                 {showInner ? (
    
  1319.                   <Activity mode="visible">
    
  1320.                     <div>
    
  1321.                       <Child label="Inner" />
    
  1322.                     </div>
    
  1323.                   </Activity>
    
  1324.                 ) : null}
    
  1325.               </div>
    
  1326.             ),
    
  1327.             [showInner],
    
  1328.           )}
    
  1329.         </Activity>
    
  1330.       );
    
  1331.     }
    
  1332. 
    
  1333.     const root = ReactNoop.createRoot();
    
  1334. 
    
  1335.     // Prerender the outer contents. No effects should mount.
    
  1336.     await act(() => {
    
  1337.       root.render(<App showOuter={false} showInner={false} />);
    
  1338.     });
    
  1339.     assertLog(['Outer']);
    
  1340.     expect(root).toMatchRenderedOutput(
    
  1341.       <div hidden={true}>
    
  1342.         <span prop="Outer" />
    
  1343.       </div>,
    
  1344.     );
    
  1345. 
    
  1346.     // Prerender the inner contents. No effects should mount.
    
  1347.     await act(() => {
    
  1348.       root.render(<App showOuter={false} showInner={true} />);
    
  1349.     });
    
  1350.     assertLog(['Outer', 'Inner']);
    
  1351.     expect(root).toMatchRenderedOutput(
    
  1352.       <div hidden={true}>
    
  1353.         <span prop="Outer" />
    
  1354.         <div>
    
  1355.           <span prop="Inner" />
    
  1356.         </div>
    
  1357.       </div>,
    
  1358.     );
    
  1359. 
    
  1360.     // Reveal the prerendered tree
    
  1361.     await act(() => {
    
  1362.       root.render(<App showOuter={true} showInner={true} />);
    
  1363.     });
    
  1364.     // The effects fire, but the tree is not re-rendered because it already
    
  1365.     // prerendered.
    
  1366.     assertLog(['Mount Outer', 'Mount Inner']);
    
  1367.     expect(root).toMatchRenderedOutput(
    
  1368.       <div>
    
  1369.         <span prop="Outer" />
    
  1370.         <div>
    
  1371.           <span prop="Inner" />
    
  1372.         </div>
    
  1373.       </div>,
    
  1374.     );
    
  1375.   });
    
  1376. 
    
  1377.   // @gate enableActivity
    
  1378.   it('reveal an outer Activity boundary without revealing an inner one', async () => {
    
  1379.     function Child({label}) {
    
  1380.       useEffect(() => {
    
  1381.         Scheduler.log('Mount ' + label);
    
  1382.         return () => {
    
  1383.           Scheduler.log('Unmount ' + label);
    
  1384.         };
    
  1385.       }, [label]);
    
  1386.       return <Text text={label} />;
    
  1387.     }
    
  1388. 
    
  1389.     function App({showOuter, showInner}) {
    
  1390.       return (
    
  1391.         <Activity mode={showOuter ? 'visible' : 'hidden'}>
    
  1392.           {useMemo(
    
  1393.             () => (
    
  1394.               <div>
    
  1395.                 <Child label="Outer" />
    
  1396.                 <Activity mode={showInner ? 'visible' : 'hidden'}>
    
  1397.                   <div>
    
  1398.                     <Child label="Inner" />
    
  1399.                   </div>
    
  1400.                 </Activity>
    
  1401.               </div>
    
  1402.             ),
    
  1403.             [showInner],
    
  1404.           )}
    
  1405.         </Activity>
    
  1406.       );
    
  1407.     }
    
  1408. 
    
  1409.     const root = ReactNoop.createRoot();
    
  1410. 
    
  1411.     // Prerender the whole tree.
    
  1412.     await act(() => {
    
  1413.       root.render(<App showOuter={false} showInner={false} />);
    
  1414.     });
    
  1415.     assertLog(['Outer', 'Inner']);
    
  1416.     // Both the inner and the outer tree should be hidden. Hiding the inner tree
    
  1417.     // is arguably redundant, but the advantage of hiding both is that later you
    
  1418.     // can reveal the outer tree without having to examine the inner one.
    
  1419.     expect(root).toMatchRenderedOutput(
    
  1420.       <div hidden={true}>
    
  1421.         <span prop="Outer" />
    
  1422.         <div hidden={true}>
    
  1423.           <span prop="Inner" />
    
  1424.         </div>
    
  1425.       </div>,
    
  1426.     );
    
  1427. 
    
  1428.     // Reveal the outer contents. The inner tree remains hidden.
    
  1429.     await act(() => {
    
  1430.       root.render(<App showOuter={true} showInner={false} />);
    
  1431.     });
    
  1432.     assertLog(['Mount Outer']);
    
  1433.     expect(root).toMatchRenderedOutput(
    
  1434.       <div>
    
  1435.         <span prop="Outer" />
    
  1436.         <div hidden={true}>
    
  1437.           <span prop="Inner" />
    
  1438.         </div>
    
  1439.       </div>,
    
  1440.     );
    
  1441.   });
    
  1442. 
    
  1443.   describe('manual interactivity', () => {
    
  1444.     // @gate enableActivity
    
  1445.     it('should attach ref only for mode null', async () => {
    
  1446.       let offscreenRef;
    
  1447. 
    
  1448.       function App({mode}) {
    
  1449.         offscreenRef = useRef(null);
    
  1450.         return (
    
  1451.           <Activity
    
  1452.             mode={mode}
    
  1453.             ref={ref => {
    
  1454.               offscreenRef.current = ref;
    
  1455.             }}>
    
  1456.             <div />
    
  1457.           </Activity>
    
  1458.         );
    
  1459.       }
    
  1460. 
    
  1461.       const root = ReactNoop.createRoot();
    
  1462. 
    
  1463.       await act(() => {
    
  1464.         root.render(<App mode={'manual'} />);
    
  1465.       });
    
  1466. 
    
  1467.       expect(offscreenRef.current).not.toBeNull();
    
  1468. 
    
  1469.       await act(() => {
    
  1470.         root.render(<App mode={'visible'} />);
    
  1471.       });
    
  1472. 
    
  1473.       expect(offscreenRef.current).toBeNull();
    
  1474. 
    
  1475.       await act(() => {
    
  1476.         root.render(<App mode={'hidden'} />);
    
  1477.       });
    
  1478. 
    
  1479.       expect(offscreenRef.current).toBeNull();
    
  1480. 
    
  1481.       await act(() => {
    
  1482.         root.render(<App mode={'manual'} />);
    
  1483.       });
    
  1484. 
    
  1485.       expect(offscreenRef.current).not.toBeNull();
    
  1486.     });
    
  1487. 
    
  1488.     // @gate enableActivity
    
  1489.     it('should lower update priority for detached Activity', async () => {
    
  1490.       let updateChildState;
    
  1491.       let updateHighPriorityComponentState;
    
  1492.       let offscreenRef;
    
  1493. 
    
  1494.       function Child() {
    
  1495.         const [state, _stateUpdate] = useState(0);
    
  1496.         updateChildState = _stateUpdate;
    
  1497.         const text = 'Child ' + state;
    
  1498.         return <Text text={text} />;
    
  1499.       }
    
  1500. 
    
  1501.       function HighPriorityComponent(props) {
    
  1502.         const [state, _stateUpdate] = useState(0);
    
  1503.         updateHighPriorityComponentState = _stateUpdate;
    
  1504.         const text = 'HighPriorityComponent ' + state;
    
  1505.         return (
    
  1506.           <>
    
  1507.             <Text text={text} />
    
  1508.             {props.children}
    
  1509.           </>
    
  1510.         );
    
  1511.       }
    
  1512. 
    
  1513.       function App() {
    
  1514.         offscreenRef = useRef(null);
    
  1515.         return (
    
  1516.           <>
    
  1517.             <HighPriorityComponent>
    
  1518.               <Activity mode={'manual'} ref={offscreenRef}>
    
  1519.                 <Child />
    
  1520.               </Activity>
    
  1521.             </HighPriorityComponent>
    
  1522.           </>
    
  1523.         );
    
  1524.       }
    
  1525. 
    
  1526.       const root = ReactNoop.createRoot();
    
  1527. 
    
  1528.       await act(() => {
    
  1529.         root.render(<App />);
    
  1530.       });
    
  1531. 
    
  1532.       assertLog(['HighPriorityComponent 0', 'Child 0']);
    
  1533.       expect(root).toMatchRenderedOutput(
    
  1534.         <>
    
  1535.           <span prop="HighPriorityComponent 0" />
    
  1536.           <span prop="Child 0" />
    
  1537.         </>,
    
  1538.       );
    
  1539. 
    
  1540.       expect(offscreenRef.current).not.toBeNull();
    
  1541. 
    
  1542.       // Activity is attached by default. State updates from offscreen are **not defered**.
    
  1543.       await act(async () => {
    
  1544.         updateChildState(1);
    
  1545.         updateHighPriorityComponentState(1);
    
  1546.         await waitForPaint(['HighPriorityComponent 1', 'Child 1']);
    
  1547.         expect(root).toMatchRenderedOutput(
    
  1548.           <>
    
  1549.             <span prop="HighPriorityComponent 1" />
    
  1550.             <span prop="Child 1" />
    
  1551.           </>,
    
  1552.         );
    
  1553.       });
    
  1554. 
    
  1555.       await act(() => {
    
  1556.         offscreenRef.current.detach();
    
  1557.       });
    
  1558. 
    
  1559.       // Activity is detached. State updates from offscreen are **defered**.
    
  1560.       await act(async () => {
    
  1561.         updateChildState(2);
    
  1562.         updateHighPriorityComponentState(2);
    
  1563.         await waitForPaint(['HighPriorityComponent 2']);
    
  1564.         expect(root).toMatchRenderedOutput(
    
  1565.           <>
    
  1566.             <span prop="HighPriorityComponent 2" />
    
  1567.             <span prop="Child 1" />
    
  1568.           </>,
    
  1569.         );
    
  1570.       });
    
  1571. 
    
  1572.       assertLog(['Child 2']);
    
  1573.       expect(root).toMatchRenderedOutput(
    
  1574.         <>
    
  1575.           <span prop="HighPriorityComponent 2" />
    
  1576.           <span prop="Child 2" />
    
  1577.         </>,
    
  1578.       );
    
  1579. 
    
  1580.       await act(() => {
    
  1581.         offscreenRef.current.attach();
    
  1582.       });
    
  1583. 
    
  1584.       // Activity is attached. State updates from offscreen are **not defered**.
    
  1585.       await act(async () => {
    
  1586.         updateChildState(3);
    
  1587.         updateHighPriorityComponentState(3);
    
  1588.         await waitForPaint(['HighPriorityComponent 3', 'Child 3']);
    
  1589.         expect(root).toMatchRenderedOutput(
    
  1590.           <>
    
  1591.             <span prop="HighPriorityComponent 3" />
    
  1592.             <span prop="Child 3" />
    
  1593.           </>,
    
  1594.         );
    
  1595.       });
    
  1596.     });
    
  1597. 
    
  1598.     // @gate enableActivity
    
  1599.     it('defers detachment if called during commit', async () => {
    
  1600.       let updateChildState;
    
  1601.       let updateHighPriorityComponentState;
    
  1602.       let offscreenRef;
    
  1603.       let nextRenderTriggerDetach = false;
    
  1604.       let nextRenderTriggerAttach = false;
    
  1605. 
    
  1606.       function Child() {
    
  1607.         const [state, _stateUpdate] = useState(0);
    
  1608.         updateChildState = _stateUpdate;
    
  1609.         const text = 'Child ' + state;
    
  1610.         return <Text text={text} />;
    
  1611.       }
    
  1612. 
    
  1613.       function HighPriorityComponent(props) {
    
  1614.         const [state, _stateUpdate] = useState(0);
    
  1615.         updateHighPriorityComponentState = _stateUpdate;
    
  1616.         const text = 'HighPriorityComponent ' + state;
    
  1617.         useLayoutEffect(() => {
    
  1618.           if (nextRenderTriggerDetach) {
    
  1619.             _stateUpdate(state + 1);
    
  1620.             updateChildState(state + 1);
    
  1621.             offscreenRef.current.detach();
    
  1622.             nextRenderTriggerDetach = false;
    
  1623.           }
    
  1624. 
    
  1625.           if (nextRenderTriggerAttach) {
    
  1626.             offscreenRef.current.attach();
    
  1627.             nextRenderTriggerAttach = false;
    
  1628.           }
    
  1629.         });
    
  1630.         return (
    
  1631.           <>
    
  1632.             <Text text={text} />
    
  1633.             {props.children}
    
  1634.           </>
    
  1635.         );
    
  1636.       }
    
  1637. 
    
  1638.       function App() {
    
  1639.         offscreenRef = useRef(null);
    
  1640.         return (
    
  1641.           <>
    
  1642.             <HighPriorityComponent>
    
  1643.               <Activity mode={'manual'} ref={offscreenRef}>
    
  1644.                 <Child />
    
  1645.               </Activity>
    
  1646.             </HighPriorityComponent>
    
  1647.           </>
    
  1648.         );
    
  1649.       }
    
  1650. 
    
  1651.       const root = ReactNoop.createRoot();
    
  1652. 
    
  1653.       await act(() => {
    
  1654.         root.render(<App />);
    
  1655.       });
    
  1656. 
    
  1657.       assertLog(['HighPriorityComponent 0', 'Child 0']);
    
  1658. 
    
  1659.       nextRenderTriggerDetach = true;
    
  1660. 
    
  1661.       // Activity is attached and gets detached inside useLayoutEffect.
    
  1662.       // State updates from offscreen are **defered**.
    
  1663.       await act(async () => {
    
  1664.         updateChildState(1);
    
  1665.         updateHighPriorityComponentState(1);
    
  1666.         await waitForPaint([
    
  1667.           'HighPriorityComponent 1',
    
  1668.           'Child 1',
    
  1669.           'HighPriorityComponent 2',
    
  1670.         ]);
    
  1671.         expect(root).toMatchRenderedOutput(
    
  1672.           <>
    
  1673.             <span prop="HighPriorityComponent 2" />
    
  1674.             <span prop="Child 1" />
    
  1675.           </>,
    
  1676.         );
    
  1677.       });
    
  1678. 
    
  1679.       assertLog(['Child 2']);
    
  1680.       expect(root).toMatchRenderedOutput(
    
  1681.         <>
    
  1682.           <span prop="HighPriorityComponent 2" />
    
  1683.           <span prop="Child 2" />
    
  1684.         </>,
    
  1685.       );
    
  1686. 
    
  1687.       nextRenderTriggerAttach = true;
    
  1688. 
    
  1689.       // Activity is detached. State updates from offscreen are **defered**.
    
  1690.       // Activity is attached inside useLayoutEffect;
    
  1691.       await act(async () => {
    
  1692.         updateChildState(3);
    
  1693.         updateHighPriorityComponentState(3);
    
  1694.         await waitForPaint(['HighPriorityComponent 3', 'Child 3']);
    
  1695.         expect(root).toMatchRenderedOutput(
    
  1696.           <>
    
  1697.             <span prop="HighPriorityComponent 3" />
    
  1698.             <span prop="Child 3" />
    
  1699.           </>,
    
  1700.         );
    
  1701.       });
    
  1702.     });
    
  1703.   });
    
  1704. 
    
  1705.   // @gate enableActivity
    
  1706.   it('should detach ref if Activity is unmounted', async () => {
    
  1707.     let offscreenRef;
    
  1708. 
    
  1709.     function App({showOffscreen}) {
    
  1710.       offscreenRef = useRef(null);
    
  1711.       return showOffscreen ? (
    
  1712.         <Activity
    
  1713.           mode={'manual'}
    
  1714.           ref={ref => {
    
  1715.             offscreenRef.current = ref;
    
  1716.           }}>
    
  1717.           <div />
    
  1718.         </Activity>
    
  1719.       ) : null;
    
  1720.     }
    
  1721. 
    
  1722.     const root = ReactNoop.createRoot();
    
  1723. 
    
  1724.     await act(() => {
    
  1725.       root.render(<App showOffscreen={true} />);
    
  1726.     });
    
  1727. 
    
  1728.     expect(offscreenRef.current).not.toBeNull();
    
  1729. 
    
  1730.     await act(() => {
    
  1731.       root.render(<App showOffscreen={false} />);
    
  1732.     });
    
  1733. 
    
  1734.     expect(offscreenRef.current).toBeNull();
    
  1735. 
    
  1736.     await act(() => {
    
  1737.       root.render(<App showOffscreen={true} />);
    
  1738.     });
    
  1739. 
    
  1740.     expect(offscreenRef.current).not.toBeNull();
    
  1741.   });
    
  1742. 
    
  1743.   // @gate enableActivity
    
  1744.   it('should detach ref when parent Activity is hidden', async () => {
    
  1745.     let offscreenRef;
    
  1746. 
    
  1747.     function App({mode}) {
    
  1748.       offscreenRef = useRef(null);
    
  1749.       return (
    
  1750.         <Activity mode={mode}>
    
  1751.           <Activity mode={'manual'} ref={offscreenRef}>
    
  1752.             <div />
    
  1753.           </Activity>
    
  1754.         </Activity>
    
  1755.       );
    
  1756.     }
    
  1757. 
    
  1758.     const root = ReactNoop.createRoot();
    
  1759. 
    
  1760.     await act(() => {
    
  1761.       root.render(<App mode={'hidden'} />);
    
  1762.     });
    
  1763. 
    
  1764.     expect(offscreenRef.current).toBeNull();
    
  1765. 
    
  1766.     await act(() => {
    
  1767.       root.render(<App mode={'visible'} />);
    
  1768.     });
    
  1769. 
    
  1770.     expect(offscreenRef.current).not.toBeNull();
    
  1771.     await act(() => {
    
  1772.       root.render(<App mode={'hidden'} />);
    
  1773.     });
    
  1774. 
    
  1775.     expect(offscreenRef.current).toBeNull();
    
  1776.   });
    
  1777. 
    
  1778.   // @gate enableActivity
    
  1779.   it('should change _current', async () => {
    
  1780.     let offscreenRef;
    
  1781.     const root = ReactNoop.createRoot();
    
  1782. 
    
  1783.     function App({children}) {
    
  1784.       offscreenRef = useRef(null);
    
  1785.       return (
    
  1786.         <Activity mode={'manual'} ref={offscreenRef}>
    
  1787.           {children}
    
  1788.         </Activity>
    
  1789.       );
    
  1790.     }
    
  1791. 
    
  1792.     await act(() => {
    
  1793.       root.render(
    
  1794.         <App>
    
  1795.           <div />
    
  1796.         </App>,
    
  1797.       );
    
  1798.     });
    
  1799. 
    
  1800.     expect(offscreenRef.current).not.toBeNull();
    
  1801.     const firstFiber = offscreenRef.current._current;
    
  1802. 
    
  1803.     await act(() => {
    
  1804.       root.render(
    
  1805.         <App>
    
  1806.           <span />
    
  1807.         </App>,
    
  1808.       );
    
  1809.     });
    
  1810. 
    
  1811.     expect(offscreenRef.current._current === firstFiber).toBeFalsy();
    
  1812.   });
    
  1813. 
    
  1814.   // @gate enableActivity
    
  1815.   it('does not mount tree until attach is called', async () => {
    
  1816.     let offscreenRef;
    
  1817.     let spanRef;
    
  1818. 
    
  1819.     function Child() {
    
  1820.       spanRef = useRef(null);
    
  1821.       useEffect(() => {
    
  1822.         Scheduler.log('Mount Child');
    
  1823.         return () => {
    
  1824.           Scheduler.log('Unmount Child');
    
  1825.         };
    
  1826.       });
    
  1827.       useLayoutEffect(() => {
    
  1828.         Scheduler.log('Mount Layout Child');
    
  1829.         return () => {
    
  1830.           Scheduler.log('Unmount Layout Child');
    
  1831.         };
    
  1832.       });
    
  1833. 
    
  1834.       return <span ref={spanRef}>Child</span>;
    
  1835.     }
    
  1836. 
    
  1837.     function App() {
    
  1838.       return (
    
  1839.         <Activity mode={'manual'} ref={el => (offscreenRef = el)}>
    
  1840.           <Child />
    
  1841.         </Activity>
    
  1842.       );
    
  1843.     }
    
  1844. 
    
  1845.     const root = ReactNoop.createRoot();
    
  1846. 
    
  1847.     await act(() => {
    
  1848.       root.render(<App />);
    
  1849.     });
    
  1850. 
    
  1851.     expect(offscreenRef).not.toBeNull();
    
  1852.     expect(spanRef.current).not.toBeNull();
    
  1853.     assertLog(['Mount Layout Child', 'Mount Child']);
    
  1854. 
    
  1855.     await act(() => {
    
  1856.       offscreenRef.detach();
    
  1857.     });
    
  1858. 
    
  1859.     expect(spanRef.current).toBeNull();
    
  1860.     assertLog(['Unmount Layout Child', 'Unmount Child']);
    
  1861. 
    
  1862.     // Calling attach on already attached Activity.
    
  1863.     await act(() => {
    
  1864.       offscreenRef.detach();
    
  1865.     });
    
  1866. 
    
  1867.     assertLog([]);
    
  1868. 
    
  1869.     await act(() => {
    
  1870.       offscreenRef.attach();
    
  1871.     });
    
  1872. 
    
  1873.     expect(spanRef.current).not.toBeNull();
    
  1874.     assertLog(['Mount Layout Child', 'Mount Child']);
    
  1875. 
    
  1876.     // Calling attach on already attached Activity
    
  1877.     offscreenRef.attach();
    
  1878. 
    
  1879.     assertLog([]);
    
  1880.   });
    
  1881. 
    
  1882.   // @gate enableActivity
    
  1883.   it('handles nested manual offscreens', async () => {
    
  1884.     let outerOffscreen;
    
  1885.     let innerOffscreen;
    
  1886. 
    
  1887.     function App() {
    
  1888.       return (
    
  1889.         <LoggedText text={'outer'}>
    
  1890.           <Activity mode={'manual'} ref={el => (outerOffscreen = el)}>
    
  1891.             <LoggedText text={'middle'}>
    
  1892.               <Activity mode={'manual'} ref={el => (innerOffscreen = el)}>
    
  1893.                 <LoggedText text={'inner'} />
    
  1894.               </Activity>
    
  1895.             </LoggedText>
    
  1896.           </Activity>
    
  1897.         </LoggedText>
    
  1898.       );
    
  1899.     }
    
  1900. 
    
  1901.     const root = ReactNoop.createRoot();
    
  1902. 
    
  1903.     await act(() => {
    
  1904.       root.render(<App />);
    
  1905.     });
    
  1906. 
    
  1907.     assertLog([
    
  1908.       'outer',
    
  1909.       'middle',
    
  1910.       'inner',
    
  1911.       'mount layout inner',
    
  1912.       'mount layout middle',
    
  1913.       'mount layout outer',
    
  1914.       'mount inner',
    
  1915.       'mount middle',
    
  1916.       'mount outer',
    
  1917.     ]);
    
  1918. 
    
  1919.     expect(outerOffscreen).not.toBeNull();
    
  1920.     expect(innerOffscreen).not.toBeNull();
    
  1921. 
    
  1922.     await act(() => {
    
  1923.       outerOffscreen.detach();
    
  1924.     });
    
  1925. 
    
  1926.     expect(innerOffscreen).toBeNull();
    
  1927. 
    
  1928.     assertLog([
    
  1929.       'unmount layout middle',
    
  1930.       'unmount layout inner',
    
  1931.       'unmount middle',
    
  1932.       'unmount inner',
    
  1933.     ]);
    
  1934. 
    
  1935.     await act(() => {
    
  1936.       outerOffscreen.attach();
    
  1937.     });
    
  1938. 
    
  1939.     assertLog([
    
  1940.       'mount layout inner',
    
  1941.       'mount layout middle',
    
  1942.       'mount inner',
    
  1943.       'mount middle',
    
  1944.     ]);
    
  1945. 
    
  1946.     await act(() => {
    
  1947.       innerOffscreen.detach();
    
  1948.     });
    
  1949. 
    
  1950.     assertLog(['unmount layout inner', 'unmount inner']);
    
  1951. 
    
  1952.     // Calling detach on already detached Activity.
    
  1953.     await act(() => {
    
  1954.       innerOffscreen.detach();
    
  1955.     });
    
  1956. 
    
  1957.     assertLog([]);
    
  1958. 
    
  1959.     await act(() => {
    
  1960.       innerOffscreen.attach();
    
  1961.     });
    
  1962. 
    
  1963.     assertLog(['mount layout inner', 'mount inner']);
    
  1964. 
    
  1965.     await act(() => {
    
  1966.       innerOffscreen.detach();
    
  1967.       outerOffscreen.attach();
    
  1968.     });
    
  1969. 
    
  1970.     assertLog(['unmount layout inner', 'unmount inner']);
    
  1971.   });
    
  1972. 
    
  1973.   // @gate enableActivity
    
  1974.   it('batches multiple attach and detach calls scheduled from an event handler', async () => {
    
  1975.     function Child() {
    
  1976.       useEffect(() => {
    
  1977.         Scheduler.log('attach child');
    
  1978.         return () => {
    
  1979.           Scheduler.log('detach child');
    
  1980.         };
    
  1981.       }, []);
    
  1982.       return 'child';
    
  1983.     }
    
  1984. 
    
  1985.     const offscreen = React.createRef(null);
    
  1986.     function App() {
    
  1987.       return (
    
  1988.         <Activity ref={offscreen} mode="manual">
    
  1989.           <Child />
    
  1990.         </Activity>
    
  1991.       );
    
  1992.     }
    
  1993. 
    
  1994.     const root = ReactNoop.createRoot();
    
  1995.     await act(() => {
    
  1996.       root.render(<App />);
    
  1997.     });
    
  1998. 
    
  1999.     assertLog(['attach child']);
    
  2000. 
    
  2001.     await act(() => {
    
  2002.       const instance = offscreen.current;
    
  2003.       // Detach then immediately attach the instance.
    
  2004.       instance.detach();
    
  2005.       instance.attach();
    
  2006.     });
    
  2007. 
    
  2008.     assertLog([]);
    
  2009. 
    
  2010.     await act(() => {
    
  2011.       const instance = offscreen.current;
    
  2012.       instance.detach();
    
  2013.     });
    
  2014. 
    
  2015.     assertLog(['detach child']);
    
  2016. 
    
  2017.     await act(() => {
    
  2018.       const instance = offscreen.current;
    
  2019.       // Attach then immediately detach.
    
  2020.       instance.attach();
    
  2021.       instance.detach();
    
  2022.     });
    
  2023. 
    
  2024.     assertLog([]);
    
  2025.   });
    
  2026. 
    
  2027.   // @gate enableActivity
    
  2028.   it('batches multiple attach and detach calls scheduled from an effect', async () => {
    
  2029.     function Child() {
    
  2030.       useEffect(() => {
    
  2031.         Scheduler.log('attach child');
    
  2032.         return () => {
    
  2033.           Scheduler.log('detach child');
    
  2034.         };
    
  2035.       }, []);
    
  2036.       return 'child';
    
  2037.     }
    
  2038. 
    
  2039.     function App() {
    
  2040.       const offscreen = useRef(null);
    
  2041.       useLayoutEffect(() => {
    
  2042.         const instance = offscreen.current;
    
  2043.         // Detach then immediately attach the instance.
    
  2044.         instance.detach();
    
  2045.         instance.attach();
    
  2046.       }, []);
    
  2047.       return (
    
  2048.         <Activity ref={offscreen} mode="manual">
    
  2049.           <Child />
    
  2050.         </Activity>
    
  2051.       );
    
  2052.     }
    
  2053. 
    
  2054.     const root = ReactNoop.createRoot();
    
  2055.     await act(() => {
    
  2056.       root.render(<App />);
    
  2057.     });
    
  2058.     assertLog(['attach child']);
    
  2059.   });
    
  2060. });