[DOM] disable legacy mode behind flag (#28468)

Adds a flag to disable legacy mode. Currently this flag is used to cause
legacy mode apis like render and hydrate to throw. This change also
removes render, hydrate, unmountComponentAtNode, and
unstable_renderSubtreeIntoContainer from the experiemntal entrypoint.
Right now for Meta builds this flag is off (legacy mode is still
supported). In OSS builds this flag matches __NEXT_MAJOR__ which means
it currently is on in experiemental. This means that after merging
legacy mode is effectively removed from experimental builds. While this
is a breaking change, experimental builds are not stable and users can
pin to older versions or update their use of react-dom to no longer use
legacy mode APIs.
This commit is contained in:
Josh Story
2024-03-04 08:19:17 -08:00
committed by GitHub
parent d3ec0b30a8
commit f236ca2cdc
46 changed files with 483 additions and 187 deletions
@@ -227,6 +227,7 @@ describe('Store', () => {
// @reactVersion >= 18.0
// @reactVersion < 19
// @gate !disableLegacyMode
it('should support mount and update operations for multiple roots (legacy render)', () => {
const Parent = ({count}) =>
new Array(count).fill(true).map((_, index) => <Child key={index} />);
@@ -941,6 +942,7 @@ describe('Store', () => {
// @reactVersion >= 18.0
// @reactVersion < 19
// @gate !disableLegacyMode
it('should support mount and update operations for multiple roots (legacy render)', () => {
const Parent = ({count}) =>
new Array(count).fill(true).map((_, index) => <Child key={index} />);
@@ -1469,6 +1471,7 @@ describe('Store', () => {
// @reactVersion >= 18.0
// @reactVersion < 19
// @gate !disableLegacyMode
it('detects and updates profiling support based on the attached roots (legacy render)', () => {
const Component = () => null;
@@ -1632,6 +1635,7 @@ describe('Store', () => {
// @reactVersion >= 18.0
// @reactVersion < 19
// @gate !disableLegacyMode
it('should support Lazy components (legacy render)', async () => {
const container = document.createElement('div');
@@ -1702,6 +1706,7 @@ describe('Store', () => {
// @reactVersion >= 18.0
// @reactVersion < 19
// @gate !disableLegacyMode
it('should support Lazy components that are unmounted before they finish loading (legacy render)', async () => {
const container = document.createElement('div');
@@ -36,6 +36,7 @@ describe('StoreStress (Legacy Mode)', () => {
// It renders different trees that should produce the same output.
// @reactVersion >= 16.9
// @reactVersion < 19
// @gate !disableLegacyMode
it('should handle a stress test with different tree operations (Legacy Mode)', () => {
let setShowX;
const A = () => 'a';
-4
View File
@@ -14,11 +14,7 @@ export {
hydrateRoot,
findDOMNode,
flushSync,
hydrate,
render,
unmountComponentAtNode,
unstable_batchedUpdates,
unstable_renderSubtreeIntoContainer,
unstable_runWithPriority, // DO NOT USE: Temporarily exposed to migrate off of Scheduler.runWithPriority.
useFormStatus,
useFormState,
@@ -26,6 +26,7 @@ describe('ReactComponent', () => {
act = require('internal-test-utils').act;
});
// @gate !disableLegacyMode
it('should throw on invalid render targets in legacy roots', () => {
const container = document.createElement('div');
// jQuery objects are basically arrays; people often pass them in by mistake
@@ -455,6 +456,7 @@ describe('ReactComponent', () => {
/* eslint-enable indent */
});
// @gate !disableLegacyMode
it('fires the callback after a component is rendered in legacy roots', () => {
const callback = jest.fn();
const container = document.createElement('div');
@@ -630,6 +630,7 @@ describe('ReactCompositeComponent-state', () => {
);
});
// @gate !disableLegacyMode
it('Legacy mode should support setState in componentWillUnmount (#18851)', () => {
let subscription;
class A extends React.Component {
+2
View File
@@ -164,6 +164,7 @@ describe('ReactDOM', () => {
expect(dog.className).toBe('bigdog');
});
// @gate !disableLegacyMode
it('throws in render() if the mount callback in legacy roots is not a function', async () => {
function Foo() {
this.a = 1;
@@ -216,6 +217,7 @@ describe('ReactDOM', () => {
);
});
// @gate !disableLegacyMode
it('throws in render() if the update callback in legacy roots is not a function', async () => {
function Foo() {
this.a = 1;
+13 -9
View File
@@ -332,7 +332,7 @@ describe('ReactDOMComponent', () => {
});
});
it('throws with Temporal-like objects as style values', () => {
it('throws with Temporal-like objects as style values', async () => {
class TemporalLike {
valueOf() {
// Throwing here is the behavior of ECMAScript "Temporal" date/time API.
@@ -344,14 +344,17 @@ describe('ReactDOMComponent', () => {
}
}
const style = {fontSize: new TemporalLike()};
const div = document.createElement('div');
const test = () => ReactDOM.render(<span style={style} />, div);
expect(() =>
expect(test).toThrowError(new TypeError('prod message')),
).toErrorDev(
'Warning: The provided `fontSize` CSS property is an unsupported type TemporalLike.' +
' This value must be coerced to a string before using it here.',
);
const root = ReactDOMClient.createRoot(document.createElement('div'));
await expect(async () => {
await expect(async () => {
await act(() => {
root.render(<span style={style} />);
});
}).toErrorDev(
'Warning: The provided `fontSize` CSS property is an unsupported type TemporalLike.' +
' This value must be coerced to a string before using it here.',
);
}).rejects.toThrowError(new TypeError('prod message'));
});
it('should update styles if initially null', async () => {
@@ -3688,6 +3691,7 @@ describe('ReactDOMComponent', () => {
expect(typeof portalContainer.onclick).toBe('function');
});
// @gate !disableLegacyMode
it('does not add onclick handler to the React root in legacy mode', () => {
const container = document.createElement('div');
@@ -55,6 +55,7 @@ describe('ReactDOMConsoleErrorReporting', () => {
});
describe('ReactDOM.render', () => {
// @gate !disableLegacyMode
it('logs errors during event handlers', async () => {
spyOnDevAndProd(console, 'error');
@@ -156,6 +157,7 @@ describe('ReactDOMConsoleErrorReporting', () => {
}
});
// @gate !disableLegacyMode
it('logs render errors without an error boundary', async () => {
spyOnDevAndProd(console, 'error');
@@ -223,6 +225,7 @@ describe('ReactDOMConsoleErrorReporting', () => {
}
});
// @gate !disableLegacyMode
it('logs render errors with an error boundary', async () => {
spyOnDevAndProd(console, 'error');
@@ -295,6 +298,7 @@ describe('ReactDOMConsoleErrorReporting', () => {
}
});
// @gate !disableLegacyMode
it('logs layout effect errors without an error boundary', async () => {
spyOnDevAndProd(console, 'error');
@@ -365,6 +369,7 @@ describe('ReactDOMConsoleErrorReporting', () => {
}
});
// @gate !disableLegacyMode
it('logs layout effect errors with an error boundary', async () => {
spyOnDevAndProd(console, 'error');
@@ -440,6 +445,7 @@ describe('ReactDOMConsoleErrorReporting', () => {
}
});
// @gate !disableLegacyMode
it('logs passive effect errors without an error boundary', async () => {
spyOnDevAndProd(console, 'error');
@@ -511,6 +517,7 @@ describe('ReactDOMConsoleErrorReporting', () => {
}
});
// @gate !disableLegacyMode
it('logs passive effect errors with an error boundary', async () => {
spyOnDevAndProd(console, 'error');
@@ -50,6 +50,7 @@ describe('ReactDOMFiberAsync', () => {
document.body.removeChild(container);
});
// @gate !disableLegacyMode
it('renders synchronously by default in legacy mode', () => {
const ops = [];
ReactDOM.render(<div>Hi</div>, container, () => {
+45
View File
@@ -35,6 +35,7 @@ describe('ReactDOMHooks', () => {
document.body.removeChild(container);
});
// @gate !disableLegacyMode
it('can ReactDOM.render() from useEffect', async () => {
const container2 = document.createElement('div');
const container3 = document.createElement('div');
@@ -76,6 +77,50 @@ describe('ReactDOMHooks', () => {
expect(container3.textContent).toBe('6');
});
it('can render() from useEffect', async () => {
const container2 = document.createElement('div');
const container3 = document.createElement('div');
const root1 = ReactDOMClient.createRoot(container);
const root2 = ReactDOMClient.createRoot(container2);
const root3 = ReactDOMClient.createRoot(container3);
function Example1({n}) {
React.useEffect(() => {
root2.render(<Example2 n={n} />);
});
return 1 * n;
}
function Example2({n}) {
React.useEffect(() => {
root3.render(<Example3 n={n} />);
});
return 2 * n;
}
function Example3({n}) {
return 3 * n;
}
await act(() => {
root1.render(<Example1 n={1} />);
});
await waitForAll([]);
expect(container.textContent).toBe('1');
expect(container2.textContent).toBe('2');
expect(container3.textContent).toBe('3');
await act(() => {
root1.render(<Example1 n={2} />);
});
await waitForAll([]);
expect(container.textContent).toBe('2');
expect(container2.textContent).toBe('4');
expect(container3.textContent).toBe('6');
});
// @gate !disableLegacyMode
it('should not bail out when an update is scheduled from within an event handler', () => {
const {createRef, useCallback, useState} = React;
+11 -13
View File
@@ -733,7 +733,7 @@ describe('ReactDOMInput', () => {
expect(node.value).toBe('foobar');
});
it('should throw for date inputs if `defaultValue` is an object where valueOf() throws', () => {
it('should throw for date inputs if `defaultValue` is an object where valueOf() throws', async () => {
class TemporalLike {
valueOf() {
// Throwing here is the behavior of ECMAScript "Temporal" date/time API.
@@ -744,19 +744,16 @@ describe('ReactDOMInput', () => {
return '2020-01-01';
}
}
const legacyContainer = document.createElement('div');
document.body.appendChild(legacyContainer);
const test = () =>
ReactDOM.render(
<input defaultValue={new TemporalLike()} type="date" />,
legacyContainer,
await expect(async () => {
await expect(async () => {
await act(() => {
root.render(<input defaultValue={new TemporalLike()} type="date" />);
});
}).toErrorDev(
'Form field values (value, checked, defaultValue, or defaultChecked props) must be ' +
'strings, not TemporalLike. This value must be coerced to a string before using it here.',
);
expect(() =>
expect(test).toThrowError(new TypeError('prod message')),
).toErrorDev(
'Form field values (value, checked, defaultValue, or defaultChecked props) must be ' +
'strings, not TemporalLike. This value must be coerced to a string before using it here.',
);
}).rejects.toThrowError(new TypeError('prod message'));
});
it('should throw for text inputs if `defaultValue` is an object where valueOf() throws', async () => {
@@ -1736,6 +1733,7 @@ describe('ReactDOMInput', () => {
assertInputTrackingIsCurrent(container);
});
// @gate !disableLegacyMode
it('should control radio buttons if the tree updates during render in legacy mode', async () => {
container.remove();
container = document.createElement('div');
@@ -27,6 +27,7 @@ describe('ReactDOMComponentTree', () => {
container = null;
});
// @gate !disableLegacyMode
it('finds instance of node that is attempted to be unmounted', () => {
const component = <div />;
const node = ReactDOM.render(<div>{component}</div>, container);
@@ -39,6 +40,7 @@ describe('ReactDOMComponentTree', () => {
);
});
// @gate !disableLegacyMode
it('finds instance from node to stop rendering over other react rendered components', () => {
const component = (
<div>
@@ -27,6 +27,7 @@ describe('ReactDOMLegacyFiber', () => {
jest.restoreAllMocks();
});
// @gate !disableLegacyMode
it('should render strings as children', () => {
const Box = ({value}) => <div>{value}</div>;
@@ -34,6 +35,7 @@ describe('ReactDOMLegacyFiber', () => {
expect(container.textContent).toEqual('foo');
});
// @gate !disableLegacyMode
it('should render numbers as children', () => {
const Box = ({value}) => <div>{value}</div>;
@@ -42,6 +44,7 @@ describe('ReactDOMLegacyFiber', () => {
expect(container.textContent).toEqual('10');
});
// @gate !disableLegacyMode
it('should be called a callback argument', () => {
// mounting phase
let called = false;
@@ -54,6 +57,7 @@ describe('ReactDOMLegacyFiber', () => {
expect(called).toEqual(true);
});
// @gate !disableLegacyMode
it('should call a callback argument when the same element is re-rendered', () => {
class Foo extends React.Component {
render() {
@@ -75,6 +79,7 @@ describe('ReactDOMLegacyFiber', () => {
expect(called).toEqual(true);
});
// @gate !disableLegacyMode
it('should render a component returning strings directly from render', () => {
const Text = ({value}) => value;
@@ -82,6 +87,7 @@ describe('ReactDOMLegacyFiber', () => {
expect(container.textContent).toEqual('foo');
});
// @gate !disableLegacyMode
it('should render a component returning numbers directly from render', () => {
const Text = ({value}) => value;
@@ -90,6 +96,7 @@ describe('ReactDOMLegacyFiber', () => {
expect(container.textContent).toEqual('10');
});
// @gate !disableLegacyMode
it('finds the DOM Text node of a string child', () => {
class Text extends React.Component {
render() {
@@ -109,6 +116,7 @@ describe('ReactDOMLegacyFiber', () => {
expect(textNode.nodeValue).toBe('foo');
});
// @gate !disableLegacyMode
it('finds the first child when a component returns a fragment', () => {
class Fragment extends React.Component {
render() {
@@ -126,6 +134,7 @@ describe('ReactDOMLegacyFiber', () => {
expect(firstNode.tagName).toBe('DIV');
});
// @gate !disableLegacyMode
it('finds the first child even when fragment is nested', () => {
class Wrapper extends React.Component {
render() {
@@ -154,6 +163,7 @@ describe('ReactDOMLegacyFiber', () => {
expect(firstNode.tagName).toBe('DIV');
});
// @gate !disableLegacyMode
it('finds the first child even when first child renders null', () => {
class NullComponent extends React.Component {
render() {
@@ -177,6 +187,7 @@ describe('ReactDOMLegacyFiber', () => {
expect(firstNode.tagName).toBe('DIV');
});
// @gate !disableLegacyMode
it('renders an empty fragment', () => {
const Div = () => <div />;
const EmptyFragment = () => <></>;
@@ -232,6 +243,7 @@ describe('ReactDOMLegacyFiber', () => {
expect(testContainer.innerHTML).toBe('');
};
// @gate !disableLegacyMode
it('should render one portal', () => {
const portalContainer = document.createElement('div');
@@ -247,6 +259,7 @@ describe('ReactDOMLegacyFiber', () => {
expect(container.innerHTML).toBe('');
});
// @gate !disableLegacyMode
it('should render many portals', () => {
const portalContainer1 = document.createElement('div');
const portalContainer2 = document.createElement('div');
@@ -347,6 +360,7 @@ describe('ReactDOMLegacyFiber', () => {
]);
});
// @gate !disableLegacyMode
it('should render nested portals', () => {
const portalContainer1 = document.createElement('div');
const portalContainer2 = document.createElement('div');
@@ -390,6 +404,7 @@ describe('ReactDOMLegacyFiber', () => {
expect(container.innerHTML).toBe('');
});
// @gate !disableLegacyMode
it('should reconcile portal children', () => {
const portalContainer = document.createElement('div');
@@ -436,6 +451,7 @@ describe('ReactDOMLegacyFiber', () => {
expect(container.innerHTML).toBe('<div></div>');
});
// @gate !disableLegacyMode
it('should unmount empty portal component wherever it appears', () => {
const portalContainer = document.createElement('div');
@@ -470,6 +486,7 @@ describe('ReactDOMLegacyFiber', () => {
expect(container.innerHTML).toBe('<div><div>parent</div></div>');
});
// @gate !disableLegacyMode
it('should keep track of namespace across portals (simple)', () => {
assertNamespacesMatch(
<svg {...expectSVG}>
@@ -498,6 +515,7 @@ describe('ReactDOMLegacyFiber', () => {
);
});
// @gate !disableLegacyMode
it('should keep track of namespace across portals (medium)', () => {
assertNamespacesMatch(
<svg {...expectSVG}>
@@ -562,6 +580,7 @@ describe('ReactDOMLegacyFiber', () => {
);
});
// @gate !disableLegacyMode
it('should keep track of namespace across portals (complex)', () => {
assertNamespacesMatch(
<div {...expectHTML}>
@@ -636,6 +655,7 @@ describe('ReactDOMLegacyFiber', () => {
);
});
// @gate !disableLegacyMode
it('should unwind namespaces on uncaught errors', () => {
function BrokenRender() {
throw new Error('Hello');
@@ -651,6 +671,7 @@ describe('ReactDOMLegacyFiber', () => {
assertNamespacesMatch(<div {...expectHTML} />);
});
// @gate !disableLegacyMode
it('should unwind namespaces on caught errors', () => {
function BrokenRender() {
throw new Error('Hello');
@@ -684,6 +705,7 @@ describe('ReactDOMLegacyFiber', () => {
assertNamespacesMatch(<div {...expectHTML} />);
});
// @gate !disableLegacyMode
it('should unwind namespaces on caught errors in a portal', () => {
function BrokenRender() {
throw new Error('Hello');
@@ -719,6 +741,7 @@ describe('ReactDOMLegacyFiber', () => {
});
// @gate !disableLegacyContext
// @gate !disableLegacyMode
it('should pass portal context when rendering subtree elsewhere', () => {
const portalContainer = document.createElement('div');
@@ -754,6 +777,7 @@ describe('ReactDOMLegacyFiber', () => {
});
// @gate !disableLegacyContext
// @gate !disableLegacyMode
it('should update portal context if it changes due to setState', () => {
const portalContainer = document.createElement('div');
@@ -799,6 +823,7 @@ describe('ReactDOMLegacyFiber', () => {
});
// @gate !disableLegacyContext
// @gate !disableLegacyMode
it('should update portal context if it changes due to re-render', () => {
const portalContainer = document.createElement('div');
@@ -839,6 +864,7 @@ describe('ReactDOMLegacyFiber', () => {
expect(container.innerHTML).toBe('');
});
// @gate !disableLegacyMode
it('findDOMNode should find dom element after expanding a fragment', () => {
class MyNode extends React.Component {
render() {
@@ -859,6 +885,7 @@ describe('ReactDOMLegacyFiber', () => {
expect(b.tagName).toBe('SPAN');
});
// @gate !disableLegacyMode
it('should bubble events from the portal to the parent', () => {
const portalContainer = document.createElement('div');
document.body.appendChild(portalContainer);
@@ -890,6 +917,7 @@ describe('ReactDOMLegacyFiber', () => {
}
});
// @gate !disableLegacyMode
it('should not onMouseLeave when staying in the portal', () => {
const portalContainer = document.createElement('div');
document.body.appendChild(portalContainer);
@@ -966,6 +994,7 @@ describe('ReactDOMLegacyFiber', () => {
});
// Regression test for https://github.com/facebook/react/issues/19562
// @gate !disableLegacyMode
it('does not fire mouseEnter twice when relatedTarget is the root node', () => {
let ops = [];
let target = null;
@@ -1016,6 +1045,7 @@ describe('ReactDOMLegacyFiber', () => {
expect(ops).toEqual([]);
});
// @gate !disableLegacyMode
it('listens to events that do not exist in the Portal subtree', () => {
const onClick = jest.fn();
@@ -1043,6 +1073,7 @@ describe('ReactDOMLegacyFiber', () => {
}).toThrow('Target container is not a DOM element.');
});
// @gate !disableLegacyMode
it('should warn for non-functional event listeners', () => {
class Example extends React.Component {
render() {
@@ -1056,6 +1087,7 @@ describe('ReactDOMLegacyFiber', () => {
);
});
// @gate !disableLegacyMode
it('should warn with a special message for `false` event listeners', () => {
class Example extends React.Component {
render() {
@@ -1071,6 +1103,7 @@ describe('ReactDOMLegacyFiber', () => {
);
});
// @gate !disableLegacyMode
it('should not update event handlers until commit', () => {
spyOnDev(console, 'error');
@@ -1168,6 +1201,7 @@ describe('ReactDOMLegacyFiber', () => {
}
});
// @gate !disableLegacyMode
it('should not crash encountering low-priority tree', () => {
ReactDOM.render(
<div hidden={true}>
@@ -1177,6 +1211,7 @@ describe('ReactDOMLegacyFiber', () => {
);
});
// @gate !disableLegacyMode
it('should not warn when rendering into an empty container', () => {
ReactDOM.render(<div>foo</div>, container);
expect(container.innerHTML).toBe('<div>foo</div>');
@@ -1186,6 +1221,7 @@ describe('ReactDOMLegacyFiber', () => {
expect(container.innerHTML).toBe('<div>bar</div>');
});
// @gate !disableLegacyMode
it('should warn when replacing a container which was manually updated outside of React', () => {
// when not messing with the DOM outside of React
ReactDOM.render(<div key="1">foo</div>, container);
@@ -1210,6 +1246,7 @@ describe('ReactDOMLegacyFiber', () => {
}).toThrowError();
});
// @gate !disableLegacyMode
it('should warn when doing an update to a container manually updated outside of React', () => {
// when not messing with the DOM outside of React
ReactDOM.render(<div>foo</div>, container);
@@ -1227,6 +1264,7 @@ describe('ReactDOMLegacyFiber', () => {
);
});
// @gate !disableLegacyMode
it('should warn when doing an update to a container manually cleared outside of React', () => {
// when not messing with the DOM outside of React
ReactDOM.render(<div>foo</div>, container);
@@ -1244,6 +1282,7 @@ describe('ReactDOMLegacyFiber', () => {
);
});
// @gate !disableLegacyMode
it('should render a text component with a text DOM node on the same document as the container', () => {
// 1. Create a new document through the use of iframe
// 2. Set up the spy to make asserts when a text component
@@ -1274,6 +1313,7 @@ describe('ReactDOMLegacyFiber', () => {
expect(iframeContainer.appendChild).toHaveBeenCalledTimes(1);
});
// @gate !disableLegacyMode
it('should mount into a document fragment', () => {
const fragment = document.createDocumentFragment();
ReactDOM.render(<div>foo</div>, fragment);
@@ -1283,6 +1323,7 @@ describe('ReactDOMLegacyFiber', () => {
});
// Regression test for https://github.com/facebook/react/issues/12643#issuecomment-413727104
// @gate !disableLegacyMode
it('should not diff memoized host components', () => {
const inputRef = React.createRef();
let didCallOnChange = false;
@@ -1341,6 +1382,7 @@ describe('ReactDOMLegacyFiber', () => {
expect(didCallOnChange).toBe(true);
});
// @gate !disableLegacyMode
it('unmounted legacy roots should never clear newer root content from a container', () => {
const ref = React.createRef();
+62 -63
View File
@@ -13,7 +13,6 @@ describe('ReactDOMOption', () => {
let React;
let ReactDOMClient;
let ReactDOMServer;
let ReactTestUtils;
let act;
beforeEach(() => {
@@ -21,41 +20,47 @@ describe('ReactDOMOption', () => {
React = require('react');
ReactDOMClient = require('react-dom/client');
ReactDOMServer = require('react-dom/server');
ReactTestUtils = require('react-dom/test-utils');
act = require('internal-test-utils').act;
});
it('should flatten children to a string', () => {
async function renderIntoDocument(children) {
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
await act(async () => root.render(children));
return container;
}
it('should flatten children to a string', async () => {
const stub = (
<option>
{1} {'foo'}
</option>
);
const node = ReactTestUtils.renderIntoDocument(stub);
const container = await renderIntoDocument(stub);
expect(node.innerHTML).toBe('1 foo');
expect(container.firstChild.innerHTML).toBe('1 foo');
});
it('should warn for invalid child tags', () => {
it('should warn for invalid child tags', async () => {
const el = (
<option value="12">
{1} <div /> {2}
</option>
);
let node;
expect(() => {
node = ReactTestUtils.renderIntoDocument(el);
let container;
await expect(async () => {
container = await renderIntoDocument(el);
}).toErrorDev(
'In HTML, <div> cannot be a child of <option>.\n' +
'This will cause a hydration error.\n' +
' in div (at **)\n' +
' in option (at **)',
);
expect(node.innerHTML).toBe('1 <div></div> 2');
ReactTestUtils.renderIntoDocument(el);
expect(container.firstChild.innerHTML).toBe('1 <div></div> 2');
await renderIntoDocument(el);
});
it('should warn for component child if no value prop is provided', () => {
it('should warn for component child if no value prop is provided', async () => {
function Foo() {
return '2';
}
@@ -64,18 +69,18 @@ describe('ReactDOMOption', () => {
{1} <Foo /> {3}
</option>
);
let node;
expect(() => {
node = ReactTestUtils.renderIntoDocument(el);
let container;
await expect(async () => {
container = await renderIntoDocument(el);
}).toErrorDev(
'Cannot infer the option value of complex children. ' +
'Pass a `value` prop or use a plain string as children to <option>.',
);
expect(node.innerHTML).toBe('1 2 3');
ReactTestUtils.renderIntoDocument(el);
expect(container.firstChild.innerHTML).toBe('1 2 3');
await renderIntoDocument(el);
});
it('should not warn for component child if value prop is provided', () => {
it('should not warn for component child if value prop is provided', async () => {
function Foo() {
return '2';
}
@@ -84,12 +89,12 @@ describe('ReactDOMOption', () => {
{1} <Foo /> {3}
</option>
);
const node = ReactTestUtils.renderIntoDocument(el);
expect(node.innerHTML).toBe('1 2 3');
ReactTestUtils.renderIntoDocument(el);
const container = await renderIntoDocument(el);
expect(container.firstChild.innerHTML).toBe('1 2 3');
await renderIntoDocument(el);
});
it('should ignore null/undefined/false children without warning', () => {
it('should ignore null/undefined/false children without warning', async () => {
const stub = (
<option>
{1} {false}
@@ -98,38 +103,38 @@ describe('ReactDOMOption', () => {
{undefined} {2}
</option>
);
const node = ReactTestUtils.renderIntoDocument(stub);
const container = await renderIntoDocument(stub);
expect(node.innerHTML).toBe('1 2');
expect(container.firstChild.innerHTML).toBe('1 2');
});
it('should throw on object children', () => {
expect(() => {
ReactTestUtils.renderIntoDocument(<option>{{}}</option>);
}).toThrow('Objects are not valid as a React child');
expect(() => {
ReactTestUtils.renderIntoDocument(<option>{[{}]}</option>);
}).toThrow('Objects are not valid as a React child');
expect(() => {
ReactTestUtils.renderIntoDocument(
it('should throw on object children', async () => {
await expect(async () =>
renderIntoDocument(<option>{{}}</option>),
).rejects.toThrow('Objects are not valid as a React child');
await expect(async () => {
await renderIntoDocument(<option>{[{}]}</option>);
}).rejects.toThrow('Objects are not valid as a React child');
await expect(async () => {
await renderIntoDocument(
<option>
{{}}
<span />
</option>,
);
}).toThrow('Objects are not valid as a React child');
expect(() => {
ReactTestUtils.renderIntoDocument(
}).rejects.toThrow('Objects are not valid as a React child');
await expect(async () => {
await renderIntoDocument(
<option>
{'1'}
{{}}
{2}
</option>,
);
}).toThrow('Objects are not valid as a React child');
}).rejects.toThrow('Objects are not valid as a React child');
});
it('should support element-ish child', () => {
it('should support element-ish child', async () => {
// This is similar to <fbt>.
// We don't toString it because you must instead provide a value prop.
const obj = {
@@ -145,51 +150,45 @@ describe('ReactDOMOption', () => {
},
};
let node = ReactTestUtils.renderIntoDocument(
<option value="a">{obj}</option>,
);
expect(node.innerHTML).toBe('hello');
let container = await renderIntoDocument(<option value="a">{obj}</option>);
expect(container.firstChild.innerHTML).toBe('hello');
node = ReactTestUtils.renderIntoDocument(
<option value="b">{[obj]}</option>,
);
expect(node.innerHTML).toBe('hello');
container = await renderIntoDocument(<option value="b">{[obj]}</option>);
expect(container.firstChild.innerHTML).toBe('hello');
node = ReactTestUtils.renderIntoDocument(
<option value={obj}>{obj}</option>,
);
expect(node.innerHTML).toBe('hello');
expect(node.value).toBe('hello');
container = await renderIntoDocument(<option value={obj}>{obj}</option>);
expect(container.firstChild.innerHTML).toBe('hello');
expect(container.firstChild.value).toBe('hello');
node = ReactTestUtils.renderIntoDocument(
container = await renderIntoDocument(
<option value={obj}>
{'1'}
{obj}
{2}
</option>,
);
expect(node.innerHTML).toBe('1hello2');
expect(node.value).toBe('hello');
expect(container.firstChild.innerHTML).toBe('1hello2');
expect(container.firstChild.value).toBe('hello');
});
// @gate enableBigIntSupport
it('should support bigint values', () => {
const node = ReactTestUtils.renderIntoDocument(<option>{5n}</option>);
expect(node.innerHTML).toBe('5');
expect(node.value).toBe('5');
it('should support bigint values', async () => {
const container = await renderIntoDocument(<option>{5n}</option>);
expect(container.firstChild.innerHTML).toBe('5');
expect(container.firstChild.value).toBe('5');
});
it('should be able to use dangerouslySetInnerHTML on option', () => {
it('should be able to use dangerouslySetInnerHTML on option', async () => {
const stub = <option dangerouslySetInnerHTML={{__html: 'foobar'}} />;
let node;
expect(() => {
node = ReactTestUtils.renderIntoDocument(stub);
let container;
await expect(async () => {
container = await renderIntoDocument(stub);
}).toErrorDev(
'Pass a `value` prop if you set dangerouslyInnerHTML so React knows which value should be selected.\n' +
' in option (at **)',
);
expect(node.innerHTML).toBe('foobar');
expect(container.firstChild.innerHTML).toBe('foobar');
});
it('should set attribute for empty value', async () => {
@@ -965,6 +965,7 @@ describe('ReactDOMSelect', () => {
expect(node.options[2].selected).toBe(false); // c
});
// @gate !disableLegacyMode
it('should allow controlling `value` in a nested legacy render', async () => {
let selectNode;
@@ -477,6 +477,7 @@ describe('ReactDOMServerIntegration (legacy)', () => {
resetModules();
});
// @gate !disableLegacyMode
it('legacy mode can explicitly ignore errors reconnecting different element types of children', () =>
expectMarkupMatch(
<div>
@@ -487,6 +488,7 @@ describe('ReactDOMServerIntegration (legacy)', () => {
</div>,
));
// @gate !disableLegacyMode
it('legacy mode can explicitly ignore reconnecting more children', () =>
expectMarkupMatch(
<div>
@@ -498,6 +500,7 @@ describe('ReactDOMServerIntegration (legacy)', () => {
</div>,
));
// @gate !disableLegacyMode
it('legacy mode can explicitly ignore reconnecting fewer children', () =>
expectMarkupMatch(
<div>
@@ -509,6 +512,7 @@ describe('ReactDOMServerIntegration (legacy)', () => {
</div>,
));
// @gate !disableLegacyMode
it('legacy mode can explicitly ignore reconnecting reordered children', () =>
expectMarkupMatch(
<div suppressHydrationWarning={true}>
@@ -718,6 +718,7 @@ describe('ReactDOMServerPartialHydration', () => {
expect(deleted.length).toBe(1);
});
// @gate !disableLegacyMode
it('warns and replaces the boundary content in legacy mode', async () => {
let suspend = false;
let resolve;
@@ -996,16 +996,19 @@ describe('ReactDOM HostSingleton', () => {
});
// https://github.com/facebook/react/issues/26128
// @gate !disableLegacyMode
it('(#26128) does not throw when rendering at body in legacy mode', async () => {
ReactDOM.render(<div />, document.body);
});
// https://github.com/facebook/react/issues/26128
// @gate !disableLegacyMode
it('(#26128) does not throw when rendering at <html> in legacy mode', async () => {
ReactDOM.render(<body />, document.documentElement);
});
// https://github.com/facebook/react/issues/26128
// @gate !disableLegacyMode
it('(#26128) does not throw when rendering at document in legacy mode', async () => {
ReactDOM.render(<html />, document);
});
@@ -100,6 +100,7 @@ describe('ReactDOMSuspensePlaceholder', () => {
return text;
}
// @gate !disableLegacyMode
it('hides and unhides timed out DOM elements in legacy roots', async () => {
const divs = [
React.createRef(null),
@@ -160,6 +161,7 @@ describe('ReactDOMSuspensePlaceholder', () => {
expect(container.textContent).toEqual('ABC');
});
// @gate !disableLegacyMode
it(
'in legacy roots, re-hides children if their display is updated ' +
'but the boundary is still showing the fallback',
@@ -213,6 +215,7 @@ describe('ReactDOMSuspensePlaceholder', () => {
);
// Regression test for https://github.com/facebook/react/issues/14188
// @gate !disableLegacyMode
it('can call findDOMNode() in a suspended component commit phase in legacy roots', async () => {
const log = [];
const Lazy = React.lazy(
@@ -25,6 +25,7 @@ describe('ReactLegacyCompositeComponent', () => {
act = require('internal-test-utils').act;
});
// @gate !disableLegacyMode
it('should warn about `setState` in render in legacy mode', () => {
const container = document.createElement('div');
@@ -377,6 +378,7 @@ describe('ReactLegacyCompositeComponent', () => {
});
// @gate !disableLegacyContext
// @gate !disableLegacyMode
it('unmasked context propagates through updates', () => {
class Leaf extends React.Component {
static contextTypes = {
@@ -441,6 +443,7 @@ describe('ReactLegacyCompositeComponent', () => {
});
// @gate !disableLegacyContext
// @gate !disableLegacyMode
it('should trigger componentWillReceiveProps for context changes', () => {
let contextChanges = 0;
let propChanges = 0;
@@ -553,6 +556,7 @@ describe('ReactLegacyCompositeComponent', () => {
expect(contextChanges).toBe(3); // ChildWithContext, GrandChild x 2
});
// @gate !disableLegacyMode
it('only renders once if updated in componentWillReceiveProps in legacy mode', () => {
let renders = 0;
@@ -581,6 +585,7 @@ describe('ReactLegacyCompositeComponent', () => {
expect(instance.state.updated).toBe(true);
});
// @gate !disableLegacyMode
it('only renders once if updated in componentWillReceiveProps when batching in legacy mode', () => {
let renders = 0;
@@ -611,6 +616,7 @@ describe('ReactLegacyCompositeComponent', () => {
expect(instance.state.updated).toBe(true);
});
// @gate !disableLegacyMode
it('should update refs if shouldComponentUpdate gives false in legacy mode', () => {
class Static extends React.Component {
shouldComponentUpdate() {
@@ -665,6 +671,7 @@ describe('ReactLegacyCompositeComponent', () => {
expect(ReactDOM.findDOMNode(comp.static1Ref.current).textContent).toBe('A');
});
// @gate !disableLegacyMode
it('should allow access to findDOMNode in componentWillUnmount in legacy mode', () => {
let a = null;
let b = null;
@@ -693,6 +700,7 @@ describe('ReactLegacyCompositeComponent', () => {
});
// @gate !disableLegacyContext || !__DEV__
// @gate !disableLegacyMode
it('context should be passed down from the parent', () => {
class Parent extends React.Component {
static childContextTypes = {
@@ -813,6 +821,7 @@ describe('ReactLegacyCompositeComponent', () => {
expect(moo.state.amIImmutable).toBe(undefined);
});
// @gate !disableLegacyMode
it('should not warn about unmounting during unmounting in legacy mode', () => {
const container = document.createElement('div');
const layer = document.createElement('div');
@@ -588,6 +588,7 @@ describe('ReactLegacyErrorBoundaries', () => {
jest.restoreAllMocks();
});
// @gate !disableLegacyMode
it('does not swallow exceptions on mounting without boundaries', () => {
let container = document.createElement('div');
expect(() => {
@@ -605,6 +606,7 @@ describe('ReactLegacyErrorBoundaries', () => {
}).toThrow('Hello');
});
// @gate !disableLegacyMode
it('does not swallow exceptions on updating without boundaries', () => {
let container = document.createElement('div');
ReactDOM.render(<BrokenComponentWillUpdate />, container);
@@ -625,6 +627,7 @@ describe('ReactLegacyErrorBoundaries', () => {
}).toThrow('Hello');
});
// @gate !disableLegacyMode
it('does not swallow exceptions on unmounting without boundaries', () => {
const container = document.createElement('div');
ReactDOM.render(<BrokenComponentWillUnmount />, container);
@@ -633,6 +636,7 @@ describe('ReactLegacyErrorBoundaries', () => {
}).toThrow('Hello');
});
// @gate !disableLegacyMode
it('prevents errors from leaking into other roots', () => {
const container1 = document.createElement('div');
const container2 = document.createElement('div');
@@ -670,6 +674,7 @@ describe('ReactLegacyErrorBoundaries', () => {
expect(container3.firstChild).toBe(null);
});
// @gate !disableLegacyMode
it('logs a single error using both error boundaries', () => {
const container = document.createElement('div');
spyOnDev(console, 'error');
@@ -715,6 +720,7 @@ describe('ReactLegacyErrorBoundaries', () => {
expect(log).toEqual(['BothErrorBoundaries componentWillUnmount']);
});
// @gate !disableLegacyMode
it('renders an error state if child throws in render', () => {
const container = document.createElement('div');
ReactDOM.render(
@@ -745,6 +751,7 @@ describe('ReactLegacyErrorBoundaries', () => {
expect(log).toEqual(['ErrorBoundary componentWillUnmount']);
});
// @gate !disableLegacyMode
it('renders an error state if child throws in constructor', () => {
const container = document.createElement('div');
ReactDOM.render(
@@ -773,6 +780,7 @@ describe('ReactLegacyErrorBoundaries', () => {
expect(log).toEqual(['ErrorBoundary componentWillUnmount']);
});
// @gate !disableLegacyMode
it('renders an error state if child throws in componentWillMount', () => {
const container = document.createElement('div');
ReactDOM.render(
@@ -801,6 +809,7 @@ describe('ReactLegacyErrorBoundaries', () => {
});
// @gate !disableLegacyContext || !__DEV__
// @gate !disableLegacyMode
it('renders an error state if context provider throws in componentWillMount', () => {
class BrokenComponentWillMountWithContext extends React.Component {
static childContextTypes = {foo: PropTypes.number};
@@ -826,6 +835,7 @@ describe('ReactLegacyErrorBoundaries', () => {
});
if (!require('shared/ReactFeatureFlags').disableModulePatternComponents) {
// @gate !disableLegacyMode
it('renders an error state if module-style context provider throws in componentWillMount', () => {
function BrokenComponentWillMountWithContext() {
return {
@@ -864,6 +874,7 @@ describe('ReactLegacyErrorBoundaries', () => {
});
}
// @gate !disableLegacyMode
it('mounts the error message if mounting fails', () => {
function renderError(error) {
return <ErrorMessage message={error.message} />;
@@ -902,6 +913,7 @@ describe('ReactLegacyErrorBoundaries', () => {
]);
});
// @gate !disableLegacyMode
it('propagates errors on retry on mounting', () => {
const container = document.createElement('div');
ReactDOM.render(
@@ -946,6 +958,7 @@ describe('ReactLegacyErrorBoundaries', () => {
expect(log).toEqual(['ErrorBoundary componentWillUnmount']);
});
// @gate !disableLegacyMode
it('propagates errors inside boundary during componentWillMount', () => {
const container = document.createElement('div');
ReactDOM.render(
@@ -974,6 +987,7 @@ describe('ReactLegacyErrorBoundaries', () => {
expect(log).toEqual(['ErrorBoundary componentWillUnmount']);
});
// @gate !disableLegacyMode
it('propagates errors inside boundary while rendering error state', () => {
const container = document.createElement('div');
ReactDOM.render(
@@ -1017,6 +1031,7 @@ describe('ReactLegacyErrorBoundaries', () => {
expect(log).toEqual(['ErrorBoundary componentWillUnmount']);
});
// @gate !disableLegacyMode
it('does not call componentWillUnmount when aborting initial mount', () => {
const container = document.createElement('div');
ReactDOM.render(
@@ -1056,6 +1071,7 @@ describe('ReactLegacyErrorBoundaries', () => {
expect(log).toEqual(['ErrorBoundary componentWillUnmount']);
});
// @gate !disableLegacyMode
it('resets callback refs if mounting aborts', () => {
function childRef(x) {
log.push('Child ref is set to ' + x);
@@ -1100,6 +1116,7 @@ describe('ReactLegacyErrorBoundaries', () => {
]);
});
// @gate !disableLegacyMode
it('resets object refs if mounting aborts', () => {
const childRef = React.createRef();
const errorMessageRef = React.createRef();
@@ -1140,6 +1157,7 @@ describe('ReactLegacyErrorBoundaries', () => {
expect(errorMessageRef.current).toEqual(null);
});
// @gate !disableLegacyMode
it('successfully mounts if no error occurs', () => {
const container = document.createElement('div');
ReactDOM.render(
@@ -1161,6 +1179,7 @@ describe('ReactLegacyErrorBoundaries', () => {
expect(log).toEqual(['ErrorBoundary componentWillUnmount']);
});
// @gate !disableLegacyMode
it('catches if child throws in constructor during update', () => {
const container = document.createElement('div');
ReactDOM.render(
@@ -1209,6 +1228,7 @@ describe('ReactLegacyErrorBoundaries', () => {
expect(log).toEqual(['ErrorBoundary componentWillUnmount']);
});
// @gate !disableLegacyMode
it('catches if child throws in componentWillMount during update', () => {
const container = document.createElement('div');
ReactDOM.render(
@@ -1258,6 +1278,7 @@ describe('ReactLegacyErrorBoundaries', () => {
expect(log).toEqual(['ErrorBoundary componentWillUnmount']);
});
// @gate !disableLegacyMode
it('catches if child throws in componentWillReceiveProps during update', () => {
const container = document.createElement('div');
ReactDOM.render(
@@ -1302,6 +1323,7 @@ describe('ReactLegacyErrorBoundaries', () => {
expect(log).toEqual(['ErrorBoundary componentWillUnmount']);
});
// @gate !disableLegacyMode
it('catches if child throws in componentWillUpdate during update', () => {
const container = document.createElement('div');
ReactDOM.render(
@@ -1347,6 +1369,7 @@ describe('ReactLegacyErrorBoundaries', () => {
expect(log).toEqual(['ErrorBoundary componentWillUnmount']);
});
// @gate !disableLegacyMode
it('catches if child throws in render during update', () => {
const container = document.createElement('div');
ReactDOM.render(
@@ -1396,6 +1419,7 @@ describe('ReactLegacyErrorBoundaries', () => {
expect(log).toEqual(['ErrorBoundary componentWillUnmount']);
});
// @gate !disableLegacyMode
it('keeps refs up-to-date during updates', () => {
function child1Ref(x) {
log.push('Child1 ref is set to ' + x);
@@ -1460,6 +1484,7 @@ describe('ReactLegacyErrorBoundaries', () => {
]);
});
// @gate !disableLegacyMode
it('recovers from componentWillUnmount errors on update', () => {
const container = document.createElement('div');
ReactDOM.render(
@@ -1516,6 +1541,7 @@ describe('ReactLegacyErrorBoundaries', () => {
expect(log).toEqual(['ErrorBoundary componentWillUnmount']);
});
// @gate !disableLegacyMode
it('recovers from nested componentWillUnmount errors on update', () => {
const container = document.createElement('div');
ReactDOM.render(
@@ -1577,6 +1603,7 @@ describe('ReactLegacyErrorBoundaries', () => {
expect(log).toEqual(['ErrorBoundary componentWillUnmount']);
});
// @gate !disableLegacyMode
it('picks the right boundary when handling unmounting errors', () => {
function renderInnerError(error) {
return <div>Caught an inner error: {error.message}.</div>;
@@ -1646,6 +1673,7 @@ describe('ReactLegacyErrorBoundaries', () => {
]);
});
// @gate !disableLegacyMode
it('can recover from error state', () => {
const container = document.createElement('div');
ReactDOM.render(
@@ -1694,6 +1722,7 @@ describe('ReactLegacyErrorBoundaries', () => {
]);
});
// @gate !disableLegacyMode
it('can update multiple times in error state', () => {
const container = document.createElement('div');
ReactDOM.render(
@@ -1718,6 +1747,7 @@ describe('ReactLegacyErrorBoundaries', () => {
ReactDOM.unmountComponentAtNode(container);
});
// @gate !disableLegacyMode
it("doesn't get into inconsistent state during removals", () => {
const container = document.createElement('div');
ReactDOM.render(
@@ -1737,6 +1767,7 @@ describe('ReactLegacyErrorBoundaries', () => {
expect(log).toEqual(['ErrorBoundary componentWillUnmount']);
});
// @gate !disableLegacyMode
it("doesn't get into inconsistent state during additions", () => {
const container = document.createElement('div');
ReactDOM.render(<ErrorBoundary />, container);
@@ -1755,6 +1786,7 @@ describe('ReactLegacyErrorBoundaries', () => {
expect(log).toEqual(['ErrorBoundary componentWillUnmount']);
});
// @gate !disableLegacyMode
it("doesn't get into inconsistent state during reorders", () => {
function getAMixOfNormalAndBrokenRenderElements() {
const elements = [];
@@ -1803,6 +1835,7 @@ describe('ReactLegacyErrorBoundaries', () => {
expect(log).toEqual(['ErrorBoundary componentWillUnmount']);
});
// @gate !disableLegacyMode
it('catches errors originating downstream', () => {
let fail = false;
class Stateful extends React.Component {
@@ -1845,6 +1878,7 @@ describe('ReactLegacyErrorBoundaries', () => {
expect(log).toEqual(['ErrorBoundary componentWillUnmount']);
});
// @gate !disableLegacyMode
it('catches errors in componentDidMount', () => {
const container = document.createElement('div');
ReactDOM.render(
@@ -1904,6 +1938,7 @@ describe('ReactLegacyErrorBoundaries', () => {
expect(log).toEqual(['ErrorBoundary componentWillUnmount']);
});
// @gate !disableLegacyMode
it('catches errors in componentDidUpdate', () => {
const container = document.createElement('div');
ReactDOM.render(
@@ -1943,6 +1978,7 @@ describe('ReactLegacyErrorBoundaries', () => {
expect(log).toEqual(['ErrorBoundary componentWillUnmount']);
});
// @gate !disableLegacyMode
it('propagates errors inside boundary during componentDidMount', () => {
const container = document.createElement('div');
ReactDOM.render(
@@ -1980,6 +2016,7 @@ describe('ReactLegacyErrorBoundaries', () => {
expect(log).toEqual(['ErrorBoundary componentWillUnmount']);
});
// @gate !disableLegacyMode
it('calls componentDidCatch for each error that is captured', () => {
function renderUnmountError(error) {
return <div>Caught an unmounting error: {error.message}.</div>;
@@ -2081,6 +2118,7 @@ describe('ReactLegacyErrorBoundaries', () => {
]);
});
// @gate !disableLegacyMode
it('discards a bad root if the root component fails', () => {
const X = null;
const Y = undefined;
@@ -2112,6 +2150,7 @@ describe('ReactLegacyErrorBoundaries', () => {
expect(err2.message).toMatch(/got: undefined/);
});
// @gate !disableLegacyMode
it('renders empty output if error boundary does not handle the error', () => {
const container = document.createElement('div');
expect(() => {
@@ -2146,6 +2185,7 @@ describe('ReactLegacyErrorBoundaries', () => {
expect(log).toEqual(['NoopErrorBoundary componentWillUnmount']);
});
// @gate !disableLegacyMode
it('passes first error when two errors happen in commit', () => {
const errors = [];
let caughtError;
@@ -2185,6 +2225,7 @@ describe('ReactLegacyErrorBoundaries', () => {
expect(caughtError.message).toBe('child sad');
});
// @gate !disableLegacyMode
it('propagates uncaught error inside unbatched initial mount', () => {
function Foo() {
throw new Error('foo error');
@@ -2197,6 +2238,7 @@ describe('ReactLegacyErrorBoundaries', () => {
}).toThrow('foo error');
});
// @gate !disableLegacyMode
it('handles errors that occur in before-mutation commit hook', () => {
const errors = [];
let caughtError;
@@ -37,6 +37,7 @@ describe('ReactMount', () => {
});
describe('unmountComponentAtNode', () => {
// @gate !disableLegacyMode
it('throws when given a non-node', () => {
const nodeArray = document.getElementsByTagName('div');
expect(() => {
@@ -44,6 +45,7 @@ describe('ReactMount', () => {
}).toThrowError('Target container is not a DOM element.');
});
// @gate !disableLegacyMode
it('returns false on non-React containers', () => {
const d = document.createElement('div');
d.innerHTML = '<b>hellooo</b>';
@@ -51,6 +53,7 @@ describe('ReactMount', () => {
expect(d.textContent).toBe('hellooo');
});
// @gate !disableLegacyMode
it('returns true on React containers', () => {
const d = document.createElement('div');
ReactDOM.render(<b>hellooo</b>, d);
@@ -60,6 +63,7 @@ describe('ReactMount', () => {
});
});
// @gate !disableLegacyMode
it('warns when given a factory', () => {
class Component extends React.Component {
render() {
@@ -76,6 +80,7 @@ describe('ReactMount', () => {
);
});
// @gate !disableLegacyMode
it('should render different components in same root', () => {
const container = document.createElement('container');
document.body.appendChild(container);
@@ -87,6 +92,7 @@ describe('ReactMount', () => {
expect(container.firstChild.nodeName).toBe('SPAN');
});
// @gate !disableLegacyMode
it('should unmount and remount if the key changes', () => {
const container = document.createElement('container');
@@ -122,6 +128,7 @@ describe('ReactMount', () => {
expect(mockUnmount).toHaveBeenCalledTimes(1);
});
// @gate !disableLegacyMode
it('should reuse markup if rendering to the same target twice', () => {
const container = document.createElement('container');
const instance1 = ReactDOM.render(<div />, container);
@@ -130,6 +137,7 @@ describe('ReactMount', () => {
expect(instance1 === instance2).toBe(true);
});
// @gate !disableLegacyMode
it('does not warn if mounting into left padded rendered markup', () => {
const container = document.createElement('container');
container.innerHTML = ReactDOMServer.renderToString(<div />) + ' ';
@@ -138,6 +146,7 @@ describe('ReactMount', () => {
ReactDOM.hydrate(<div />, container);
});
// @gate !disableLegacyMode
it('should warn if mounting into right padded rendered markup', () => {
const container = document.createElement('container');
container.innerHTML = ' ' + ReactDOMServer.renderToString(<div />);
@@ -147,6 +156,7 @@ describe('ReactMount', () => {
);
});
// @gate !disableLegacyMode
it('should not warn if mounting into non-empty node', () => {
const container = document.createElement('container');
container.innerHTML = '<div></div>';
@@ -154,6 +164,7 @@ describe('ReactMount', () => {
ReactDOM.render(<div />, container);
});
// @gate !disableLegacyMode
it('should warn when mounting into document.body', () => {
const iFrame = document.createElement('iframe');
document.body.appendChild(iFrame);
@@ -162,6 +173,7 @@ describe('ReactMount', () => {
ReactDOM.render(<div />, iFrame.contentDocument.body);
});
// @gate !disableLegacyMode
it('should account for escaping on a checksum mismatch', () => {
const div = document.createElement('div');
const markup = ReactDOMServer.renderToString(
@@ -180,6 +192,7 @@ describe('ReactMount', () => {
);
});
// @gate !disableLegacyMode
it('should warn if render removes React-rendered children', () => {
const container = document.createElement('container');
@@ -207,6 +220,7 @@ describe('ReactMount', () => {
);
});
// @gate !disableLegacyMode
it('should warn if the unmounted node was rendered by another copy of React', () => {
jest.resetModules();
const ReactDOMOther = require('react-dom');
@@ -236,6 +250,7 @@ describe('ReactMount', () => {
ReactDOM.unmountComponentAtNode(container);
});
// @gate !disableLegacyMode
it('passes the correct callback context', () => {
const container = document.createElement('div');
let calls = 0;
@@ -276,6 +291,7 @@ describe('ReactMount', () => {
expect(calls).toBe(5);
});
// @gate !disableLegacyMode
it('initial mount of legacy root is sync inside batchedUpdates, as if it were wrapped in flushSync', () => {
const container1 = document.createElement('div');
const container2 = document.createElement('div');
@@ -322,6 +338,7 @@ describe('ReactMount', () => {
expect(mountPoint.nodeType).toBe(COMMENT_NODE);
});
// @gate !disableLegacyMode
it('renders at a comment node', () => {
function Char(props) {
return props.children;
@@ -347,6 +364,7 @@ describe('ReactMount', () => {
});
});
// @gate !disableLegacyMode
it('clears existing children with legacy API', async () => {
const container = document.createElement('div');
container.innerHTML = '<div>a</div><div>b</div>';
@@ -369,6 +387,7 @@ describe('ReactMount', () => {
expect(container.textContent).toEqual('dc');
});
// @gate !disableLegacyMode
it('warns when rendering with legacy API into createRoot() container', async () => {
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
@@ -393,6 +412,7 @@ describe('ReactMount', () => {
expect(container.textContent).toEqual('Bye');
});
// @gate !disableLegacyMode
it('callback passed to legacy hydrate() API', () => {
const container = document.createElement('div');
container.innerHTML = '<div>Hi</div>';
@@ -403,6 +423,7 @@ describe('ReactMount', () => {
assertLog(['callback']);
});
// @gate !disableLegacyMode
it('warns when unmounting with legacy API (no previous content)', async () => {
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
@@ -430,6 +451,7 @@ describe('ReactMount', () => {
expect(container.textContent).toEqual('');
});
// @gate !disableLegacyMode
it('warns when unmounting with legacy API (has previous content)', async () => {
const container = document.createElement('div');
// Currently createRoot().render() doesn't clear this.
@@ -458,6 +480,7 @@ describe('ReactMount', () => {
expect(container.textContent).toEqual('');
});
// @gate !disableLegacyMode
it('warns when passing legacy container to createRoot()', () => {
const container = document.createElement('div');
ReactDOM.render(<div>Hi</div>, container);
@@ -13,6 +13,7 @@ describe('ReactDOMRoot', () => {
jest.restoreAllMocks();
});
// @gate !disableLegacyMode
test('deprecation warning for ReactDOM.render', () => {
spyOnDev(console, 'error');
@@ -26,6 +27,7 @@ describe('ReactDOMRoot', () => {
}
});
// @gate !disableLegacyMode
test('deprecation warning for ReactDOM.hydrate', () => {
spyOnDev(console, 'error');
@@ -31,6 +31,7 @@ describe('ReactLegacyUpdates', () => {
assertLog = InternalTestUtils.assertLog;
});
// @gate !disableLegacyMode
it('should batch state when updating state twice', () => {
let updateCount = 0;
@@ -60,6 +61,7 @@ describe('ReactLegacyUpdates', () => {
expect(updateCount).toBe(1);
});
// @gate !disableLegacyMode
it('should batch state when updating two different state keys', () => {
let updateCount = 0;
@@ -92,6 +94,7 @@ describe('ReactLegacyUpdates', () => {
expect(updateCount).toBe(1);
});
// @gate !disableLegacyMode
it('should batch state and props together', () => {
let updateCount = 0;
@@ -125,6 +128,7 @@ describe('ReactLegacyUpdates', () => {
expect(updateCount).toBe(1);
});
// @gate !disableLegacyMode
it('should batch parent/child state updates together', () => {
let parentUpdateCount = 0;
@@ -179,6 +183,7 @@ describe('ReactLegacyUpdates', () => {
expect(childUpdateCount).toBe(1);
});
// @gate !disableLegacyMode
it('should batch child/parent state updates together', () => {
let parentUpdateCount = 0;
@@ -235,6 +240,7 @@ describe('ReactLegacyUpdates', () => {
expect(childUpdateCount).toBe(1);
});
// @gate !disableLegacyMode
it('should support chained state updates', () => {
let updateCount = 0;
@@ -274,6 +280,7 @@ describe('ReactLegacyUpdates', () => {
expect(updateCount).toBe(2);
});
// @gate !disableLegacyMode
it('should batch forceUpdate together', () => {
let shouldUpdateCount = 0;
let updateCount = 0;
@@ -316,6 +323,7 @@ describe('ReactLegacyUpdates', () => {
expect(updateCount).toBe(1);
});
// @gate !disableLegacyMode
it('should update children even if parent blocks updates', () => {
let parentRenderCount = 0;
let childRenderCount = 0;
@@ -364,6 +372,7 @@ describe('ReactLegacyUpdates', () => {
expect(childRenderCount).toBe(2);
});
// @gate !disableLegacyMode
it('should not reconcile children passed via props', () => {
let numMiddleRenders = 0;
let numBottomRenders = 0;
@@ -401,6 +410,7 @@ describe('ReactLegacyUpdates', () => {
expect(numBottomRenders).toBe(1);
});
// @gate !disableLegacyMode
it('should flow updates correctly', () => {
let willUpdates = [];
let didUpdates = [];
@@ -530,6 +540,7 @@ describe('ReactLegacyUpdates', () => {
);
});
// @gate !disableLegacyMode
it('should queue mount-ready handlers across different roots', () => {
// We'll define two components A and B, then update both of them. When A's
// componentDidUpdate handlers is called, B's DOM should already have been
@@ -579,6 +590,7 @@ describe('ReactLegacyUpdates', () => {
expect(aUpdated).toBe(true);
});
// @gate !disableLegacyMode
it('should flush updates in the correct order', () => {
const updates = [];
@@ -661,6 +673,7 @@ describe('ReactLegacyUpdates', () => {
/* eslint-enable indent */
});
// @gate !disableLegacyMode
it('should flush updates in the correct order across roots', () => {
const instances = [];
const updates = [];
@@ -699,6 +712,7 @@ describe('ReactLegacyUpdates', () => {
expect(updates).toEqual([0, 1, 2, 0, 1, 2]);
});
// @gate !disableLegacyMode
it('should queue nested updates', () => {
// See https://github.com/facebook/react/issues/1147
@@ -752,6 +766,7 @@ describe('ReactLegacyUpdates', () => {
expect(ReactDOM.findDOMNode(x).textContent).toBe('1');
});
// @gate !disableLegacyMode
it('should queue updates from during mount', () => {
// See https://github.com/facebook/react/issues/1353
let a;
@@ -791,6 +806,7 @@ describe('ReactLegacyUpdates', () => {
expect(ReactDOM.findDOMNode(a).textContent).toBe('A1');
});
// @gate !disableLegacyMode
it('calls componentWillReceiveProps setState callback properly', () => {
let callbackCount = 0;
@@ -817,6 +833,7 @@ describe('ReactLegacyUpdates', () => {
expect(callbackCount).toBe(1);
});
// @gate !disableLegacyMode
it('does not call render after a component as been deleted', () => {
let renderCount = 0;
let componentB = null;
@@ -854,6 +871,7 @@ describe('ReactLegacyUpdates', () => {
expect(renderCount).toBe(1);
});
// @gate !disableLegacyMode
it('throws in setState if the update callback is not a function', () => {
function Foo() {
this.a = 1;
@@ -897,6 +915,7 @@ describe('ReactLegacyUpdates', () => {
);
});
// @gate !disableLegacyMode
it('throws in forceUpdate if the update callback is not a function', () => {
function Foo() {
this.a = 1;
@@ -940,6 +959,7 @@ describe('ReactLegacyUpdates', () => {
);
});
// @gate !disableLegacyMode
it('does not update one component twice in a batch (#2410)', () => {
class Parent extends React.Component {
childRef = React.createRef();
@@ -992,6 +1012,7 @@ describe('ReactLegacyUpdates', () => {
});
});
// @gate !disableLegacyMode
it('does not update one component twice in a batch (#6371)', () => {
let callbacks = [];
function emitChange() {
@@ -1049,6 +1070,7 @@ describe('ReactLegacyUpdates', () => {
expect(result).toEqual(42);
});
// @gate !disableLegacyMode
it('unmounts and remounts a root in the same batch', () => {
const container = document.createElement('div');
ReactDOM.render(<span>a</span>, container);
@@ -1059,6 +1081,7 @@ describe('ReactLegacyUpdates', () => {
expect(container.textContent).toBe('b');
});
// @gate !disableLegacyMode
it('handles reentrant mounting in synchronous mode', () => {
let mounts = 0;
class Editor extends React.Component {
@@ -1096,6 +1119,7 @@ describe('ReactLegacyUpdates', () => {
expect(mounts).toBe(1);
});
// @gate !disableLegacyMode
it('mounts and unmounts are sync even in a batch', () => {
const ops = [];
const container = document.createElement('div');
@@ -1108,6 +1132,7 @@ describe('ReactLegacyUpdates', () => {
expect(ops).toEqual(['Hello', '']);
});
// @gate !disableLegacyMode
it(
'in legacy mode, updates in componentWillUpdate and componentDidUpdate ' +
'should both flush in the immediately subsequent commit',
@@ -1151,6 +1176,7 @@ describe('ReactLegacyUpdates', () => {
},
);
// @gate !disableLegacyMode
it(
'in legacy mode, updates in componentWillUpdate and componentDidUpdate ' +
'(on a sibling) should both flush in the immediately subsequent commit',
@@ -1222,6 +1248,7 @@ describe('ReactLegacyUpdates', () => {
},
);
// @gate !disableLegacyMode
it('uses correct base state for setState inside render phase', () => {
const ops = [];
@@ -1245,6 +1272,7 @@ describe('ReactLegacyUpdates', () => {
expect(ops).toEqual(['base: 0, memoized: 0', 'base: 1, memoized: 1']);
});
// @gate !disableLegacyMode
it('does not re-render if state update is null', () => {
const container = document.createElement('div');
@@ -1265,6 +1293,7 @@ describe('ReactLegacyUpdates', () => {
});
// Will change once we switch to async by default
// @gate !disableLegacyMode
it('synchronously renders hidden subtrees', () => {
const container = document.createElement('div');
let ops = [];
@@ -1301,6 +1330,7 @@ describe('ReactLegacyUpdates', () => {
expect(ops).toEqual(['Foo', 'Bar', 'Baz']);
});
// @gate !disableLegacyMode
it('can render ridiculously large number of roots without triggering infinite update loop error', () => {
class Foo extends React.Component {
componentDidMount() {
@@ -1325,6 +1355,7 @@ describe('ReactLegacyUpdates', () => {
ReactDOM.render(<Foo />, container);
});
// @gate !disableLegacyMode
it('resets the update counter for unrelated updates', () => {
const container = document.createElement('div');
const ref = React.createRef();
@@ -1365,6 +1396,7 @@ describe('ReactLegacyUpdates', () => {
expect(ref.current).toBe(null);
});
// @gate !disableLegacyMode
it('does not fall into an infinite update loop', () => {
class NonTerminating extends React.Component {
state = {step: 0};
@@ -1390,6 +1422,7 @@ describe('ReactLegacyUpdates', () => {
}).toThrow('Maximum');
});
// @gate !disableLegacyMode
it('does not fall into an infinite update loop with useLayoutEffect', () => {
function NonTerminating() {
const [step, setStep] = React.useState(0);
@@ -1405,6 +1438,7 @@ describe('ReactLegacyUpdates', () => {
}).toThrow('Maximum');
});
// @gate !disableLegacyMode
it('can recover after falling into an infinite update loop', () => {
class NonTerminating extends React.Component {
state = {step: 0};
@@ -1445,6 +1479,7 @@ describe('ReactLegacyUpdates', () => {
expect(container.textContent).toBe('1');
});
// @gate !disableLegacyMode
it('does not fall into mutually recursive infinite update loop with same container', () => {
// Note: this test would fail if there were two or more different roots.
@@ -1472,6 +1507,7 @@ describe('ReactLegacyUpdates', () => {
}).toThrow('Maximum');
});
// @gate !disableLegacyMode
it('does not fall into an infinite error loop', () => {
function BadRender() {
throw new Error('error');
@@ -1505,6 +1541,7 @@ describe('ReactLegacyUpdates', () => {
}).toThrow('Maximum');
});
// @gate !disableLegacyMode
it('can schedule ridiculously many updates within the same batch without triggering a maximum update error', () => {
const subscribers = [];
@@ -1540,6 +1577,7 @@ describe('ReactLegacyUpdates', () => {
// TODO: Replace this branch with @gate pragmas
if (__DEV__) {
// @gate !disableLegacyMode
it('can have nested updates if they do not cross the limit', async () => {
let _setStep;
const LIMIT = 50;
@@ -1567,6 +1605,7 @@ describe('ReactLegacyUpdates', () => {
expect(container.textContent).toBe('50');
});
// @gate !disableLegacyMode
it('can have many updates inside useEffect without triggering a warning', async () => {
function Terminating() {
const [step, setStep] = React.useState(0);
@@ -49,6 +49,7 @@ describe('ReactMount', () => {
});
});
// @gate !disableLegacyMode
it('should warn when unmounting a non-container root node', () => {
const mainContainerDiv = document.createElement('div');
@@ -71,6 +72,7 @@ describe('ReactMount', () => {
);
});
// @gate !disableLegacyMode
it('should warn when unmounting a non-container, non-root node', () => {
const mainContainerDiv = document.createElement('div');
@@ -373,6 +373,7 @@ describe('rendering React components at document', () => {
expect(testDocument.body.innerHTML).toBe('Hello world');
});
// @gate !disableLegacyMode
it('supports findDOMNode on full-page components in legacy mode', () => {
const tree = (
<html>
@@ -505,6 +505,7 @@ describe('ReactDOMServerHydration', () => {
await act(() => root.render(<div />));
});
// @gate !disableLegacyMode
it('Suspense + hydration in legacy mode', () => {
const element = document.createElement('div');
element.innerHTML = '<div><div>Hello World</div></div>';
@@ -530,6 +531,7 @@ describe('ReactDOMServerHydration', () => {
expect(element.innerHTML).toBe('<div><div>Hello World</div></div>');
});
// @gate !disableLegacyMode
it('Suspense + hydration in legacy mode (at root)', () => {
const element = document.createElement('div');
element.innerHTML = '<div>Hello World</div>';
@@ -550,6 +552,7 @@ describe('ReactDOMServerHydration', () => {
);
});
// @gate !disableLegacyMode
it('Suspense + hydration in legacy mode with no fallback', () => {
const element = document.createElement('div');
element.innerHTML = '<div>Hello World</div>';
+6
View File
@@ -19,6 +19,7 @@ describe('findDOMNode', () => {
expect(ReactDOM.findDOMNode(null)).toBe(null);
});
// @gate !disableLegacyMode
it('findDOMNode should find dom element', () => {
class MyNode extends React.Component {
render() {
@@ -37,6 +38,7 @@ describe('findDOMNode', () => {
expect(mySameDiv).toBe(myDiv);
});
// @gate !disableLegacyMode
it('findDOMNode should find dom element after an update from null', () => {
function Bar({flag}) {
if (flag) {
@@ -69,6 +71,7 @@ describe('findDOMNode', () => {
}).toThrowError('Argument appears to not be a ReactComponent. Keys: foo');
});
// @gate !disableLegacyMode
it('findDOMNode should reject unmounted objects with render func', () => {
class Foo extends React.Component {
render() {
@@ -85,6 +88,7 @@ describe('findDOMNode', () => {
);
});
// @gate !disableLegacyMode
it('findDOMNode should not throw an error when called within a component that is not mounted', () => {
class Bar extends React.Component {
UNSAFE_componentWillMount() {
@@ -98,6 +102,7 @@ describe('findDOMNode', () => {
expect(() => ReactTestUtils.renderIntoDocument(<Bar />)).not.toThrow();
});
// @gate !disableLegacyMode
it('findDOMNode should warn if used to find a host component inside StrictMode', () => {
let parent = undefined;
let child = undefined;
@@ -129,6 +134,7 @@ describe('findDOMNode', () => {
expect(match).toBe(child);
});
// @gate !disableLegacyMode
it('findDOMNode should warn if passed a component that is inside StrictMode', () => {
let parent = undefined;
let child = undefined;
+1
View File
@@ -19,6 +19,7 @@ describe('root level refs with legacy APIs', () => {
ReactDOM = require('react-dom');
});
// @gate !disableLegacyMode
it('attaches and detaches root refs', () => {
let inst = null;
@@ -20,6 +20,7 @@ const renderSubtreeIntoContainer =
describe('renderSubtreeIntoContainer', () => {
// @gate !disableLegacyContext
// @gate !disableLegacyMode
it('should pass context when rendering subtree elsewhere', () => {
const portal = document.createElement('div');
@@ -63,46 +64,8 @@ describe('renderSubtreeIntoContainer', () => {
expect(portal.firstChild.innerHTML).toBe('bar');
});
it('should throw if parentComponent is invalid', () => {
const portal = document.createElement('div');
class Component extends React.Component {
static contextTypes = {
foo: PropTypes.string.isRequired,
};
render() {
return <div>{this.context.foo}</div>;
}
}
// ESLint is confused here and thinks Parent is unused, presumably because
// it is only used inside of the class body?
// eslint-disable-next-line no-unused-vars
class Parent extends React.Component {
static childContextTypes = {
foo: PropTypes.string.isRequired,
};
getChildContext() {
return {
foo: 'bar',
};
}
render() {
return null;
}
componentDidMount() {
expect(function () {
renderSubtreeIntoContainer(<Parent />, <Component />, portal);
}).toThrowError('parentComponentmust be a valid React Component');
}
}
});
// @gate !disableLegacyContext
// @gate !disableLegacyMode
it('should update context if it changes due to setState', async () => {
const container = document.createElement('div');
document.body.appendChild(container);
@@ -171,6 +134,7 @@ describe('renderSubtreeIntoContainer', () => {
});
// @gate !disableLegacyContext
// @gate !disableLegacyMode
it('should update context if it changes due to re-render', async () => {
const container = document.createElement('div');
document.body.appendChild(container);
@@ -232,6 +196,7 @@ describe('renderSubtreeIntoContainer', () => {
expect(portal.firstChild.innerHTML).toBe('changed-changed');
});
// @gate !disableLegacyMode
it('should render portal with non-context-provider parent', async () => {
const container = document.createElement('div');
document.body.appendChild(container);
@@ -259,6 +224,7 @@ describe('renderSubtreeIntoContainer', () => {
});
// @gate !disableLegacyContext
// @gate !disableLegacyMode
it('should get context through non-context-provider parent', async () => {
const container = document.createElement('div');
document.body.appendChild(container);
@@ -306,6 +272,7 @@ describe('renderSubtreeIntoContainer', () => {
});
// @gate !disableLegacyContext
// @gate !disableLegacyMode
it('should get context through middle non-context-provider layer', async () => {
const container = document.createElement('div');
document.body.appendChild(container);
@@ -360,6 +327,7 @@ describe('renderSubtreeIntoContainer', () => {
expect(portal2.textContent).toBe('foo');
});
// @gate !disableLegacyMode
it('legacy test: fails gracefully when mixing React 15 and 16', () => {
class C extends React.Component {
render() {
+25
View File
@@ -14,6 +14,7 @@ import type {
import type {FiberRoot} from 'react-reconciler/src/ReactInternalTypes';
import type {ReactNodeList} from 'shared/ReactTypes';
import {disableLegacyMode} from 'shared/ReactFeatureFlags';
import {clearContainer} from 'react-dom-bindings/src/client/ReactFiberConfigDOM';
import {
getInstanceFromNode,
@@ -264,6 +265,14 @@ export function hydrate(
container: Container,
callback: ?Function,
): React$Component<any, any> | PublicInstance | null {
if (disableLegacyMode) {
if (__DEV__) {
console.error(
'ReactDOM.hydrate is no longer supported in React 18. Use hydrateRoot instead',
);
}
throw new Error('ReactDOM: Unsupported Legacy Mode API.');
}
if (__DEV__) {
console.error(
'ReactDOM.hydrate is no longer supported in React 18. Use hydrateRoot ' +
@@ -304,6 +313,14 @@ export function render(
container: Container,
callback: ?Function,
): React$Component<any, any> | PublicInstance | null {
if (disableLegacyMode) {
if (__DEV__) {
console.error(
'ReactDOM.render is no longer supported in React 18. Use createRoot instead.',
);
}
throw new Error('ReactDOM: Unsupported Legacy Mode API.');
}
if (__DEV__) {
console.error(
'ReactDOM.render is no longer supported in React 18. Use createRoot ' +
@@ -344,6 +361,14 @@ export function unstable_renderSubtreeIntoContainer(
containerNode: Container,
callback: ?Function,
): React$Component<any, any> | PublicInstance | null {
if (disableLegacyMode) {
if (__DEV__) {
console.error(
'ReactDOM.unstable_renderSubtreeIntoContainer() is no longer supported in React 18. Consider using a portal instead.',
);
}
throw new Error('ReactDOM: Unsupported Legacy Mode API.');
}
if (__DEV__) {
console.error(
'ReactDOM.unstable_renderSubtreeIntoContainer() is no longer supported ' +
@@ -433,7 +433,7 @@ describe('ChangeEventPlugin', () => {
expect(called2).toBe(1);
});
it('should deduplicate input value change events', () => {
it('should deduplicate input value change events', async () => {
let called = 0;
function cb(e) {
@@ -441,59 +441,81 @@ describe('ChangeEventPlugin', () => {
expect(e.type).toBe('change');
}
let input;
['text', 'number', 'range'].forEach(type => {
const inputTypes = ['text', 'number', 'range'];
while (inputTypes.length) {
const type = inputTypes.pop();
called = 0;
input = ReactDOM.render(<input type={type} onChange={cb} />, container);
// Should be ignored (no change):
input.dispatchEvent(
new Event('change', {bubbles: true, cancelable: true}),
);
setUntrackedValue.call(input, '42');
input.dispatchEvent(
new Event('change', {bubbles: true, cancelable: true}),
);
// Should be ignored (no change):
input.dispatchEvent(
new Event('change', {bubbles: true, cancelable: true}),
);
let root = ReactDOMClient.createRoot(container);
let ref = {current: null};
await act(() => {
root.render(<input ref={ref} type={type} onChange={cb} />);
});
let input = ref.current;
await act(() => {
// Should be ignored (no change):
input.dispatchEvent(
new Event('change', {bubbles: true, cancelable: true}),
);
setUntrackedValue.call(input, '42');
input.dispatchEvent(
new Event('change', {bubbles: true, cancelable: true}),
);
// Should be ignored (no change):
input.dispatchEvent(
new Event('change', {bubbles: true, cancelable: true}),
);
});
expect(called).toBe(1);
ReactDOM.unmountComponentAtNode(container);
root.unmount();
called = 0;
input = ReactDOM.render(<input type={type} onChange={cb} />, container);
// Should be ignored (no change):
input.dispatchEvent(
new Event('input', {bubbles: true, cancelable: true}),
);
setUntrackedValue.call(input, '42');
input.dispatchEvent(
new Event('input', {bubbles: true, cancelable: true}),
);
// Should be ignored (no change):
input.dispatchEvent(
new Event('input', {bubbles: true, cancelable: true}),
);
root = ReactDOMClient.createRoot(container);
ref = {current: null};
await act(() => {
root.render(<input ref={ref} type={type} onChange={cb} />);
});
input = ref.current;
await act(() => {
// Should be ignored (no change):
input.dispatchEvent(
new Event('input', {bubbles: true, cancelable: true}),
);
setUntrackedValue.call(input, '42');
input.dispatchEvent(
new Event('input', {bubbles: true, cancelable: true}),
);
// Should be ignored (no change):
input.dispatchEvent(
new Event('input', {bubbles: true, cancelable: true}),
);
});
expect(called).toBe(1);
ReactDOM.unmountComponentAtNode(container);
root.unmount();
called = 0;
input = ReactDOM.render(<input type={type} onChange={cb} />, container);
// Should be ignored (no change):
input.dispatchEvent(
new Event('change', {bubbles: true, cancelable: true}),
);
setUntrackedValue.call(input, '42');
input.dispatchEvent(
new Event('input', {bubbles: true, cancelable: true}),
);
// Should be ignored (no change):
input.dispatchEvent(
new Event('change', {bubbles: true, cancelable: true}),
);
root = ReactDOMClient.createRoot(container);
ref = {current: null};
await act(() => {
root.render(<input ref={ref} type={type} onChange={cb} />);
});
input = ref.current;
await act(() => {
// Should be ignored (no change):
input.dispatchEvent(
new Event('change', {bubbles: true, cancelable: true}),
);
setUntrackedValue.call(input, '42');
input.dispatchEvent(
new Event('input', {bubbles: true, cancelable: true}),
);
// Should be ignored (no change):
input.dispatchEvent(
new Event('change', {bubbles: true, cancelable: true}),
);
});
expect(called).toBe(1);
ReactDOM.unmountComponentAtNode(container);
});
root.unmount();
}
});
it('should listen for both change and input events when supported', async () => {
@@ -158,7 +158,8 @@ describe('EnterLeaveEventPlugin', () => {
});
// Test for https://github.com/facebook/react/issues/16763.
it('should call mouseEnter once from sibling rendered inside a rendered component in legacy roots', done => {
// @gate !disableLegacyMode
it('should call mouseEnter once from sibling rendered inside a rendered component in legacy roots', async () => {
const mockFn = jest.fn();
class Parent extends React.Component {
@@ -191,8 +192,6 @@ describe('EnterLeaveEventPlugin', () => {
relatedTarget: this.firstEl.current,
}),
);
expect(mockFn.mock.calls.length).toBe(1);
done();
}
render() {
@@ -205,10 +204,14 @@ describe('EnterLeaveEventPlugin', () => {
}
}
ReactDOM.render(<Parent />, container);
await act(() => {
ReactDOM.render(<Parent />, container);
});
expect(mockFn.mock.calls.length).toBe(1);
});
it('should call mouseEnter when pressing a non tracked React node in legacy root', done => {
// @gate !disableLegacyMode
it('should call mouseEnter when pressing a non tracked React node in legacy root', async () => {
const mockFn = jest.fn();
class Parent extends React.Component {
@@ -243,8 +246,6 @@ describe('EnterLeaveEventPlugin', () => {
relatedTarget: this.siblingEl.current,
}),
);
expect(mockFn.mock.calls.length).toBe(1);
done();
}
render() {
@@ -256,7 +257,10 @@ describe('EnterLeaveEventPlugin', () => {
}
}
ReactDOM.render(<Parent />, container);
await act(() => {
ReactDOM.render(<Parent />, container);
});
expect(mockFn.mock.calls.length).toBe(1);
});
it('should work with portals outside of the root that has onMouseLeave', async () => {
@@ -11,11 +11,7 @@ export {
createPortal,
findDOMNode,
flushSync,
hydrate,
render,
unmountComponentAtNode,
unstable_batchedUpdates,
unstable_renderSubtreeIntoContainer,
useFormStatus,
useFormState,
prefetchDNS,
@@ -399,6 +399,7 @@ describe('ReactSuspense', () => {
},
);
// @gate !disableLegacyMode
it('mounts a lazy class component in non-concurrent mode (legacy)', async () => {
class Class extends React.Component {
componentDidMount() {
@@ -685,6 +686,7 @@ describe('ReactSuspense', () => {
});
describe('outside concurrent mode (legacy)', () => {
// @gate !disableLegacyMode
it('a mounted class component can suspend without losing state', async () => {
class TextWithLifecycle extends React.Component {
componentDidMount() {
@@ -763,6 +765,7 @@ describe('ReactSuspense', () => {
expect(container.textContent).toEqual('AB:2C');
});
// @gate !disableLegacyMode
it('bails out on timed-out primary children even if they receive an update', async () => {
let instance;
class Stateful extends React.Component {
@@ -803,6 +806,7 @@ describe('ReactSuspense', () => {
expect(container.textContent).toEqual('Stateful: 2B');
});
// @gate !disableLegacyMode
it('when updating a timed-out tree, always retries the suspended component', async () => {
let instance;
class Stateful extends React.Component {
@@ -858,6 +862,7 @@ describe('ReactSuspense', () => {
expect(container.textContent).toEqual('Stateful: 2B');
});
// @gate !disableLegacyMode
it('suspends in a class that has componentWillUnmount and is then deleted', async () => {
class AsyncTextWithUnmount extends React.Component {
componentWillUnmount() {
@@ -884,6 +889,7 @@ describe('ReactSuspense', () => {
expect(container.textContent).toEqual('B');
});
// @gate !disableLegacyMode
it('suspends in a component that also contains useEffect', async () => {
const {useLayoutEffect} = React;
@@ -911,6 +917,7 @@ describe('ReactSuspense', () => {
assertLog(['A', 'Did commit: A']);
});
// @gate !disableLegacyMode
it('retries when an update is scheduled on a timed out tree', async () => {
let instance;
class Stateful extends React.Component {
@@ -958,6 +965,7 @@ describe('ReactSuspense', () => {
expect(container.textContent).toEqual('Step: 3');
});
// @gate !disableLegacyMode
it('does not remount the fallback while suspended children resolve in legacy mode', async () => {
let mounts = 0;
class ShouldMountOnce extends React.Component {
@@ -1004,6 +1012,7 @@ describe('ReactSuspense', () => {
expect(mounts).toBe(1);
});
// @gate !disableLegacyMode
it('reuses effects, including deletions, from the suspended tree', async () => {
const {useState} = React;
@@ -1045,6 +1054,7 @@ describe('ReactSuspense', () => {
expect(container.textContent).toEqual('Tab: 2 + sibling');
});
// @gate !disableLegacyMode
it('does not warn if a mounted component is pinged', async () => {
const {useState} = React;
@@ -1076,6 +1086,7 @@ describe('ReactSuspense', () => {
expect(container.textContent).toEqual('Loading...');
});
// @gate !disableLegacyMode
it('memoizes promise listeners per thread ID to prevent redundant renders', async () => {
function App() {
return (
@@ -1118,6 +1129,7 @@ describe('ReactSuspense', () => {
]);
});
// @gate !disableLegacyMode
it('#14162', async () => {
const {lazy} = React;
@@ -1155,6 +1167,7 @@ describe('ReactSuspense', () => {
ReactDOM.render(<App name="world" />, container);
});
// @gate !disableLegacyMode
it('updates memoized child of suspense component when context updates (simple memo)', async () => {
const {useContext, createContext, useState, memo} = React;
@@ -1194,6 +1207,7 @@ describe('ReactSuspense', () => {
expect(container.textContent).toEqual('new value');
});
// @gate !disableLegacyMode
it('updates memoized child of suspense component when context updates (manual memo)', async () => {
const {useContext, createContext, useState, memo} = React;
@@ -1238,6 +1252,7 @@ describe('ReactSuspense', () => {
expect(container.textContent).toEqual('new value');
});
// @gate !disableLegacyMode
it('updates memoized child of suspense component when context updates (function)', async () => {
const {useContext, createContext, useState} = React;
@@ -1282,6 +1297,7 @@ describe('ReactSuspense', () => {
expect(container.textContent).toEqual('new value');
});
// @gate !disableLegacyMode
it('updates memoized child of suspense component when context updates (forwardRef)', async () => {
const {forwardRef, useContext, createContext, useState} = React;
@@ -1321,6 +1337,7 @@ describe('ReactSuspense', () => {
expect(container.textContent).toEqual('new value');
});
// @gate !disableLegacyMode
it('updates context consumer within child of suspended suspense component when context updates', async () => {
const {createContext, useState} = React;
@@ -448,6 +448,7 @@ describe('ReactSuspenseEffectsSemanticsDOM', () => {
expect(container.innerHTML).toBe('<h1>Hello</h1>');
});
// @gate !disableLegacyMode
it('regression: unmount hidden tree, in legacy mode', async () => {
// In legacy mode, when a tree suspends and switches to a fallback, the
// effects are not unmounted. So we have to unmount them during a deletion.
@@ -262,6 +262,9 @@ describe('updaters', () => {
await waitForAll([]);
});
// This test should be convertable to createRoot but the allScheduledTypes assertions are no longer the same
// So I'm leaving it in legacy mode for now and just disabling if legacy mode is turned off
// @gate !disableLegacyMode
it('should cover suspense pings', async () => {
let data = null;
let resolver = null;
@@ -77,6 +77,7 @@ describe('ReactStrictMode', () => {
});
// @gate __DEV__
// @gate !disableLegacyMode
it('should invoke only precommit lifecycle methods twice in legacy roots', async () => {
let log = [];
let shouldComponentUpdate = false;
@@ -242,6 +243,7 @@ describe('ReactStrictMode', () => {
]);
});
// @gate !disableLegacyMode
it('should invoke only precommit lifecycle methods twice in DEV legacy roots', async () => {
const {StrictMode} = React;
+5
View File
@@ -195,6 +195,11 @@ export const disableStringRefs = __NEXT_MAJOR__;
// Warn on any usage of ReactTestRenderer
export const enableReactTestRendererWarning = false;
// Disables legacy mode
// This allows us to land breaking changes to remove legacy mode APIs in experimental builds
// before removing them in stable in the next Major
export const disableLegacyMode = __NEXT_MAJOR__;
// -----------------------------------------------------------------------------
// Chopping Block
//
@@ -104,6 +104,7 @@ export const enableRefAsProp = false;
export const disableStringRefs = false;
export const enableReactTestRendererWarning = false;
export const disableLegacyMode = false;
export const enableBigIntSupport = false;
@@ -97,6 +97,7 @@ export const disableStringRefs = false;
export const enableReactTestRendererWarning = false;
export const enableBigIntSupport = false;
export const disableLegacyMode = false;
// Flow magic to verify the exports of this file match the original version.
((((null: any): ExportsType): FeatureFlagsType): ExportsType);
@@ -101,6 +101,7 @@ export const enableRefAsProp = __NEXT_MAJOR__;
export const disableStringRefs = __NEXT_MAJOR__;
export const enableReactTestRendererWarning = false;
export const enableBigIntSupport = __NEXT_MAJOR__;
export const disableLegacyMode = __NEXT_MAJOR__;
// Flow magic to verify the exports of this file match the original version.
((((null: any): ExportsType): FeatureFlagsType): ExportsType);
@@ -91,6 +91,7 @@ export const enableRefAsProp = false;
export const disableStringRefs = false;
export const enableReactTestRendererWarning = false;
export const disableLegacyMode = false;
export const enableBigIntSupport = false;
@@ -94,6 +94,7 @@ export const enableRefAsProp = false;
export const disableStringRefs = false;
export const enableReactTestRendererWarning = false;
export const disableLegacyMode = false;
export const enableBigIntSupport = false;
@@ -126,5 +126,7 @@ export const enableBigIntSupport = false;
// because JSX is an extremely hot path.
export const disableStringRefs = false;
export const disableLegacyMode = false;
// Flow magic to verify the exports of this file match the original version.
((((null: any): ExportsType): FeatureFlagsType): ExportsType);
+2 -1
View File
@@ -493,5 +493,6 @@
"505": "Cannot render an Async Component, Promise or React.Lazy inside React.Children. We recommend not iterating over children and just rendering them plain.",
"506": "Functions are not valid as a child of Client Components. This may happen if you return %s instead of <%s /> from render. Or maybe you meant to call this function rather than return it.%s",
"507": "Expected the last optional `callback` argument to be a function. Instead received: %s.",
"508": "The first argument must be a React class instance. Instead received: %s."
"508": "The first argument must be a React class instance. Instead received: %s.",
"509": "ReactDOM: Unsupported Legacy Mode API."
}