/**
* 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';
let React;
let ReactDOM;
let ReactTestUtils;
let TogglingComponent;
let log;
describe('ReactEmptyComponent', () => {
beforeEach(() => {
jest.resetModules();
React = require('react');
ReactDOM = require('react-dom');
ReactTestUtils = require('react-dom/test-utils');
log = jest.fn();
TogglingComponent = class extends React.Component {
state = {component: this.props.firstComponent};
componentDidMount() {
log(ReactDOM.findDOMNode(this));
this.setState({component: this.props.secondComponent});
}
componentDidUpdate() {
log(ReactDOM.findDOMNode(this));
}
render() {
const Component = this.state.component;
return Component ? <Component /> : null;
}
};
});
describe.each([null, undefined])('when %s', nullORUndefined => {
it('should not throw when rendering', () => {
class Component extends React.Component {
render() {
return nullORUndefined;
}
}
expect(function () {
ReactTestUtils.renderIntoDocument(<Component />);
}).not.toThrowError();
});
it('should not produce child DOM nodes for nullish and false', () => {
class Component1 extends React.Component {
render() {
return nullORUndefined;
}
}
class Component2 extends React.Component {
render() {
return false;
}
}
const container1 = document.createElement('div');
ReactDOM.render(<Component1 />, container1);
expect(container1.children.length).toBe(0);
const container2 = document.createElement('div');
ReactDOM.render(<Component2 />, container2);
expect(container2.children.length).toBe(0);
});
it('should be able to switch between rendering nullish and a normal tag', () => {
const instance1 = (
<TogglingComponent
firstComponent={nullORUndefined}
secondComponent={'div'}
/>
);
const instance2 = (
<TogglingComponent
firstComponent={'div'}
secondComponent={nullORUndefined}
/>
);
ReactTestUtils.renderIntoDocument(instance1);
ReactTestUtils.renderIntoDocument(instance2);
expect(log).toHaveBeenCalledTimes(4);
expect(log).toHaveBeenNthCalledWith(1, null);
expect(log).toHaveBeenNthCalledWith(
2,
expect.objectContaining({tagName: 'DIV'}),
);
expect(log).toHaveBeenNthCalledWith(
3,
expect.objectContaining({tagName: 'DIV'}),
);
expect(log).toHaveBeenNthCalledWith(4, null);
});
it('should be able to switch in a list of children', () => {
const instance1 = (
<TogglingComponent
firstComponent={nullORUndefined}
secondComponent={'div'}
/>
);
ReactTestUtils.renderIntoDocument(
<div>
{instance1}
{instance1}
{instance1}
</div>,
);
expect(log).toHaveBeenCalledTimes(6);
expect(log).toHaveBeenNthCalledWith(1, null);
expect(log).toHaveBeenNthCalledWith(2, null);
expect(log).toHaveBeenNthCalledWith(3, null);
expect(log).toHaveBeenNthCalledWith(
4,
expect.objectContaining({tagName: 'DIV'}),
);
expect(log).toHaveBeenNthCalledWith(
5,
expect.objectContaining({tagName: 'DIV'}),
);
expect(log).toHaveBeenNthCalledWith(
6,
expect.objectContaining({tagName: 'DIV'}),
);
});
it('should distinguish between a script placeholder and an actual script tag', () => {
const instance1 = (
<TogglingComponent
firstComponent={nullORUndefined}
secondComponent={'script'}
/>
);
const instance2 = (
<TogglingComponent
firstComponent={'script'}
secondComponent={nullORUndefined}
/>
);
expect(function () {
ReactTestUtils.renderIntoDocument(instance1);
}).not.toThrow();
expect(function () {
ReactTestUtils.renderIntoDocument(instance2);
}).not.toThrow();
expect(log).toHaveBeenCalledTimes(4);
expect(log).toHaveBeenNthCalledWith(1, null);
expect(log).toHaveBeenNthCalledWith(
2,
expect.objectContaining({tagName: 'SCRIPT'}),
);
expect(log).toHaveBeenNthCalledWith(
3,
expect.objectContaining({tagName: 'SCRIPT'}),
);
expect(log).toHaveBeenNthCalledWith(4, null);
});
it(
'should have findDOMNode return null when multiple layers of composite ' +
'components render to the same nullish placeholder',
() => {
class GrandChild extends React.Component {
render() {
return nullORUndefined;
}
}
class Child extends React.Component {
render() {
return <GrandChild />;
}
}
const instance1 = (
<TogglingComponent firstComponent={'div'} secondComponent={Child} />
);
const instance2 = (
<TogglingComponent firstComponent={Child} secondComponent={'div'} />
);
expect(function () {
ReactTestUtils.renderIntoDocument(instance1);
}).not.toThrow();
expect(function () {
ReactTestUtils.renderIntoDocument(instance2);
}).not.toThrow();
expect(log).toHaveBeenCalledTimes(4);
expect(log).toHaveBeenNthCalledWith(
1,
expect.objectContaining({tagName: 'DIV'}),
);
expect(log).toHaveBeenNthCalledWith(2, null);
expect(log).toHaveBeenNthCalledWith(3, null);
expect(log).toHaveBeenNthCalledWith(
4,
expect.objectContaining({tagName: 'DIV'}),
);
},
);
it('works when switching components', () => {
let assertions = 0;
class Inner extends React.Component {
render() {
return <span />;
}
componentDidMount() {
// Make sure the DOM node resolves properly even if we're replacing a
// `null` component
expect(ReactDOM.findDOMNode(this)).not.toBe(null);
assertions++;
}
componentWillUnmount() {
// Even though we're getting replaced by `null`, we haven't been
// replaced yet!
expect(ReactDOM.findDOMNode(this)).not.toBe(null);
assertions++;
}
}
class Wrapper extends React.Component {
render() {
return this.props.showInner ? <Inner /> : nullORUndefined;
}
}
const el = document.createElement('div');
let component;
// Render the <Inner /> component...
component = ReactDOM.render(<Wrapper showInner={true} />, el);
expect(ReactDOM.findDOMNode(component)).not.toBe(null);
// Switch to null...
component = ReactDOM.render(<Wrapper showInner={false} />, el);
expect(ReactDOM.findDOMNode(component)).toBe(null);
// ...then switch back.
component = ReactDOM.render(<Wrapper showInner={true} />, el);
expect(ReactDOM.findDOMNode(component)).not.toBe(null);
expect(assertions).toBe(3);
});
it('can render nullish at the top level', () => {
const div = document.createElement('div');
ReactDOM.render(nullORUndefined, div);
expect(div.innerHTML).toBe('');
});
it('does not break when updating during mount', () => {
class Child extends React.Component {
componentDidMount() {
if (this.props.onMount) {
this.props.onMount();
}
}
render() {
if (!this.props.visible) {
return nullORUndefined;
}
return <div>hello world</div>;
}
}
class Parent extends React.Component {
update = () => {
this.forceUpdate();
};
render() {
return (
<div>
<Child key="1" visible={false} />
<Child key="0" visible={true} onMount={this.update} />
<Child key="2" visible={false} />
</div>
);
}
}
expect(function () {
ReactTestUtils.renderIntoDocument(<Parent />);
}).not.toThrow();
});
it('preserves the dom node during updates', () => {
class Empty extends React.Component {
render() {
return nullORUndefined;
}
}
const container = document.createElement('div');
ReactDOM.render(<Empty />, container);
const noscript1 = container.firstChild;
expect(noscript1).toBe(null);
// This update shouldn't create a DOM node
ReactDOM.render(<Empty />, container);
const noscript2 = container.firstChild;
expect(noscript2).toBe(null);
});
it('should not warn about React.forwardRef that returns nullish', () => {
const Empty = () => {
return nullORUndefined;
};
const EmptyForwardRef = React.forwardRef(Empty);
expect(() => {
ReactTestUtils.renderIntoDocument(<EmptyForwardRef />);
}).not.toThrowError();
});
it('should not warn about React.memo that returns nullish', () => {
const Empty = () => {
return nullORUndefined;
};
const EmptyMemo = React.memo(Empty);
expect(() => {
ReactTestUtils.renderIntoDocument(<EmptyMemo />);
}).not.toThrowError();
});
});
});