Files
react/packages/react-refresh/src/__tests__/ReactFresh-test.js
T
Andrew Clark 62cd5af08e Codemod redundant async act scopes (#26350)
Prior to #26347, our internal `act` API (not the public API) behaved
differently depending on whether the scope function returned a promise
(i.e. was an async function), for historical reasons that no longer
apply. Now that this is fixed, I've codemodded all async act scopes that
don't contain an await to be sync.

No pressing motivation other than it looks nicer and the codemod was
easy. Might help avoid confusion for new contributors who see async act
scopes with nothing async inside and infer it must be like that for a
reason.
2023-03-08 16:40:23 -05:00

3887 lines
112 KiB
JavaScript

/**
* 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 ReactDOMClient;
let ReactFreshRuntime;
let Scheduler;
let act;
let internalAct;
let createReactClass;
let waitFor;
let assertLog;
describe('ReactFresh', () => {
let container;
beforeEach(() => {
if (__DEV__) {
jest.resetModules();
React = require('react');
ReactFreshRuntime = require('react-refresh/runtime');
ReactFreshRuntime.injectIntoGlobalHook(global);
ReactDOM = require('react-dom');
ReactDOMClient = require('react-dom/client');
Scheduler = require('scheduler');
act = require('react-dom/test-utils').act;
internalAct = require('internal-test-utils').act;
const InternalTestUtils = require('internal-test-utils');
waitFor = InternalTestUtils.waitFor;
assertLog = InternalTestUtils.assertLog;
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(<Component {...props} />, container);
});
return Component;
}
function patch(version) {
const Component = version();
ReactFreshRuntime.performReactRefresh();
return Component;
}
function $RefreshReg$(type, id) {
ReactFreshRuntime.register(type, id);
}
function $RefreshSig$(type, key, forceReset, getCustomHooks) {
ReactFreshRuntime.setSignature(type, key, forceReset, getCustomHooks);
return type;
}
// Note: This is based on a similar component we use in www. We can delete
// once the extra div wrapper is no longer necessary.
function LegacyHiddenDiv({children, mode}) {
return (
<div hidden={mode === 'hidden'}>
<React.unstable_LegacyHidden
mode={mode === 'hidden' ? 'unstable-defer-without-hiding' : mode}>
{children}
</React.unstable_LegacyHidden>
</div>
);
}
it('can preserve state for compatible types', () => {
if (__DEV__) {
const HelloV1 = render(() => {
function Hello() {
const [val, setVal] = React.useState(0);
return (
<p style={{color: 'blue'}} onClick={() => setVal(val + 1)}>
{val}
</p>
);
}
$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 (
<p style={{color: 'red'}} onClick={() => setVal(val + 1)}>
{val}
</p>
);
}
$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 (
<p style={{color: 'blue'}} onClick={() => setVal(val + 1)}>
{val}
</p>
);
}
// 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 (
<p style={{color: 'blue'}} onClick={() => setVal(val + 1)}>
{val}
</p>
);
}
$RefreshReg$(Hello, 'Hello');
const Outer = React.forwardRef(() => <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 (
<p style={{color: 'red'}} onClick={() => setVal(val + 1)}>
{val}
</p>
);
}
$RefreshReg$(Hello, 'Hello');
const Outer = React.forwardRef(() => <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 (
<p style={{color: 'blue'}} onClick={() => setVal(val + 1)}>
{val}
</p>
);
}
$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 (
<p style={{color: 'blue'}} onClick={() => setVal(val + 1)}>
{val}
</p>
);
}
$RefreshReg$(Hello, 'Hello');
function renderInner() {
return <Hello />;
}
// Both of these are wrappers around the same inner function.
// They should be treated as distinct types across reloads.
const ForwardRefA = React.forwardRef(renderInner);
$RefreshReg$(ForwardRefA, 'ForwardRefA');
const ForwardRefB = React.forwardRef(renderInner);
$RefreshReg$(ForwardRefB, 'ForwardRefB');
function Parent({cond}) {
return cond ? <ForwardRefA /> : <ForwardRefB />;
}
$RefreshReg$(Parent, 'Parent');
return Parent;
},
{cond: true},
);
// Bump the state before switching up types.
let 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');
// Switching up the inner types should reset the state.
render(() => ParentV1, {cond: false});
expect(el).not.toBe(container.firstChild);
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');
// Switch them up back again.
render(() => ParentV1, {cond: true});
expect(el).not.toBe(container.firstChild);
el = container.firstChild;
expect(el.textContent).toBe('0');
expect(el.style.color).toBe('blue');
// Now bump up the state to prepare for patching.
act(() => {
el.dispatchEvent(new MouseEvent('click', {bubbles: true}));
});
expect(el.textContent).toBe('1');
// Patch to change the color.
const ParentV2 = patch(() => {
function Hello() {
const [val, setVal] = React.useState(0);
return (
<p style={{color: 'red'}} onClick={() => setVal(val + 1)}>
{val}
</p>
);
}
$RefreshReg$(Hello, 'Hello');
function renderInner() {
return <Hello />;
}
// Both of these are wrappers around the same inner function.
// They should be treated as distinct types across reloads.
const ForwardRefA = React.forwardRef(renderInner);
$RefreshReg$(ForwardRefA, 'ForwardRefA');
const ForwardRefB = React.forwardRef(renderInner);
$RefreshReg$(ForwardRefB, 'ForwardRefB');
function Parent({cond}) {
return cond ? <ForwardRefA /> : <ForwardRefB />;
}
$RefreshReg$(Parent, 'Parent');
return Parent;
});
// The state should be intact; the color should change.
expect(el).toBe(container.firstChild);
expect(el.textContent).toBe('1');
expect(el.style.color).toBe('red');
// Switching up the condition should still reset the state.
render(() => ParentV2, {cond: false});
expect(el).not.toBe(container.firstChild);
el = container.firstChild;
expect(el.textContent).toBe('0');
expect(el.style.color).toBe('red');
// Now bump up the state to prepare for top-level renders.
act(() => {
el.dispatchEvent(new MouseEvent('click', {bubbles: true}));
});
expect(el).toBe(container.firstChild);
expect(el.textContent).toBe('1');
expect(el.style.color).toBe('red');
// Finally, verify using top-level render with stale type keeps state.
render(() => ParentV1);
render(() => ParentV2);
render(() => ParentV1);
expect(container.firstChild).toBe(el);
expect(el.textContent).toBe('1');
expect(el.style.color).toBe('red');
}
});
it('can update forwardRef render function with its wrapper', () => {
if (__DEV__) {
render(() => {
function Hello({color}) {
const [val, setVal] = React.useState(0);
return (
<p style={{color}} onClick={() => setVal(val + 1)}>
{val}
</p>
);
}
$RefreshReg$(Hello, 'Hello');
const Outer = React.forwardRef(() => <Hello color="blue" />);
$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.
patch(() => {
function Hello({color}) {
const [val, setVal] = React.useState(0);
return (
<p style={{color}} onClick={() => setVal(val + 1)}>
{val}
</p>
);
}
$RefreshReg$(Hello, 'Hello');
const Outer = React.forwardRef(() => <Hello color="red" />);
$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');
}
});
it('can update forwardRef render function in isolation', () => {
if (__DEV__) {
render(() => {
function Hello({color}) {
const [val, setVal] = React.useState(0);
return (
<p style={{color}} onClick={() => setVal(val + 1)}>
{val}
</p>
);
}
$RefreshReg$(Hello, 'Hello');
function renderHello() {
return <Hello color="blue" />;
}
$RefreshReg$(renderHello, 'renderHello');
return React.forwardRef(renderHello);
});
// 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({color}) {
const [val, setVal] = React.useState(0);
return (
<p style={{color}} onClick={() => setVal(val + 1)}>
{val}
</p>
);
}
$RefreshReg$(Hello, 'Hello');
function renderHello() {
return <Hello color="red" />;
}
$RefreshReg$(renderHello, 'renderHello');
// 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 simple memo', () => {
if (__DEV__) {
const OuterV1 = render(() => {
function Hello() {
const [val, setVal] = React.useState(0);
return (
<p style={{color: 'blue'}} onClick={() => setVal(val + 1)}>
{val}
</p>
);
}
$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 (
<p style={{color: 'red'}} onClick={() => setVal(val + 1)}>
{val}
</p>
);
}
$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 (
<p style={{color: 'blue'}} onClick={() => setVal(val + 1)}>
{val}
</p>
);
}
$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 (
<p style={{color: 'blue'}} onClick={() => setVal(val + 1)}>
{val}
</p>
);
}
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 (
<p style={{color: 'red'}} onClick={() => setVal(val + 1)}>
{val}
</p>
);
}
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 (
<p style={{color: 'blue'}} onClick={() => setVal(val + 1)}>
{val}
</p>
);
}
$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 (
<p style={{color: 'blue'}} onClick={() => setVal(val + 1)}>
{val}
</p>
);
}
$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 (
<p style={{color: 'red'}} onClick={() => setVal(val + 1)}>
{val}
</p>
);
}
$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 (
<p style={{color: 'blue'}} onClick={() => setVal(val + 1)}>
{val}
</p>
);
}
$RefreshReg$(Hello, 'Hello');
const Outer = React.memo(React.forwardRef(() => <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 (
<p style={{color: 'red'}} onClick={() => setVal(val + 1)}>
{val}
</p>
);
}
$RefreshReg$(Hello, 'Hello');
const Outer = React.memo(React.forwardRef(() => <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 (
<p style={{color: 'blue'}} onClick={() => setVal(val + 1)}>
{val}
</p>
);
}
$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 (
<p style={{color: 'blue'}} onClick={() => setVal(val + 1)}>
{val}
</p>
);
}
$RefreshReg$(Hello, 'Hello');
const Outer = React.lazy(
() =>
new Promise(resolve => {
setTimeout(() => resolve({default: Hello}), 100);
}),
);
$RefreshReg$(Outer, 'Outer');
function App() {
return (
<React.Suspense fallback={<p>Loading</p>}>
<Outer />
</React.Suspense>
);
}
$RefreshReg$(App, 'App');
return App;
});
expect(container.textContent).toBe('Loading');
await act(() => {
jest.runAllTimers();
});
expect(container.textContent).toBe('0');
// 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 AppV2 = patch(() => {
function Hello() {
const [val, setVal] = React.useState(0);
return (
<p style={{color: 'red'}} onClick={() => setVal(val + 1)}>
{val}
</p>
);
}
$RefreshReg$(Hello, 'Hello');
const Outer = React.lazy(
() =>
new Promise(resolve => {
setTimeout(() => resolve({default: Hello}), 100);
}),
);
$RefreshReg$(Outer, 'Outer');
function App() {
return (
<React.Suspense fallback={<p>Loading</p>}>
<Outer />
</React.Suspense>
);
}
$RefreshReg$(App, 'App');
return App;
});
// 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(() => AppV1);
render(() => AppV2);
render(() => AppV1);
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 (
<p style={{color: 'blue'}} onClick={() => setVal(val + 1)}>
{val}
</p>
);
}
$RefreshReg$(Hello, 'Hello');
// Note: no lazy wrapper this time.
function App() {
return (
<React.Suspense fallback={<p>Loading</p>}>
<Hello />
</React.Suspense>
);
}
$RefreshReg$(App, 'App');
return App;
});
expect(container.firstChild).not.toBe(el);
const newEl = container.firstChild;
expect(newEl.textContent).toBe('0');
expect(newEl.style.color).toBe('blue');
}
});
it('can patch lazy before resolution', async () => {
if (__DEV__) {
render(() => {
function Hello() {
const [val, setVal] = React.useState(0);
return (
<p style={{color: 'blue'}} onClick={() => setVal(val + 1)}>
{val}
</p>
);
}
$RefreshReg$(Hello, 'Hello');
const Outer = React.lazy(
() =>
new Promise(resolve => {
setTimeout(() => resolve({default: Hello}), 100);
}),
);
$RefreshReg$(Outer, 'Outer');
function App() {
return (
<React.Suspense fallback={<p>Loading</p>}>
<Outer />
</React.Suspense>
);
}
return App;
});
expect(container.textContent).toBe('Loading');
// Perform a hot update.
patch(() => {
function Hello() {
const [val, setVal] = React.useState(0);
return (
<p style={{color: 'red'}} onClick={() => setVal(val + 1)}>
{val}
</p>
);
}
$RefreshReg$(Hello, 'Hello');
});
await act(() => {
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 (
<p style={{color: 'orange'}} onClick={() => setVal(val + 1)}>
{val}
</p>
);
}
$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 (
<p style={{color: 'blue'}} onClick={() => setVal(val + 1)}>
{val}
</p>
);
}
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 (
<React.Suspense fallback={<p>Loading</p>}>
<Outer />
</React.Suspense>
);
}
return App;
});
expect(container.textContent).toBe('Loading');
// Perform a hot update.
patch(() => {
function renderHello() {
const [val, setVal] = React.useState(0);
return (
<p style={{color: 'red'}} onClick={() => setVal(val + 1)}>
{val}
</p>
);
}
const Hello = React.forwardRef(renderHello);
$RefreshReg$(Hello, 'Hello');
});
await act(() => {
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 (
<p style={{color: 'orange'}} onClick={() => setVal(val + 1)}>
{val}
</p>
);
}
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 (
<p style={{color: 'blue'}} onClick={() => setVal(val + 1)}>
{val}
</p>
);
}
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 (
<React.Suspense fallback={<p>Loading</p>}>
<Outer />
</React.Suspense>
);
}
return App;
});
expect(container.textContent).toBe('Loading');
// Perform a hot update.
patch(() => {
function renderHello() {
const [val, setVal] = React.useState(0);
return (
<p style={{color: 'red'}} onClick={() => setVal(val + 1)}>
{val}
</p>
);
}
const Hello = React.memo(renderHello);
$RefreshReg$(Hello, 'Hello');
});
await act(() => {
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 (
<p style={{color: 'orange'}} onClick={() => setVal(val + 1)}>
{val}
</p>
);
}
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 (
<p style={{color: 'blue'}} onClick={() => setVal(val + 1)}>
{val}
</p>
);
}
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 (
<React.Suspense fallback={<p>Loading</p>}>
<Outer />
</React.Suspense>
);
}
return App;
});
expect(container.textContent).toBe('Loading');
// Perform a hot update.
patch(() => {
function renderHello() {
const [val, setVal] = React.useState(0);
return (
<p style={{color: 'red'}} onClick={() => setVal(val + 1)}>
{val}
</p>
);
}
const Hello = React.memo(React.forwardRef(renderHello));
$RefreshReg$(Hello, 'Hello');
});
await act(() => {
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 (
<p style={{color: 'orange'}} onClick={() => setVal(val + 1)}>
{val}
</p>
);
}
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 (
<p style={{color: 'blue'}} onClick={() => setVal(val + 1)}>
{children} {val}
</p>
);
}
$RefreshReg$(Hello, 'Hello');
function Never() {
throw new Promise(resolve => {});
}
function App({shouldSuspend}) {
return (
<React.Suspense fallback={<Hello>Fallback</Hello>}>
<Hello>Content</Hello>
{shouldSuspend && <Never />}
</React.Suspense>
);
}
return App;
},
{shouldSuspend: false},
);
// We start with just the primary tree.
expect(container.childNodes.length).toBe(1);
const primaryChild = container.firstChild;
expect(primaryChild.textContent).toBe('Content 0');
expect(primaryChild.style.color).toBe('blue');
expect(primaryChild.style.display).toBe('');
// Bump primary content state.
act(() => {
primaryChild.dispatchEvent(new MouseEvent('click', {bubbles: true}));
});
expect(container.childNodes.length).toBe(1);
expect(container.childNodes[0]).toBe(primaryChild);
expect(primaryChild.textContent).toBe('Content 1');
expect(primaryChild.style.color).toBe('blue');
expect(primaryChild.style.display).toBe('');
// Perform a hot update.
patch(() => {
function Hello({children}) {
const [val, setVal] = React.useState(0);
return (
<p style={{color: 'green'}} onClick={() => setVal(val + 1)}>
{children} {val}
</p>
);
}
$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 (
<p style={{color: 'red'}} onClick={() => setVal(val + 1)}>
{children} {val}
</p>
);
}
$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 (
<p style={{color: 'orange'}} onClick={() => setVal(val + 1)}>
{children} {val}
</p>
);
}
$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 (
<p style={{color: 'blue'}} onClick={() => setVal(val + 1)}>
{val}
</p>
);
}
$RefreshReg$(Hello, 'Hello');
function App() {
appRenders++;
return <Hello />;
}
$RefreshReg$(App, 'App');
return App;
});
expect(appRenders).toBe(1);
// 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');
// No re-renders from the top.
expect(appRenders).toBe(1);
// Perform a hot update for Hello only.
patch(() => {
function Hello() {
const [val, setVal] = React.useState(0);
return (
<p style={{color: 'red'}} onClick={() => setVal(val + 1)}>
{val}
</p>
);
}
$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++;
return <div>X{children}X</div>;
}
$RefreshReg$(Hello, 'Hello');
function App() {
return (
<Hello>
<Hello>
<Hello />
</Hello>
<Hello>
<Hello />
</Hello>
</Hello>
);
}
return App;
});
expect(helloRenders).toBe(5);
expect(container.textContent).toBe('XXXXXXXXXX');
helloRenders = 0;
patch(() => {
function Hello({children}) {
helloRenders++;
return <div>O{children}O</div>;
}
$RefreshReg$(Hello, 'Hello');
});
expect(helloRenders).toBe(5);
expect(container.textContent).toBe('OOOOOOOOOO');
}
});
it('does not leak state between components', () => {
if (__DEV__) {
const AppV1 = render(
() => {
function Hello1() {
const [val, setVal] = React.useState(0);
return (
<p style={{color: 'blue'}} onClick={() => setVal(val + 1)}>
{val}
</p>
);
}
$RefreshReg$(Hello1, 'Hello1');
function Hello2() {
const [val, setVal] = React.useState(0);
return (
<p style={{color: 'blue'}} onClick={() => setVal(val + 1)}>
{val}
</p>
);
}
$RefreshReg$(Hello2, 'Hello2');
function App({cond}) {
return cond ? <Hello1 /> : <Hello2 />;
}
$RefreshReg$(App, 'App');
return App;
},
{cond: false},
);
// 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');
// Switch the condition, flipping inner content.
// This should reset the state.
render(() => AppV1, {cond: true});
const el2 = container.firstChild;
expect(el2).not.toBe(el);
expect(el2.textContent).toBe('0');
expect(el2.style.color).toBe('blue');
// Bump it again.
act(() => {
el2.dispatchEvent(new MouseEvent('click', {bubbles: true}));
});
expect(el2.textContent).toBe('1');
// Perform a hot update for both inner components.
patch(() => {
function Hello1() {
const [val, setVal] = React.useState(0);
return (
<p style={{color: 'red'}} onClick={() => setVal(val + 1)}>
{val}
</p>
);
}
$RefreshReg$(Hello1, 'Hello1');
function Hello2() {
const [val, setVal] = React.useState(0);
return (
<p style={{color: 'red'}} onClick={() => setVal(val + 1)}>
{val}
</p>
);
}
$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 (
<p style={{color: 'blue'}} onClick={() => setVal(val + 1)}>
{val}
</p>
);
}
$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 (
<p style={{color: 'red'}} onClick={() => setVal(val + 1)}>
{val}
</p>
);
}
$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 (
<p style={{color: 'yellow'}} onClick={() => setVal(val + 1)}>
{val}
</p>
);
}
// 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 (
<p style={{color: 'purple'}} onClick={() => setVal(val + 1)}>
{val}
</p>
);
}
// 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 (
<p style={{color: 'orange'}} onClick={() => setVal(val + 1)}>
{val}
</p>
);
}
// 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 = [
<div>
<HelloV1 />
<div>
<HelloV1 />
<Bailout>
<HelloV1 />
</Bailout>
</div>
</div>,
<div>
<div>
<HelloV1>
<HelloV1 />
</HelloV1>
<HelloV1 />
</div>
</div>,
<div>
<span />
<HelloV1 />
<HelloV1 />
<HelloV1 />
</div>,
<div>
<HelloV1 />
<span />
<HelloV1 />
<HelloV1 />
</div>,
<div>
<div>foo</div>
<HelloV1 />
<div>
<HelloV1 />
</div>
<HelloV1 />
<span />
</div>,
<div>
<HelloV1>
<span />
Hello
<span />
</HelloV1>
,
<HelloV1>
<>
<HelloV1 />
</>
</HelloV1>
,
</div>,
<HelloV1>
<HelloV1>
<Bailout>
<span />
<HelloV1>
<span />
</HelloV1>
<span />
</Bailout>
</HelloV1>
</HelloV1>,
<div>
<span />
<HelloV1 key="0" />
<HelloV1 key="1" />
<HelloV1 key="2" />
<span />
</div>,
<div>
<span />
{null}
<HelloV1 key="1" />
{null}
<HelloV1 />
<HelloV1 />
<span />
</div>,
<div>
<HelloV1 key="2" />
<span />
<HelloV1 key="0" />
<span />
<HelloV1 key="1" />
</div>,
<div>
{[[<HelloV1 key="2" />]]}
<span>
<HelloV1 key="0" />
{[null]}
<HelloV1 key="1" />
</span>
</div>,
<div>
{['foo', <HelloV1 key="hi" />, null, <HelloV1 key="2" />]}
<span>
{[null]}
<HelloV1 key="x" />
</span>
</div>,
<HelloV1>
<HelloV1>
<span />
<Bailout>
<HelloV1>hi</HelloV1>
<span />
</Bailout>
</HelloV1>
</HelloV1>,
];
// First, check that each tree handles remounts in isolation.
ReactDOM.render(null, container);
for (let i = 0; i < trees.length; i++) {
runRemountingStressTest(trees[i]);
}
// Then check that each tree is resilient to updates from another tree.
for (let i = 0; i < trees.length; i++) {
for (let j = 0; j < trees.length; j++) {
ReactDOM.render(null, container);
// Intentionally don't clean up between the tests:
runRemountingStressTest(trees[i]);
runRemountingStressTest(trees[j]);
runRemountingStressTest(trees[i]);
}
}
}
});
function runRemountingStressTest(tree) {
patch(() => {
function Hello({children}) {
return <section data-color="blue">{children}</section>;
}
$RefreshReg$(Hello, 'Hello');
$RefreshSig$(Hello, '1');
return Hello;
});
ReactDOM.render(tree, container);
const elements = container.querySelectorAll('section');
// Each tree above produces exactly three <section> elements:
expect(elements.length).toBe(3);
elements.forEach(el => {
expect(el.dataset.color).toBe('blue');
});
// Patch color without changing the signature.
patch(() => {
function Hello({children}) {
return <section data-color="red">{children}</section>;
}
$RefreshReg$(Hello, 'Hello');
$RefreshSig$(Hello, '1');
return Hello;
});
const elementsAfterPatch = container.querySelectorAll('section');
expect(elementsAfterPatch.length).toBe(3);
elementsAfterPatch.forEach((el, index) => {
// The signature hasn't changed so we expect DOM nodes to stay the same.
expect(el).toBe(elements[index]);
// However, the color should have changed:
expect(el.dataset.color).toBe('red');
});
// Patch color *and* change the signature.
patch(() => {
function Hello({children}) {
return <section data-color="orange">{children}</section>;
}
$RefreshReg$(Hello, 'Hello');
$RefreshSig$(Hello, '2'); // Remount
return Hello;
});
const elementsAfterRemount = container.querySelectorAll('section');
expect(elementsAfterRemount.length).toBe(3);
elementsAfterRemount.forEach((el, index) => {
// The signature changed so we expect DOM nodes to be different.
expect(el).not.toBe(elements[index]);
// They should all be using the new color:
expect(el.dataset.color).toBe('orange');
});
// Now patch color but *don't* change the signature.
patch(() => {
function Hello({children}) {
return <section data-color="black">{children}</section>;
}
$RefreshReg$(Hello, 'Hello');
$RefreshSig$(Hello, '2'); // Same signature as before
return Hello;
});
expect(container.querySelectorAll('section').length).toBe(3);
container.querySelectorAll('section').forEach((el, index) => {
// The signature didn't change so DOM nodes should stay the same.
expect(el).toBe(elementsAfterRemount[index]);
// They should all be using the new color:
expect(el.dataset.color).toBe('black');
});
// Do another render just in case.
ReactDOM.render(tree, container);
expect(container.querySelectorAll('section').length).toBe(3);
container.querySelectorAll('section').forEach((el, index) => {
expect(el).toBe(elementsAfterRemount[index]);
expect(el.dataset.color).toBe('black');
});
}
it('can remount on signature change within a <root> wrapper', () => {
if (__DEV__) {
testRemountingWithWrapper(Hello => Hello);
}
});
it('can remount on signature change within a simple memo wrapper', () => {
if (__DEV__) {
testRemountingWithWrapper(Hello => React.memo(Hello));
}
});
it('can remount on signature change within a lazy simple memo wrapper', () => {
if (__DEV__) {
testRemountingWithWrapper(Hello =>
React.lazy(() => ({
then(cb) {
cb({default: React.memo(Hello)});
},
})),
);
}
});
it('can remount on signature change within forwardRef', () => {
if (__DEV__) {
testRemountingWithWrapper(Hello => React.forwardRef(Hello));
}
});
it('can remount on signature change within forwardRef render function', () => {
if (__DEV__) {
testRemountingWithWrapper(Hello => React.forwardRef(() => <Hello />));
}
});
it('can remount on signature change within nested memo', () => {
if (__DEV__) {
testRemountingWithWrapper(Hello =>
React.memo(React.memo(React.memo(Hello))),
);
}
});
it('can remount on signature change within a memo wrapper and custom comparison', () => {
if (__DEV__) {
testRemountingWithWrapper(Hello => React.memo(Hello, () => true));
}
});
it('can remount on signature change within a class', () => {
if (__DEV__) {
testRemountingWithWrapper(Hello => {
const child = <Hello />;
return class Wrapper extends React.PureComponent {
render() {
return child;
}
};
});
}
});
it('can remount on signature change within a context provider', () => {
if (__DEV__) {
testRemountingWithWrapper(Hello => {
const Context = React.createContext();
const child = (
<Context.Provider value="constant">
<Hello />
</Context.Provider>
);
return function Wrapper() {
return child;
};
});
}
});
it('can remount on signature change within a context consumer', () => {
if (__DEV__) {
testRemountingWithWrapper(Hello => {
const Context = React.createContext();
const child = <Context.Consumer>{() => <Hello />}</Context.Consumer>;
return function Wrapper() {
return child;
};
});
}
});
it('can remount on signature change within a suspense node', () => {
if (__DEV__) {
testRemountingWithWrapper(Hello => {
// TODO: we'll probably want to test fallback trees too.
const child = (
<React.Suspense>
<Hello />
</React.Suspense>
);
return function Wrapper() {
return child;
};
});
}
});
it('can remount on signature change within a mode node', () => {
if (__DEV__) {
testRemountingWithWrapper(Hello => {
const child = (
<React.StrictMode>
<Hello />
</React.StrictMode>
);
return function Wrapper() {
return child;
};
});
}
});
it('can remount on signature change within a fragment node', () => {
if (__DEV__) {
testRemountingWithWrapper(Hello => {
const child = (
<>
<Hello />
</>
);
return function Wrapper() {
return child;
};
});
}
});
it('can remount on signature change within multiple siblings', () => {
if (__DEV__) {
testRemountingWithWrapper(Hello => {
const child = (
<>
<>
<React.Fragment />
</>
<Hello />
<React.Fragment />
</>
);
return function Wrapper() {
return child;
};
});
}
});
it('can remount on signature change within a profiler node', () => {
if (__DEV__) {
testRemountingWithWrapper(Hello => {
const child = <Hello />;
return function Wrapper() {
return (
<React.Profiler onRender={() => {}} id="foo">
{child}
</React.Profiler>
);
};
});
}
});
function testRemountingWithWrapper(wrap) {
render(() => {
function Hello() {
const [val, setVal] = React.useState(0);
return (
<p style={{color: 'blue'}} onClick={() => setVal(val + 1)}>
{val}
</p>
);
}
$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 (
<p style={{color: 'red'}} onClick={() => setVal(val + 1)}>
{val}
</p>
);
}
$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 (
<p style={{color: 'yellow'}} onClick={() => setVal(val + 1)}>
{val}
</p>
);
}
// 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 (
<p style={{color: 'purple'}} onClick={() => setVal(val + 1)}>
{val}
</p>
);
}
// 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 (
<p style={{color: 'orange'}} onClick={() => setVal(val + 1)}>
{val}
</p>
);
}
// 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 (
<p style={{color: 'blue'}} onClick={handleClick}>
{tranformed}
</p>
);
}
$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 (
<p style={{color: 'red'}} onClick={handleClick}>
{tranformed}
</p>
);
}
$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 <p style={{color: 'blue'}}>{state.value}</p>;
}
$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 <p style={{color: 'red'}}>{state.value}</p>;
}
$RefreshReg$(Hello, 'Hello');
return Hello;
});
});
expect(container.firstChild).toBe(el);
expect(el.textContent).toBe('20');
expect(el.style.color).toBe('red');
}
});
// @gate www && __DEV__
it('can hot reload offscreen components', async () => {
const AppV1 = prepare(() => {
function Hello() {
React.useLayoutEffect(() => {
Scheduler.log('Hello#layout');
});
const [val, setVal] = React.useState(0);
return (
<p style={{color: 'blue'}} onClick={() => setVal(val + 1)}>
{val}
</p>
);
}
$RefreshReg$(Hello, 'Hello');
return function App({offscreen}) {
React.useLayoutEffect(() => {
Scheduler.log('App#layout');
});
return (
<LegacyHiddenDiv mode={offscreen ? 'hidden' : 'visible'}>
<Hello />
</LegacyHiddenDiv>
);
};
});
const root = ReactDOMClient.createRoot(container);
root.render(<AppV1 offscreen={true} />);
await waitFor(['App#layout']);
const el = container.firstChild;
expect(el.hidden).toBe(true);
expect(el.firstChild).toBe(null); // Offscreen content not flushed yet.
// Perform a hot update.
patch(() => {
function Hello() {
React.useLayoutEffect(() => {
Scheduler.log('Hello#layout');
});
const [val, setVal] = React.useState(0);
return (
<p style={{color: 'red'}} onClick={() => setVal(val + 1)}>
{val}
</p>
);
}
$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.
await waitFor(['Hello#layout']);
expect(container.firstChild).toBe(el);
expect(el.firstChild.textContent).toBe('0');
expect(el.firstChild.style.color).toBe('red');
await internalAct(() => {
el.firstChild.dispatchEvent(
new MouseEvent('click', {
bubbles: true,
}),
);
});
assertLog(['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.log('Hello#layout');
});
const [val, setVal] = React.useState(0);
return (
<p style={{color: 'orange'}} onClick={() => setVal(val + 1)}>
{val}
</p>
);
}
$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.
await waitFor(['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() {
return <h1>Hi</h1>;
}
$RefreshReg$(Hello, 'Hello');
class Boundary extends React.Component {
state = {error: null};
componentDidCatch(error) {
this.setState({error});
}
render() {
if (this.state.error) {
return <h1>Oops: {this.state.error.message}</h1>;
}
return this.props.children;
}
}
function App() {
return (
<>
<p>A</p>
<Boundary>
<Hello />
</Boundary>
<p>B</p>
</>
);
}
return App;
});
expect(container.innerHTML).toBe('<p>A</p><h1>Hi</h1><p>B</p>');
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('<p>A</p><h1>Oops: No</h1><p>B</p>');
expect(container.firstChild).toBe(firstP);
expect(container.firstChild.nextSibling.nextSibling).toBe(secondP);
// Perform a hot update that fixes the error.
patch(() => {
function Hello() {
return <h1>Fixed!</h1>;
}
$RefreshReg$(Hello, 'Hello');
});
// This should remount the error boundary (but not anything above it).
expect(container.innerHTML).toBe('<p>A</p><h1>Fixed!</h1><p>B</p>');
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() {
return <h1>Nice.</h1>;
}
$RefreshReg$(Hello, 'Hello');
});
expect(container.firstChild.nextSibling).toBe(helloNode);
expect(helloNode.textContent).toBe('Nice.');
}
});
it('remounts failed error boundaries (getDerivedStateFromError)', () => {
if (__DEV__) {
render(() => {
function Hello() {
return <h1>Hi</h1>;
}
$RefreshReg$(Hello, 'Hello');
class Boundary extends React.Component {
state = {error: null};
static getDerivedStateFromError(error) {
return {error};
}
render() {
if (this.state.error) {
return <h1>Oops: {this.state.error.message}</h1>;
}
return this.props.children;
}
}
function App() {
return (
<>
<p>A</p>
<Boundary>
<Hello />
</Boundary>
<p>B</p>
</>
);
}
return App;
});
expect(container.innerHTML).toBe('<p>A</p><h1>Hi</h1><p>B</p>');
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('<p>A</p><h1>Oops: No</h1><p>B</p>');
expect(container.firstChild).toBe(firstP);
expect(container.firstChild.nextSibling.nextSibling).toBe(secondP);
// Perform a hot update that fixes the error.
patch(() => {
function Hello() {
return <h1>Fixed!</h1>;
}
$RefreshReg$(Hello, 'Hello');
});
// This should remount the error boundary (but not anything above it).
expect(container.innerHTML).toBe('<p>A</p><h1>Fixed!</h1><p>B</p>');
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() {
return <h1>Nice.</h1>;
}
$RefreshReg$(Hello, 'Hello');
});
expect(container.firstChild.nextSibling).toBe(helloNode);
expect(helloNode.textContent).toBe('Nice.');
}
});
it('remounts error boundaries that failed asynchronously after hot update', () => {
if (__DEV__) {
render(() => {
function Hello() {
const [x] = React.useState('');
React.useEffect(() => {}, []);
x.slice(); // Doesn't throw initially.
return <h1>Hi</h1>;
}
$RefreshReg$(Hello, 'Hello');
class Boundary extends React.Component {
state = {error: null};
static getDerivedStateFromError(error) {
return {error};
}
render() {
if (this.state.error) {
return <h1>Oops: {this.state.error.message}</h1>;
}
return this.props.children;
}
}
function App() {
return (
<>
<p>A</p>
<Boundary>
<Hello />
</Boundary>
<p>B</p>
</>
);
}
return App;
});
expect(container.innerHTML).toBe('<p>A</p><h1>Hi</h1><p>B</p>');
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();
return <h1>Hi</h1>;
}
$RefreshReg$(Hello, 'Hello');
});
});
expect(container.innerHTML).toBe('<p>A</p><h1>Hi</h1><p>B</p>');
// Run timeout inside effect:
act(() => {
jest.runAllTimers();
});
expect(container.innerHTML).toBe(
'<p>A</p><h1>Oops: x.slice is not a function</h1><p>B</p>',
);
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.
return <h1>Fixed!</h1>;
}
$RefreshReg$(Hello, 'Hello');
});
});
// This should remount the error boundary (but not anything above it).
expect(container.innerHTML).toBe('<p>A</p><h1>Fixed!</h1><p>B</p>');
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 <h1>Nice.</h1>;
}
$RefreshReg$(Hello, 'Hello');
});
});
expect(container.firstChild.nextSibling).toBe(helloNode);
expect(helloNode.textContent).toBe('Nice.');
}
});
it('remounts a failed root on mount', () => {
if (__DEV__) {
expect(() => {
render(() => {
function Hello() {
throw new Error('No');
}
$RefreshReg$(Hello, 'Hello');
return Hello;
});
}).toThrow('No');
expect(container.innerHTML).toBe('');
// A bad retry
expect(() => {
patch(() => {
function Hello() {
throw new Error('Not yet');
}
$RefreshReg$(Hello, 'Hello');
});
}).toThrow('Not yet');
expect(container.innerHTML).toBe('');
// Perform a hot update that fixes the error.
patch(() => {
function Hello() {
return <h1>Fixed!</h1>;
}
$RefreshReg$(Hello, 'Hello');
});
// This should mount the root.
expect(container.innerHTML).toBe('<h1>Fixed!</h1>');
// Ensure we can keep failing and recovering later.
expect(() => {
patch(() => {
function Hello() {
throw new Error('No 2');
}
$RefreshReg$(Hello, 'Hello');
});
}).toThrow('No 2');
expect(container.innerHTML).toBe('');
expect(() => {
patch(() => {
function Hello() {
throw new Error('Not yet 2');
}
$RefreshReg$(Hello, 'Hello');
});
}).toThrow('Not yet 2');
expect(container.innerHTML).toBe('');
patch(() => {
function Hello() {
return <h1>Fixed 2!</h1>;
}
$RefreshReg$(Hello, 'Hello');
});
expect(container.innerHTML).toBe('<h1>Fixed 2!</h1>');
// Updates after intentional unmount are ignored.
ReactDOM.unmountComponentAtNode(container);
patch(() => {
function Hello() {
throw new Error('Ignored');
}
$RefreshReg$(Hello, 'Hello');
});
expect(container.innerHTML).toBe('');
patch(() => {
function Hello() {
return <h1>Ignored</h1>;
}
$RefreshReg$(Hello, 'Hello');
});
expect(container.innerHTML).toBe('');
}
});
it('does not retry an intentionally unmounted failed root', () => {
if (__DEV__) {
expect(() => {
render(() => {
function Hello() {
throw new Error('No');
}
$RefreshReg$(Hello, 'Hello');
return Hello;
});
}).toThrow('No');
expect(container.innerHTML).toBe('');
// Intentional unmount.
ReactDOM.unmountComponentAtNode(container);
// Perform a hot update that fixes the error.
patch(() => {
function Hello() {
return <h1>Fixed!</h1>;
}
$RefreshReg$(Hello, 'Hello');
});
// This should stay unmounted.
expect(container.innerHTML).toBe('');
}
});
it('remounts a failed root on update', () => {
if (__DEV__) {
render(() => {
function Hello() {
return <h1>Hi</h1>;
}
$RefreshReg$(Hello, 'Hello');
return Hello;
});
expect(container.innerHTML).toBe('<h1>Hi</h1>');
// Perform a hot update that fails.
// This removes the root.
expect(() => {
patch(() => {
function Hello() {
throw new Error('No');
}
$RefreshReg$(Hello, 'Hello');
});
}).toThrow('No');
expect(container.innerHTML).toBe('');
// A bad retry
expect(() => {
patch(() => {
function Hello() {
throw new Error('Not yet');
}
$RefreshReg$(Hello, 'Hello');
});
}).toThrow('Not yet');
expect(container.innerHTML).toBe('');
// Perform a hot update that fixes the error.
patch(() => {
function Hello() {
return <h1>Fixed!</h1>;
}
$RefreshReg$(Hello, 'Hello');
});
// This should remount the root.
expect(container.innerHTML).toBe('<h1>Fixed!</h1>');
// Verify next hot reload doesn't remount anything.
const helloNode = container.firstChild;
patch(() => {
function Hello() {
return <h1>Nice.</h1>;
}
$RefreshReg$(Hello, 'Hello');
});
expect(container.firstChild).toBe(helloNode);
expect(helloNode.textContent).toBe('Nice.');
// Break again.
expect(() => {
patch(() => {
function Hello() {
throw new Error('Oops');
}
$RefreshReg$(Hello, 'Hello');
});
}).toThrow('Oops');
expect(container.innerHTML).toBe('');
// Perform a hot update that fixes the error.
patch(() => {
function Hello() {
return <h1>At last.</h1>;
}
$RefreshReg$(Hello, 'Hello');
});
// This should remount the root.
expect(container.innerHTML).toBe('<h1>At last.</h1>');
// Check we don't attempt to reverse an intentional unmount.
ReactDOM.unmountComponentAtNode(container);
expect(container.innerHTML).toBe('');
patch(() => {
function Hello() {
return <h1>Never mind me!</h1>;
}
$RefreshReg$(Hello, 'Hello');
});
expect(container.innerHTML).toBe('');
// Mount a new container.
render(() => {
function Hello() {
return <h1>Hi</h1>;
}
$RefreshReg$(Hello, 'Hello');
return Hello;
});
expect(container.innerHTML).toBe('<h1>Hi</h1>');
// Break again.
expect(() => {
patch(() => {
function Hello() {
throw new Error('Oops');
}
$RefreshReg$(Hello, 'Hello');
});
}).toThrow('Oops');
expect(container.innerHTML).toBe('');
// Check we don't attempt to reverse an intentional unmount, even after an error.
ReactDOM.unmountComponentAtNode(container);
expect(container.innerHTML).toBe('');
patch(() => {
function Hello() {
return <h1>Never mind me!</h1>;
}
$RefreshReg$(Hello, 'Hello');
});
expect(container.innerHTML).toBe('');
}
});
it('regression test: does not get into an infinite loop', () => {
if (__DEV__) {
const containerA = document.createElement('div');
const containerB = document.createElement('div');
// Initially, nothing interesting.
const RootAV1 = () => {
return 'A1';
};
$RefreshReg$(RootAV1, 'RootA');
const RootBV1 = () => {
return 'B1';
};
$RefreshReg$(RootBV1, 'RootB');
act(() => {
ReactDOM.render(<RootAV1 />, containerA);
ReactDOM.render(<RootBV1 />, containerB);
});
expect(containerA.innerHTML).toBe('A1');
expect(containerB.innerHTML).toBe('B1');
// Then make the first root fail.
const RootAV2 = () => {
throw new Error('A2!');
};
$RefreshReg$(RootAV2, 'RootA');
expect(() => ReactFreshRuntime.performReactRefresh()).toThrow('A2!');
expect(containerA.innerHTML).toBe('');
expect(containerB.innerHTML).toBe('B1');
// Then patch the first root, but make it fail in the commit phase.
// This used to trigger an infinite loop due to a list of failed roots
// being mutated while it was being iterated on.
const RootAV3 = () => {
React.useLayoutEffect(() => {
throw new Error('A3!');
}, []);
return 'A3';
};
$RefreshReg$(RootAV3, 'RootA');
expect(() => ReactFreshRuntime.performReactRefresh()).toThrow('A3!');
expect(containerA.innerHTML).toBe('');
expect(containerB.innerHTML).toBe('B1');
const RootAV4 = () => {
return 'A4';
};
$RefreshReg$(RootAV4, 'RootA');
ReactFreshRuntime.performReactRefresh();
expect(containerA.innerHTML).toBe('A4');
expect(containerB.innerHTML).toBe('B1');
}
});
it('remounts classes on every edit', () => {
if (__DEV__) {
const HelloV1 = render(() => {
class Hello extends React.Component {
state = {count: 0};
handleClick = () => {
this.setState(prev => ({
count: prev.count + 1,
}));
};
render() {
return (
<p style={{color: 'blue'}} onClick={this.handleClick}>
{this.state.count}
</p>
);
}
}
// 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 (
<p style={{color: 'red'}} onClick={this.handleClick}>
{this.state.count}
</p>
);
}
}
$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 (
<p style={{color: 'orange'}} onClick={this.handleClick}>
{this.state.count}
</p>
);
}
}
$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 <p />;
}
}
$RefreshReg$(Hello, 'Hello');
return Hello;
},
{ref: testRef},
);
expect(testRef.current.getColor()).toBe('green');
patch(() => {
class Hello extends React.Component {
getColor() {
return 'orange';
}
render() {
return <p />;
}
}
$RefreshReg$(Hello, 'Hello');
});
expect(testRef.current.getColor()).toBe('orange');
patch(() => {
const Hello = React.forwardRef((props, ref) => {
React.useImperativeHandle(ref, () => ({
getColor() {
return 'pink';
},
}));
return <p />;
});
$RefreshReg$(Hello, 'Hello');
});
expect(testRef.current.getColor()).toBe('pink');
patch(() => {
const Hello = React.forwardRef((props, ref) => {
React.useImperativeHandle(ref, () => ({
getColor() {
return 'yellow';
},
}));
return <p />;
});
$RefreshReg$(Hello, 'Hello');
});
expect(testRef.current.getColor()).toBe('yellow');
patch(() => {
const Hello = React.forwardRef((props, ref) => {
React.useImperativeHandle(ref, () => ({
getColor() {
return 'yellow';
},
}));
return <p />;
});
$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 (
<p style={{color: 'blue'}} onClick={() => setVal(val + 1)}>
{val}
</p>
);
}
$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 (
<p style={{color: 'red'}} onClick={this.handleClick}>
{this.state.count}
</p>
);
}
}
$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 (
<p style={{color: 'orange'}} onClick={() => setVal(val + 1)}>
{val}
</p>
);
}
$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 (
<p style={{color: 'purple'}} onClick={() => setVal(val + 1)}>
{val}
</p>
);
}
$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}) {
return <div className="Child">{children}</div>;
}
$RefreshReg$(Child, 'Child');
function Parent({children}) {
return (
<div className="Parent">
<div>
<Child />
</div>
<div>
<Child />
</div>
</div>
);
}
$RefreshReg$(Parent, 'Parent');
function App() {
return (
<div className="App">
<Parent />
<Cls>
<Parent />
</Cls>
<Indirection>
<Empty />
</Indirection>
</div>
);
}
$RefreshReg$(App, 'App');
class Cls extends React.Component {
render() {
return this.props.children;
}
}
function Indirection({children}) {
return children;
}
function Empty() {
return null;
}
$RefreshReg$(Empty, 'Empty');
function Frag() {
return (
<>
<div className="Frag">
<div />
</div>
<div className="Frag">
<div />
</div>
</>
);
}
$RefreshReg$(Frag, 'Frag');
return App;
});
const parentFamily = ReactFreshRuntime.getFamilyByID('Parent');
const childFamily = ReactFreshRuntime.getFamilyByID('Child');
const emptyFamily = ReactFreshRuntime.getFamilyByID('Empty');
testFindHostInstancesForFamilies(
[parentFamily],
container.querySelectorAll('.Parent'),
);
testFindHostInstancesForFamilies(
[childFamily],
container.querySelectorAll('.Child'),
);
// When searching for both Parent and Child,
// we'll stop visual highlighting at the Parent.
testFindHostInstancesForFamilies(
[parentFamily, childFamily],
container.querySelectorAll('.Parent'),
);
// When we can't find host nodes, use the closest parent.
testFindHostInstancesForFamilies(
[emptyFamily],
container.querySelectorAll('.App'),
);
}
});
function testFindHostInstancesForFamilies(families, expectedNodes) {
const foundInstances = Array.from(
ReactFreshRuntime.findAffectedHostInstances(families),
);
expect(foundInstances.length).toEqual(expectedNodes.length);
foundInstances.forEach((node, i) => {
expect(node).toBe(expectedNodes[i]);
});
}
it('can update multiple roots independently', () => {
if (__DEV__) {
// Declare the first version.
const HelloV1 = () => {
const [val, setVal] = React.useState(0);
return (
<p style={{color: 'blue'}} onClick={() => setVal(val + 1)}>
{val}
</p>
);
};
$RefreshReg$(HelloV1, 'Hello');
// Perform a hot update before any roots exist.
const HelloV2 = () => {
const [val, setVal] = React.useState(0);
return (
<p style={{color: 'red'}} onClick={() => setVal(val + 1)}>
{val}
</p>
);
};
$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(<HelloV1 id={1} />, cont1);
ReactDOM.render(<HelloV2 id={2} />, cont2);
ReactDOM.render(<HelloV1 id={3} />, cont3);
// Expect we see the V2 color.
expect(cont1.firstChild.style.color).toBe('red');
expect(cont2.firstChild.style.color).toBe('red');
expect(cont3.firstChild.style.color).toBe('red');
expect(cont1.firstChild.textContent).toBe('0');
expect(cont2.firstChild.textContent).toBe('0');
expect(cont3.firstChild.textContent).toBe('0');
// Bump the state for each of them.
act(() => {
cont1.firstChild.dispatchEvent(
new MouseEvent('click', {bubbles: true}),
);
cont2.firstChild.dispatchEvent(
new MouseEvent('click', {bubbles: true}),
);
cont3.firstChild.dispatchEvent(
new MouseEvent('click', {bubbles: true}),
);
});
expect(cont1.firstChild.style.color).toBe('red');
expect(cont2.firstChild.style.color).toBe('red');
expect(cont3.firstChild.style.color).toBe('red');
expect(cont1.firstChild.textContent).toBe('1');
expect(cont2.firstChild.textContent).toBe('1');
expect(cont3.firstChild.textContent).toBe('1');
// Perform another hot update.
const HelloV3 = () => {
const [val, setVal] = React.useState(0);
return (
<p style={{color: 'green'}} onClick={() => setVal(val + 1)}>
{val}
</p>
);
};
$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 (
<p style={{color: 'orange'}} onClick={() => setVal(val + 1)}>
{val}
</p>
);
};
$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);
const rogueProxy = new Proxy(
{},
{
get(target, property) {
throw new Error();
},
},
);
expect(ReactFreshRuntime.isLikelyComponentType(rogueProxy)).toBe(false);
// These seem like function components.
const Button = () => {};
expect(ReactFreshRuntime.isLikelyComponentType(Button)).toBe(true);
expect(ReactFreshRuntime.isLikelyComponentType(Widget)).toBe(true);
const ProxyButton = new Proxy(Button, {
get(target, property) {
return target[property];
},
});
expect(ReactFreshRuntime.isLikelyComponentType(ProxyButton)).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 {}
const ProxyBtn = new Proxy(Btn, {
get(target, property) {
return target[property];
},
});
expect(ReactFreshRuntime.isLikelyComponentType(Btn)).toBe(true);
expect(ReactFreshRuntime.isLikelyComponentType(PureBtn)).toBe(true);
expect(ReactFreshRuntime.isLikelyComponentType(ProxyBtn)).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 (
<p style={{color: 'blue'}} onClick={() => setVal(val + 1)}>
{val}
</p>
);
};
$RefreshReg$(HelloV1, 'Hello');
const HelloV2 = () => {
const [val, setVal] = React.useState(0);
return (
<p style={{color: 'red'}} onClick={() => setVal(val + 1)}>
{val}
</p>
);
};
$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.
}
});
function initFauxDevToolsHook() {
const onCommitFiberRoot = jest.fn();
const onCommitFiberUnmount = jest.fn();
let idCounter = 0;
const renderers = new Map();
// This is a minimal shim for the global hook installed by DevTools.
// The real one is in packages/react-devtools-shared/src/hook.js.
global.__REACT_DEVTOOLS_GLOBAL_HOOK__ = {
renderers,
supportsFiber: true,
inject(renderer) {
const id = ++idCounter;
renderers.set(id, renderer);
return id;
},
onCommitFiberRoot,
onCommitFiberUnmount,
};
}
// This simulates the scenario in https://github.com/facebook/react/issues/17626
it('can inject the runtime after the renderer executes', async () => {
if (__DEV__) {
initFauxDevToolsHook();
// 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;
internalAct = require('internal-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 (
<p style={{color: 'blue'}} onClick={() => setVal(val + 1)}>
{val}
</p>
);
}
$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 (
<p style={{color: 'red'}} onClick={() => setVal(val + 1)}>
{val}
</p>
);
}
$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');
}
});
// This simulates the scenario in https://github.com/facebook/react/issues/20100
it('does not block DevTools when an unsupported renderer is injected', () => {
if (__DEV__) {
initFauxDevToolsHook();
const onCommitFiberRoot =
global.__REACT_DEVTOOLS_GLOBAL_HOOK__.onCommitFiberRoot;
// Redirect all React/ReactDOM requires to v16.8.0
// This version predates Fast Refresh support.
jest.mock('scheduler', () => jest.requireActual('scheduler-0-13'));
jest.mock('scheduler/tracing', () =>
jest.requireActual('scheduler-0-13/tracing'),
);
jest.mock('react', () => jest.requireActual('react-16-8'));
jest.mock('react-dom', () => jest.requireActual('react-dom-16-8'));
// Load React and company.
jest.resetModules();
React = require('react');
ReactDOM = require('react-dom');
Scheduler = require('scheduler');
// Important! Inject into the global hook *after* ReactDOM runs:
ReactFreshRuntime = require('react-refresh/runtime');
ReactFreshRuntime.injectIntoGlobalHook(global);
render(() => {
function Hello() {
return <div>Hi!</div>;
}
$RefreshReg$(Hello, 'Hello');
return Hello;
});
expect(onCommitFiberRoot).toHaveBeenCalled();
}
});
});