/**
* 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
* @jest-environment node
*/
'use strict';
const ReactFeatureFlags = require('shared/ReactFeatureFlags');
ReactFeatureFlags.replayFailedUnitOfWorkWithInvokeGuardedCallback = false;
const React = require('react');
const ReactTestRenderer = require('react-test-renderer');
const {format: prettyFormat} = require('pretty-format');
// Isolate noop renderer
jest.resetModules();
const ReactNoop = require('react-noop-renderer');
const InternalTestUtils = require('internal-test-utils');
const waitForAll = InternalTestUtils.waitForAll;
// Kind of hacky, but we nullify all the instances to test the tree structure
// with jasmine's deep equality function, and test the instances separate. We
// also delete children props because testing them is more annoying and not
// really important to verify.
function cleanNodeOrArray(node) {
if (!node) {
return;
}
if (Array.isArray(node)) {
node.forEach(cleanNodeOrArray);
return;
}
if (node && node.instance) {
node.instance = null;
}
if (node && node.props && node.props.children) {
// eslint-disable-next-line no-unused-vars
const {children, ...props} = node.props;
node.props = props;
}
if (Array.isArray(node.rendered)) {
node.rendered.forEach(cleanNodeOrArray);
} else if (typeof node.rendered === 'object') {
cleanNodeOrArray(node.rendered);
}
}
describe('ReactTestRenderer', () => {
it('renders a simple component', () => {
function Link() {
return <a role="link" />;
}
const renderer = ReactTestRenderer.create(<Link />);
expect(renderer.toJSON()).toEqual({
type: 'a',
props: {role: 'link'},
children: null,
});
});
it('renders a top-level empty component', () => {
function Empty() {
return null;
}
const renderer = ReactTestRenderer.create(<Empty />);
expect(renderer.toJSON()).toEqual(null);
});
it('exposes a type flag', () => {
function Link() {
return <a role="link" />;
}
const renderer = ReactTestRenderer.create(<Link />);
const object = renderer.toJSON();
expect(object.$$typeof).toBe(Symbol.for('react.test.json'));
// $$typeof should not be enumerable.
for (const key in object) {
if (object.hasOwnProperty(key)) {
expect(key).not.toBe('$$typeof');
}
}
});
it('can render a composite component', () => {
class Component extends React.Component {
render() {
return (
<div className="purple">
<Child />
</div>
);
}
}
const Child = () => {
return <moo />;
};
const renderer = ReactTestRenderer.create(<Component />);
expect(renderer.toJSON()).toEqual({
type: 'div',
props: {className: 'purple'},
children: [{type: 'moo', props: {}, children: null}],
});
});
it('renders some basics with an update', () => {
let renders = 0;
class Component extends React.Component {
state = {x: 3};
render() {
renders++;
return (
<div className="purple">
{this.state.x}
<Child />
<Null />
</div>
);
}
componentDidMount() {
this.setState({x: 7});
}
}
const Child = () => {
renders++;
return <moo />;
};
const Null = () => {
renders++;
return null;
};
const renderer = ReactTestRenderer.create(<Component />);
expect(renderer.toJSON()).toEqual({
type: 'div',
props: {className: 'purple'},
children: ['7', {type: 'moo', props: {}, children: null}],
});
expect(renders).toBe(6);
});
it('exposes the instance', () => {
class Mouse extends React.Component {
constructor() {
super();
this.state = {mouse: 'mouse'};
}
handleMoose() {
this.setState({mouse: 'moose'});
}
render() {
return <div>{this.state.mouse}</div>;
}
}
const renderer = ReactTestRenderer.create(<Mouse />);
expect(renderer.toJSON()).toEqual({
type: 'div',
props: {},
children: ['mouse'],
});
const mouse = renderer.getInstance();
mouse.handleMoose();
expect(renderer.toJSON()).toEqual({
type: 'div',
children: ['moose'],
props: {},
});
});
it('updates types', () => {
const renderer = ReactTestRenderer.create(<div>mouse</div>);
expect(renderer.toJSON()).toEqual({
type: 'div',
props: {},
children: ['mouse'],
});
renderer.update(<span>mice</span>);
expect(renderer.toJSON()).toEqual({
type: 'span',
props: {},
children: ['mice'],
});
});
it('updates children', () => {
const renderer = ReactTestRenderer.create(
<div>
<span key="a">A</span>
<span key="b">B</span>
<span key="c">C</span>
</div>,
);
expect(renderer.toJSON()).toEqual({
type: 'div',
props: {},
children: [
{type: 'span', props: {}, children: ['A']},
{type: 'span', props: {}, children: ['B']},
{type: 'span', props: {}, children: ['C']},
],
});
renderer.update(
<div>
<span key="d">D</span>
<span key="c">C</span>
<span key="b">B</span>
</div>,
);
expect(renderer.toJSON()).toEqual({
type: 'div',
props: {},
children: [
{type: 'span', props: {}, children: ['D']},
{type: 'span', props: {}, children: ['C']},
{type: 'span', props: {}, children: ['B']},
],
});
});
it('does the full lifecycle', () => {
const log = [];
class Log extends React.Component {
render() {
log.push('render ' + this.props.name);
return <div />;
}
componentDidMount() {
log.push('mount ' + this.props.name);
}
componentWillUnmount() {
log.push('unmount ' + this.props.name);
}
}
const renderer = ReactTestRenderer.create(<Log key="foo" name="Foo" />);
renderer.update(<Log key="bar" name="Bar" />);
renderer.unmount();
expect(log).toEqual([
'render Foo',
'mount Foo',
'render Bar',
'unmount Foo',
'mount Bar',
'unmount Bar',
]);
});
it('gives a ref to native components', () => {
const log = [];
ReactTestRenderer.create(<div ref={r => log.push(r)} />);
expect(log).toEqual([null]);
});
it('warns correctly for refs on SFCs', () => {
function Bar() {
return <div>Hello, world</div>;
}
class Foo extends React.Component {
fooRef = React.createRef();
render() {
return <Bar ref={this.fooRef} />;
}
}
class Baz extends React.Component {
bazRef = React.createRef();
render() {
return <div ref={this.bazRef} />;
}
}
ReactTestRenderer.create(<Baz />);
expect(() => ReactTestRenderer.create(<Foo />)).toErrorDev(
'Warning: Function components cannot be given refs. Attempts ' +
'to access this ref will fail. ' +
'Did you mean to use React.forwardRef()?\n\n' +
'Check the render method of `Foo`.\n' +
' in Bar (at **)\n' +
' in Foo (at **)',
);
});
it('allows an optional createNodeMock function', () => {
const mockDivInstance = {appendChild: () => {}};
const mockInputInstance = {focus: () => {}};
const mockListItemInstance = {click: () => {}};
const mockAnchorInstance = {hover: () => {}};
const log = [];
class Foo extends React.Component {
barRef = React.createRef();
componentDidMount() {
log.push(this.barRef.current);
}
render() {
return <a ref={this.barRef}>Hello, world</a>;
}
}
function createNodeMock(element) {
switch (element.type) {
case 'div':
return mockDivInstance;
case 'input':
return mockInputInstance;
case 'li':
return mockListItemInstance;
case 'a':
return mockAnchorInstance;
default:
return {};
}
}
ReactTestRenderer.create(<div ref={r => log.push(r)} />, {createNodeMock});
ReactTestRenderer.create(<input ref={r => log.push(r)} />, {
createNodeMock,
});
ReactTestRenderer.create(
<div>
<span>
<ul>
<li ref={r => log.push(r)} />
</ul>
<ul>
<li ref={r => log.push(r)} />
<li ref={r => log.push(r)} />
</ul>
</span>
</div>,
{createNodeMock, foobar: true},
);
ReactTestRenderer.create(<Foo />, {createNodeMock});
ReactTestRenderer.create(<div ref={r => log.push(r)} />);
ReactTestRenderer.create(<div ref={r => log.push(r)} />, {});
expect(log).toEqual([
mockDivInstance,
mockInputInstance,
mockListItemInstance,
mockListItemInstance,
mockListItemInstance,
mockAnchorInstance,
null,
null,
]);
});
it('supports unmounting when using refs', () => {
class Foo extends React.Component {
render() {
return <div ref={React.createRef()} />;
}
}
const inst = ReactTestRenderer.create(<Foo />, {
createNodeMock: () => 'foo',
});
expect(() => inst.unmount()).not.toThrow();
});
it('supports unmounting inner instances', () => {
let count = 0;
class Foo extends React.Component {
componentWillUnmount() {
count++;
}
render() {
return <div />;
}
}
const inst = ReactTestRenderer.create(
<div>
<Foo />
</div>,
{
createNodeMock: () => 'foo',
},
);
expect(() => inst.unmount()).not.toThrow();
expect(count).toEqual(1);
});
it('supports updates when using refs', () => {
const log = [];
const createNodeMock = element => {
log.push(element.type);
return element.type;
};
class Foo extends React.Component {
render() {
return this.props.useDiv ? (
<div ref={React.createRef()} />
) : (
<span ref={React.createRef()} />
);
}
}
const inst = ReactTestRenderer.create(<Foo useDiv={true} />, {
createNodeMock,
});
inst.update(<Foo useDiv={false} />);
expect(log).toEqual(['div', 'span']);
});
it('supports error boundaries', () => {
const log = [];
class Angry extends React.Component {
render() {
log.push('Angry render');
throw new Error('Please, do not render me.');
}
componentDidMount() {
log.push('Angry componentDidMount');
}
componentWillUnmount() {
log.push('Angry componentWillUnmount');
}
}
class Boundary extends React.Component {
constructor(props) {
super(props);
this.state = {error: false};
}
render() {
log.push('Boundary render');
if (!this.state.error) {
return (
<div>
<button onClick={this.onClick}>ClickMe</button>
<Angry />
</div>
);
} else {
return <div>Happy Birthday!</div>;
}
}
componentDidMount() {
log.push('Boundary componentDidMount');
}
componentWillUnmount() {
log.push('Boundary componentWillUnmount');
}
onClick() {
/* do nothing */
}
componentDidCatch() {
log.push('Boundary componentDidCatch');
this.setState({error: true});
}
}
const renderer = ReactTestRenderer.create(<Boundary />);
expect(renderer.toJSON()).toEqual({
type: 'div',
props: {},
children: ['Happy Birthday!'],
});
expect(log).toEqual([
'Boundary render',
'Angry render',
'Boundary componentDidMount',
'Boundary componentDidCatch',
'Boundary render',
]);
});
it('can update text nodes', () => {
class Component extends React.Component {
render() {
return <div>{this.props.children}</div>;
}
}
const renderer = ReactTestRenderer.create(<Component>Hi</Component>);
expect(renderer.toJSON()).toEqual({
type: 'div',
children: ['Hi'],
props: {},
});
renderer.update(<Component>{['Hi', 'Bye']}</Component>);
expect(renderer.toJSON()).toEqual({
type: 'div',
children: ['Hi', 'Bye'],
props: {},
});
renderer.update(<Component>Bye</Component>);
expect(renderer.toJSON()).toEqual({
type: 'div',
children: ['Bye'],
props: {},
});
renderer.update(<Component>{42}</Component>);
expect(renderer.toJSON()).toEqual({
type: 'div',
children: ['42'],
props: {},
});
renderer.update(
<Component>
<div />
</Component>,
);
expect(renderer.toJSON()).toEqual({
type: 'div',
children: [
{
type: 'div',
children: null,
props: {},
},
],
props: {},
});
});
it('toTree() renders simple components returning host components', () => {
const Qoo = () => <span className="Qoo">Hello World!</span>;
const renderer = ReactTestRenderer.create(<Qoo />);
const tree = renderer.toTree();
cleanNodeOrArray(tree);
expect(prettyFormat(tree)).toEqual(
prettyFormat({
nodeType: 'component',
type: Qoo,
props: {},
instance: null,
rendered: {
nodeType: 'host',
type: 'span',
props: {className: 'Qoo'},
instance: null,
rendered: ['Hello World!'],
},
}),
);
});
it('toTree() handles nested Fragments', () => {
const Foo = () => (
<>
<>foo</>
</>
);
const renderer = ReactTestRenderer.create(<Foo />);
const tree = renderer.toTree();
cleanNodeOrArray(tree);
expect(prettyFormat(tree)).toEqual(
prettyFormat({
nodeType: 'component',
type: Foo,
instance: null,
props: {},
rendered: 'foo',
}),
);
});
it('toTree() handles null rendering components', () => {
class Foo extends React.Component {
render() {
return null;
}
}
const renderer = ReactTestRenderer.create(<Foo />);
const tree = renderer.toTree();
expect(tree.instance).toBeInstanceOf(Foo);
cleanNodeOrArray(tree);
expect(tree).toEqual({
type: Foo,
nodeType: 'component',
props: {},
instance: null,
rendered: null,
});
});
it('toTree() handles simple components that return arrays', () => {
const Foo = ({children}) => children;
const renderer = ReactTestRenderer.create(
<Foo>
<div>One</div>
<div>Two</div>
</Foo>,
);
const tree = renderer.toTree();
cleanNodeOrArray(tree);
expect(prettyFormat(tree)).toEqual(
prettyFormat({
type: Foo,
nodeType: 'component',
props: {},
instance: null,
rendered: [
{
instance: null,
nodeType: 'host',
props: {},
rendered: ['One'],
type: 'div',
},
{
instance: null,
nodeType: 'host',
props: {},
rendered: ['Two'],
type: 'div',
},
],
}),
);
});
it('toTree() handles complicated tree of arrays', () => {
class Foo extends React.Component {
render() {
return this.props.children;
}
}
const renderer = ReactTestRenderer.create(
<div>
<Foo>
<div>One</div>
<div>Two</div>
<Foo>
<div>Three</div>
</Foo>
</Foo>
<div>Four</div>
</div>,
);
const tree = renderer.toTree();
cleanNodeOrArray(tree);
expect(prettyFormat(tree)).toEqual(
prettyFormat({
type: 'div',
instance: null,
nodeType: 'host',
props: {},
rendered: [
{
type: Foo,
nodeType: 'component',
props: {},
instance: null,
rendered: [
{
type: 'div',
nodeType: 'host',
props: {},
instance: null,
rendered: ['One'],
},
{
type: 'div',
nodeType: 'host',
props: {},
instance: null,
rendered: ['Two'],
},
{
type: Foo,
nodeType: 'component',
props: {},
instance: null,
rendered: {
type: 'div',
nodeType: 'host',
props: {},
instance: null,
rendered: ['Three'],
},
},
],
},
{
type: 'div',
nodeType: 'host',
props: {},
instance: null,
rendered: ['Four'],
},
],
}),
);
});
it('toTree() handles complicated tree of fragments', () => {
const renderer = ReactTestRenderer.create(
<>
<>
<div>One</div>
<div>Two</div>
<>
<div>Three</div>
</>
</>
<div>Four</div>
</>,
);
const tree = renderer.toTree();
cleanNodeOrArray(tree);
expect(prettyFormat(tree)).toEqual(
prettyFormat([
{
type: 'div',
nodeType: 'host',
props: {},
instance: null,
rendered: ['One'],
},
{
type: 'div',
nodeType: 'host',
props: {},
instance: null,
rendered: ['Two'],
},
{
type: 'div',
nodeType: 'host',
props: {},
instance: null,
rendered: ['Three'],
},
{
type: 'div',
nodeType: 'host',
props: {},
instance: null,
rendered: ['Four'],
},
]),
);
});
it('root instance and createNodeMock ref return the same value', () => {
const createNodeMock = ref => ({node: ref});
let refInst = null;
const renderer = ReactTestRenderer.create(
<div ref={ref => (refInst = ref)} />,
{createNodeMock},
);
const root = renderer.getInstance();
expect(root).toEqual(refInst);
});
it('toTree() renders complicated trees of composites and hosts', () => {
// SFC returning host. no children props.
const Qoo = () => <span className="Qoo">Hello World!</span>;
// SFC returning host. passes through children.
const Foo = ({className, children}) => (
<div className={'Foo ' + className}>
<span className="Foo2">Literal</span>
{children}
</div>
);
// class composite returning composite. passes through children.
class Bar extends React.Component {
render() {
const {special, children} = this.props;
return <Foo className={special ? 'special' : 'normal'}>{children}</Foo>;
}
}
// class composite return composite. no children props.
class Bam extends React.Component {
render() {
return (
<Bar special={true}>
<Qoo />
</Bar>
);
}
}
const renderer = ReactTestRenderer.create(<Bam />);
const tree = renderer.toTree();
// we test for the presence of instances before nulling them out
expect(tree.instance).toBeInstanceOf(Bam);
expect(tree.rendered.instance).toBeInstanceOf(Bar);
cleanNodeOrArray(tree);
expect(prettyFormat(tree)).toEqual(
prettyFormat({
type: Bam,
nodeType: 'component',
props: {},
instance: null,
rendered: {
type: Bar,
nodeType: 'component',
props: {special: true},
instance: null,
rendered: {
type: Foo,
nodeType: 'component',
props: {className: 'special'},
instance: null,
rendered: {
type: 'div',
nodeType: 'host',
props: {className: 'Foo special'},
instance: null,
rendered: [
{
type: 'span',
nodeType: 'host',
props: {className: 'Foo2'},
instance: null,
rendered: ['Literal'],
},
{
type: Qoo,
nodeType: 'component',
props: {},
instance: null,
rendered: {
type: 'span',
nodeType: 'host',
props: {className: 'Qoo'},
instance: null,
rendered: ['Hello World!'],
},
},
],
},
},
},
}),
);
});
it('can update text nodes when rendered as root', () => {
const renderer = ReactTestRenderer.create(['Hello', 'world']);
expect(renderer.toJSON()).toEqual(['Hello', 'world']);
renderer.update(42);
expect(renderer.toJSON()).toEqual('42');
renderer.update([42, 'world']);
expect(renderer.toJSON()).toEqual(['42', 'world']);
});
it('can render and update root fragments', () => {
const Component = props => props.children;
const renderer = ReactTestRenderer.create([
<Component key="a">Hi</Component>,
<Component key="b">Bye</Component>,
]);
expect(renderer.toJSON()).toEqual(['Hi', 'Bye']);
renderer.update(<div />);
expect(renderer.toJSON()).toEqual({
type: 'div',
children: null,
props: {},
});
renderer.update([<div key="a">goodbye</div>, 'world']);
expect(renderer.toJSON()).toEqual([
{
type: 'div',
children: ['goodbye'],
props: {},
},
'world',
]);
});
it('supports context providers and consumers', () => {
const {Consumer, Provider} = React.createContext('a');
function Child(props) {
return props.value;
}
function App() {
return (
<Provider value="b">
<Consumer>{value => <Child value={value} />}</Consumer>
</Provider>
);
}
const renderer = ReactTestRenderer.create(<App />);
const child = renderer.root.findByType(Child);
expect(child.children).toEqual(['b']);
expect(prettyFormat(renderer.toTree())).toEqual(
prettyFormat({
instance: null,
nodeType: 'component',
props: {},
rendered: {
instance: null,
nodeType: 'component',
props: {
value: 'b',
},
rendered: 'b',
type: Child,
},
type: App,
}),
);
});
it('supports modes', () => {
function Child(props) {
return props.value;
}
function App(props) {
return (
<React.StrictMode>
<Child value={props.value} />
</React.StrictMode>
);
}
const renderer = ReactTestRenderer.create(<App value="a" />);
const child = renderer.root.findByType(Child);
expect(child.children).toEqual(['a']);
expect(prettyFormat(renderer.toTree())).toEqual(
prettyFormat({
instance: null,
nodeType: 'component',
props: {
value: 'a',
},
rendered: {
instance: null,
nodeType: 'component',
props: {
value: 'a',
},
rendered: 'a',
type: Child,
},
type: App,
}),
);
});
it('supports forwardRef', () => {
const InnerRefed = React.forwardRef((props, ref) => (
<div>
<span ref={ref} />
</div>
));
class App extends React.Component {
render() {
return <InnerRefed ref={r => (this.ref = r)} />;
}
}
const renderer = ReactTestRenderer.create(<App />);
const tree = renderer.toTree();
cleanNodeOrArray(tree);
expect(prettyFormat(tree)).toEqual(
prettyFormat({
instance: null,
nodeType: 'component',
props: {},
rendered: {
instance: null,
nodeType: 'host',
props: {},
rendered: [
{
instance: null,
nodeType: 'host',
props: {},
rendered: [],
type: 'span',
},
],
type: 'div',
},
type: App,
}),
);
});
it('can concurrently render context with a "primary" renderer', async () => {
const Context = React.createContext(null);
const Indirection = React.Fragment;
const App = () => (
<Context.Provider value={null}>
<Indirection>
<Context.Consumer>{() => null}</Context.Consumer>
</Indirection>
</Context.Provider>
);
ReactNoop.render(<App />);
await waitForAll([]);
ReactTestRenderer.create(<App />);
});
it('calling findByType() with an invalid component will fall back to "Unknown" for component name', () => {
const App = () => null;
const renderer = ReactTestRenderer.create(<App />);
const NonComponent = {};
expect(() => {
renderer.root.findByType(NonComponent);
}).toThrowError(`No instances found with node type: "Unknown"`);
});
});