/**
* Copyright (c) Facebook, Inc. and its 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 Scheduler;
let act;
let createReactClass;
describe('ReactFresh', () => {
let container;
beforeEach(() => {
if (__DEV__) {
jest.resetModules();
React = require('react');
ReactFreshRuntime = require('react-refresh/runtime');
ReactFreshRuntime.injectIntoGlobalHook(global);
ReactDOM = require('react-dom');
Scheduler = require('scheduler');
act = require('react-dom/test-utils').act;
createReactClass = require('create-react-class/factory')(
React.Component,
React.isValidElement,
new React.Component().updater,
);
container = document.createElement('div');
document.body.appendChild(container);
}
});
afterEach(() => {
if (__DEV__) {
delete global.__REACT_DEVTOOLS_GLOBAL_HOOK__;
document.body.removeChild(container);
}
});
function prepare(version) {
const Component = version();
return Component;
}
function render(version, props) {
const Component = version();
act(() => {
ReactDOM.render(
setVal(val + 1)}> {val}
); } $RefreshReg$(Hello, 'Hello'); return Hello; }); // Bump the state before patching. const el = container.firstChild; expect(el.textContent).toBe('0'); expect(el.style.color).toBe('blue'); act(() => { el.dispatchEvent(new MouseEvent('click', {bubbles: true})); }); expect(el.textContent).toBe('1'); // Perform a hot update. const HelloV2 = patch(() => { function Hello() { const [val, setVal] = React.useState(0); return (setVal(val + 1)}> {val}
); } $RefreshReg$(Hello, 'Hello'); return Hello; }); // Assert the state was preserved but color changed. expect(container.firstChild).toBe(el); expect(el.textContent).toBe('1'); expect(el.style.color).toBe('red'); // Bump the state again. act(() => { el.dispatchEvent(new MouseEvent('click', {bubbles: true})); }); expect(container.firstChild).toBe(el); expect(el.textContent).toBe('2'); expect(el.style.color).toBe('red'); // Perform top-down renders with both fresh and stale types. // Neither should change the state or color. // They should always resolve to the latest version. render(() => HelloV1); render(() => HelloV2); render(() => HelloV1); expect(container.firstChild).toBe(el); expect(el.textContent).toBe('2'); expect(el.style.color).toBe('red'); // Bump the state again. act(() => { el.dispatchEvent(new MouseEvent('click', {bubbles: true})); }); expect(container.firstChild).toBe(el); expect(el.textContent).toBe('3'); expect(el.style.color).toBe('red'); // Finally, a render with incompatible type should reset it. render(() => { function Hello() { const [val, setVal] = React.useState(0); return (setVal(val + 1)}> {val}
); } // No register call. // This is considered a new type. return Hello; }); expect(container.firstChild).not.toBe(el); const newEl = container.firstChild; expect(newEl.textContent).toBe('0'); expect(newEl.style.color).toBe('blue'); } }); it('can preserve state for forwardRef', () => { if (__DEV__) { const OuterV1 = render(() => { function Hello() { const [val, setVal] = React.useState(0); return (setVal(val + 1)}> {val}
); } $RefreshReg$(Hello, 'Hello'); const Outer = React.forwardRef(() =>setVal(val + 1)}> {val}
); } $RefreshReg$(Hello, 'Hello'); const Outer = React.forwardRef(() =>setVal(val + 1)}> {val}
); } $RefreshReg$(Hello, 'Hello'); // Note: no forwardRef wrapper this time. return Hello; }); expect(container.firstChild).not.toBe(el); const newEl = container.firstChild; expect(newEl.textContent).toBe('0'); expect(newEl.style.color).toBe('blue'); } }); it('should not consider two forwardRefs around the same type to be equivalent', () => { if (__DEV__) { const ParentV1 = render( () => { function Hello() { const [val, setVal] = React.useState(0); return (setVal(val + 1)}> {val}
); } $RefreshReg$(Hello, 'Hello'); function renderInner() { returnsetVal(val + 1)}> {val}
); } $RefreshReg$(Hello, 'Hello'); function renderInner() { returnsetVal(val + 1)}> {val}
); } $RefreshReg$(Hello, 'Hello'); const Outer = React.forwardRef(() =>setVal(val + 1)}> {val}
); } $RefreshReg$(Hello, 'Hello'); const Outer = React.forwardRef(() =>setVal(val + 1)}> {val}
); } $RefreshReg$(Hello, 'Hello'); function renderHello() { returnsetVal(val + 1)}> {val}
); } $RefreshReg$(Hello, 'Hello'); function renderHello() { returnsetVal(val + 1)}> {val}
); } $RefreshReg$(Hello, 'Hello'); const Outer = React.memo(Hello); $RefreshReg$(Outer, 'Outer'); return Outer; }); // Bump the state before patching. const el = container.firstChild; expect(el.textContent).toBe('0'); expect(el.style.color).toBe('blue'); act(() => { el.dispatchEvent(new MouseEvent('click', {bubbles: true})); }); expect(el.textContent).toBe('1'); // Perform a hot update. const OuterV2 = patch(() => { function Hello() { const [val, setVal] = React.useState(0); return (setVal(val + 1)}> {val}
); } $RefreshReg$(Hello, 'Hello'); const Outer = React.memo(Hello); $RefreshReg$(Outer, 'Outer'); return Outer; }); // Assert the state was preserved but color changed. expect(container.firstChild).toBe(el); expect(el.textContent).toBe('1'); expect(el.style.color).toBe('red'); // Bump the state again. act(() => { el.dispatchEvent(new MouseEvent('click', {bubbles: true})); }); expect(container.firstChild).toBe(el); expect(el.textContent).toBe('2'); expect(el.style.color).toBe('red'); // Perform top-down renders with both fresh and stale types. // Neither should change the state or color. // They should always resolve to the latest version. render(() => OuterV1); render(() => OuterV2); render(() => OuterV1); expect(container.firstChild).toBe(el); expect(el.textContent).toBe('2'); expect(el.style.color).toBe('red'); // Finally, a render with incompatible type should reset it. render(() => { function Hello() { const [val, setVal] = React.useState(0); return (setVal(val + 1)}> {val}
); } $RefreshReg$(Hello, 'Hello'); // Note: no wrapper this time. return Hello; }); expect(container.firstChild).not.toBe(el); const newEl = container.firstChild; expect(newEl.textContent).toBe('0'); expect(newEl.style.color).toBe('blue'); } }); it('can preserve state for memo with custom comparison', () => { if (__DEV__) { const OuterV1 = render(() => { function Hello() { const [val, setVal] = React.useState(0); return (setVal(val + 1)}> {val}
); } const Outer = React.memo(Hello, () => true); $RefreshReg$(Outer, 'Outer'); return Outer; }); // Bump the state before patching. const el = container.firstChild; expect(el.textContent).toBe('0'); expect(el.style.color).toBe('blue'); act(() => { el.dispatchEvent(new MouseEvent('click', {bubbles: true})); }); expect(el.textContent).toBe('1'); // Perform a hot update. const OuterV2 = patch(() => { function Hello() { const [val, setVal] = React.useState(0); return (setVal(val + 1)}> {val}
); } const Outer = React.memo(Hello, () => true); $RefreshReg$(Outer, 'Outer'); return Outer; }); // Assert the state was preserved but color changed. expect(container.firstChild).toBe(el); expect(el.textContent).toBe('1'); expect(el.style.color).toBe('red'); // Bump the state again. act(() => { el.dispatchEvent(new MouseEvent('click', {bubbles: true})); }); expect(container.firstChild).toBe(el); expect(el.textContent).toBe('2'); expect(el.style.color).toBe('red'); // Perform top-down renders with both fresh and stale types. // Neither should change the state or color. // They should always resolve to the latest version. render(() => OuterV1); render(() => OuterV2); render(() => OuterV1); expect(container.firstChild).toBe(el); expect(el.textContent).toBe('2'); expect(el.style.color).toBe('red'); // Finally, a render with incompatible type should reset it. render(() => { function Hello() { const [val, setVal] = React.useState(0); return (setVal(val + 1)}> {val}
); } $RefreshReg$(Hello, 'Hello'); // Note: no wrapper this time. return Hello; }); expect(container.firstChild).not.toBe(el); const newEl = container.firstChild; expect(newEl.textContent).toBe('0'); expect(newEl.style.color).toBe('blue'); } }); it('can update simple memo function in isolation', () => { if (__DEV__) { render(() => { function Hello() { const [val, setVal] = React.useState(0); return (setVal(val + 1)}> {val}
); } $RefreshReg$(Hello, 'Hello'); return React.memo(Hello); }); // Bump the state before patching. const el = container.firstChild; expect(el.textContent).toBe('0'); expect(el.style.color).toBe('blue'); act(() => { el.dispatchEvent(new MouseEvent('click', {bubbles: true})); }); expect(el.textContent).toBe('1'); // Perform a hot update of just the rendering function. patch(() => { function Hello() { const [val, setVal] = React.useState(0); return (setVal(val + 1)}> {val}
); } $RefreshReg$(Hello, 'Hello'); // Not updating the wrapper. }); // Assert the state was preserved but color changed. expect(container.firstChild).toBe(el); expect(el.textContent).toBe('1'); expect(el.style.color).toBe('red'); } }); it('can preserve state for memo(forwardRef)', () => { if (__DEV__) { const OuterV1 = render(() => { function Hello() { const [val, setVal] = React.useState(0); return (setVal(val + 1)}> {val}
); } $RefreshReg$(Hello, 'Hello'); const Outer = React.memo(React.forwardRef(() =>setVal(val + 1)}> {val}
); } $RefreshReg$(Hello, 'Hello'); const Outer = React.memo(React.forwardRef(() =>setVal(val + 1)}> {val}
); } $RefreshReg$(Hello, 'Hello'); // Note: no wrapper this time. return Hello; }); expect(container.firstChild).not.toBe(el); const newEl = container.firstChild; expect(newEl.textContent).toBe('0'); expect(newEl.style.color).toBe('blue'); } }); it('can preserve state for lazy after resolution', async () => { if (__DEV__) { const AppV1 = render(() => { function Hello() { const [val, setVal] = React.useState(0); return (setVal(val + 1)}> {val}
); } $RefreshReg$(Hello, 'Hello'); const Outer = React.lazy( () => new Promise(resolve => { setTimeout(() => resolve({default: Hello}), 100); }), ); $RefreshReg$(Outer, 'Outer'); function App() { return (setVal(val + 1)}> {val}
); } $RefreshReg$(Hello, 'Hello'); const Outer = React.lazy( () => new Promise(resolve => { setTimeout(() => resolve({default: Hello}), 100); }), ); $RefreshReg$(Outer, 'Outer'); function App() { return (setVal(val + 1)}> {val}
); } $RefreshReg$(Hello, 'Hello'); // Note: no lazy wrapper this time. function App() { return (setVal(val + 1)}> {val}
); } $RefreshReg$(Hello, 'Hello'); const Outer = React.lazy( () => new Promise(resolve => { setTimeout(() => resolve({default: Hello}), 100); }), ); $RefreshReg$(Outer, 'Outer'); function App() { return (setVal(val + 1)}> {val}
); } $RefreshReg$(Hello, 'Hello'); }); await act(async () => { jest.runAllTimers(); }); // Expect different color on initial mount. const el = container.firstChild; expect(el.textContent).toBe('0'); expect(el.style.color).toBe('red'); // Bump state. act(() => { el.dispatchEvent(new MouseEvent('click', {bubbles: true})); }); expect(container.firstChild).toBe(el); expect(el.textContent).toBe('1'); expect(el.style.color).toBe('red'); // Test another reload. patch(() => { function Hello() { const [val, setVal] = React.useState(0); return (setVal(val + 1)}> {val}
); } $RefreshReg$(Hello, 'Hello'); }); expect(container.firstChild).toBe(el); expect(el.textContent).toBe('1'); expect(el.style.color).toBe('orange'); } }); it('can patch lazy(forwardRef) before resolution', async () => { if (__DEV__) { render(() => { function renderHello() { const [val, setVal] = React.useState(0); return (setVal(val + 1)}> {val}
); } const Hello = React.forwardRef(renderHello); $RefreshReg$(Hello, 'Hello'); const Outer = React.lazy( () => new Promise(resolve => { setTimeout(() => resolve({default: Hello}), 100); }), ); $RefreshReg$(Outer, 'Outer'); function App() { return (setVal(val + 1)}> {val}
); } const Hello = React.forwardRef(renderHello); $RefreshReg$(Hello, 'Hello'); }); await act(async () => { jest.runAllTimers(); }); // Expect different color on initial mount. const el = container.firstChild; expect(el.textContent).toBe('0'); expect(el.style.color).toBe('red'); // Bump state. act(() => { el.dispatchEvent(new MouseEvent('click', {bubbles: true})); }); expect(container.firstChild).toBe(el); expect(el.textContent).toBe('1'); expect(el.style.color).toBe('red'); // Test another reload. patch(() => { function renderHello() { const [val, setVal] = React.useState(0); return (setVal(val + 1)}> {val}
); } const Hello = React.forwardRef(renderHello); $RefreshReg$(Hello, 'Hello'); }); expect(container.firstChild).toBe(el); expect(el.textContent).toBe('1'); expect(el.style.color).toBe('orange'); } }); it('can patch lazy(memo) before resolution', async () => { if (__DEV__) { render(() => { function renderHello() { const [val, setVal] = React.useState(0); return (setVal(val + 1)}> {val}
); } const Hello = React.memo(renderHello); $RefreshReg$(Hello, 'Hello'); const Outer = React.lazy( () => new Promise(resolve => { setTimeout(() => resolve({default: Hello}), 100); }), ); $RefreshReg$(Outer, 'Outer'); function App() { return (setVal(val + 1)}> {val}
); } const Hello = React.memo(renderHello); $RefreshReg$(Hello, 'Hello'); }); await act(async () => { jest.runAllTimers(); }); // Expect different color on initial mount. const el = container.firstChild; expect(el.textContent).toBe('0'); expect(el.style.color).toBe('red'); // Bump state. act(() => { el.dispatchEvent(new MouseEvent('click', {bubbles: true})); }); expect(container.firstChild).toBe(el); expect(el.textContent).toBe('1'); expect(el.style.color).toBe('red'); // Test another reload. patch(() => { function renderHello() { const [val, setVal] = React.useState(0); return (setVal(val + 1)}> {val}
); } const Hello = React.memo(renderHello); $RefreshReg$(Hello, 'Hello'); }); expect(container.firstChild).toBe(el); expect(el.textContent).toBe('1'); expect(el.style.color).toBe('orange'); } }); it('can patch lazy(memo(forwardRef)) before resolution', async () => { if (__DEV__) { render(() => { function renderHello() { const [val, setVal] = React.useState(0); return (setVal(val + 1)}> {val}
); } const Hello = React.memo(React.forwardRef(renderHello)); $RefreshReg$(Hello, 'Hello'); const Outer = React.lazy( () => new Promise(resolve => { setTimeout(() => resolve({default: Hello}), 100); }), ); $RefreshReg$(Outer, 'Outer'); function App() { return (setVal(val + 1)}> {val}
); } const Hello = React.memo(React.forwardRef(renderHello)); $RefreshReg$(Hello, 'Hello'); }); await act(async () => { jest.runAllTimers(); }); // Expect different color on initial mount. const el = container.firstChild; expect(el.textContent).toBe('0'); expect(el.style.color).toBe('red'); // Bump state. act(() => { el.dispatchEvent(new MouseEvent('click', {bubbles: true})); }); expect(container.firstChild).toBe(el); expect(el.textContent).toBe('1'); expect(el.style.color).toBe('red'); // Test another reload. patch(() => { function renderHello() { const [val, setVal] = React.useState(0); return (setVal(val + 1)}> {val}
); } const Hello = React.memo(React.forwardRef(renderHello)); $RefreshReg$(Hello, 'Hello'); }); expect(container.firstChild).toBe(el); expect(el.textContent).toBe('1'); expect(el.style.color).toBe('orange'); } }); it('can patch both trees while suspense is displaying the fallback', async () => { if (__DEV__) { const AppV1 = render( () => { function Hello({children}) { const [val, setVal] = React.useState(0); return (setVal(val + 1)}> {children} {val}
); } $RefreshReg$(Hello, 'Hello'); function Never() { throw new Promise(resolve => {}); } function App({shouldSuspend}) { return (setVal(val + 1)}> {children} {val}
); } $RefreshReg$(Hello, 'Hello'); }); expect(container.childNodes.length).toBe(1); expect(container.childNodes[0]).toBe(primaryChild); expect(primaryChild.textContent).toBe('Content 1'); expect(primaryChild.style.color).toBe('green'); expect(primaryChild.style.display).toBe(''); // Now force the tree to suspend. render(() => AppV1, {shouldSuspend: true}); // Expect to see two trees, one of them is hidden. expect(container.childNodes.length).toBe(2); expect(container.childNodes[0]).toBe(primaryChild); const fallbackChild = container.childNodes[1]; expect(primaryChild.textContent).toBe('Content 1'); expect(primaryChild.style.color).toBe('green'); expect(primaryChild.style.display).toBe('none'); expect(fallbackChild.textContent).toBe('Fallback 0'); expect(fallbackChild.style.color).toBe('green'); expect(fallbackChild.style.display).toBe(''); // Bump fallback state. act(() => { fallbackChild.dispatchEvent(new MouseEvent('click', {bubbles: true})); }); expect(container.childNodes.length).toBe(2); expect(container.childNodes[0]).toBe(primaryChild); expect(container.childNodes[1]).toBe(fallbackChild); expect(primaryChild.textContent).toBe('Content 1'); expect(primaryChild.style.color).toBe('green'); expect(primaryChild.style.display).toBe('none'); expect(fallbackChild.textContent).toBe('Fallback 1'); expect(fallbackChild.style.color).toBe('green'); expect(fallbackChild.style.display).toBe(''); // Perform a hot update. patch(() => { function Hello({children}) { const [val, setVal] = React.useState(0); return (setVal(val + 1)}> {children} {val}
); } $RefreshReg$(Hello, 'Hello'); }); // Colors inside both trees should change: expect(container.childNodes.length).toBe(2); expect(container.childNodes[0]).toBe(primaryChild); expect(container.childNodes[1]).toBe(fallbackChild); expect(primaryChild.textContent).toBe('Content 1'); expect(primaryChild.style.color).toBe('red'); expect(primaryChild.style.display).toBe('none'); expect(fallbackChild.textContent).toBe('Fallback 1'); expect(fallbackChild.style.color).toBe('red'); expect(fallbackChild.style.display).toBe(''); // Only primary tree should exist now: render(() => AppV1, {shouldSuspend: false}); expect(container.childNodes.length).toBe(1); expect(container.childNodes[0]).toBe(primaryChild); expect(primaryChild.textContent).toBe('Content 1'); expect(primaryChild.style.color).toBe('red'); expect(primaryChild.style.display).toBe(''); // Perform a hot update. patch(() => { function Hello({children}) { const [val, setVal] = React.useState(0); return (setVal(val + 1)}> {children} {val}
); } $RefreshReg$(Hello, 'Hello'); }); expect(container.childNodes.length).toBe(1); expect(container.childNodes[0]).toBe(primaryChild); expect(primaryChild.textContent).toBe('Content 1'); expect(primaryChild.style.color).toBe('orange'); expect(primaryChild.style.display).toBe(''); } }); it('does not re-render ancestor components unnecessarily during a hot update', () => { if (__DEV__) { let appRenders = 0; render(() => { function Hello() { const [val, setVal] = React.useState(0); return (setVal(val + 1)}> {val}
); } $RefreshReg$(Hello, 'Hello'); function App() { appRenders++; returnsetVal(val + 1)}> {val}
); } $RefreshReg$(Hello, 'Hello'); }); // Assert the state was preserved but color changed. expect(container.firstChild).toBe(el); expect(el.textContent).toBe('1'); expect(el.style.color).toBe('red'); // Still no re-renders from the top. expect(appRenders).toBe(1); // Bump the state. act(() => { el.dispatchEvent(new MouseEvent('click', {bubbles: true})); }); expect(el.textContent).toBe('2'); // Still no re-renders from the top. expect(appRenders).toBe(1); } }); it('batches re-renders during a hot update', () => { if (__DEV__) { let helloRenders = 0; render(() => { function Hello({children}) { helloRenders++; returnsetVal(val + 1)}> {val}
); } $RefreshReg$(Hello1, 'Hello1'); function Hello2() { const [val, setVal] = React.useState(0); return (setVal(val + 1)}> {val}
); } $RefreshReg$(Hello2, 'Hello2'); function App({cond}) { return cond ?setVal(val + 1)}> {val}
); } $RefreshReg$(Hello1, 'Hello1'); function Hello2() { const [val, setVal] = React.useState(0); return (setVal(val + 1)}> {val}
); } $RefreshReg$(Hello2, 'Hello2'); }); // Assert the state was preserved but color changed. expect(container.firstChild).toBe(el2); expect(el2.textContent).toBe('1'); expect(el2.style.color).toBe('red'); // Flip the condition again. render(() => AppV1, {cond: false}); const el3 = container.firstChild; expect(el3).not.toBe(el2); expect(el3.textContent).toBe('0'); expect(el3.style.color).toBe('red'); } }); it('can force remount by changing signature', () => { if (__DEV__) { const HelloV1 = render(() => { function Hello() { const [val, setVal] = React.useState(0); return (setVal(val + 1)}> {val}
); } $RefreshReg$(Hello, 'Hello'); // When this changes, we'll expect a remount: $RefreshSig$(Hello, '1'); return Hello; }); // Bump the state before patching. const el = container.firstChild; expect(el.textContent).toBe('0'); expect(el.style.color).toBe('blue'); act(() => { el.dispatchEvent(new MouseEvent('click', {bubbles: true})); }); expect(el.textContent).toBe('1'); // Perform a hot update. const HelloV2 = patch(() => { function Hello() { const [val, setVal] = React.useState(0); return (setVal(val + 1)}> {val}
); } $RefreshReg$(Hello, 'Hello'); // The signature hasn't changed since the last time: $RefreshSig$(Hello, '1'); return Hello; }); // Assert the state was preserved but color changed. expect(container.firstChild).toBe(el); expect(el.textContent).toBe('1'); expect(el.style.color).toBe('red'); // Perform a hot update. const HelloV3 = patch(() => { function Hello() { const [val, setVal] = React.useState(0); return (setVal(val + 1)}> {val}
); } // We're changing the signature now so it will remount: $RefreshReg$(Hello, 'Hello'); $RefreshSig$(Hello, '2'); return Hello; }); // Expect a remount. expect(container.firstChild).not.toBe(el); const newEl = container.firstChild; expect(newEl.textContent).toBe('0'); expect(newEl.style.color).toBe('yellow'); // Bump state again. act(() => { newEl.dispatchEvent(new MouseEvent('click', {bubbles: true})); }); expect(newEl.textContent).toBe('1'); expect(newEl.style.color).toBe('yellow'); // Perform top-down renders with both fresh and stale types. // Neither should change the state or color. // They should always resolve to the latest version. render(() => HelloV1); render(() => HelloV2); render(() => HelloV3); render(() => HelloV2); render(() => HelloV1); expect(container.firstChild).toBe(newEl); expect(newEl.textContent).toBe('1'); expect(newEl.style.color).toBe('yellow'); // Verify we can patch again while preserving the signature. patch(() => { function Hello() { const [val, setVal] = React.useState(0); return (setVal(val + 1)}> {val}
); } // Same signature as last time. $RefreshReg$(Hello, 'Hello'); $RefreshSig$(Hello, '2'); return Hello; }); expect(container.firstChild).toBe(newEl); expect(newEl.textContent).toBe('1'); expect(newEl.style.color).toBe('purple'); // Check removing the signature also causes a remount. patch(() => { function Hello() { const [val, setVal] = React.useState(0); return (setVal(val + 1)}> {val}
); } // No signature this time. $RefreshReg$(Hello, 'Hello'); return Hello; }); // Expect a remount. expect(container.firstChild).not.toBe(newEl); const finalEl = container.firstChild; expect(finalEl.textContent).toBe('0'); expect(finalEl.style.color).toBe('orange'); } }); it('keeps a valid tree when forcing remount', () => { if (__DEV__) { const HelloV1 = prepare(() => { function Hello() { return null; } $RefreshReg$(Hello, 'Hello'); $RefreshSig$(Hello, '1'); return Hello; }); const Bailout = React.memo(({children}) => { return children; }); // Each of those renders three instances of HelloV1, // but in different ways. const trees = [setVal(val + 1)}> {val}
); } $RefreshReg$(Hello, 'Hello'); // When this changes, we'll expect a remount: $RefreshSig$(Hello, '1'); // Use the passed wrapper. // This will be different in every test. return wrap(Hello); }); // Bump the state before patching. const el = container.firstChild; expect(el.textContent).toBe('0'); expect(el.style.color).toBe('blue'); act(() => { el.dispatchEvent(new MouseEvent('click', {bubbles: true})); }); expect(el.textContent).toBe('1'); // Perform a hot update that doesn't remount. patch(() => { function Hello() { const [val, setVal] = React.useState(0); return (setVal(val + 1)}> {val}
); } $RefreshReg$(Hello, 'Hello'); // The signature hasn't changed since the last time: $RefreshSig$(Hello, '1'); return Hello; }); // Assert the state was preserved but color changed. expect(container.firstChild).toBe(el); expect(el.textContent).toBe('1'); expect(el.style.color).toBe('red'); // Perform a hot update that remounts. patch(() => { function Hello() { const [val, setVal] = React.useState(0); return (setVal(val + 1)}> {val}
); } // We're changing the signature now so it will remount: $RefreshReg$(Hello, 'Hello'); $RefreshSig$(Hello, '2'); return Hello; }); // Expect a remount. expect(container.firstChild).not.toBe(el); const newEl = container.firstChild; expect(newEl.textContent).toBe('0'); expect(newEl.style.color).toBe('yellow'); // Bump state again. act(() => { newEl.dispatchEvent(new MouseEvent('click', {bubbles: true})); }); expect(newEl.textContent).toBe('1'); expect(newEl.style.color).toBe('yellow'); // Verify we can patch again while preserving the signature. patch(() => { function Hello() { const [val, setVal] = React.useState(0); return (setVal(val + 1)}> {val}
); } // Same signature as last time. $RefreshReg$(Hello, 'Hello'); $RefreshSig$(Hello, '2'); return Hello; }); expect(container.firstChild).toBe(newEl); expect(newEl.textContent).toBe('1'); expect(newEl.style.color).toBe('purple'); // Check removing the signature also causes a remount. patch(() => { function Hello() { const [val, setVal] = React.useState(0); return (setVal(val + 1)}> {val}
); } // No signature this time. $RefreshReg$(Hello, 'Hello'); return Hello; }); // Expect a remount. expect(container.firstChild).not.toBe(newEl); const finalEl = container.firstChild; expect(finalEl.textContent).toBe('0'); expect(finalEl.style.color).toBe('orange'); } it('resets hooks with dependencies on hot reload', () => { if (__DEV__) { let useEffectWithEmptyArrayCalls = 0; render(() => { function Hello() { const [val, setVal] = React.useState(0); const tranformed = React.useMemo(() => val * 2, [val]); const handleClick = React.useCallback(() => setVal(v => v + 1), []); React.useEffect(() => { useEffectWithEmptyArrayCalls++; }, []); return ({tranformed}
); } $RefreshReg$(Hello, 'Hello'); return Hello; }); // Bump the state before patching. const el = container.firstChild; expect(el.textContent).toBe('0'); expect(el.style.color).toBe('blue'); expect(useEffectWithEmptyArrayCalls).toBe(1); // useEffect ran act(() => { el.dispatchEvent(new MouseEvent('click', {bubbles: true})); }); expect(el.textContent).toBe('2'); // val * 2 expect(useEffectWithEmptyArrayCalls).toBe(1); // useEffect didn't re-run // Perform a hot update. act(() => { patch(() => { function Hello() { const [val, setVal] = React.useState(0); const tranformed = React.useMemo(() => val * 10, [val]); const handleClick = React.useCallback(() => setVal(v => v - 1), []); React.useEffect(() => { useEffectWithEmptyArrayCalls++; }, []); return ({tranformed}
); } $RefreshReg$(Hello, 'Hello'); return Hello; }); }); // Assert the state was preserved but memo was evicted. expect(container.firstChild).toBe(el); expect(el.textContent).toBe('10'); // val * 10 expect(el.style.color).toBe('red'); expect(useEffectWithEmptyArrayCalls).toBe(2); // useEffect re-ran // This should fire the new callback which decreases the counter. act(() => { el.dispatchEvent(new MouseEvent('click', {bubbles: true})); }); expect(el.textContent).toBe('0'); expect(el.style.color).toBe('red'); expect(useEffectWithEmptyArrayCalls).toBe(2); // useEffect didn't re-run } }); // This pattern is inspired by useSubscription and similar mechanisms. it('does not get into infinite loops during render phase updates', () => { if (__DEV__) { render(() => { function Hello() { const source = React.useMemo(() => ({value: 10}), []); const [state, setState] = React.useState({value: null}); if (state !== source) { setState(source); } return{state.value}
; } $RefreshReg$(Hello, 'Hello'); return Hello; }); const el = container.firstChild; expect(el.textContent).toBe('10'); expect(el.style.color).toBe('blue'); // Perform a hot update. act(() => { patch(() => { function Hello() { const source = React.useMemo(() => ({value: 20}), []); const [state, setState] = React.useState({value: null}); if (state !== source) { // This should perform a single render-phase update. setState(source); } return{state.value}
; } $RefreshReg$(Hello, 'Hello'); return Hello; }); }); expect(container.firstChild).toBe(el); expect(el.textContent).toBe('20'); expect(el.style.color).toBe('red'); } }); it('can hot reload offscreen components', () => { if (__DEV__ && __EXPERIMENTAL__) { const AppV1 = prepare(() => { function Hello() { React.useLayoutEffect(() => { Scheduler.unstable_yieldValue('Hello#layout'); }); const [val, setVal] = React.useState(0); return (setVal(val + 1)}> {val}
); } $RefreshReg$(Hello, 'Hello'); return function App({offscreen}) { React.useLayoutEffect(() => { Scheduler.unstable_yieldValue('App#layout'); }); return (setVal(val + 1)}> {val}
); } $RefreshReg$(Hello, 'Hello'); }); // It's still offscreen so we don't see anything. expect(container.firstChild).toBe(el); expect(el.hidden).toBe(true); expect(el.firstChild).toBe(null); // Process the offscreen updates. expect(Scheduler).toFlushAndYieldThrough(['Hello#layout']); expect(container.firstChild).toBe(el); expect(el.firstChild.textContent).toBe('0'); expect(el.firstChild.style.color).toBe('red'); el.firstChild.dispatchEvent(new MouseEvent('click', {bubbles: true})); expect(el.firstChild.textContent).toBe('0'); expect(el.firstChild.style.color).toBe('red'); expect(Scheduler).toFlushAndYieldThrough(['Hello#layout']); expect(el.firstChild.textContent).toBe('1'); expect(el.firstChild.style.color).toBe('red'); // Hot reload while we're offscreen. patch(() => { function Hello() { React.useLayoutEffect(() => { Scheduler.unstable_yieldValue('Hello#layout'); }); const [val, setVal] = React.useState(0); return (setVal(val + 1)}> {val}
); } $RefreshReg$(Hello, 'Hello'); }); // It's still offscreen so we don't see the updates. expect(container.firstChild).toBe(el); expect(el.firstChild.textContent).toBe('1'); expect(el.firstChild.style.color).toBe('red'); // Process the offscreen updates. expect(Scheduler).toFlushAndYieldThrough(['Hello#layout']); expect(container.firstChild).toBe(el); expect(el.firstChild.textContent).toBe('1'); expect(el.firstChild.style.color).toBe('orange'); } }); it('remounts failed error boundaries (componentDidCatch)', () => { if (__DEV__) { render(() => { function Hello() { returnA
B
> ); } return App; }); expect(container.innerHTML).toBe('A
B
'); const firstP = container.firstChild; const secondP = firstP.nextSibling.nextSibling; // Perform a hot update that fails. patch(() => { function Hello() { throw new Error('No'); } $RefreshReg$(Hello, 'Hello'); }); expect(container.innerHTML).toBe('A
B
'); expect(container.firstChild).toBe(firstP); expect(container.firstChild.nextSibling.nextSibling).toBe(secondP); // Perform a hot update that fixes the error. patch(() => { function Hello() { returnA
B
'); expect(container.firstChild).toBe(firstP); expect(container.firstChild.nextSibling.nextSibling).toBe(secondP); // Verify next hot reload doesn't remount anything. const helloNode = container.firstChild.nextSibling; patch(() => { function Hello() { returnA
B
> ); } return App; }); expect(container.innerHTML).toBe('A
B
'); const firstP = container.firstChild; const secondP = firstP.nextSibling.nextSibling; // Perform a hot update that fails. patch(() => { function Hello() { throw new Error('No'); } $RefreshReg$(Hello, 'Hello'); }); expect(container.innerHTML).toBe('A
B
'); expect(container.firstChild).toBe(firstP); expect(container.firstChild.nextSibling.nextSibling).toBe(secondP); // Perform a hot update that fixes the error. patch(() => { function Hello() { returnA
B
'); expect(container.firstChild).toBe(firstP); expect(container.firstChild.nextSibling.nextSibling).toBe(secondP); // Verify next hot reload doesn't remount anything. const helloNode = container.firstChild.nextSibling; patch(() => { function Hello() { returnA
B
> ); } return App; }); expect(container.innerHTML).toBe('A
B
'); const firstP = container.firstChild; const secondP = firstP.nextSibling.nextSibling; // Perform a hot update that fails. act(() => { patch(() => { function Hello() { const [x, setX] = React.useState(''); React.useEffect(() => { setTimeout(() => { setX(42); // This will crash next render. }, 1); }, []); x.slice(); returnA
B
'); // Run timeout inside effect: act(() => { jest.runAllTimers(); }); expect(container.innerHTML).toBe( 'A
B
', ); expect(container.firstChild).toBe(firstP); expect(container.firstChild.nextSibling.nextSibling).toBe(secondP); // Perform a hot update that fixes the error. act(() => { patch(() => { function Hello() { const [x] = React.useState(''); React.useEffect(() => {}, []); // Removes the bad effect code. x.slice(); // Doesn't throw initially. returnA
B
'); expect(container.firstChild).toBe(firstP); expect(container.firstChild.nextSibling.nextSibling).toBe(secondP); // Verify next hot reload doesn't remount anything. const helloNode = container.firstChild.nextSibling; act(() => { patch(() => { function Hello() { const [x] = React.useState(''); React.useEffect(() => {}, []); x.slice(); return{this.state.count}
); } } // For classes, we wouldn't do this call via Babel plugin. // Instead, we'd do it at module boundaries. // Normally classes would get a different type and remount anyway, // but at module boundaries we may want to prevent propagation. // However we still want to force a remount and use latest version. $RefreshReg$(Hello, 'Hello'); return Hello; }); // Bump the state before patching. const el = container.firstChild; expect(el.textContent).toBe('0'); expect(el.style.color).toBe('blue'); act(() => { el.dispatchEvent(new MouseEvent('click', {bubbles: true})); }); expect(el.textContent).toBe('1'); // Perform a hot update. const HelloV2 = patch(() => { class Hello extends React.Component { state = {count: 0}; handleClick = () => { this.setState(prev => ({ count: prev.count + 1, })); }; render() { return ({this.state.count}
); } } $RefreshReg$(Hello, 'Hello'); return Hello; }); // It should have remounted the class. expect(container.firstChild).not.toBe(el); const newEl = container.firstChild; expect(newEl.textContent).toBe('0'); expect(newEl.style.color).toBe('red'); act(() => { newEl.dispatchEvent(new MouseEvent('click', {bubbles: true})); }); expect(newEl.textContent).toBe('1'); // Now top-level renders of both types resolve to latest. render(() => HelloV1); render(() => HelloV2); expect(container.firstChild).toBe(newEl); expect(newEl.style.color).toBe('red'); expect(newEl.textContent).toBe('1'); const HelloV3 = patch(() => { class Hello extends React.Component { state = {count: 0}; handleClick = () => { this.setState(prev => ({ count: prev.count + 1, })); }; render() { return ({this.state.count}
); } } $RefreshReg$(Hello, 'Hello'); return Hello; }); // It should have remounted the class again. expect(container.firstChild).not.toBe(el); const finalEl = container.firstChild; expect(finalEl.textContent).toBe('0'); expect(finalEl.style.color).toBe('orange'); act(() => { finalEl.dispatchEvent(new MouseEvent('click', {bubbles: true})); }); expect(finalEl.textContent).toBe('1'); render(() => HelloV3); render(() => HelloV2); render(() => HelloV1); expect(container.firstChild).toBe(finalEl); expect(finalEl.style.color).toBe('orange'); expect(finalEl.textContent).toBe('1'); } }); it('updates refs when remounting', () => { if (__DEV__) { const testRef = React.createRef(); render( () => { class Hello extends React.Component { getColor() { return 'green'; } render() { return ; } } $RefreshReg$(Hello, 'Hello'); return Hello; }, {ref: testRef}, ); expect(testRef.current.getColor()).toBe('green'); patch(() => { class Hello extends React.Component { getColor() { return 'orange'; } render() { return ; } } $RefreshReg$(Hello, 'Hello'); }); expect(testRef.current.getColor()).toBe('orange'); patch(() => { const Hello = React.forwardRef((props, ref) => { React.useImperativeHandle(ref, () => ({ getColor() { return 'pink'; }, })); return ; }); $RefreshReg$(Hello, 'Hello'); }); expect(testRef.current.getColor()).toBe('pink'); patch(() => { const Hello = React.forwardRef((props, ref) => { React.useImperativeHandle(ref, () => ({ getColor() { return 'yellow'; }, })); return ; }); $RefreshReg$(Hello, 'Hello'); }); expect(testRef.current.getColor()).toBe('yellow'); patch(() => { const Hello = React.forwardRef((props, ref) => { React.useImperativeHandle(ref, () => ({ getColor() { return 'yellow'; }, })); return ; }); $RefreshReg$(Hello, 'Hello'); }); expect(testRef.current.getColor()).toBe('yellow'); } }); it('remounts on conversion from class to function and back', () => { if (__DEV__) { const HelloV1 = render(() => { function Hello() { const [val, setVal] = React.useState(0); return (setVal(val + 1)}> {val}
); } $RefreshReg$(Hello, 'Hello'); return Hello; }); // Bump the state before patching. const el = container.firstChild; expect(el.textContent).toBe('0'); expect(el.style.color).toBe('blue'); act(() => { el.dispatchEvent(new MouseEvent('click', {bubbles: true})); }); expect(el.textContent).toBe('1'); // Perform a hot update that turns it into a class. const HelloV2 = patch(() => { class Hello extends React.Component { state = {count: 0}; handleClick = () => { this.setState(prev => ({ count: prev.count + 1, })); }; render() { return ({this.state.count}
); } } $RefreshReg$(Hello, 'Hello'); return Hello; }); // It should have remounted. expect(container.firstChild).not.toBe(el); const newEl = container.firstChild; expect(newEl.textContent).toBe('0'); expect(newEl.style.color).toBe('red'); act(() => { newEl.dispatchEvent(new MouseEvent('click', {bubbles: true})); }); expect(newEl.textContent).toBe('1'); // Now top-level renders of both types resolve to latest. render(() => HelloV1); render(() => HelloV2); expect(container.firstChild).toBe(newEl); expect(newEl.style.color).toBe('red'); expect(newEl.textContent).toBe('1'); // Now convert it back to a function. const HelloV3 = patch(() => { function Hello() { const [val, setVal] = React.useState(0); return (setVal(val + 1)}> {val}
); } $RefreshReg$(Hello, 'Hello'); return Hello; }); // It should have remounted again. expect(container.firstChild).not.toBe(el); const finalEl = container.firstChild; expect(finalEl.textContent).toBe('0'); expect(finalEl.style.color).toBe('orange'); act(() => { finalEl.dispatchEvent(new MouseEvent('click', {bubbles: true})); }); expect(finalEl.textContent).toBe('1'); render(() => HelloV3); render(() => HelloV2); render(() => HelloV1); expect(container.firstChild).toBe(finalEl); expect(finalEl.style.color).toBe('orange'); expect(finalEl.textContent).toBe('1'); // Now that it's a function, verify edits keep state. patch(() => { function Hello() { const [val, setVal] = React.useState(0); return (setVal(val + 1)}> {val}
); } $RefreshReg$(Hello, 'Hello'); return Hello; }); expect(container.firstChild).toBe(finalEl); expect(finalEl.style.color).toBe('purple'); expect(finalEl.textContent).toBe('1'); } }); it('can find host instances for a family', () => { if (__DEV__) { render(() => { function Child({children}) { returnsetVal(val + 1)}> {val}
); }; $RefreshReg$(HelloV1, 'Hello'); // Perform a hot update before any roots exist. const HelloV2 = () => { const [val, setVal] = React.useState(0); return (setVal(val + 1)}> {val}
); }; $RefreshReg$(HelloV2, 'Hello'); ReactFreshRuntime.performReactRefresh(); // Mount three roots. const cont1 = document.createElement('div'); const cont2 = document.createElement('div'); const cont3 = document.createElement('div'); document.body.appendChild(cont1); document.body.appendChild(cont2); document.body.appendChild(cont3); try { ReactDOM.render(setVal(val + 1)}> {val}
); }; $RefreshReg$(HelloV3, 'Hello'); ReactFreshRuntime.performReactRefresh(); // It should affect all roots. expect(cont1.firstChild.style.color).toBe('green'); expect(cont2.firstChild.style.color).toBe('green'); expect(cont3.firstChild.style.color).toBe('green'); expect(cont1.firstChild.textContent).toBe('1'); expect(cont2.firstChild.textContent).toBe('1'); expect(cont3.firstChild.textContent).toBe('1'); // Unmount the second root. ReactDOM.unmountComponentAtNode(cont2); // Make the first root throw and unmount on hot update. const HelloV4 = ({id}) => { if (id === 1) { throw new Error('Oops.'); } const [val, setVal] = React.useState(0); return (setVal(val + 1)}> {val}
); }; $RefreshReg$(HelloV4, 'Hello'); expect(() => { ReactFreshRuntime.performReactRefresh(); }).toThrow('Oops.'); // Still, we expect the last root to be updated. expect(cont1.innerHTML).toBe(''); expect(cont2.innerHTML).toBe(''); expect(cont3.firstChild.style.color).toBe('orange'); expect(cont3.firstChild.textContent).toBe('1'); } finally { document.body.removeChild(cont1); document.body.removeChild(cont2); document.body.removeChild(cont3); } } }); // Module runtimes can use this to decide whether // to propagate an update up to the modules that imported it, // or to stop at the current module because it's a component. // This can't and doesn't need to be 100% precise. it('can detect likely component types', () => { function useTheme() {} function Widget() {} if (__DEV__) { expect(ReactFreshRuntime.isLikelyComponentType(false)).toBe(false); expect(ReactFreshRuntime.isLikelyComponentType(null)).toBe(false); expect(ReactFreshRuntime.isLikelyComponentType('foo')).toBe(false); // We need to hit a balance here. // If we lean towards assuming everything is a component, // editing modules that export plain functions won't trigger // a proper reload because we will bottle up the update. // So we're being somewhat conservative. expect(ReactFreshRuntime.isLikelyComponentType(() => {})).toBe(false); expect(ReactFreshRuntime.isLikelyComponentType(function() {})).toBe( false, ); expect( ReactFreshRuntime.isLikelyComponentType(function lightenColor() {}), ).toBe(false); const loadUser = () => {}; expect(ReactFreshRuntime.isLikelyComponentType(loadUser)).toBe(false); const useStore = () => {}; expect(ReactFreshRuntime.isLikelyComponentType(useStore)).toBe(false); expect(ReactFreshRuntime.isLikelyComponentType(useTheme)).toBe(false); // These seem like function components. const Button = () => {}; expect(ReactFreshRuntime.isLikelyComponentType(Button)).toBe(true); expect(ReactFreshRuntime.isLikelyComponentType(Widget)).toBe(true); const anon = (() => () => {})(); anon.displayName = 'Foo'; expect(ReactFreshRuntime.isLikelyComponentType(anon)).toBe(true); // These seem like class components. class Btn extends React.Component {} class PureBtn extends React.PureComponent {} expect(ReactFreshRuntime.isLikelyComponentType(Btn)).toBe(true); expect(ReactFreshRuntime.isLikelyComponentType(PureBtn)).toBe(true); expect( ReactFreshRuntime.isLikelyComponentType( createReactClass({render() {}}), ), ).toBe(true); // These don't. class Figure { move() {} } expect(ReactFreshRuntime.isLikelyComponentType(Figure)).toBe(false); class Point extends Figure {} expect(ReactFreshRuntime.isLikelyComponentType(Point)).toBe(false); // Run the same tests without Babel. // This tests real arrow functions and classes, as implemented in Node. // eslint-disable-next-line no-new-func new Function( 'global', 'React', 'ReactFreshRuntime', 'expect', 'createReactClass', ` expect(ReactFreshRuntime.isLikelyComponentType(() => {})).toBe(false); expect(ReactFreshRuntime.isLikelyComponentType(function() {})).toBe(false); expect( ReactFreshRuntime.isLikelyComponentType(function lightenColor() {}), ).toBe(false); const loadUser = () => {}; expect(ReactFreshRuntime.isLikelyComponentType(loadUser)).toBe(false); const useStore = () => {}; expect(ReactFreshRuntime.isLikelyComponentType(useStore)).toBe(false); function useTheme() {} expect(ReactFreshRuntime.isLikelyComponentType(useTheme)).toBe(false); // These seem like function components. let Button = () => {}; expect(ReactFreshRuntime.isLikelyComponentType(Button)).toBe(true); function Widget() {} expect(ReactFreshRuntime.isLikelyComponentType(Widget)).toBe(true); let anon = (() => () => {})(); anon.displayName = 'Foo'; expect(ReactFreshRuntime.isLikelyComponentType(anon)).toBe(true); // These seem like class components. class Btn extends React.Component {} class PureBtn extends React.PureComponent {} expect(ReactFreshRuntime.isLikelyComponentType(Btn)).toBe(true); expect(ReactFreshRuntime.isLikelyComponentType(PureBtn)).toBe(true); expect( ReactFreshRuntime.isLikelyComponentType(createReactClass({render() {}})), ).toBe(true); // These don't. class Figure { move() {} } expect(ReactFreshRuntime.isLikelyComponentType(Figure)).toBe(false); class Point extends Figure {} expect(ReactFreshRuntime.isLikelyComponentType(Point)).toBe(false); `, )(global, React, ReactFreshRuntime, expect, createReactClass); } }); it('reports updated and remounted families to the caller', () => { if (__DEV__) { const HelloV1 = () => { const [val, setVal] = React.useState(0); return (setVal(val + 1)}> {val}
); }; $RefreshReg$(HelloV1, 'Hello'); const HelloV2 = () => { const [val, setVal] = React.useState(0); return (setVal(val + 1)}> {val}
); }; $RefreshReg$(HelloV2, 'Hello'); const update = ReactFreshRuntime.performReactRefresh(); expect(update.updatedFamilies.size).toBe(1); expect(update.staleFamilies.size).toBe(0); const family = update.updatedFamilies.values().next().value; expect(family.current.name).toBe('HelloV2'); // For example, we can use this to print a log of what was updated. } }); // This simulates the scenario in https://github.com/facebook/react/issues/17626. it('can inject the runtime after the renderer executes', () => { if (__DEV__) { // This is a minimal shim for the global hook installed by DevTools. // The real one is in packages/react-devtools-shared/src/hook.js. let idCounter = 0; const renderers = new Map(); global.__REACT_DEVTOOLS_GLOBAL_HOOK__ = { renderers, supportsFiber: true, inject(renderer) { const id = ++idCounter; renderers.set(id, renderer); return id; }, onCommitFiberRoot() {}, onCommitFiberUnmount() {}, }; // Load these first, as if they're coming from a CDN. jest.resetModules(); React = require('react'); ReactDOM = require('react-dom'); Scheduler = require('scheduler'); act = require('react-dom/test-utils').act; // Important! Inject into the global hook *after* ReactDOM runs: ReactFreshRuntime = require('react-refresh/runtime'); ReactFreshRuntime.injectIntoGlobalHook(global); // We're verifying that we're able to track roots mounted after this point. // The rest of this test is taken from the simplest first test case. render(() => { function Hello() { const [val, setVal] = React.useState(0); return (setVal(val + 1)}> {val}
); } $RefreshReg$(Hello, 'Hello'); return Hello; }); // Bump the state before patching. const el = container.firstChild; expect(el.textContent).toBe('0'); expect(el.style.color).toBe('blue'); act(() => { el.dispatchEvent(new MouseEvent('click', {bubbles: true})); }); expect(el.textContent).toBe('1'); // Perform a hot update. patch(() => { function Hello() { const [val, setVal] = React.useState(0); return (setVal(val + 1)}> {val}
); } $RefreshReg$(Hello, 'Hello'); return Hello; }); // Assert the state was preserved but color changed. expect(container.firstChild).toBe(el); expect(el.textContent).toBe('1'); expect(el.style.color).toBe('red'); } }); });