/*** Copyright (c) Meta Platforms, Inc. and affiliates.** This source code is licensed under the MIT license found in the* LICENSE file in the root directory of this source tree.** @emails react-core*//* eslint-disable no-for-of-loops/no-for-of-loops */'use strict';
let React;
let ReactDOM;
let ReactFreshRuntime;
let act;
const babel = require('@babel/core');
const freshPlugin = require('react-refresh/babel');
const ts = require('typescript');
describe('ReactFreshIntegration', () => {
let container;
let exportsObj;
beforeEach(() => {
if (__DEV__) {
jest.resetModules();
React = require('react');
ReactFreshRuntime = require('react-refresh/runtime');
ReactFreshRuntime.injectIntoGlobalHook(global);
ReactDOM = require('react-dom');
act = require('react-dom/test-utils').act;
container = document.createElement('div');
document.body.appendChild(container);
exportsObj = undefined;
}});afterEach(() => {
if (__DEV__) {
ReactDOM.unmountComponentAtNode(container);
// Ensure we don't leak memory by holding onto dead roots.
expect(ReactFreshRuntime._getMountedRootCount()).toBe(0);
document.body.removeChild(container);
}});function executeCommon(source, compileDestructuring) {
const compiled = babel.transform(source, {
babelrc: false,
presets: ['@babel/react'],
plugins: [[freshPlugin, {skipEnvCheck: true}],
'@babel/plugin-transform-modules-commonjs',
compileDestructuring && '@babel/plugin-transform-destructuring',
].filter(Boolean),
}).code;return executeCompiled(compiled);
}function executeCompiled(compiled) {
exportsObj = {};
// eslint-disable-next-line no-new-func
new Function(
'global',
'React',
'exports',
'$RefreshReg$',
'$RefreshSig$',
compiled,
)(global, React, exportsObj, $RefreshReg$, $RefreshSig$);
// Module systems will register exports as a fallback.
// This is useful for cases when e.g. a class is exported,
// and we don't want to propagate the update beyond this module.
$RefreshReg$(exportsObj.default, 'exports.default');
return exportsObj.default;
}function $RefreshReg$(type, id) {
ReactFreshRuntime.register(type, id);
}function $RefreshSig$() {
return ReactFreshRuntime.createSignatureFunctionForTransform();
}describe('with compiled destructuring', () => {
runTests(executeCommon, testCommon);
});describe('without compiled destructuring', () => {
runTests(executeCommon, testCommon);
});describe('with typescript syntax', () => {
runTests(function (source) {
const typescriptSource = babel.transform(source, {
babelrc: false,
configFile: false,
presets: ['@babel/react'],
plugins: [[freshPlugin, {skipEnvCheck: true}],
['@babel/plugin-syntax-typescript', {isTSX: true}],
],}).code;const compiled = ts.transpileModule(typescriptSource, {
module: ts.ModuleKind.CommonJS,
}).outputText;return executeCompiled(compiled);
}, testTypescript);
});function runTests(execute, test) {
function render(source) {
const Component = execute(source);
act(() => {
ReactDOM.render(<Component />, container);
});// Module initialization shouldn't be counted as a hot update.
expect(ReactFreshRuntime.performReactRefresh()).toBe(null);
}function patch(source) {
const prevExports = exportsObj;
execute(source);
const nextExports = exportsObj;
// Check if exported families have changed.
// (In a real module system we'd do this for *all* exports.)
// For example, this can happen if you convert a class to a function.
// Or if you wrap something in a HOC.
const didExportsChange =
ReactFreshRuntime.getFamilyByType(prevExports.default) !==
ReactFreshRuntime.getFamilyByType(nextExports.default);
if (didExportsChange) {
// In a real module system, we would propagate such updates upwards,
// and re-execute modules that imported this one. (Just like if we edited them.)
// This makes adding/removing/renaming exports re-render references to them.
// Here, we'll just force a re-render using the newer type to emulate this.
const NextComponent = nextExports.default;
act(() => {
ReactDOM.render(<NextComponent />, container);
});}act(() => {
const result = ReactFreshRuntime.performReactRefresh();
if (!didExportsChange) {
// Normally we expect that some components got updated in our tests.
expect(result).not.toBe(null);
} else {
// However, we have tests where we convert functions to classes,
// and in those cases it's expected nothing would get updated.
// (Instead, the export change branch above would take care of it.)
}});expect(ReactFreshRuntime._getMountedRootCount()).toBe(1);
}test(render, patch);
}function testCommon(render, patch) {
it('reloads function declarations', () => {
if (__DEV__) {
render(`
function Parent() {return <Child prop="A" />;};function Child({prop}) {return <h1>{prop}1</h1>;};export default Parent;`);
const el = container.firstChild;
expect(el.textContent).toBe('A1');
patch(`
function Parent() {return <Child prop="B" />;};function Child({prop}) {return <h1>{prop}2</h1>;};export default Parent;`);
expect(container.firstChild).toBe(el);
expect(el.textContent).toBe('B2');
}});it('reloads arrow functions', () => {
if (__DEV__) {
render(`
const Parent = () => {return <Child prop="A" />;};const Child = ({prop}) => {return <h1>{prop}1</h1>;};export default Parent;`);
const el = container.firstChild;
expect(el.textContent).toBe('A1');
patch(`
const Parent = () => {return <Child prop="B" />;};const Child = ({prop}) => {return <h1>{prop}2</h1>;};export default Parent;`);
expect(container.firstChild).toBe(el);
expect(el.textContent).toBe('B2');
}});it('reloads a combination of memo and forwardRef', () => {
if (__DEV__) {
render(`
const {memo} = React;const Parent = memo(React.forwardRef(function (props, ref) {return <Child prop="A" ref={ref} />;}));const Child = React.memo(({prop}) => {return <h1>{prop}1</h1>;});export default React.memo(Parent);`);
const el = container.firstChild;
expect(el.textContent).toBe('A1');
patch(`
const {memo} = React;const Parent = memo(React.forwardRef(function (props, ref) {return <Child prop="B" ref={ref} />;}));const Child = React.memo(({prop}) => {return <h1>{prop}2</h1>;});export default React.memo(Parent);`);
expect(container.firstChild).toBe(el);
expect(el.textContent).toBe('B2');
}});it('reloads default export with named memo', () => {
if (__DEV__) {
render(`
const {memo} = React;const Child = React.memo(({prop}) => {return <h1>{prop}1</h1>;});export default memo(React.forwardRef(function Parent(props, ref) {return <Child prop="A" ref={ref} />;}));`);
const el = container.firstChild;
expect(el.textContent).toBe('A1');
patch(`
const {memo} = React;const Child = React.memo(({prop}) => {return <h1>{prop}2</h1>;});export default memo(React.forwardRef(function Parent(props, ref) {return <Child prop="B" ref={ref} />;}));`);
expect(container.firstChild).toBe(el);
expect(el.textContent).toBe('B2');
}});it('reloads HOCs if they return functions', () => {
if (__DEV__) {
render(`
function hoc(letter) {return function() {return <h1>{letter}1</h1>;}}export default function Parent() {return <Child />;}const Child = hoc('A');`);
const el = container.firstChild;
expect(el.textContent).toBe('A1');
patch(`
function hoc(letter) {return function() {return <h1>{letter}2</h1>;}}export default function Parent() {return React.createElement(Child);}const Child = hoc('B');`);
expect(container.firstChild).toBe(el);
expect(el.textContent).toBe('B2');
}});it('resets state when renaming a state variable', () => {
if (__DEV__) {
render(`
const {useState} = React;const S = 1;export default function App() {const [foo, setFoo] = useState(S);return <h1>A{foo}</h1>;}`);
const el = container.firstChild;
expect(el.textContent).toBe('A1');
patch(`
const {useState} = React;const S = 2;export default function App() {const [foo, setFoo] = useState(S);return <h1>B{foo}</h1>;}`);
// Same state variable name, so state is preserved.
expect(container.firstChild).toBe(el);
expect(el.textContent).toBe('B1');
patch(`
const {useState} = React;const S = 3;export default function App() {const [bar, setBar] = useState(S);return <h1>C{bar}</h1>;}`);
// Different state variable name, so state is reset.
expect(container.firstChild).not.toBe(el);
const newEl = container.firstChild;
expect(newEl.textContent).toBe('C3');
}});it('resets state when renaming a state variable in a HOC', () => {
if (__DEV__) {
render(`
const {useState} = React;const S = 1;function hoc(Wrapped) {return function Generated() {const [foo, setFoo] = useState(S);return <Wrapped value={foo} />;};}export default hoc(({ value }) => {return <h1>A{value}</h1>;});`);
const el = container.firstChild;
expect(el.textContent).toBe('A1');
patch(`
const {useState} = React;const S = 2;function hoc(Wrapped) {return function Generated() {const [foo, setFoo] = useState(S);return <Wrapped value={foo} />;};}export default hoc(({ value }) => {return <h1>B{value}</h1>;});`);
// Same state variable name, so state is preserved.
expect(container.firstChild).toBe(el);
expect(el.textContent).toBe('B1');
patch(`
const {useState} = React;const S = 3;function hoc(Wrapped) {return function Generated() {const [bar, setBar] = useState(S);return <Wrapped value={bar} />;};}export default hoc(({ value }) => {return <h1>C{value}</h1>;});`);
// Different state variable name, so state is reset.
expect(container.firstChild).not.toBe(el);
const newEl = container.firstChild;
expect(newEl.textContent).toBe('C3');
}});it('resets state when renaming a state variable in a HOC with indirection', () => {
if (__DEV__) {
render(`
const {useState} = React;const S = 1;function hoc(Wrapped) {return function Generated() {const [foo, setFoo] = useState(S);return <Wrapped value={foo} />;};}function Indirection({ value }) {return <h1>A{value}</h1>;}export default hoc(Indirection);`);
const el = container.firstChild;
expect(el.textContent).toBe('A1');
patch(`
const {useState} = React;const S = 2;function hoc(Wrapped) {return function Generated() {const [foo, setFoo] = useState(S);return <Wrapped value={foo} />;};}function Indirection({ value }) {return <h1>B{value}</h1>;}export default hoc(Indirection);`);
// Same state variable name, so state is preserved.
expect(container.firstChild).toBe(el);
expect(el.textContent).toBe('B1');
patch(`
const {useState} = React;const S = 3;function hoc(Wrapped) {return function Generated() {const [bar, setBar] = useState(S);return <Wrapped value={bar} />;};}function Indirection({ value }) {return <h1>C{value}</h1>;}export default hoc(Indirection);`);
// Different state variable name, so state is reset.
expect(container.firstChild).not.toBe(el);
const newEl = container.firstChild;
expect(newEl.textContent).toBe('C3');
}});it('resets state when renaming a state variable inside a HOC with direct call', () => {
if (__DEV__) {
render(`
const {useState} = React;const S = 1;function hocWithDirectCall(Wrapped) {return function Generated() {return Wrapped();};}export default hocWithDirectCall(() => {const [foo, setFoo] = useState(S);return <h1>A{foo}</h1>;});`);
const el = container.firstChild;
expect(el.textContent).toBe('A1');
patch(`
const {useState} = React;const S = 2;function hocWithDirectCall(Wrapped) {return function Generated() {return Wrapped();};}export default hocWithDirectCall(() => {const [foo, setFoo] = useState(S);return <h1>B{foo}</h1>;});`);
// Same state variable name, so state is preserved.
expect(container.firstChild).toBe(el);
expect(el.textContent).toBe('B1');
patch(`
const {useState} = React;const S = 3;function hocWithDirectCall(Wrapped) {return function Generated() {return Wrapped();};}export default hocWithDirectCall(() => {const [bar, setBar] = useState(S);return <h1>C{bar}</h1>;});`);
// Different state variable name, so state is reset.
expect(container.firstChild).not.toBe(el);
const newEl = container.firstChild;
expect(newEl.textContent).toBe('C3');
}});it('does not crash when changing Hook order inside a HOC with direct call', () => {
if (__DEV__) {
render(`
const {useEffect} = React;function hocWithDirectCall(Wrapped) {return function Generated() {return Wrapped();};}export default hocWithDirectCall(() => {useEffect(() => {}, []);return <h1>A</h1>;});`);
const el = container.firstChild;
expect(el.textContent).toBe('A');
patch(`
const {useEffect} = React;function hocWithDirectCall(Wrapped) {return function Generated() {return Wrapped();};}export default hocWithDirectCall(() => {useEffect(() => {}, []);useEffect(() => {}, []);return <h1>B</h1>;});`);
// Hook order changed, so we remount.
expect(container.firstChild).not.toBe(el);
const newEl = container.firstChild;
expect(newEl.textContent).toBe('B');
}});it('does not crash when changing Hook order inside a memo-ed HOC with direct call', () => {
if (__DEV__) {
render(`
const {useEffect, memo} = React;function hocWithDirectCall(Wrapped) {return memo(function Generated() {return Wrapped();});}export default hocWithDirectCall(() => {useEffect(() => {}, []);return <h1>A</h1>;});`);
const el = container.firstChild;
expect(el.textContent).toBe('A');
patch(`
const {useEffect, memo} = React;function hocWithDirectCall(Wrapped) {return memo(function Generated() {return Wrapped();});}export default hocWithDirectCall(() => {useEffect(() => {}, []);useEffect(() => {}, []);return <h1>B</h1>;});`);
// Hook order changed, so we remount.
expect(container.firstChild).not.toBe(el);
const newEl = container.firstChild;
expect(newEl.textContent).toBe('B');
}});it('does not crash when changing Hook order inside a memo+forwardRef-ed HOC with direct call', () => {
if (__DEV__) {
render(`
const {useEffect, memo, forwardRef} = React;function hocWithDirectCall(Wrapped) {return memo(forwardRef(function Generated() {return Wrapped();}));}export default hocWithDirectCall(() => {useEffect(() => {}, []);return <h1>A</h1>;});`);
const el = container.firstChild;
expect(el.textContent).toBe('A');
patch(`
const {useEffect, memo, forwardRef} = React;function hocWithDirectCall(Wrapped) {return memo(forwardRef(function Generated() {return Wrapped();}));}export default hocWithDirectCall(() => {useEffect(() => {}, []);useEffect(() => {}, []);return <h1>B</h1>;});`);
// Hook order changed, so we remount.
expect(container.firstChild).not.toBe(el);
const newEl = container.firstChild;
expect(newEl.textContent).toBe('B');
}});it('does not crash when changing Hook order inside a HOC returning an object', () => {
if (__DEV__) {
render(`
const {useEffect} = React;function hocWithDirectCall(Wrapped) {return {Wrapped: Wrapped};}export default hocWithDirectCall(() => {useEffect(() => {}, []);return <h1>A</h1>;}).Wrapped;`);
const el = container.firstChild;
expect(el.textContent).toBe('A');
patch(`
const {useEffect} = React;function hocWithDirectCall(Wrapped) {return {Wrapped: Wrapped};}export default hocWithDirectCall(() => {useEffect(() => {}, []);useEffect(() => {}, []);return <h1>B</h1>;}).Wrapped;`);
// Hook order changed, so we remount.
expect(container.firstChild).not.toBe(el);
const newEl = container.firstChild;
expect(newEl.textContent).toBe('B');
}});it('resets effects while preserving state', () => {
if (__DEV__) {
render(`
const {useState} = React;export default function App() {const [value, setValue] = useState(0);return <h1>A{value}</h1>;}`);
let el = container.firstChild;
expect(el.textContent).toBe('A0');
// Add an effect.
patch(`
const {useState} = React;export default function App() {const [value, setValue] = useState(0);React.useEffect(() => {const id = setInterval(() => {setValue(v => v + 1);}, 1000);return () => clearInterval(id);}, []);return <h1>B{value}</h1>;}`);
// We added an effect, thereby changing Hook order.
// This causes a remount.
expect(container.firstChild).not.toBe(el);
el = container.firstChild;
expect(el.textContent).toBe('B0');
act(() => {
jest.advanceTimersByTime(1000);
});expect(el.textContent).toBe('B1');
patch(`
const {useState} = React;export default function App() {const [value, setValue] = useState(0);React.useEffect(() => {const id = setInterval(() => {setValue(v => v + 10);}, 1000);return () => clearInterval(id);}, []);return <h1>C{value}</h1>;}`);
// Same Hooks are called, so state is preserved.
expect(container.firstChild).toBe(el);
expect(el.textContent).toBe('C1');
// Effects are always reset, so timer was reinstalled.
// The new version increments by 10 rather than 1.
act(() => {
jest.advanceTimersByTime(1000);
});expect(el.textContent).toBe('C11');
patch(`
const {useState} = React;export default function App() {const [value, setValue] = useState(0);return <h1>D{value}</h1>;}`);
// Removing the effect changes the signature
// and causes the component to remount.
expect(container.firstChild).not.toBe(el);
el = container.firstChild;
expect(el.textContent).toBe('D0');
}});it('does not get confused when custom hooks are reordered', () => {
if (__DEV__) {
render(`
function useFancyState(initialState) {return React.useState(initialState);}const App = () => {const [x, setX] = useFancyState('X');const [y, setY] = useFancyState('Y');return <h1>A{x}{y}</h1>;};export default App;`);
let el = container.firstChild;
expect(el.textContent).toBe('AXY');
patch(`
function useFancyState(initialState) {return React.useState(initialState);}const App = () => {const [x, setX] = useFancyState('X');const [y, setY] = useFancyState('Y');return <h1>B{x}{y}</h1>;};export default App;`);
// Same state variables, so no remount.
expect(container.firstChild).toBe(el);
expect(el.textContent).toBe('BXY');
patch(`
function useFancyState(initialState) {return React.useState(initialState);}const App = () => {const [y, setY] = useFancyState('Y');const [x, setX] = useFancyState('X');return <h1>B{x}{y}</h1>;};export default App;`);
// Hooks were re-ordered. This causes a remount.
// Therefore, Hook calls don't accidentally share state.
expect(container.firstChild).not.toBe(el);
el = container.firstChild;
expect(el.textContent).toBe('BXY');
}});it('does not get confused when component is called early', () => {
if (__DEV__) {
render(`
// This isn't really a valid pattern but it's close enough// to simulate what happens when you call ReactDOM.render// in the same file. We want to ensure this doesn't confuse// the runtime.App();function App() {const [x, setX] = useFancyState('X');const [y, setY] = useFancyState('Y');return <h1>A{x}{y}</h1>;};function useFancyState(initialState) {// No real Hook calls to avoid triggering invalid call invariant.// We only want to verify that we can still call this function early.return initialState;}export default App;`);
let el = container.firstChild;
expect(el.textContent).toBe('AXY');
patch(`
// This isn't really a valid pattern but it's close enough// to simulate what happens when you call ReactDOM.render// in the same file. We want to ensure this doesn't confuse// the runtime.App();function App() {const [x, setX] = useFancyState('X');const [y, setY] = useFancyState('Y');return <h1>B{x}{y}</h1>;};function useFancyState(initialState) {// No real Hook calls to avoid triggering invalid call invariant.// We only want to verify that we can still call this function early.return initialState;}export default App;`);
// Same state variables, so no remount.
expect(container.firstChild).toBe(el);
expect(el.textContent).toBe('BXY');
patch(`
// This isn't really a valid pattern but it's close enough// to simulate what happens when you call ReactDOM.render// in the same file. We want to ensure this doesn't confuse// the runtime.App();function App() {const [y, setY] = useFancyState('Y');const [x, setX] = useFancyState('X');return <h1>B{x}{y}</h1>;};function useFancyState(initialState) {// No real Hook calls to avoid triggering invalid call invariant.// We only want to verify that we can still call this function early.return initialState;}export default App;`);
// Hooks were re-ordered. This causes a remount.
// Therefore, Hook calls don't accidentally share state.
expect(container.firstChild).not.toBe(el);
el = container.firstChild;
expect(el.textContent).toBe('BXY');
}});it('does not get confused by Hooks defined inline', () => {
// This is not a recommended pattern but at least it shouldn't break.
if (__DEV__) {
render(`
const App = () => {const useFancyState = (initialState) => {const result = React.useState(initialState);return result;};const [x, setX] = useFancyState('X1');const [y, setY] = useFancyState('Y1');return <h1>A{x}{y}</h1>;};export default App;`);
let el = container.firstChild;
expect(el.textContent).toBe('AX1Y1');
patch(`
const App = () => {const useFancyState = (initialState) => {const result = React.useState(initialState);return result;};const [x, setX] = useFancyState('X2');const [y, setY] = useFancyState('Y2');return <h1>B{x}{y}</h1>;};export default App;`);
// Remount even though nothing changed because
// the custom Hook is inside -- and so we don't
// really know whether its signature has changed.
// We could potentially make it work, but for now
// let's assert we don't crash with confusing errors.
expect(container.firstChild).not.toBe(el);
el = container.firstChild;
expect(el.textContent).toBe('BX2Y2');
}});it('remounts component if custom hook it uses changes order', () => {
if (__DEV__) {
render(`
const App = () => {const [x, setX] = useFancyState('X');const [y, setY] = useFancyState('Y');return <h1>A{x}{y}</h1>;};const useFancyState = (initialState) => {const result = useIndirection(initialState);return result;};function useIndirection(initialState) {return React.useState(initialState);}export default App;`);
let el = container.firstChild;
expect(el.textContent).toBe('AXY');
patch(`
const App = () => {const [x, setX] = useFancyState('X');const [y, setY] = useFancyState('Y');return <h1>B{x}{y}</h1>;};const useFancyState = (initialState) => {const result = useIndirection();return result;};function useIndirection(initialState) {return React.useState(initialState);}export default App;`);
// We didn't change anything except the header text.
// So we don't expect a remount.
expect(container.firstChild).toBe(el);
expect(el.textContent).toBe('BXY');
patch(`
const App = () => {const [x, setX] = useFancyState('X');const [y, setY] = useFancyState('Y');return <h1>C{x}{y}</h1>;};const useFancyState = (initialState) => {const result = useIndirection(initialState);return result;};function useIndirection(initialState) {React.useEffect(() => {});return React.useState(initialState);}export default App;`);
// The useIndirection Hook added an affect,
// so we had to remount the component.
expect(container.firstChild).not.toBe(el);
el = container.firstChild;
expect(el.textContent).toBe('CXY');
patch(`
const App = () => {const [x, setX] = useFancyState('X');const [y, setY] = useFancyState('Y');return <h1>D{x}{y}</h1>;};const useFancyState = (initialState) => {const result = useIndirection();return result;};function useIndirection(initialState) {React.useEffect(() => {});return React.useState(initialState);}export default App;`);
// We didn't change anything except the header text.
// So we don't expect a remount.
expect(container.firstChild).toBe(el);
expect(el.textContent).toBe('DXY');
}});it('does not lose the inferred arrow names', () => {
if (__DEV__) {
render(`
const Parent = () => {return <Child/>;};const Child = () => {useMyThing();return <h1>{Parent.name} {Child.name} {useMyThing.name}</h1>;};const useMyThing = () => {React.useState();};export default Parent;`);
expect(container.textContent).toBe('Parent Child useMyThing');
}});it('does not lose the inferred function names', () => {
if (__DEV__) {
render(`
var Parent = function() {return <Child/>;};var Child = function() {useMyThing();return <h1>{Parent.name} {Child.name} {useMyThing.name}</h1>;};var useMyThing = function() {React.useState();};export default Parent;`);
expect(container.textContent).toBe('Parent Child useMyThing');
}});it('resets state on every edit with @refresh reset annotation', () => {
if (__DEV__) {
render(`
const {useState} = React;const S = 1;export default function App() {const [foo, setFoo] = useState(S);return <h1>A{foo}</h1>;}`);
let el = container.firstChild;
expect(el.textContent).toBe('A1');
patch(`
const {useState} = React;const S = 2;export default function App() {const [foo, setFoo] = useState(S);return <h1>B{foo}</h1>;}`);
// Same state variable name, so state is preserved.
expect(container.firstChild).toBe(el);
expect(el.textContent).toBe('B1');
patch(`
const {useState} = React;const S = 3;/* @refresh reset */export default function App() {const [foo, setFoo] = useState(S);return <h1>C{foo}</h1>;}`);
// Found remount annotation, so state is reset.
expect(container.firstChild).not.toBe(el);
el = container.firstChild;
expect(el.textContent).toBe('C3');
patch(`
const {useState} = React;const S = 4;export default function App() {// @refresh resetconst [foo, setFoo] = useState(S);return <h1>D{foo}</h1>;}`);
// Found remount annotation, so state is reset.
expect(container.firstChild).not.toBe(el);
el = container.firstChild;
expect(el.textContent).toBe('D4');
patch(`
const {useState} = React;const S = 5;export default function App() {const [foo, setFoo] = useState(S);return <h1>E{foo}</h1>;}`);
// There is no remount annotation anymore,
// so preserve the previous state.
expect(container.firstChild).toBe(el);
expect(el.textContent).toBe('E4');
patch(`
const {useState} = React;const S = 6;export default function App() {const [foo, setFoo] = useState(S);return <h1>F{foo}</h1>;}`);
// Continue editing.
expect(container.firstChild).toBe(el);
expect(el.textContent).toBe('F4');
patch(`
const {useState} = React;const S = 7;export default function App() {/* @refresh reset */const [foo, setFoo] = useState(S);return <h1>G{foo}</h1>;}`);
// Force remount one last time.
expect(container.firstChild).not.toBe(el);
el = container.firstChild;
expect(el.textContent).toBe('G7');
}});// This is best effort for simple cases.
// We won't attempt to resolve identifiers.
it('resets state when useState initial state is edited', () => {
if (__DEV__) {
render(`
const {useState} = React;export default function App() {const [foo, setFoo] = useState(1);return <h1>A{foo}</h1>;}`);
let el = container.firstChild;
expect(el.textContent).toBe('A1');
patch(`
const {useState} = React;export default function App() {const [foo, setFoo] = useState(1);return <h1>B{foo}</h1>;}`);
// Same initial state, so it's preserved.
expect(container.firstChild).toBe(el);
expect(el.textContent).toBe('B1');
patch(`
const {useState} = React;export default function App() {const [foo, setFoo] = useState(2);return <h1>C{foo}</h1>;}`);
// Different initial state, so state is reset.
expect(container.firstChild).not.toBe(el);
el = container.firstChild;
expect(el.textContent).toBe('C2');
}});// This is best effort for simple cases.
// We won't attempt to resolve identifiers.
it('resets state when useReducer initial state is edited', () => {
if (__DEV__) {
render(`
const {useReducer} = React;export default function App() {const [foo, setFoo] = useReducer(x => x, 1);return <h1>A{foo}</h1>;}`);
let el = container.firstChild;
expect(el.textContent).toBe('A1');
patch(`
const {useReducer} = React;export default function App() {const [foo, setFoo] = useReducer(x => x, 1);return <h1>B{foo}</h1>;}`);
// Same initial state, so it's preserved.
expect(container.firstChild).toBe(el);
expect(el.textContent).toBe('B1');
patch(`
const {useReducer} = React;export default function App() {const [foo, setFoo] = useReducer(x => x, 2);return <h1>C{foo}</h1>;}`);
// Different initial state, so state is reset.
expect(container.firstChild).not.toBe(el);
el = container.firstChild;
expect(el.textContent).toBe('C2');
}});it('remounts when switching export from function to class', () => {
if (__DEV__) {
render(`
export default function App() {return <h1>A1</h1>;}`);
let el = container.firstChild;
expect(el.textContent).toBe('A1');
patch(`
export default function App() {return <h1>A2</h1>;}`);
// Keep state.
expect(container.firstChild).toBe(el);
expect(el.textContent).toBe('A2');
patch(`
export default class App extends React.Component {render() {return <h1>B1</h1>}}`);
// Reset (function -> class).
expect(container.firstChild).not.toBe(el);
el = container.firstChild;
expect(el.textContent).toBe('B1');
patch(`
export default class App extends React.Component {render() {return <h1>B2</h1>}}`);
// Reset (classes always do).
expect(container.firstChild).not.toBe(el);
el = container.firstChild;
expect(el.textContent).toBe('B2');
patch(`
export default function App() {return <h1>C1</h1>;}`);
// Reset (class -> function).
expect(container.firstChild).not.toBe(el);
el = container.firstChild;
expect(el.textContent).toBe('C1');
patch(`
export default function App() {return <h1>C2</h1>;}`);
expect(container.firstChild).toBe(el);
expect(el.textContent).toBe('C2');
patch(`
export default function App() {return <h1>D1</h1>;}`);
el = container.firstChild;
expect(el.textContent).toBe('D1');
patch(`
export default function App() {return <h1>D2</h1>;}`);
// Keep state.
expect(container.firstChild).toBe(el);
expect(el.textContent).toBe('D2');
}});it('remounts when switching export from class to function', () => {
if (__DEV__) {
render(`
export default class App extends React.Component {render() {return <h1>A1</h1>}}`);
let el = container.firstChild;
expect(el.textContent).toBe('A1');
patch(`
export default class App extends React.Component {render() {return <h1>A2</h1>}}`);
// Reset (classes always do).
expect(container.firstChild).not.toBe(el);
el = container.firstChild;
expect(el.textContent).toBe('A2');
patch(`
export default function App() {return <h1>B1</h1>;}`);
// Reset (class -> function).
expect(container.firstChild).not.toBe(el);
el = container.firstChild;
expect(el.textContent).toBe('B1');
patch(`
export default function App() {return <h1>B2</h1>;}`);
// Keep state.
expect(container.firstChild).toBe(el);
expect(el.textContent).toBe('B2');
patch(`
export default class App extends React.Component {render() {return <h1>C1</h1>}}`);
// Reset (function -> class).
expect(container.firstChild).not.toBe(el);
el = container.firstChild;
expect(el.textContent).toBe('C1');
}});it('remounts when wrapping export in a HOC', () => {
if (__DEV__) {
render(`
export default function App() {return <h1>A1</h1>;}`);
let el = container.firstChild;
expect(el.textContent).toBe('A1');
patch(`
export default function App() {return <h1>A2</h1>;}`);
// Keep state.
expect(container.firstChild).toBe(el);
expect(el.textContent).toBe('A2');
patch(`
function hoc(Inner) {return function Wrapper() {return <Inner />;}}function App() {return <h1>B1</h1>;}export default hoc(App);`);
// Reset (wrapped in HOC).
expect(container.firstChild).not.toBe(el);
el = container.firstChild;
expect(el.textContent).toBe('B1');
patch(`
function hoc(Inner) {return function Wrapper() {return <Inner />;}}function App() {return <h1>B2</h1>;}export default hoc(App);`);
// Keep state.
expect(container.firstChild).toBe(el);
expect(el.textContent).toBe('B2');
patch(`
export default function App() {return <h1>C1</h1>;}`);
// Reset (unwrapped).
expect(container.firstChild).not.toBe(el);
el = container.firstChild;
expect(el.textContent).toBe('C1');
patch(`
export default function App() {return <h1>C2</h1>;}`);
expect(container.firstChild).toBe(el);
expect(el.textContent).toBe('C2');
}});it('remounts when wrapping export in memo()', () => {
if (__DEV__) {
render(`
export default function App() {return <h1>A1</h1>;}`);
let el = container.firstChild;
expect(el.textContent).toBe('A1');
patch(`
export default function App() {return <h1>A2</h1>;}`);
// Keep state.
expect(container.firstChild).toBe(el);
expect(el.textContent).toBe('A2');
patch(`
function App() {return <h1>B1</h1>;}export default React.memo(App);`);
// Reset (wrapped in HOC).
expect(container.firstChild).not.toBe(el);
el = container.firstChild;
expect(el.textContent).toBe('B1');
patch(`
function App() {return <h1>B2</h1>;}export default React.memo(App);`);
// Keep state.
expect(container.firstChild).toBe(el);
expect(el.textContent).toBe('B2');
patch(`
export default function App() {return <h1>C1</h1>;}`);
// Reset (unwrapped).
expect(container.firstChild).not.toBe(el);
el = container.firstChild;
expect(el.textContent).toBe('C1');
patch(`
export default function App() {return <h1>C2</h1>;}`);
expect(container.firstChild).toBe(el);
expect(el.textContent).toBe('C2');
}});it('remounts when wrapping export in forwardRef()', () => {
if (__DEV__) {
render(`
export default function App() {return <h1>A1</h1>;}`);
let el = container.firstChild;
expect(el.textContent).toBe('A1');
patch(`
export default function App() {return <h1>A2</h1>;}`);
// Keep state.
expect(container.firstChild).toBe(el);
expect(el.textContent).toBe('A2');
patch(`
function App() {return <h1>B1</h1>;}export default React.forwardRef(App);`);
// Reset (wrapped in HOC).
expect(container.firstChild).not.toBe(el);
el = container.firstChild;
expect(el.textContent).toBe('B1');
patch(`
function App() {return <h1>B2</h1>;}export default React.forwardRef(App);`);
// Keep state.
expect(container.firstChild).toBe(el);
expect(el.textContent).toBe('B2');
patch(`
export default function App() {return <h1>C1</h1>;}`);
// Reset (unwrapped).
expect(container.firstChild).not.toBe(el);
el = container.firstChild;
expect(el.textContent).toBe('C1');
patch(`
export default function App() {return <h1>C2</h1>;}`);
expect(container.firstChild).toBe(el);
expect(el.textContent).toBe('C2');
}});if (!require('shared/ReactFeatureFlags').disableModulePatternComponents) {
it('remounts deprecated factory components', () => {
if (__DEV__) {
expect(() => {
render(`
function Parent() {return {render() {return <Child prop="A" />;}};};function Child({prop}) {return <h1>{prop}1</h1>;};export default Parent;`);
}).toErrorDev(
'The <Parent /> component appears to be a function component ' +
'that returns a class instance.',
);const el = container.firstChild;
expect(el.textContent).toBe('A1');
patch(`
function Parent() {return {render() {return <Child prop="B" />;}};};function Child({prop}) {return <h1>{prop}2</h1>;};export default Parent;`);
// Like classes, factory components always remount.
expect(container.firstChild).not.toBe(el);
const newEl = container.firstChild;
expect(newEl.textContent).toBe('B2');
}});}describe('with inline requires', () => {
beforeEach(() => {
global.FakeModuleSystem = {};});afterEach(() => {
delete global.FakeModuleSystem;});it('remounts component if custom hook it uses changes order on first edit', () => {
// This test verifies that remounting works even if calls to custom Hooks
// were transformed with an inline requires transform, like we have on RN.
// Inline requires make it harder to compare previous and next signatures
// because useFancyState inline require always resolves to the newest version.
// We're not actually using inline requires in the test, but it has similar semantics.
if (__DEV__) {
render(`
const FakeModuleSystem = global.FakeModuleSystem;FakeModuleSystem.useFancyState = function(initialState) {return React.useState(initialState);};const App = () => {const [x, setX] = FakeModuleSystem.useFancyState('X');const [y, setY] = FakeModuleSystem.useFancyState('Y');return <h1>A{x}{y}</h1>;};export default App;`);
let el = container.firstChild;
expect(el.textContent).toBe('AXY');
patch(`
const FakeModuleSystem = global.FakeModuleSystem;FakeModuleSystem.useFancyState = function(initialState) {React.useEffect(() => {});return React.useState(initialState);};const App = () => {const [x, setX] = FakeModuleSystem.useFancyState('X');const [y, setY] = FakeModuleSystem.useFancyState('Y');return <h1>B{x}{y}</h1>;};export default App;`);
// The useFancyState Hook added an effect,
// so we had to remount the component.
expect(container.firstChild).not.toBe(el);
el = container.firstChild;
expect(el.textContent).toBe('BXY');
patch(`
const FakeModuleSystem = global.FakeModuleSystem;FakeModuleSystem.useFancyState = function(initialState) {React.useEffect(() => {});return React.useState(initialState);};const App = () => {const [x, setX] = FakeModuleSystem.useFancyState('X');const [y, setY] = FakeModuleSystem.useFancyState('Y');return <h1>C{x}{y}</h1>;};export default App;`);
// We didn't change anything except the header text.
// So we don't expect a remount.
expect(container.firstChild).toBe(el);
expect(el.textContent).toBe('CXY');
}});it('remounts component if custom hook it uses changes order on second edit', () => {
if (__DEV__) {
render(`
const FakeModuleSystem = global.FakeModuleSystem;FakeModuleSystem.useFancyState = function(initialState) {return React.useState(initialState);};const App = () => {const [x, setX] = FakeModuleSystem.useFancyState('X');const [y, setY] = FakeModuleSystem.useFancyState('Y');return <h1>A{x}{y}</h1>;};export default App;`);
let el = container.firstChild;
expect(el.textContent).toBe('AXY');
patch(`
const FakeModuleSystem = global.FakeModuleSystem;FakeModuleSystem.useFancyState = function(initialState) {return React.useState(initialState);};const App = () => {const [x, setX] = FakeModuleSystem.useFancyState('X');const [y, setY] = FakeModuleSystem.useFancyState('Y');return <h1>B{x}{y}</h1>;};export default App;`);
expect(container.firstChild).toBe(el);
expect(el.textContent).toBe('BXY');
patch(`
const FakeModuleSystem = global.FakeModuleSystem;FakeModuleSystem.useFancyState = function(initialState) {React.useEffect(() => {});return React.useState(initialState);};const App = () => {const [x, setX] = FakeModuleSystem.useFancyState('X');const [y, setY] = FakeModuleSystem.useFancyState('Y');return <h1>C{x}{y}</h1>;};export default App;`);
// The useFancyState Hook added an effect,
// so we had to remount the component.
expect(container.firstChild).not.toBe(el);
el = container.firstChild;
expect(el.textContent).toBe('CXY');
patch(`
const FakeModuleSystem = global.FakeModuleSystem;FakeModuleSystem.useFancyState = function(initialState) {React.useEffect(() => {});return React.useState(initialState);};const App = () => {const [x, setX] = FakeModuleSystem.useFancyState('X');const [y, setY] = FakeModuleSystem.useFancyState('Y');return <h1>D{x}{y}</h1>;};export default App;`);
// We didn't change anything except the header text.
// So we don't expect a remount.
expect(container.firstChild).toBe(el);
expect(el.textContent).toBe('DXY');
}});it('recovers if evaluating Hook list throws', () => {
if (__DEV__) {
render(`
let FakeModuleSystem = null;global.FakeModuleSystem.useFancyState = function(initialState) {return React.useState(initialState);};const App = () => {FakeModuleSystem = global.FakeModuleSystem;const [x, setX] = FakeModuleSystem.useFancyState('X');const [y, setY] = FakeModuleSystem.useFancyState('Y');return <h1>A{x}{y}</h1>;};export default App;`);
let el = container.firstChild;
expect(el.textContent).toBe('AXY');
patch(`
let FakeModuleSystem = null;global.FakeModuleSystem.useFancyState = function(initialState) {React.useEffect(() => {});return React.useState(initialState);};const App = () => {FakeModuleSystem = global.FakeModuleSystem;const [x, setX] = FakeModuleSystem.useFancyState('X');const [y, setY] = FakeModuleSystem.useFancyState('Y');return <h1>B{x}{y}</h1>;};export default App;`);
// We couldn't evaluate the Hook signatures
// so we had to remount the component.
expect(container.firstChild).not.toBe(el);
el = container.firstChild;
expect(el.textContent).toBe('BXY');
}});it('remounts component if custom hook it uses changes order behind an indirection', () => {
if (__DEV__) {
render(`
const FakeModuleSystem = global.FakeModuleSystem;FakeModuleSystem.useFancyState = function(initialState) {return FakeModuleSystem.useIndirection(initialState);};FakeModuleSystem.useIndirection = function(initialState) {return FakeModuleSystem.useOtherIndirection(initialState);};FakeModuleSystem.useOtherIndirection = function(initialState) {return React.useState(initialState);};const App = () => {const [x, setX] = FakeModuleSystem.useFancyState('X');const [y, setY] = FakeModuleSystem.useFancyState('Y');return <h1>A{x}{y}</h1>;};export default App;`);
let el = container.firstChild;
expect(el.textContent).toBe('AXY');
patch(`
const FakeModuleSystem = global.FakeModuleSystem;FakeModuleSystem.useFancyState = function(initialState) {return FakeModuleSystem.useIndirection(initialState);};FakeModuleSystem.useIndirection = function(initialState) {return FakeModuleSystem.useOtherIndirection(initialState);};FakeModuleSystem.useOtherIndirection = function(initialState) {React.useEffect(() => {});return React.useState(initialState);};const App = () => {const [x, setX] = FakeModuleSystem.useFancyState('X');const [y, setY] = FakeModuleSystem.useFancyState('Y');return <h1>B{x}{y}</h1>;};export default App;`);
// The useFancyState Hook added an effect,
// so we had to remount the component.
expect(container.firstChild).not.toBe(el);
el = container.firstChild;
expect(el.textContent).toBe('BXY');
patch(`
const FakeModuleSystem = global.FakeModuleSystem;FakeModuleSystem.useFancyState = function(initialState) {return FakeModuleSystem.useIndirection(initialState);};FakeModuleSystem.useIndirection = function(initialState) {return FakeModuleSystem.useOtherIndirection(initialState);};FakeModuleSystem.useOtherIndirection = function(initialState) {React.useEffect(() => {});return React.useState(initialState);};const App = () => {const [x, setX] = FakeModuleSystem.useFancyState('X');const [y, setY] = FakeModuleSystem.useFancyState('Y');return <h1>C{x}{y}</h1>;};export default App;`);
// We didn't change anything except the header text.
// So we don't expect a remount.
expect(container.firstChild).toBe(el);
expect(el.textContent).toBe('CXY');
}});});}function testTypescript(render, patch) {
it('reloads component exported in typescript namespace', () => {
if (__DEV__) {
render(`
namespace Foo {export namespace Bar {export const Child = ({prop}) => {return <h1>{prop}1</h1>};}}export default function Parent() {return <Foo.Bar.Child prop={'A'} />;}`);
const el = container.firstChild;
expect(el.textContent).toBe('A1');
patch(`
namespace Foo {export namespace Bar {export const Child = ({prop}) => {return <h1>{prop}2</h1>};}}export default function Parent() {return <Foo.Bar.Child prop={'B'} />;}`);
expect(container.firstChild).toBe(el);
expect(el.textContent).toBe('B2');
}});}});