1. /**
    
  2.  * Copyright (c) Meta Platforms, Inc. and affiliates.
    
  3.  *
    
  4.  * This source code is licensed under the MIT license found in the
    
  5.  * LICENSE file in the root directory of this source tree.
    
  6.  *
    
  7.  * @flow
    
  8.  */
    
  9. 
    
  10. describe('Fast Refresh', () => {
    
  11.   let React;
    
  12.   let ReactFreshRuntime;
    
  13.   let act;
    
  14.   let babel;
    
  15.   let container;
    
  16.   let exportsObj;
    
  17.   let freshPlugin;
    
  18.   let legacyRender;
    
  19.   let store;
    
  20.   let withErrorsOrWarningsIgnored;
    
  21. 
    
  22.   afterEach(() => {
    
  23.     jest.resetModules();
    
  24.   });
    
  25. 
    
  26.   beforeEach(() => {
    
  27.     exportsObj = undefined;
    
  28.     container = document.createElement('div');
    
  29. 
    
  30.     babel = require('@babel/core');
    
  31.     freshPlugin = require('react-refresh/babel');
    
  32. 
    
  33.     store = global.store;
    
  34. 
    
  35.     React = require('react');
    
  36. 
    
  37.     ReactFreshRuntime = require('react-refresh/runtime');
    
  38.     ReactFreshRuntime.injectIntoGlobalHook(global);
    
  39. 
    
  40.     const utils = require('./utils');
    
  41.     act = utils.act;
    
  42.     legacyRender = utils.legacyRender;
    
  43.     withErrorsOrWarningsIgnored = utils.withErrorsOrWarningsIgnored;
    
  44.   });
    
  45. 
    
  46.   function execute(source) {
    
  47.     const compiled = babel.transform(source, {
    
  48.       babelrc: false,
    
  49.       presets: ['@babel/react'],
    
  50.       plugins: [
    
  51.         [freshPlugin, {skipEnvCheck: true}],
    
  52.         '@babel/plugin-transform-modules-commonjs',
    
  53.         '@babel/plugin-transform-destructuring',
    
  54.       ].filter(Boolean),
    
  55.     }).code;
    
  56.     exportsObj = {};
    
  57.     // eslint-disable-next-line no-new-func
    
  58.     new Function(
    
  59.       'global',
    
  60.       'React',
    
  61.       'exports',
    
  62.       '$RefreshReg$',
    
  63.       '$RefreshSig$',
    
  64.       compiled,
    
  65.     )(global, React, exportsObj, $RefreshReg$, $RefreshSig$);
    
  66.     // Module systems will register exports as a fallback.
    
  67.     // This is useful for cases when e.g. a class is exported,
    
  68.     // and we don't want to propagate the update beyond this module.
    
  69.     $RefreshReg$(exportsObj.default, 'exports.default');
    
  70.     return exportsObj.default;
    
  71.   }
    
  72. 
    
  73.   function render(source) {
    
  74.     const Component = execute(source);
    
  75.     act(() => {
    
  76.       legacyRender(<Component />, container);
    
  77.     });
    
  78.     // Module initialization shouldn't be counted as a hot update.
    
  79.     expect(ReactFreshRuntime.performReactRefresh()).toBe(null);
    
  80.   }
    
  81. 
    
  82.   function patch(source) {
    
  83.     const prevExports = exportsObj;
    
  84.     execute(source);
    
  85.     const nextExports = exportsObj;
    
  86. 
    
  87.     // Check if exported families have changed.
    
  88.     // (In a real module system we'd do this for *all* exports.)
    
  89.     // For example, this can happen if you convert a class to a function.
    
  90.     // Or if you wrap something in a HOC.
    
  91.     const didExportsChange =
    
  92.       ReactFreshRuntime.getFamilyByType(prevExports.default) !==
    
  93.       ReactFreshRuntime.getFamilyByType(nextExports.default);
    
  94.     if (didExportsChange) {
    
  95.       // In a real module system, we would propagate such updates upwards,
    
  96.       // and re-execute modules that imported this one. (Just like if we edited them.)
    
  97.       // This makes adding/removing/renaming exports re-render references to them.
    
  98.       // Here, we'll just force a re-render using the newer type to emulate this.
    
  99.       const NextComponent = nextExports.default;
    
  100.       act(() => {
    
  101.         legacyRender(<NextComponent />, container);
    
  102.       });
    
  103.     }
    
  104.     act(() => {
    
  105.       const result = ReactFreshRuntime.performReactRefresh();
    
  106.       if (!didExportsChange) {
    
  107.         // Normally we expect that some components got updated in our tests.
    
  108.         expect(result).not.toBe(null);
    
  109.       } else {
    
  110.         // However, we have tests where we convert functions to classes,
    
  111.         // and in those cases it's expected nothing would get updated.
    
  112.         // (Instead, the export change branch above would take care of it.)
    
  113.       }
    
  114.     });
    
  115.     expect(ReactFreshRuntime._getMountedRootCount()).toBe(1);
    
  116.   }
    
  117. 
    
  118.   function $RefreshReg$(type, id) {
    
  119.     ReactFreshRuntime.register(type, id);
    
  120.   }
    
  121. 
    
  122.   function $RefreshSig$() {
    
  123.     return ReactFreshRuntime.createSignatureFunctionForTransform();
    
  124.   }
    
  125. 
    
  126.   // @reactVersion >= 16.9
    
  127.   it('should not break the DevTools store', () => {
    
  128.     render(`
    
  129.       function Parent() {
    
  130.         return <Child key="A" />;
    
  131.       };
    
  132. 
    
  133.       function Child() {
    
  134.         return <div />;
    
  135.       };
    
  136. 
    
  137.       export default Parent;
    
  138.     `);
    
  139.     expect(store).toMatchInlineSnapshot(`
    
  140.       [root]
    
  141.         ▾ <Parent>
    
  142.             <Child key="A">
    
  143.     `);
    
  144. 
    
  145.     let element = container.firstChild;
    
  146.     expect(container.firstChild).not.toBe(null);
    
  147. 
    
  148.     patch(`
    
  149.       function Parent() {
    
  150.         return <Child key="A" />;
    
  151.       };
    
  152. 
    
  153.       function Child() {
    
  154.         return <div />;
    
  155.       };
    
  156. 
    
  157.       export default Parent;
    
  158.     `);
    
  159.     expect(store).toMatchInlineSnapshot(`
    
  160.       [root]
    
  161.         ▾ <Parent>
    
  162.             <Child key="A">
    
  163.     `);
    
  164. 
    
  165.     // State is preserved; this verifies that Fast Refresh is wired up.
    
  166.     expect(container.firstChild).toBe(element);
    
  167.     element = container.firstChild;
    
  168. 
    
  169.     patch(`
    
  170.       function Parent() {
    
  171.         return <Child key="B" />;
    
  172.       };
    
  173. 
    
  174.       function Child() {
    
  175.         return <div />;
    
  176.       };
    
  177. 
    
  178.       export default Parent;
    
  179.     `);
    
  180.     expect(store).toMatchInlineSnapshot(`
    
  181.       [root]
    
  182.         ▾ <Parent>
    
  183.             <Child key="B">
    
  184.     `);
    
  185. 
    
  186.     // State is reset because hooks changed.
    
  187.     expect(container.firstChild).not.toBe(element);
    
  188.   });
    
  189. 
    
  190.   // @reactVersion >= 16.9
    
  191.   it('should not break when there are warnings in between patching', () => {
    
  192.     withErrorsOrWarningsIgnored(['Expected:'], () => {
    
  193.       render(`
    
  194.       const {useState} = React;
    
  195. 
    
  196.       export default function Component() {
    
  197.         const [state, setState] = useState(1);
    
  198.         console.warn("Expected: warning during render");
    
  199.         return null;
    
  200.       }
    
  201.     `);
    
  202.     });
    
  203.     expect(store).toMatchInlineSnapshot(`
    
  204.       ✕ 0, ⚠ 1
    
  205.       [root]
    
  206.           <Component> ⚠
    
  207.     `);
    
  208. 
    
  209.     withErrorsOrWarningsIgnored(['Expected:'], () => {
    
  210.       patch(`
    
  211.       const {useEffect, useState} = React;
    
  212. 
    
  213.       export default function Component() {
    
  214.         const [state, setState] = useState(1);
    
  215.         console.warn("Expected: warning during render");
    
  216.         return null;
    
  217.       }
    
  218.     `);
    
  219.     });
    
  220.     expect(store).toMatchInlineSnapshot(`
    
  221.       ✕ 0, ⚠ 2
    
  222.       [root]
    
  223.           <Component> ⚠
    
  224.     `);
    
  225. 
    
  226.     withErrorsOrWarningsIgnored(['Expected:'], () => {
    
  227.       patch(`
    
  228.       const {useEffect, useState} = React;
    
  229. 
    
  230.       export default function Component() {
    
  231.         const [state, setState] = useState(1);
    
  232.         useEffect(() => {
    
  233.           console.error("Expected: error during effect");
    
  234.         });
    
  235.         console.warn("Expected: warning during render");
    
  236.         return null;
    
  237.       }
    
  238.     `);
    
  239.     });
    
  240.     expect(store).toMatchInlineSnapshot(`
    
  241.       ✕ 1, ⚠ 1
    
  242.       [root]
    
  243.           <Component> ✕⚠
    
  244.     `);
    
  245. 
    
  246.     withErrorsOrWarningsIgnored(['Expected:'], () => {
    
  247.       patch(`
    
  248.       const {useEffect, useState} = React;
    
  249. 
    
  250.       export default function Component() {
    
  251.         const [state, setState] = useState(1);
    
  252.         console.warn("Expected: warning during render");
    
  253.         return null;
    
  254.       }
    
  255.     `);
    
  256.     });
    
  257.     expect(store).toMatchInlineSnapshot(`
    
  258.       ✕ 0, ⚠ 1
    
  259.       [root]
    
  260.           <Component> ⚠
    
  261.     `);
    
  262.   });
    
  263. 
    
  264.   // TODO (bvaughn) Write a test that checks in between the steps of patch
    
  265. });