/**
* 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('ReactDOMFiber', () => {
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}) => <div>{value}</div>;
ReactDOM.render(<Box value="foo" />, container);
expect(container.textContent).toEqual('foo');
});
it('should render numbers as children', () => {
const Box = ({value}) => <div>{value}</div>;
ReactDOM.render(<Box value={10} />, container);
expect(container.textContent).toEqual('10');
});
it('should be called a callback argument', () => {
// mounting phase
let called = false;
ReactDOM.render(<div>Foo</div>, container, () => (called = true));
expect(called).toEqual(true);
// updating phase
called = false;
ReactDOM.render(<div>Foo</div>, 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 <div>Foo</div>;
}
}
const element = <Foo />;
// 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(<Text value="foo" />, container);
expect(container.textContent).toEqual('foo');
});
it('should render a component returning numbers directly from render', () => {
const Text = ({value}) => value;
ReactDOM.render(<Text value={10} />, 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(
<Text value="foo" ref={ref => (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 [<div key="a" />, <span key="b" />];
}
}
let instance = null;
ReactDOM.render(<Fragment ref={ref => (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 [
<Wrapper key="a">
<div />
</Wrapper>,
<span key="b" />,
];
}
}
let instance = null;
ReactDOM.render(<Fragment ref={ref => (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 [<NullComponent key="a" />, <div key="b" />, <span key="c" />];
}
}
let instance = null;
ReactDOM.render(<Fragment ref={ref => (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 = () => <div />;
const EmptyFragment = () => <></>;
const NonEmptyFragment = () => (
<>
<Div />
</>
);
ReactDOM.render(<EmptyFragment />, container);
expect(container.firstChild).toBe(null);
ReactDOM.render(<NonEmptyFragment />, container);
expect(container.firstChild.tagName).toBe('DIV');
ReactDOM.render(<EmptyFragment />, container);
expect(container.firstChild).toBe(null);
ReactDOM.render(<Div />, container);
expect(container.firstChild.tagName).toBe('DIV');
ReactDOM.render(<EmptyFragment />, 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(
<div>{ReactDOM.createPortal(<div>portal</div>, portalContainer)}</div>,
container,
);
expect(portalContainer.innerHTML).toBe('<div>portal</div>');
expect(container.innerHTML).toBe('<div></div>');
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 <div>{this.props.name}</div>;
}
}
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 [
<Child key="a" name={`normal[0]:${step}`} />,
ReactDOM.createPortal(
<Child key="b" name={`portal1[0]:${step}`} />,
portalContainer1,
),
<Child key="c" name={`normal[1]:${step}`} />,
ReactDOM.createPortal(
[
<Child key="d" name={`portal2[0]:${step}`} />,
<Child key="e" name={`portal2[1]:${step}`} />,
],
portalContainer2,
),
];
}
}
ReactDOM.render(<Parent step="a" />, container);
expect(portalContainer1.innerHTML).toBe('<div>portal1[0]:a</div>');
expect(portalContainer2.innerHTML).toBe(
'<div>portal2[0]:a</div><div>portal2[1]:a</div>',
);
expect(container.innerHTML).toBe(
'<div>normal[0]:a</div><div>normal[1]:a</div>',
);
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(<Parent step="b" />, container);
expect(portalContainer1.innerHTML).toBe('<div>portal1[0]:b</div>');
expect(portalContainer2.innerHTML).toBe(
'<div>portal2[0]:b</div><div>portal2[1]:b</div>',
);
expect(container.innerHTML).toBe(
'<div>normal[0]:b</div><div>normal[1]:b</div>',
);
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(
[
<div key="a">normal[0]</div>,
ReactDOM.createPortal(
[
<div key="b">portal1[0]</div>,
ReactDOM.createPortal(
<div key="c">portal2[0]</div>,
portalContainer2,
),
ReactDOM.createPortal(
<div key="d">portal3[0]</div>,
portalContainer3,
),
<div key="e">portal1[1]</div>,
],
portalContainer1,
),
<div key="f">normal[1]</div>,
],
container,
);
expect(portalContainer1.innerHTML).toBe(
'<div>portal1[0]</div><div>portal1[1]</div>',
);
expect(portalContainer2.innerHTML).toBe('<div>portal2[0]</div>');
expect(portalContainer3.innerHTML).toBe('<div>portal3[0]</div>');
expect(container.innerHTML).toBe(
'<div>normal[0]</div><div>normal[1]</div>',
);
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(
<div>{ReactDOM.createPortal(<div>portal:1</div>, portalContainer)}</div>,
container,
);
expect(portalContainer.innerHTML).toBe('<div>portal:1</div>');
expect(container.innerHTML).toBe('<div></div>');
ReactDOM.render(
<div>{ReactDOM.createPortal(<div>portal:2</div>, portalContainer)}</div>,
container,
);
expect(portalContainer.innerHTML).toBe('<div>portal:2</div>');
expect(container.innerHTML).toBe('<div></div>');
ReactDOM.render(
<div>{ReactDOM.createPortal(<p>portal:3</p>, portalContainer)}</div>,
container,
);
expect(portalContainer.innerHTML).toBe('<p>portal:3</p>');
expect(container.innerHTML).toBe('<div></div>');
ReactDOM.render(
<div>{ReactDOM.createPortal(['Hi', 'Bye'], portalContainer)}</div>,
container,
);
expect(portalContainer.innerHTML).toBe('HiBye');
expect(container.innerHTML).toBe('<div></div>');
ReactDOM.render(
<div>{ReactDOM.createPortal(['Bye', 'Hi'], portalContainer)}</div>,
container,
);
expect(portalContainer.innerHTML).toBe('ByeHi');
expect(container.innerHTML).toBe('<div></div>');
ReactDOM.render(
<div>{ReactDOM.createPortal(null, portalContainer)}</div>,
container,
);
expect(portalContainer.innerHTML).toBe('');
expect(container.innerHTML).toBe('<div></div>');
});
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 (
<div>
{this.state.show && (
<>
{ReactDOM.createPortal(null, portalContainer)}
<div>child</div>
</>
)}
<div>parent</div>
</div>
);
}
}
const instance = ReactDOM.render(<Wrapper />, container);
expect(container.innerHTML).toBe(
'<div><div>child</div><div>parent</div></div>',
);
instance.setState({show: false});
expect(instance.state.show).toBe(false);
expect(container.innerHTML).toBe('<div><div>parent</div></div>');
});
it('should keep track of namespace across portals (simple)', () => {
assertNamespacesMatch(
<svg {...expectSVG}>
<image {...expectSVG} />
{usePortal(<div {...expectHTML} />)}
<image {...expectSVG} />
</svg>,
);
assertNamespacesMatch(
<math {...expectMath}>
<mi {...expectMath} />
{usePortal(<div {...expectHTML} />)}
<mi {...expectMath} />
</math>,
);
assertNamespacesMatch(
<div {...expectHTML}>
<p {...expectHTML} />
{usePortal(
<svg {...expectSVG}>
<image {...expectSVG} />
</svg>,
)}
<p {...expectHTML} />
</div>,
);
});
it('should keep track of namespace across portals (medium)', () => {
assertNamespacesMatch(
<svg {...expectSVG}>
<image {...expectSVG} />
{usePortal(<div {...expectHTML} />)}
<image {...expectSVG} />
{usePortal(<div {...expectHTML} />)}
<image {...expectSVG} />
</svg>,
);
assertNamespacesMatch(
<div {...expectHTML}>
<math {...expectMath}>
<mi {...expectMath} />
{usePortal(
<svg {...expectSVG}>
<image {...expectSVG} />
</svg>,
)}
</math>
<p {...expectHTML} />
</div>,
);
assertNamespacesMatch(
<math {...expectMath}>
<mi {...expectMath} />
{usePortal(
<svg {...expectSVG}>
<image {...expectSVG} />
<foreignObject {...expectSVG}>
<p {...expectHTML} />
<math {...expectMath}>
<mi {...expectMath} />
</math>
<p {...expectHTML} />
</foreignObject>
<image {...expectSVG} />
</svg>,
)}
<mi {...expectMath} />
</math>,
);
assertNamespacesMatch(
<div {...expectHTML}>
{usePortal(
<svg {...expectSVG}>
{usePortal(<div {...expectHTML} />)}
<image {...expectSVG} />
</svg>,
)}
<p {...expectHTML} />
</div>,
);
assertNamespacesMatch(
<svg {...expectSVG}>
<svg {...expectSVG}>
{usePortal(<div {...expectHTML} />)}
<image {...expectSVG} />
</svg>
<image {...expectSVG} />
</svg>,
);
});
it('should keep track of namespace across portals (complex)', () => {
assertNamespacesMatch(
<div {...expectHTML}>
{usePortal(
<svg {...expectSVG}>
<image {...expectSVG} />
</svg>,
)}
<p {...expectHTML} />
<svg {...expectSVG}>
<image {...expectSVG} />
</svg>
<svg {...expectSVG}>
<svg {...expectSVG}>
<image {...expectSVG} />
</svg>
<image {...expectSVG} />
</svg>
<p {...expectHTML} />
</div>,
);
assertNamespacesMatch(
<div {...expectHTML}>
<svg {...expectSVG}>
<svg {...expectSVG}>
<image {...expectSVG} />
{usePortal(
<svg {...expectSVG}>
<image {...expectSVG} />
<svg {...expectSVG}>
<image {...expectSVG} />
</svg>
<image {...expectSVG} />
</svg>,
)}
<image {...expectSVG} />
<foreignObject {...expectSVG}>
<p {...expectHTML} />
{usePortal(<p {...expectHTML} />)}
<p {...expectHTML} />
</foreignObject>
</svg>
<image {...expectSVG} />
</svg>
<p {...expectHTML} />
</div>,
);
assertNamespacesMatch(
<div {...expectHTML}>
<svg {...expectSVG}>
<foreignObject {...expectSVG}>
<p {...expectHTML} />
{usePortal(
<svg {...expectSVG}>
<image {...expectSVG} />
<svg {...expectSVG}>
<image {...expectSVG} />
<foreignObject {...expectSVG}>
<p {...expectHTML} />
</foreignObject>
{usePortal(<p {...expectHTML} />)}
</svg>
<image {...expectSVG} />
</svg>,
)}
<p {...expectHTML} />
</foreignObject>
<image {...expectSVG} />
</svg>
<p {...expectHTML} />
</div>,
);
});
it('should unwind namespaces on uncaught errors', () => {
function BrokenRender() {
throw new Error('Hello');
}
expect(() => {
assertNamespacesMatch(
<svg {...expectSVG}>
<BrokenRender />
</svg>,
);
}).toThrow('Hello');
assertNamespacesMatch(<div {...expectHTML} />);
});
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 <p {...expectHTML} />;
}
return this.props.children;
}
}
assertNamespacesMatch(
<svg {...expectSVG}>
<foreignObject {...expectSVG}>
<ErrorBoundary>
<math {...expectMath}>
<BrokenRender />
</math>
</ErrorBoundary>
</foreignObject>
<image {...expectSVG} />
</svg>,
);
assertNamespacesMatch(<div {...expectHTML} />);
});
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 <image {...expectSVG} />;
}
return this.props.children;
}
}
assertNamespacesMatch(
<svg {...expectSVG}>
<ErrorBoundary>
{usePortal(
<div {...expectHTML}>
<math {...expectMath}>
<BrokenRender />)
</math>
</div>,
)}
</ErrorBoundary>
{usePortal(<div {...expectHTML} />)}
</svg>,
);
});
// @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 <div>{this.context.foo}</div>;
}
}
class Parent extends React.Component {
static childContextTypes = {
foo: PropTypes.string.isRequired,
};
getChildContext() {
return {
foo: 'bar',
};
}
render() {
return ReactDOM.createPortal(<Component />, portalContainer);
}
}
ReactDOM.render(<Parent />, container);
expect(container.innerHTML).toBe('');
expect(portalContainer.innerHTML).toBe('<div>bar</div>');
});
// @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 <div>{this.context.foo + '-' + this.context.getFoo()}</div>;
}
}
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(<Component />, portalContainer);
}
}
const instance = ReactDOM.render(<Parent />, container);
expect(portalContainer.innerHTML).toBe('<div>initial-initial</div>');
expect(container.innerHTML).toBe('');
instance.setState({bar: 'changed'});
expect(portalContainer.innerHTML).toBe('<div>changed-changed</div>');
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 <div>{this.context.foo + '-' + this.context.getFoo()}</div>;
}
}
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(<Component />, portalContainer);
}
}
ReactDOM.render(<Parent bar="initial" />, container);
expect(portalContainer.innerHTML).toBe('<div>initial-initial</div>');
expect(container.innerHTML).toBe('');
ReactDOM.render(<Parent bar="changed" />, container);
expect(portalContainer.innerHTML).toBe('<div>changed-changed</div>');
expect(container.innerHTML).toBe('');
});
it('findDOMNode should find dom element after expanding a fragment', () => {
class MyNode extends React.Component {
render() {
return !this.props.flag
? [<div key="a" />]
: [<span key="b" />, <div key="a" />];
}
}
const myNodeA = ReactDOM.render(<MyNode />, container);
const a = ReactDOM.findDOMNode(myNodeA);
expect(a.tagName).toBe('DIV');
const myNodeB = ReactDOM.render(<MyNode flag={true} />, 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(
<div onClick={() => ops.push('parent clicked')}>
{ReactDOM.createPortal(
<div
onClick={() => ops.push('portal clicked')}
ref={n => (portal = n)}>
portal
</div>,
portalContainer,
)}
</div>,
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(
<div>
<div
onMouseEnter={() => ops.push('enter parent')}
onMouseLeave={() => ops.push('leave parent')}>
<div ref={n => (firstTarget = n)} />
{ReactDOM.createPortal(
<div
onMouseEnter={() => ops.push('enter portal')}
onMouseLeave={() => ops.push('leave portal')}
ref={n => (secondTarget = n)}>
portal
</div>,
portalContainer,
)}
</div>
<div ref={n => (thirdTarget = n)} />
</div>,
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(
<div
ref={n => (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(
<div onClick={onClick}>
{ReactDOM.createPortal(<button ref={ref}>click</button>, document.body)}
</div>,
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(<div>portal</div>, null);
}).toThrow('Target container is not a DOM element.');
expect(() => {
ReactDOM.createPortal(<div>portal</div>, 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 <div onClick="woops" />;
}
}
expect(() => ReactDOM.render(<Example />, 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 <div onClick={false} />;
}
}
expect(() => ReactDOM.render(<Example />, 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 <div onClick={useB ? handlerB : handlerA} />;
}
}
class Click extends React.Component {
constructor() {
super();
node.click();
}
render() {
return null;
}
}
let inst;
ReactDOM.render([<Example key="a" ref={n => (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(
[<Example key="a" forceA={true} />, <Click key="b" />],
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(
<div hidden={true}>
<div />
</div>,
container,
);
});
it('should not warn when rendering into an empty container', () => {
ReactDOM.render(<div>foo</div>, container);
expect(container.innerHTML).toBe('<div>foo</div>');
ReactDOM.render(null, container);
expect(container.innerHTML).toBe('');
ReactDOM.render(<div>bar</div>, container);
expect(container.innerHTML).toBe('<div>bar</div>');
});
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);
ReactDOM.render(<div key="1">bar</div>, container);
expect(container.innerHTML).toBe('<div>bar</div>');
// 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 = '<div>MEOW.</div>';
expect(() => {
expect(() =>
ReactDOM.render(<div key="2">baz</div>, 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(<div>foo</div>, container);
ReactDOM.render(<div>bar</div>, container);
expect(container.innerHTML).toBe('<div>bar</div>');
// then we mess with the DOM before an update
container.innerHTML = '<div>MEOW.</div>';
expect(() => ReactDOM.render(<div>baz</div>, 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(<div>foo</div>, container);
ReactDOM.render(<div>bar</div>, container);
expect(container.innerHTML).toBe('<div>bar</div>');
// then we mess with the DOM before an update
container.innerHTML = '';
expect(() => ReactDOM.render(<div>baz</div>, 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(
'<!DOCTYPE html><html><head></head><body><div></div></body></html>',
);
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(<div>foo</div>, fragment);
expect(container.innerHTML).toBe('');
container.appendChild(fragment);
expect(container.innerHTML).toBe('<div>foo</div>');
});
// 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 <div />;
}
}
class Parent extends React.Component {
handleChange = val => {
didCallOnChange = true;
};
render() {
return (
<div>
<Child />
<input
ref={inputRef}
type="checkbox"
checked={true}
onChange={this.handleChange}
/>
</div>
);
}
}
ReactDOM.render(<Parent />, 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 (
<button onFocus={hideOnFocus} ref={ref}>
old
</button>
);
}
function NewApp() {
return <button ref={ref}>new</button>;
}
ReactDOM.render(<OldApp />, container);
ref.current.focus();
ReactDOM.render(<NewApp />, 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');
});
});