'use strict';
let act;
let React;
let ReactDOMClient;
describe('ReactElementClone', () => {
let ComponentClass;
beforeEach(() => {
jest.resetModules();
act = require('internal-test-utils').act;
React = require('react');
ReactDOMClient = require('react-dom/client');
ComponentClass = class extends React.Component {
render() {
return React.createElement('div');
}
};
});
it('should clone a DOM component with new props', async () => {
let div;
class Grandparent extends React.Component {
render() {
return (
<Parent
child={<div ref={node => (div = node)} className="child" />}
/>
);
}
}
class Parent extends React.Component {
render() {
return (
<div className="parent">
{React.cloneElement(this.props.child, {className: 'xyz'})}
</div>
);
}
}
const root = ReactDOMClient.createRoot(document.createElement('div'));
await act(() => {
root.render(<Grandparent />);
});
expect(div.className).toBe('xyz');
});
it('should clone a composite component with new props', async () => {
let div;
class Child extends React.Component {
render() {
return (
<div ref={node => (div = node)} className={this.props.className} />
);
}
}
class Grandparent extends React.Component {
render() {
return <Parent child={<Child className="child" />} />;
}
}
class Parent extends React.Component {
render() {
return (
<div className="parent">
{React.cloneElement(this.props.child, {className: 'xyz'})}
</div>
);
}
}
const root = ReactDOMClient.createRoot(document.createElement('div'));
await act(() => {
root.render(<Grandparent />);
});
expect(div.className).toBe('xyz');
});
it('does not fail if config has no prototype', () => {
const config = Object.create(null, {foo: {value: 1, enumerable: true}});
React.cloneElement(<div />, config);
});
it('should keep the original ref if it is not overridden', async () => {
let component;
class Grandparent extends React.Component {
yoloRef = React.createRef();
componentDidMount() {
component = this;
}
render() {
return <Parent child={<div ref={this.yoloRef} />} />;
}
}
class Parent extends React.Component {
render() {
return (
<div>{React.cloneElement(this.props.child, {className: 'xyz'})}</div>
);
}
}
const root = ReactDOMClient.createRoot(document.createElement('div'));
await act(() => {
root.render(<Grandparent />);
});
expect(component.yoloRef.current.tagName).toBe('DIV');
});
it('should transfer the key property', () => {
class Component extends React.Component {
render() {
return null;
}
}
const clone = React.cloneElement(<Component />, {key: 'xyz'});
expect(clone.key).toBe('xyz');
});
it('should transfer children', async () => {
class Component extends React.Component {
render() {
expect(this.props.children).toBe('xyz');
return <div />;
}
}
const root = ReactDOMClient.createRoot(document.createElement('div'));
await act(() => {
root.render(React.cloneElement(<Component />, {children: 'xyz'}));
});
});
it('should shallow clone children', async () => {
class Component extends React.Component {
render() {
expect(this.props.children).toBe('xyz');
return <div />;
}
}
const root = ReactDOMClient.createRoot(document.createElement('div'));
await act(() => {
root.render(React.cloneElement(<Component>xyz</Component>, {}));
});
});
it('should accept children as rest arguments', () => {
class Component extends React.Component {
render() {
return null;
}
}
const clone = React.cloneElement(
<Component>xyz</Component>,
{children: <Component />},
<div />,
<span />,
);
expect(clone.props.children).toEqual([<div />, <span />]);
});
it('should override children if undefined is provided as an argument', () => {
const element = React.createElement(
ComponentClass,
{
children: 'text',
},
undefined,
);
expect(element.props.children).toBe(undefined);
const element2 = React.cloneElement(
React.createElement(ComponentClass, {
children: 'text',
}),
{},
undefined,
);
expect(element2.props.children).toBe(undefined);
});
it('should support keys and refs', async () => {
let component;
class Parent extends React.Component {
xyzRef = React.createRef();
render() {
const clone = React.cloneElement(this.props.children, {
key: 'xyz',
ref: this.xyzRef,
});
expect(clone.key).toBe('xyz');
if (gate(flags => flags.enableRefAsProp)) {
expect(clone.props.ref).toBe(this.xyzRef);
} else {
expect(clone.ref).toBe(this.xyzRef);
}
return <div>{clone}</div>;
}
}
class Grandparent extends React.Component {
parentRef = React.createRef();
componentDidMount() {
component = this;
}
render() {
return (
<Parent ref={this.parentRef}>
<span key="abc" />
</Parent>
);
}
}
const root = ReactDOMClient.createRoot(document.createElement('div'));
await act(() => root.render(<Grandparent />));
expect(component.parentRef.current.xyzRef.current.tagName).toBe('SPAN');
});
it('should steal the ref if a new ref is specified', async () => {
let component;
class Parent extends React.Component {
xyzRef = React.createRef();
render() {
const clone = React.cloneElement(this.props.children, {
ref: this.xyzRef,
});
return <div>{clone}</div>;
}
}
class Grandparent extends React.Component {
parentRef = React.createRef();
childRef = React.createRef();
componentDidMount() {
component = this;
}
render() {
return (
<Parent ref={this.parentRef}>
<span ref={this.childRef} />
</Parent>
);
}
}
const root = ReactDOMClient.createRoot(document.createElement('div'));
await act(() => root.render(<Grandparent />));
if (gate(flags => flags.enableRefAsProp && flags.disableStringRefs)) {
expect(component.childRef).toEqual({current: null});
expect(component.parentRef.current.xyzRef.current.tagName).toBe('SPAN');
} else if (
gate(flags => !flags.enableRefAsProp && !flags.disableStringRefs)
) {
expect(component.childRef).toEqual({current: null});
expect(component.parentRef.current.xyzRef.current.tagName).toBe('SPAN');
} else if (
gate(flags => flags.enableRefAsProp && !flags.disableStringRefs)
) {
expect(component.childRef).toEqual({current: null});
expect(component.parentRef.current.xyzRef.current.tagName).toBe('SPAN');
} else {
}
});
it('should steal the ref if a new string ref is specified without an owner', async () => {
await expect(async () => {
const element = React.createElement('div', {id: 'some-id'});
class Parent extends React.Component {
render() {
return <Child>{element}</Child>;
}
}
let child;
class Child extends React.Component {
render() {
child = this;
const clone = React.cloneElement(this.props.children, {
ref: 'xyz',
});
return <div>{clone}</div>;
}
}
const root = ReactDOMClient.createRoot(document.createElement('div'));
await act(() => root.render(<Parent />));
expect(child.refs.xyz.tagName).toBe('DIV');
}).toErrorDev([
'Component "Child" contains the string ref "xyz". Support for ' +
'string refs will be removed in a future major release. We recommend ' +
'using useRef() or createRef() instead. Learn more about using refs ' +
'safely here: https://react.dev/link/strict-mode-string-ref',
]);
});
it('should overwrite props', async () => {
class Component extends React.Component {
render() {
expect(this.props.myprop).toBe('xyz');
return <div />;
}
}
const root = ReactDOMClient.createRoot(document.createElement('div'));
await act(() =>
root.render(
React.cloneElement(<Component myprop="abc" />, {myprop: 'xyz'}),
),
);
});
it('should normalize props with default values', () => {
class Component extends React.Component {
render() {
return <span />;
}
}
Component.defaultProps = {prop: 'testKey'};
const instance = React.createElement(Component);
const clonedInstance = React.cloneElement(instance, {prop: undefined});
expect(clonedInstance.props.prop).toBe('testKey');
const clonedInstance2 = React.cloneElement(instance, {prop: null});
expect(clonedInstance2.props.prop).toBe(null);
const instance2 = React.createElement(Component, {prop: 'newTestKey'});
const cloneInstance3 = React.cloneElement(instance2, {prop: undefined});
expect(cloneInstance3.props.prop).toBe('testKey');
const cloneInstance4 = React.cloneElement(instance2, {});
expect(cloneInstance4.props.prop).toBe('newTestKey');
});
it('warns for keys for arrays of elements in rest args', async () => {
const root = ReactDOMClient.createRoot(document.createElement('div'));
await expect(async () => {
await act(() => {
root.render(React.cloneElement(<div />, null, [<div />, <div />]));
});
}).toErrorDev('Each child in a list should have a unique "key" prop.');
});
it('does not warns for arrays of elements with keys', async () => {
const root = ReactDOMClient.createRoot(document.createElement('div'));
await act(() => {
root.render(
React.cloneElement(<div />, null, [<div key="#1" />, <div key="#2" />]),
);
});
});
it('does not warn when the element is directly in rest args', async () => {
const root = ReactDOMClient.createRoot(document.createElement('div'));
await act(() => {
root.render(React.cloneElement(<div />, null, <div />, <div />));
});
});
it('does not warn when the array contains a non-element', () => {
React.cloneElement(<div />, null, [{}, {}]);
});
it('should ignore key and ref warning getters', () => {
const elementA = React.createElement('div');
const elementB = React.cloneElement(elementA, elementA.props);
expect(elementB.key).toBe(null);
if (gate(flags => flags.enableRefAsProp)) {
expect(elementB.ref).toBe(null);
} else {
expect(elementB.ref).toBe(null);
}
});
it('should ignore undefined key and ref', () => {
const element = React.createElement(ComponentClass, {
key: '12',
ref: '34',
foo: '56',
});
const props = {
key: undefined,
ref: undefined,
foo: 'ef',
};
const clone = React.cloneElement(element, props);
expect(clone.type).toBe(ComponentClass);
expect(clone.key).toBe('12');
if (gate(flags => flags.enableRefAsProp && flags.disableStringRefs)) {
expect(clone.props.ref).toBe('34');
expect(() => expect(clone.ref).toBe('34')).toErrorDev(
'Accessing element.ref was removed in React 19',
{withoutStack: true},
);
expect(clone.props).toEqual({foo: 'ef', ref: '34'});
} else if (
gate(flags => !flags.enableRefAsProp && !flags.disableStringRefs)
) {
expect(clone.ref).toBe(element.ref);
expect(clone.props).toEqual({foo: 'ef'});
} else if (
gate(flags => flags.enableRefAsProp && !flags.disableStringRefs)
) {
expect(() => {
expect(clone.ref).toBe(element.ref);
}).toErrorDev('Accessing element.ref was removed in React 19', {
withoutStack: true,
});
expect(clone.props).toEqual({foo: 'ef', ref: element.ref});
} else {
}
if (__DEV__) {
expect(Object.isFrozen(element)).toBe(true);
expect(Object.isFrozen(element.props)).toBe(true);
}
});
it('should extract null key and ref', () => {
const element = React.createElement(ComponentClass, {
key: '12',
ref: '34',
foo: '56',
});
const props = {
key: null,
ref: null,
foo: 'ef',
};
const clone = React.cloneElement(element, props);
expect(clone.type).toBe(ComponentClass);
expect(clone.key).toBe('null');
if (gate(flags => flags.enableRefAsProp)) {
expect(clone.ref).toBe(null);
expect(clone.props).toEqual({foo: 'ef', ref: null});
} else {
expect(clone.ref).toBe(null);
expect(clone.props).toEqual({foo: 'ef'});
}
if (__DEV__) {
expect(Object.isFrozen(element)).toBe(true);
expect(Object.isFrozen(element.props)).toBe(true);
}
});
it('throws an error if passed null', () => {
const element = null;
expect(() => React.cloneElement(element)).toThrow(
'The argument must be a React element, but you passed null.',
);
});
it('throws an error if passed undefined', () => {
let element;
expect(() => React.cloneElement(element)).toThrow(
'The argument must be a React element, but you passed undefined.',
);
});
});