(target = n)}
- onMouseEnter={() => ops.push('enter')}
- onMouseLeave={() => ops.push('leave')}
- />,
- container,
- );
+ await act(() => {
+ root.render(
+
(target = n)}
+ onMouseEnter={() => Scheduler.log('enter')}
+ onMouseLeave={() => Scheduler.log('leave')}
+ />,
+ );
+ });
- simulateMouseMove(null, container);
- expect(ops).toEqual([]);
+ await act(() => {
+ simulateMouseMove(null, container);
+ });
+ assertLog([]);
- ops = [];
- simulateMouseMove(container, target);
- expect(ops).toEqual(['enter']);
+ await act(() => {
+ simulateMouseMove(container, target);
+ });
+ assertLog(['enter']);
- ops = [];
- simulateMouseMove(target, container);
- expect(ops).toEqual(['leave']);
+ await act(() => {
+ simulateMouseMove(target, container);
+ });
+ assertLog(['leave']);
- ops = [];
- simulateMouseMove(container, null);
- expect(ops).toEqual([]);
+ await act(() => {
+ simulateMouseMove(container, null);
+ });
+ assertLog([]);
});
- it('listens to events that do not exist in the Portal subtree', () => {
+ it('listens to events that do not exist in the Portal subtree', async () => {
const onClick = jest.fn();
const ref = React.createRef();
- ReactDOM.render(
-
- {ReactDOM.createPortal(, document.body)}
-
,
- container,
- );
+ await act(() => {
+ root.render(
+
+ {ReactDOM.createPortal(
+ ,
+ document.body,
+ )}
+
,
+ );
+ });
const event = new MouseEvent('click', {
bubbles: true,
});
- ref.current.dispatchEvent(event);
+ await act(() => {
+ ref.current.dispatchEvent(event);
+ });
expect(onClick).toHaveBeenCalledTimes(1);
});
@@ -1049,7 +930,11 @@ describe('ReactDOMFiber', () => {
return
;
}
}
- expect(() => ReactDOM.render(
, container)).toErrorDev(
+ expect(() => {
+ ReactDOM.flushSync(() => {
+ root.render(
);
+ });
+ }).toErrorDev(
'Expected `onClick` listener to be a function, instead got a value of `string` type.\n' +
' in div (at **)\n' +
' in Example (at **)',
@@ -1062,7 +947,11 @@ describe('ReactDOMFiber', () => {
return
;
}
}
- expect(() => ReactDOM.render(
, container)).toErrorDev(
+ expect(() => {
+ ReactDOM.flushSync(() => {
+ root.render(
);
+ });
+ }).toErrorDev(
'Expected `onClick` listener to be a function, instead got `false`.\n\n' +
'If you used to conditionally omit it with onClick={condition && value}, ' +
'pass onClick={condition ? value : undefined} instead.\n' +
@@ -1071,12 +960,9 @@ describe('ReactDOMFiber', () => {
);
});
- it('should not update event handlers until commit', () => {
- spyOnDev(console, 'error');
-
- let ops = [];
- const handlerA = () => ops.push('A');
- const handlerB = () => ops.push('B');
+ it('should not update event handlers until commit', async () => {
+ const handlerA = () => Scheduler.log('A');
+ const handlerB = () => Scheduler.log('B');
function click() {
const event = new MouseEvent('click', {
@@ -1114,137 +1000,155 @@ describe('ReactDOMFiber', () => {
}
let inst;
- ReactDOM.render([
(inst = n)} />], container);
+ await act(() => {
+ root.render([ (inst = n)} />]);
+ });
const node = container.firstChild;
expect(node.tagName).toEqual('DIV');
- click();
+ await act(() => {
+ click();
+ });
- expect(ops).toEqual(['A']);
- ops = [];
+ assertLog(['A']);
// Render with the other event handler.
- inst.flip();
+ await act(() => {
+ inst.flip();
+ });
- click();
+ await act(() => {
+ click();
+ });
- expect(ops).toEqual(['B']);
- ops = [];
+ assertLog(['B']);
// Rerender without changing any props.
- inst.tick();
+ await act(() => {
+ inst.tick();
+ });
- click();
+ await act(() => {
+ click();
+ });
- expect(ops).toEqual(['B']);
- ops = [];
+ assertLog(['B']);
// Render a flip back to the A handler. The second component invokes the
// click handler during render to simulate a click during an aborted
// render. I use this hack because at current time we don't have a way to
// test aborted ReactDOM renders.
- ReactDOM.render(
- [, ],
- container,
- );
+ await act(() => {
+ root.render([, ]);
+ });
// Because the new click handler has not yet committed, we should still
// invoke B.
- expect(ops).toEqual(['B']);
- ops = [];
+ assertLog(['B']);
// Any click that happens after commit, should invoke A.
- click();
- expect(ops).toEqual(['A']);
+ await act(() => {
+ click();
+ });
+ assertLog(['A']);
+ });
- if (__DEV__) {
- expect(console.error).toHaveBeenCalledTimes(2);
- expect(console.error.mock.calls[0][0]).toMatch(
- 'ReactDOM.render is no longer supported in React 18',
- );
- expect(console.error.mock.calls[1][0]).toMatch(
- 'ReactDOM.render is no longer supported in React 18',
+ it('should not crash encountering low-priority tree', async () => {
+ await act(() => {
+ root.render(
+ ,
);
- }
- });
+ });
- it('should not crash encountering low-priority tree', () => {
- ReactDOM.render(
- ,
- container,
- );
+ expect(container.innerHTML).toBe('');
});
- it('should not warn when rendering into an empty container', () => {
- ReactDOM.render(foo
, container);
+ it('should not warn when rendering into an empty container', async () => {
+ await act(() => {
+ root.render(foo
);
+ });
expect(container.innerHTML).toBe('foo
');
- ReactDOM.render(null, container);
+ await act(() => {
+ root.render(null);
+ });
expect(container.innerHTML).toBe('');
- ReactDOM.render(bar
, container);
+ await act(() => {
+ root.render(bar
);
+ });
expect(container.innerHTML).toBe('bar
');
});
- it('should warn when replacing a container which was manually updated outside of React', () => {
+ it('should warn when replacing a container which was manually updated outside of React', async () => {
// when not messing with the DOM outside of React
- ReactDOM.render(foo
, container);
- ReactDOM.render(bar
, container);
+ await act(() => {
+ root.render(foo
);
+ });
+ expect(container.innerHTML).toBe('foo
');
+
+ await act(() => {
+ root.render(bar
);
+ });
expect(container.innerHTML).toBe('bar
');
+
// then we mess with the DOM before an update
// we know this will error - that is expected right now
// It's an error of type 'NotFoundError' with no message
container.innerHTML = 'MEOW.
';
expect(() => {
- expect(() =>
- ReactDOM.render(baz
, container),
- ).toErrorDev(
- 'render(...): ' +
- 'It looks like the React-rendered content of this container was ' +
- 'removed without using React. This is not supported and will ' +
- 'cause errors. Instead, call ReactDOM.unmountComponentAtNode ' +
- 'to empty a container.',
- {withoutStack: true},
- );
- }).toThrowError();
+ ReactDOM.flushSync(() => {
+ root.render(baz
);
+ });
+ }).toThrow('The node to be removed is not a child of this node');
});
- it('should warn when doing an update to a container manually updated outside of React', () => {
+ it('should not warn when doing an update to a container manually updated outside of React', async () => {
// when not messing with the DOM outside of React
- ReactDOM.render(foo
, container);
- ReactDOM.render(bar
, container);
+ await act(() => {
+ root.render(foo
);
+ });
+ expect(container.innerHTML).toBe('foo
');
+
+ await act(() => {
+ root.render(bar
);
+ });
expect(container.innerHTML).toBe('bar
');
+
// then we mess with the DOM before an update
container.innerHTML = 'MEOW.
';
- expect(() => ReactDOM.render(baz
, container)).toErrorDev(
- 'render(...): ' +
- 'It looks like the React-rendered content of this container was ' +
- 'removed without using React. This is not supported and will ' +
- 'cause errors. Instead, call ReactDOM.unmountComponentAtNode ' +
- 'to empty a container.',
- {withoutStack: true},
- );
+
+ await act(() => {
+ root.render(baz
);
+ });
+ // TODO: why not, and no error?
+ expect(container.innerHTML).toBe('MEOW.
');
});
- it('should warn when doing an update to a container manually cleared outside of React', () => {
+ it('should not warn when doing an update to a container manually cleared outside of React', async () => {
// when not messing with the DOM outside of React
- ReactDOM.render(foo
, container);
- ReactDOM.render(bar
, container);
+ await act(() => {
+ root.render(foo
);
+ });
+ expect(container.innerHTML).toBe('foo
');
+
+ await act(() => {
+ root.render(bar
);
+ });
expect(container.innerHTML).toBe('bar
');
+
// then we mess with the DOM before an update
container.innerHTML = '';
- expect(() => ReactDOM.render(baz
, container)).toErrorDev(
- 'render(...): ' +
- 'It looks like the React-rendered content of this container was ' +
- 'removed without using React. This is not supported and will ' +
- 'cause errors. Instead, call ReactDOM.unmountComponentAtNode ' +
- 'to empty a container.',
- {withoutStack: true},
- );
+
+ await act(() => {
+ root.render(baz
);
+ });
+ // TODO: why not, and no error?
+ expect(container.innerHTML).toBe('');
});
- it('should render a text component with a text DOM node on the same document as the container', () => {
+ it('should render a text component with a text DOM node on the same document as the container', async () => {
// 1. Create a new document through the use of iframe
// 2. Set up the spy to make asserts when a text component
// is rendered inside the iframe container
@@ -1266,7 +1170,10 @@ describe('ReactDOMFiber', () => {
textNode = node;
});
- ReactDOM.render(textContent, iframeContainer);
+ const iFrameRoot = ReactDOMClient.createRoot(iframeContainer);
+ await act(() => {
+ iFrameRoot.render(textContent);
+ });
expect(textNode.textContent).toBe(textContent);
expect(actualDocument).not.toBe(document);
@@ -1274,16 +1181,19 @@ describe('ReactDOMFiber', () => {
expect(iframeContainer.appendChild).toHaveBeenCalledTimes(1);
});
- it('should mount into a document fragment', () => {
+ it('should mount into a document fragment', async () => {
const fragment = document.createDocumentFragment();
- ReactDOM.render(foo
, fragment);
+ const fragmentRoot = ReactDOMClient.createRoot(fragment);
+ await act(() => {
+ fragmentRoot.render(foo
);
+ });
expect(container.innerHTML).toBe('');
container.appendChild(fragment);
expect(container.innerHTML).toBe('foo
');
});
// Regression test for https://github.com/facebook/react/issues/12643#issuecomment-413727104
- it('should not diff memoized host components', () => {
+ it('should not diff memoized host components', async () => {
const inputRef = React.createRef();
let didCallOnChange = false;
@@ -1332,44 +1242,16 @@ describe('ReactDOMFiber', () => {
}
}
- ReactDOM.render(, container);
- inputRef.current.dispatchEvent(
- new MouseEvent('click', {
- bubbles: true,
- }),
- );
- expect(didCallOnChange).toBe(true);
- });
-
- it('unmounted legacy roots should never clear newer root content from a container', () => {
- const ref = React.createRef();
-
- function OldApp() {
- const hideOnFocus = () => {
- // This app unmounts itself inside of a focus event.
- ReactDOM.unmountComponentAtNode(container);
- };
-
- return (
-
+ await act(() => {
+ root.render();
+ });
+ await act(() => {
+ inputRef.current.dispatchEvent(
+ new MouseEvent('click', {
+ bubbles: true,
+ }),
);
- }
-
- function NewApp() {
- return ;
- }
-
- ReactDOM.render(, container);
- ref.current.focus();
-
- ReactDOM.render(, container);
-
- // Calling focus again will flush previously scheduled discrete work for the old root-
- // but this should not clear out the newly mounted app.
- ref.current.focus();
-
- expect(container.textContent).toBe('new');
+ });
+ expect(didCallOnChange).toBe(true);
});
});
diff --git a/packages/react-dom/src/__tests__/ReactDOMLegacyFiber-test.js b/packages/react-dom/src/__tests__/ReactDOMLegacyFiber-test.js
new file mode 100644
index 0000000000000..1c2a83a1327d0
--- /dev/null
+++ b/packages/react-dom/src/__tests__/ReactDOMLegacyFiber-test.js
@@ -0,0 +1,1375 @@
+/**
+ * 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
+ */
+
+'use strict';
+
+const React = require('react');
+const ReactDOM = require('react-dom');
+const PropTypes = require('prop-types');
+
+describe('ReactDOMLegacyFiber', () => {
+ let container;
+
+ beforeEach(() => {
+ container = document.createElement('div');
+ document.body.appendChild(container);
+ });
+
+ afterEach(() => {
+ document.body.removeChild(container);
+ container = null;
+ jest.restoreAllMocks();
+ });
+
+ it('should render strings as children', () => {
+ const Box = ({value}) => {value}
;
+
+ ReactDOM.render(, container);
+ expect(container.textContent).toEqual('foo');
+ });
+
+ it('should render numbers as children', () => {
+ const Box = ({value}) => {value}
;
+
+ ReactDOM.render(, container);
+
+ expect(container.textContent).toEqual('10');
+ });
+
+ it('should be called a callback argument', () => {
+ // mounting phase
+ let called = false;
+ ReactDOM.render(Foo
, container, () => (called = true));
+ expect(called).toEqual(true);
+
+ // updating phase
+ called = false;
+ ReactDOM.render(Foo
, container, () => (called = true));
+ expect(called).toEqual(true);
+ });
+
+ it('should call a callback argument when the same element is re-rendered', () => {
+ class Foo extends React.Component {
+ render() {
+ return Foo
;
+ }
+ }
+ const element = ;
+
+ // mounting phase
+ let called = false;
+ ReactDOM.render(element, container, () => (called = true));
+ expect(called).toEqual(true);
+
+ // updating phase
+ called = false;
+ ReactDOM.unstable_batchedUpdates(() => {
+ ReactDOM.render(element, container, () => (called = true));
+ });
+ expect(called).toEqual(true);
+ });
+
+ it('should render a component returning strings directly from render', () => {
+ const Text = ({value}) => value;
+
+ ReactDOM.render(, container);
+ expect(container.textContent).toEqual('foo');
+ });
+
+ it('should render a component returning numbers directly from render', () => {
+ const Text = ({value}) => value;
+
+ ReactDOM.render(, container);
+
+ expect(container.textContent).toEqual('10');
+ });
+
+ it('finds the DOM Text node of a string child', () => {
+ class Text extends React.Component {
+ render() {
+ return this.props.value;
+ }
+ }
+
+ let instance = null;
+ ReactDOM.render(
+ (instance = ref)} />,
+ container,
+ );
+
+ const textNode = ReactDOM.findDOMNode(instance);
+ expect(textNode).toBe(container.firstChild);
+ expect(textNode.nodeType).toBe(3);
+ expect(textNode.nodeValue).toBe('foo');
+ });
+
+ it('finds the first child when a component returns a fragment', () => {
+ class Fragment extends React.Component {
+ render() {
+ return [, ];
+ }
+ }
+
+ let instance = null;
+ ReactDOM.render( (instance = ref)} />, container);
+
+ expect(container.childNodes.length).toBe(2);
+
+ const firstNode = ReactDOM.findDOMNode(instance);
+ expect(firstNode).toBe(container.firstChild);
+ expect(firstNode.tagName).toBe('DIV');
+ });
+
+ it('finds the first child even when fragment is nested', () => {
+ class Wrapper extends React.Component {
+ render() {
+ return this.props.children;
+ }
+ }
+
+ class Fragment extends React.Component {
+ render() {
+ return [
+
+
+ ,
+ ,
+ ];
+ }
+ }
+
+ let instance = null;
+ ReactDOM.render( (instance = ref)} />, container);
+
+ expect(container.childNodes.length).toBe(2);
+
+ const firstNode = ReactDOM.findDOMNode(instance);
+ expect(firstNode).toBe(container.firstChild);
+ expect(firstNode.tagName).toBe('DIV');
+ });
+
+ it('finds the first child even when first child renders null', () => {
+ class NullComponent extends React.Component {
+ render() {
+ return null;
+ }
+ }
+
+ class Fragment extends React.Component {
+ render() {
+ return [, , ];
+ }
+ }
+
+ let instance = null;
+ ReactDOM.render( (instance = ref)} />, container);
+
+ expect(container.childNodes.length).toBe(2);
+
+ const firstNode = ReactDOM.findDOMNode(instance);
+ expect(firstNode).toBe(container.firstChild);
+ expect(firstNode.tagName).toBe('DIV');
+ });
+
+ it('renders an empty fragment', () => {
+ const Div = () => ;
+ const EmptyFragment = () => <>>;
+ const NonEmptyFragment = () => (
+ <>
+
+ >
+ );
+
+ ReactDOM.render(, container);
+ expect(container.firstChild).toBe(null);
+
+ ReactDOM.render(, container);
+ expect(container.firstChild.tagName).toBe('DIV');
+
+ ReactDOM.render(, container);
+ expect(container.firstChild).toBe(null);
+
+ ReactDOM.render(, container);
+ expect(container.firstChild.tagName).toBe('DIV');
+
+ ReactDOM.render(, container);
+ expect(container.firstChild).toBe(null);
+ });
+
+ let svgEls, htmlEls, mathEls;
+ const expectSVG = {ref: el => svgEls.push(el)};
+ const expectHTML = {ref: el => htmlEls.push(el)};
+ const expectMath = {ref: el => mathEls.push(el)};
+
+ const usePortal = function (tree) {
+ return ReactDOM.createPortal(tree, document.createElement('div'));
+ };
+
+ const assertNamespacesMatch = function (tree) {
+ const testContainer = document.createElement('div');
+ svgEls = [];
+ htmlEls = [];
+ mathEls = [];
+
+ ReactDOM.render(tree, testContainer);
+ svgEls.forEach(el => {
+ expect(el.namespaceURI).toBe('http://www.w3.org/2000/svg');
+ });
+ htmlEls.forEach(el => {
+ expect(el.namespaceURI).toBe('http://www.w3.org/1999/xhtml');
+ });
+ mathEls.forEach(el => {
+ expect(el.namespaceURI).toBe('http://www.w3.org/1998/Math/MathML');
+ });
+
+ ReactDOM.unmountComponentAtNode(testContainer);
+ expect(testContainer.innerHTML).toBe('');
+ };
+
+ it('should render one portal', () => {
+ const portalContainer = document.createElement('div');
+
+ ReactDOM.render(
+ {ReactDOM.createPortal(
portal
, portalContainer)}
,
+ container,
+ );
+ expect(portalContainer.innerHTML).toBe('portal
');
+ expect(container.innerHTML).toBe('');
+
+ ReactDOM.unmountComponentAtNode(container);
+ expect(portalContainer.innerHTML).toBe('');
+ expect(container.innerHTML).toBe('');
+ });
+
+ it('should render many portals', () => {
+ const portalContainer1 = document.createElement('div');
+ const portalContainer2 = document.createElement('div');
+
+ const ops = [];
+ class Child extends React.Component {
+ componentDidMount() {
+ ops.push(`${this.props.name} componentDidMount`);
+ }
+ componentDidUpdate() {
+ ops.push(`${this.props.name} componentDidUpdate`);
+ }
+ componentWillUnmount() {
+ ops.push(`${this.props.name} componentWillUnmount`);
+ }
+ render() {
+ return {this.props.name}
;
+ }
+ }
+
+ class Parent extends React.Component {
+ componentDidMount() {
+ ops.push(`Parent:${this.props.step} componentDidMount`);
+ }
+ componentDidUpdate() {
+ ops.push(`Parent:${this.props.step} componentDidUpdate`);
+ }
+ componentWillUnmount() {
+ ops.push(`Parent:${this.props.step} componentWillUnmount`);
+ }
+ render() {
+ const {step} = this.props;
+ return [
+ ,
+ ReactDOM.createPortal(
+ ,
+ portalContainer1,
+ ),
+ ,
+ ReactDOM.createPortal(
+ [
+ ,
+ ,
+ ],
+ portalContainer2,
+ ),
+ ];
+ }
+ }
+
+ ReactDOM.render(, container);
+ expect(portalContainer1.innerHTML).toBe('portal1[0]:a
');
+ expect(portalContainer2.innerHTML).toBe(
+ 'portal2[0]:a
portal2[1]:a
',
+ );
+ expect(container.innerHTML).toBe(
+ 'normal[0]:a
normal[1]:a
',
+ );
+ expect(ops).toEqual([
+ 'normal[0]:a componentDidMount',
+ 'portal1[0]:a componentDidMount',
+ 'normal[1]:a componentDidMount',
+ 'portal2[0]:a componentDidMount',
+ 'portal2[1]:a componentDidMount',
+ 'Parent:a componentDidMount',
+ ]);
+
+ ops.length = 0;
+ ReactDOM.render(, container);
+ expect(portalContainer1.innerHTML).toBe('portal1[0]:b
');
+ expect(portalContainer2.innerHTML).toBe(
+ 'portal2[0]:b
portal2[1]:b
',
+ );
+ expect(container.innerHTML).toBe(
+ 'normal[0]:b
normal[1]:b
',
+ );
+ expect(ops).toEqual([
+ 'normal[0]:b componentDidUpdate',
+ 'portal1[0]:b componentDidUpdate',
+ 'normal[1]:b componentDidUpdate',
+ 'portal2[0]:b componentDidUpdate',
+ 'portal2[1]:b componentDidUpdate',
+ 'Parent:b componentDidUpdate',
+ ]);
+
+ ops.length = 0;
+ ReactDOM.unmountComponentAtNode(container);
+ expect(portalContainer1.innerHTML).toBe('');
+ expect(portalContainer2.innerHTML).toBe('');
+ expect(container.innerHTML).toBe('');
+ expect(ops).toEqual([
+ 'Parent:b componentWillUnmount',
+ 'normal[0]:b componentWillUnmount',
+ 'portal1[0]:b componentWillUnmount',
+ 'normal[1]:b componentWillUnmount',
+ 'portal2[0]:b componentWillUnmount',
+ 'portal2[1]:b componentWillUnmount',
+ ]);
+ });
+
+ it('should render nested portals', () => {
+ const portalContainer1 = document.createElement('div');
+ const portalContainer2 = document.createElement('div');
+ const portalContainer3 = document.createElement('div');
+
+ ReactDOM.render(
+ [
+ normal[0]
,
+ ReactDOM.createPortal(
+ [
+ portal1[0]
,
+ ReactDOM.createPortal(
+ portal2[0]
,
+ portalContainer2,
+ ),
+ ReactDOM.createPortal(
+ portal3[0]
,
+ portalContainer3,
+ ),
+ portal1[1]
,
+ ],
+ portalContainer1,
+ ),
+ normal[1]
,
+ ],
+ container,
+ );
+ expect(portalContainer1.innerHTML).toBe(
+ 'portal1[0]
portal1[1]
',
+ );
+ expect(portalContainer2.innerHTML).toBe('portal2[0]
');
+ expect(portalContainer3.innerHTML).toBe('portal3[0]
');
+ expect(container.innerHTML).toBe(
+ 'normal[0]
normal[1]
',
+ );
+
+ ReactDOM.unmountComponentAtNode(container);
+ expect(portalContainer1.innerHTML).toBe('');
+ expect(portalContainer2.innerHTML).toBe('');
+ expect(portalContainer3.innerHTML).toBe('');
+ expect(container.innerHTML).toBe('');
+ });
+
+ it('should reconcile portal children', () => {
+ const portalContainer = document.createElement('div');
+
+ ReactDOM.render(
+ {ReactDOM.createPortal(
portal:1
, portalContainer)}
,
+ container,
+ );
+ expect(portalContainer.innerHTML).toBe('portal:1
');
+ expect(container.innerHTML).toBe('');
+
+ ReactDOM.render(
+ {ReactDOM.createPortal(
portal:2
, portalContainer)}
,
+ container,
+ );
+ expect(portalContainer.innerHTML).toBe('portal:2
');
+ expect(container.innerHTML).toBe('');
+
+ ReactDOM.render(
+ {ReactDOM.createPortal(
portal:3
, portalContainer)}
,
+ container,
+ );
+ expect(portalContainer.innerHTML).toBe('portal:3
');
+ expect(container.innerHTML).toBe('');
+
+ ReactDOM.render(
+ {ReactDOM.createPortal(['Hi', 'Bye'], portalContainer)}
,
+ container,
+ );
+ expect(portalContainer.innerHTML).toBe('HiBye');
+ expect(container.innerHTML).toBe('');
+
+ ReactDOM.render(
+ {ReactDOM.createPortal(['Bye', 'Hi'], portalContainer)}
,
+ container,
+ );
+ expect(portalContainer.innerHTML).toBe('ByeHi');
+ expect(container.innerHTML).toBe('');
+
+ ReactDOM.render(
+ {ReactDOM.createPortal(null, portalContainer)}
,
+ container,
+ );
+ expect(portalContainer.innerHTML).toBe('');
+ expect(container.innerHTML).toBe('');
+ });
+
+ it('should unmount empty portal component wherever it appears', () => {
+ const portalContainer = document.createElement('div');
+
+ class Wrapper extends React.Component {
+ constructor(props) {
+ super(props);
+ this.state = {
+ show: true,
+ };
+ }
+ render() {
+ return (
+
+ {this.state.show && (
+ <>
+ {ReactDOM.createPortal(null, portalContainer)}
+
child
+ >
+ )}
+
parent
+
+ );
+ }
+ }
+
+ const instance = ReactDOM.render(, container);
+ expect(container.innerHTML).toBe(
+ '',
+ );
+ instance.setState({show: false});
+ expect(instance.state.show).toBe(false);
+ expect(container.innerHTML).toBe('');
+ });
+
+ it('should keep track of namespace across portals (simple)', () => {
+ assertNamespacesMatch(
+ ,
+ );
+ assertNamespacesMatch(
+ ,
+ );
+ assertNamespacesMatch(
+
+
+ {usePortal(
+
,
+ )}
+
+
,
+ );
+ });
+
+ it('should keep track of namespace across portals (medium)', () => {
+ assertNamespacesMatch(
+ ,
+ );
+ assertNamespacesMatch(
+
+
+
+
,
+ );
+ assertNamespacesMatch(
+ ,
+ );
+ assertNamespacesMatch(
+
+ {usePortal(
+
,
+ )}
+
+
,
+ );
+ assertNamespacesMatch(
+ ,
+ );
+ });
+
+ it('should keep track of namespace across portals (complex)', () => {
+ assertNamespacesMatch(
+
+ {usePortal(
+
+
+ ,
+ )}
+
+
+
+
+
+
+
+
+
+
+
+
,
+ );
+ assertNamespacesMatch(
+
+
+
+
+ {usePortal(
+
+
+
+
+
+
+ ,
+ )}
+
+
+
+ {usePortal()}
+
+
+
+
+
+
+
,
+ );
+ assertNamespacesMatch(
+
+
+
+
+ {usePortal(
+
+
+
+
+
+
+
+ {usePortal()}
+
+
+ ,
+ )}
+
+
+
+
+
+
,
+ );
+ });
+
+ it('should unwind namespaces on uncaught errors', () => {
+ function BrokenRender() {
+ throw new Error('Hello');
+ }
+
+ expect(() => {
+ assertNamespacesMatch(
+
+
+ ,
+ );
+ }).toThrow('Hello');
+ assertNamespacesMatch();
+ });
+
+ it('should unwind namespaces on caught errors', () => {
+ function BrokenRender() {
+ throw new Error('Hello');
+ }
+
+ class ErrorBoundary extends React.Component {
+ state = {error: null};
+ componentDidCatch(error) {
+ this.setState({error});
+ }
+ render() {
+ if (this.state.error) {
+ return ;
+ }
+ return this.props.children;
+ }
+ }
+
+ assertNamespacesMatch(
+
+
+
+
+
+
+
+ ,
+ );
+ assertNamespacesMatch();
+ });
+
+ it('should unwind namespaces on caught errors in a portal', () => {
+ function BrokenRender() {
+ throw new Error('Hello');
+ }
+
+ class ErrorBoundary extends React.Component {
+ state = {error: null};
+ componentDidCatch(error) {
+ this.setState({error});
+ }
+ render() {
+ if (this.state.error) {
+ return ;
+ }
+ return this.props.children;
+ }
+ }
+
+ assertNamespacesMatch(
+
+
+ {usePortal(
+
+
+
,
+ )}
+
+ {usePortal()}
+ ,
+ );
+ });
+
+ // @gate !disableLegacyContext
+ it('should pass portal context when rendering subtree elsewhere', () => {
+ const portalContainer = document.createElement('div');
+
+ class Component extends React.Component {
+ static contextTypes = {
+ foo: PropTypes.string.isRequired,
+ };
+
+ render() {
+ return {this.context.foo}
;
+ }
+ }
+
+ class Parent extends React.Component {
+ static childContextTypes = {
+ foo: PropTypes.string.isRequired,
+ };
+
+ getChildContext() {
+ return {
+ foo: 'bar',
+ };
+ }
+
+ render() {
+ return ReactDOM.createPortal(, portalContainer);
+ }
+ }
+
+ ReactDOM.render(, container);
+ expect(container.innerHTML).toBe('');
+ expect(portalContainer.innerHTML).toBe('bar
');
+ });
+
+ // @gate !disableLegacyContext
+ it('should update portal context if it changes due to setState', () => {
+ const portalContainer = document.createElement('div');
+
+ class Component extends React.Component {
+ static contextTypes = {
+ foo: PropTypes.string.isRequired,
+ getFoo: PropTypes.func.isRequired,
+ };
+
+ render() {
+ return {this.context.foo + '-' + this.context.getFoo()}
;
+ }
+ }
+
+ class Parent extends React.Component {
+ static childContextTypes = {
+ foo: PropTypes.string.isRequired,
+ getFoo: PropTypes.func.isRequired,
+ };
+
+ state = {
+ bar: 'initial',
+ };
+
+ getChildContext() {
+ return {
+ foo: this.state.bar,
+ getFoo: () => this.state.bar,
+ };
+ }
+
+ render() {
+ return ReactDOM.createPortal(, portalContainer);
+ }
+ }
+
+ const instance = ReactDOM.render(, container);
+ expect(portalContainer.innerHTML).toBe('initial-initial
');
+ expect(container.innerHTML).toBe('');
+ instance.setState({bar: 'changed'});
+ expect(portalContainer.innerHTML).toBe('changed-changed
');
+ expect(container.innerHTML).toBe('');
+ });
+
+ // @gate !disableLegacyContext
+ it('should update portal context if it changes due to re-render', () => {
+ const portalContainer = document.createElement('div');
+
+ class Component extends React.Component {
+ static contextTypes = {
+ foo: PropTypes.string.isRequired,
+ getFoo: PropTypes.func.isRequired,
+ };
+
+ render() {
+ return {this.context.foo + '-' + this.context.getFoo()}
;
+ }
+ }
+
+ class Parent extends React.Component {
+ static childContextTypes = {
+ foo: PropTypes.string.isRequired,
+ getFoo: PropTypes.func.isRequired,
+ };
+
+ getChildContext() {
+ return {
+ foo: this.props.bar,
+ getFoo: () => this.props.bar,
+ };
+ }
+
+ render() {
+ return ReactDOM.createPortal(, portalContainer);
+ }
+ }
+
+ ReactDOM.render(, container);
+ expect(portalContainer.innerHTML).toBe('initial-initial
');
+ expect(container.innerHTML).toBe('');
+ ReactDOM.render(, container);
+ expect(portalContainer.innerHTML).toBe('changed-changed
');
+ expect(container.innerHTML).toBe('');
+ });
+
+ it('findDOMNode should find dom element after expanding a fragment', () => {
+ class MyNode extends React.Component {
+ render() {
+ return !this.props.flag
+ ? []
+ : [, ];
+ }
+ }
+
+ const myNodeA = ReactDOM.render(, container);
+ const a = ReactDOM.findDOMNode(myNodeA);
+ expect(a.tagName).toBe('DIV');
+
+ const myNodeB = ReactDOM.render(, container);
+ expect(myNodeA === myNodeB).toBe(true);
+
+ const b = ReactDOM.findDOMNode(myNodeB);
+ expect(b.tagName).toBe('SPAN');
+ });
+
+ it('should bubble events from the portal to the parent', () => {
+ const portalContainer = document.createElement('div');
+ document.body.appendChild(portalContainer);
+ try {
+ const ops = [];
+ let portal = null;
+
+ ReactDOM.render(
+ ops.push('parent clicked')}>
+ {ReactDOM.createPortal(
+
ops.push('portal clicked')}
+ ref={n => (portal = n)}>
+ portal
+
,
+ portalContainer,
+ )}
+
,
+ container,
+ );
+
+ expect(portal.tagName).toBe('DIV');
+
+ portal.click();
+
+ expect(ops).toEqual(['portal clicked', 'parent clicked']);
+ } finally {
+ document.body.removeChild(portalContainer);
+ }
+ });
+
+ it('should not onMouseLeave when staying in the portal', () => {
+ const portalContainer = document.createElement('div');
+ document.body.appendChild(portalContainer);
+
+ let ops = [];
+ let firstTarget = null;
+ let secondTarget = null;
+ let thirdTarget = null;
+
+ function simulateMouseMove(from, to) {
+ if (from) {
+ from.dispatchEvent(
+ new MouseEvent('mouseout', {
+ bubbles: true,
+ cancelable: true,
+ relatedTarget: to,
+ }),
+ );
+ }
+ if (to) {
+ to.dispatchEvent(
+ new MouseEvent('mouseover', {
+ bubbles: true,
+ cancelable: true,
+ relatedTarget: from,
+ }),
+ );
+ }
+ }
+
+ try {
+ ReactDOM.render(
+
+
ops.push('enter parent')}
+ onMouseLeave={() => ops.push('leave parent')}>
+
(firstTarget = n)} />
+ {ReactDOM.createPortal(
+
ops.push('enter portal')}
+ onMouseLeave={() => ops.push('leave portal')}
+ ref={n => (secondTarget = n)}>
+ portal
+
,
+ portalContainer,
+ )}
+
+
(thirdTarget = n)} />
+
,
+ container,
+ );
+
+ simulateMouseMove(null, firstTarget);
+ expect(ops).toEqual(['enter parent']);
+
+ ops = [];
+
+ simulateMouseMove(firstTarget, secondTarget);
+ expect(ops).toEqual([
+ // Parent did not invoke leave because we're still inside the portal.
+ 'enter portal',
+ ]);
+
+ ops = [];
+
+ simulateMouseMove(secondTarget, thirdTarget);
+ expect(ops).toEqual([
+ 'leave portal',
+ 'leave parent', // Only when we leave the portal does onMouseLeave fire.
+ ]);
+ } finally {
+ document.body.removeChild(portalContainer);
+ }
+ });
+
+ // Regression test for https://github.com/facebook/react/issues/19562
+ it('does not fire mouseEnter twice when relatedTarget is the root node', () => {
+ let ops = [];
+ let target = null;
+
+ function simulateMouseMove(from, to) {
+ if (from) {
+ from.dispatchEvent(
+ new MouseEvent('mouseout', {
+ bubbles: true,
+ cancelable: true,
+ relatedTarget: to,
+ }),
+ );
+ }
+ if (to) {
+ to.dispatchEvent(
+ new MouseEvent('mouseover', {
+ bubbles: true,
+ cancelable: true,
+ relatedTarget: from,
+ }),
+ );
+ }
+ }
+
+ ReactDOM.render(
+
(target = n)}
+ onMouseEnter={() => ops.push('enter')}
+ onMouseLeave={() => ops.push('leave')}
+ />,
+ container,
+ );
+
+ simulateMouseMove(null, container);
+ expect(ops).toEqual([]);
+
+ ops = [];
+ simulateMouseMove(container, target);
+ expect(ops).toEqual(['enter']);
+
+ ops = [];
+ simulateMouseMove(target, container);
+ expect(ops).toEqual(['leave']);
+
+ ops = [];
+ simulateMouseMove(container, null);
+ expect(ops).toEqual([]);
+ });
+
+ it('listens to events that do not exist in the Portal subtree', () => {
+ const onClick = jest.fn();
+
+ const ref = React.createRef();
+ ReactDOM.render(
+
+ {ReactDOM.createPortal(, document.body)}
+
,
+ container,
+ );
+ const event = new MouseEvent('click', {
+ bubbles: true,
+ });
+ ref.current.dispatchEvent(event);
+
+ expect(onClick).toHaveBeenCalledTimes(1);
+ });
+
+ it('should throw on bad createPortal argument', () => {
+ expect(() => {
+ ReactDOM.createPortal(
portal
, null);
+ }).toThrow('Target container is not a DOM element.');
+ expect(() => {
+ ReactDOM.createPortal(
portal
, document.createTextNode('hi'));
+ }).toThrow('Target container is not a DOM element.');
+ });
+
+ it('should warn for non-functional event listeners', () => {
+ class Example extends React.Component {
+ render() {
+ return
;
+ }
+ }
+ expect(() => ReactDOM.render(
, container)).toErrorDev(
+ 'Expected `onClick` listener to be a function, instead got a value of `string` type.\n' +
+ ' in div (at **)\n' +
+ ' in Example (at **)',
+ );
+ });
+
+ it('should warn with a special message for `false` event listeners', () => {
+ class Example extends React.Component {
+ render() {
+ return
;
+ }
+ }
+ expect(() => ReactDOM.render(
, container)).toErrorDev(
+ 'Expected `onClick` listener to be a function, instead got `false`.\n\n' +
+ 'If you used to conditionally omit it with onClick={condition && value}, ' +
+ 'pass onClick={condition ? value : undefined} instead.\n' +
+ ' in div (at **)\n' +
+ ' in Example (at **)',
+ );
+ });
+
+ it('should not update event handlers until commit', () => {
+ spyOnDev(console, 'error');
+
+ let ops = [];
+ const handlerA = () => ops.push('A');
+ const handlerB = () => ops.push('B');
+
+ function click() {
+ const event = new MouseEvent('click', {
+ bubbles: true,
+ cancelable: true,
+ });
+ Object.defineProperty(event, 'timeStamp', {
+ value: 0,
+ });
+ node.dispatchEvent(event);
+ }
+
+ class Example extends React.Component {
+ state = {flip: false, count: 0};
+ flip() {
+ this.setState({flip: true, count: this.state.count + 1});
+ }
+ tick() {
+ this.setState({count: this.state.count + 1});
+ }
+ render() {
+ const useB = !this.props.forceA && this.state.flip;
+ return
;
+ }
+ }
+
+ class Click extends React.Component {
+ constructor() {
+ super();
+ node.click();
+ }
+ render() {
+ return null;
+ }
+ }
+
+ let inst;
+ ReactDOM.render([
(inst = n)} />], container);
+ const node = container.firstChild;
+ expect(node.tagName).toEqual('DIV');
+
+ click();
+
+ expect(ops).toEqual(['A']);
+ ops = [];
+
+ // Render with the other event handler.
+ inst.flip();
+
+ click();
+
+ expect(ops).toEqual(['B']);
+ ops = [];
+
+ // Rerender without changing any props.
+ inst.tick();
+
+ click();
+
+ expect(ops).toEqual(['B']);
+ ops = [];
+
+ // Render a flip back to the A handler. The second component invokes the
+ // click handler during render to simulate a click during an aborted
+ // render. I use this hack because at current time we don't have a way to
+ // test aborted ReactDOM renders.
+ ReactDOM.render(
+ [, ],
+ container,
+ );
+
+ // Because the new click handler has not yet committed, we should still
+ // invoke B.
+ expect(ops).toEqual(['B']);
+ ops = [];
+
+ // Any click that happens after commit, should invoke A.
+ click();
+ expect(ops).toEqual(['A']);
+
+ if (__DEV__) {
+ expect(console.error).toHaveBeenCalledTimes(2);
+ expect(console.error.mock.calls[0][0]).toMatch(
+ 'ReactDOM.render is no longer supported in React 18',
+ );
+ expect(console.error.mock.calls[1][0]).toMatch(
+ 'ReactDOM.render is no longer supported in React 18',
+ );
+ }
+ });
+
+ it('should not crash encountering low-priority tree', () => {
+ ReactDOM.render(
+ ,
+ container,
+ );
+ });
+
+ it('should not warn when rendering into an empty container', () => {
+ ReactDOM.render(foo
, container);
+ expect(container.innerHTML).toBe('foo
');
+ ReactDOM.render(null, container);
+ expect(container.innerHTML).toBe('');
+ ReactDOM.render(bar
, container);
+ expect(container.innerHTML).toBe('bar
');
+ });
+
+ 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(foo
, container);
+ ReactDOM.render(bar
, container);
+ expect(container.innerHTML).toBe('bar
');
+ // then we mess with the DOM before an update
+ // we know this will error - that is expected right now
+ // It's an error of type 'NotFoundError' with no message
+ container.innerHTML = 'MEOW.
';
+
+ expect(() => {
+ expect(() =>
+ ReactDOM.render(baz
, container),
+ ).toErrorDev(
+ 'render(...): ' +
+ 'It looks like the React-rendered content of this container was ' +
+ 'removed without using React. This is not supported and will ' +
+ 'cause errors. Instead, call ReactDOM.unmountComponentAtNode ' +
+ 'to empty a container.',
+ {withoutStack: true},
+ );
+ }).toThrowError();
+ });
+
+ 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(foo
, container);
+ ReactDOM.render(bar
, container);
+ expect(container.innerHTML).toBe('bar
');
+ // then we mess with the DOM before an update
+ container.innerHTML = 'MEOW.
';
+ expect(() => ReactDOM.render(baz
, container)).toErrorDev(
+ 'render(...): ' +
+ 'It looks like the React-rendered content of this container was ' +
+ 'removed without using React. This is not supported and will ' +
+ 'cause errors. Instead, call ReactDOM.unmountComponentAtNode ' +
+ 'to empty a container.',
+ {withoutStack: true},
+ );
+ });
+
+ 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(foo
, container);
+ ReactDOM.render(bar
, container);
+ expect(container.innerHTML).toBe('bar
');
+ // then we mess with the DOM before an update
+ container.innerHTML = '';
+ expect(() => ReactDOM.render(baz
, container)).toErrorDev(
+ 'render(...): ' +
+ 'It looks like the React-rendered content of this container was ' +
+ 'removed without using React. This is not supported and will ' +
+ 'cause errors. Instead, call ReactDOM.unmountComponentAtNode ' +
+ 'to empty a container.',
+ {withoutStack: true},
+ );
+ });
+
+ 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
+ // is rendered inside the iframe container
+ const textContent = 'Hello world';
+ const iframe = document.createElement('iframe');
+ document.body.appendChild(iframe);
+ const iframeDocument = iframe.contentDocument;
+ iframeDocument.write(
+ '',
+ );
+ iframeDocument.close();
+ const iframeContainer = iframeDocument.body.firstChild;
+
+ let actualDocument;
+ let textNode;
+
+ spyOnDevAndProd(iframeContainer, 'appendChild').mockImplementation(node => {
+ actualDocument = node.ownerDocument;
+ textNode = node;
+ });
+
+ ReactDOM.render(textContent, iframeContainer);
+
+ expect(textNode.textContent).toBe(textContent);
+ expect(actualDocument).not.toBe(document);
+ expect(actualDocument).toBe(iframeDocument);
+ expect(iframeContainer.appendChild).toHaveBeenCalledTimes(1);
+ });
+
+ it('should mount into a document fragment', () => {
+ const fragment = document.createDocumentFragment();
+ ReactDOM.render(foo
, fragment);
+ expect(container.innerHTML).toBe('');
+ container.appendChild(fragment);
+ expect(container.innerHTML).toBe('foo
');
+ });
+
+ // Regression test for https://github.com/facebook/react/issues/12643#issuecomment-413727104
+ it('should not diff memoized host components', () => {
+ const inputRef = React.createRef();
+ let didCallOnChange = false;
+
+ class Child extends React.Component {
+ state = {};
+ componentDidMount() {
+ document.addEventListener('click', this.update, true);
+ }
+ componentWillUnmount() {
+ document.removeEventListener('click', this.update, true);
+ }
+ update = () => {
+ // We're testing that this setState()
+ // doesn't cause React to commit updates
+ // to the input outside (which would itself
+ // prevent the parent's onChange parent handler
+ // from firing).
+ this.setState({});
+ // Note that onChange was always broken when there was an
+ // earlier setState() in a manual document capture phase
+ // listener *in the same component*. But that's very rare.
+ // Here we're testing that a *child* component doesn't break
+ // the parent if this happens.
+ };
+ render() {
+ return ;
+ }
+ }
+
+ class Parent extends React.Component {
+ handleChange = val => {
+ didCallOnChange = true;
+ };
+ render() {
+ return (
+
+
+
+
+ );
+ }
+ }
+
+ ReactDOM.render(, container);
+ inputRef.current.dispatchEvent(
+ new MouseEvent('click', {
+ bubbles: true,
+ }),
+ );
+ expect(didCallOnChange).toBe(true);
+ });
+
+ it('unmounted legacy roots should never clear newer root content from a container', () => {
+ const ref = React.createRef();
+
+ function OldApp() {
+ const hideOnFocus = () => {
+ // This app unmounts itself inside of a focus event.
+ ReactDOM.unmountComponentAtNode(container);
+ };
+
+ return (
+
+ );
+ }
+
+ function NewApp() {
+ return ;
+ }
+
+ ReactDOM.render(, container);
+ ref.current.focus();
+
+ ReactDOM.render(, container);
+
+ // Calling focus again will flush previously scheduled discrete work for the old root-
+ // but this should not clear out the newly mounted app.
+ ref.current.focus();
+
+ expect(container.textContent).toBe('new');
+ });
+});