main
js 396 lines 10.5 KB
Raw
1 /**
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 *
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
6 *
7 * @emails react-core
8 */
9
10 'use strict';
11
12 let act;
13 let React;
14 let ReactDOMClient;
15 let assertConsoleErrorDev;
16
17 describe('ReactElementClone', () => {
18 let ComponentClass;
19
20 beforeEach(() => {
21 jest.resetModules();
22
23 ({act, assertConsoleErrorDev} = require('internal-test-utils'));
24
25 React = require('react');
26 ReactDOMClient = require('react-dom/client');
27
28 // NOTE: We're explicitly not using JSX here. This is intended to test
29 // classic JS without JSX.
30 ComponentClass = class extends React.Component {
31 render() {
32 return React.createElement('div');
33 }
34 };
35 });
36
37 it('should clone a DOM component with new props', async () => {
38 let div;
39 class Grandparent extends React.Component {
40 render() {
41 return (
42 <Parent
43 child={<div ref={node => (div = node)} className="child" />}
44 />
45 );
46 }
47 }
48 class Parent extends React.Component {
49 render() {
50 return (
51 <div className="parent">
52 {React.cloneElement(this.props.child, {className: 'xyz'})}
53 </div>
54 );
55 }
56 }
57
58 const root = ReactDOMClient.createRoot(document.createElement('div'));
59 await act(() => {
60 root.render(<Grandparent />);
61 });
62 expect(div.className).toBe('xyz');
63 });
64
65 it('should clone a composite component with new props', async () => {
66 let div;
67 class Child extends React.Component {
68 render() {
69 return (
70 <div ref={node => (div = node)} className={this.props.className} />
71 );
72 }
73 }
74 class Grandparent extends React.Component {
75 render() {
76 return <Parent child={<Child className="child" />} />;
77 }
78 }
79 class Parent extends React.Component {
80 render() {
81 return (
82 <div className="parent">
83 {React.cloneElement(this.props.child, {className: 'xyz'})}
84 </div>
85 );
86 }
87 }
88 const root = ReactDOMClient.createRoot(document.createElement('div'));
89 await act(() => {
90 root.render(<Grandparent />);
91 });
92 expect(div.className).toBe('xyz');
93 });
94
95 it('does not fail if config has no prototype', () => {
96 const config = Object.create(null, {foo: {value: 1, enumerable: true}});
97 React.cloneElement(<div />, config);
98 });
99
100 it('should keep the original ref if it is not overridden', async () => {
101 let component;
102 class Grandparent extends React.Component {
103 yoloRef = React.createRef();
104
105 componentDidMount() {
106 component = this;
107 }
108
109 render() {
110 return <Parent child={<div ref={this.yoloRef} />} />;
111 }
112 }
113
114 class Parent extends React.Component {
115 render() {
116 return (
117 <div>{React.cloneElement(this.props.child, {className: 'xyz'})}</div>
118 );
119 }
120 }
121
122 const root = ReactDOMClient.createRoot(document.createElement('div'));
123 await act(() => {
124 root.render(<Grandparent />);
125 });
126
127 expect(component.yoloRef.current.tagName).toBe('DIV');
128 });
129
130 it('should transfer the key property', () => {
131 class Component extends React.Component {
132 render() {
133 return null;
134 }
135 }
136 const clone = React.cloneElement(<Component />, {key: 'xyz'});
137 expect(clone.key).toBe('xyz');
138 });
139
140 it('should transfer children', async () => {
141 class Component extends React.Component {
142 render() {
143 expect(this.props.children).toBe('xyz');
144 return <div />;
145 }
146 }
147
148 const root = ReactDOMClient.createRoot(document.createElement('div'));
149 await act(() => {
150 root.render(React.cloneElement(<Component />, {children: 'xyz'}));
151 });
152 });
153
154 it('should shallow clone children', async () => {
155 class Component extends React.Component {
156 render() {
157 expect(this.props.children).toBe('xyz');
158 return <div />;
159 }
160 }
161
162 const root = ReactDOMClient.createRoot(document.createElement('div'));
163 await act(() => {
164 root.render(React.cloneElement(<Component>xyz</Component>, {}));
165 });
166 });
167
168 it('should accept children as rest arguments', () => {
169 class Component extends React.Component {
170 render() {
171 return null;
172 }
173 }
174
175 const clone = React.cloneElement(
176 <Component>xyz</Component>,
177 {children: <Component />},
178 <div />,
179 <span />,
180 );
181
182 expect(clone.props.children).toEqual([<div />, <span />]);
183 });
184
185 it('should override children if undefined is provided as an argument', () => {
186 const element = React.createElement(
187 ComponentClass,
188 {
189 children: 'text',
190 },
191 undefined,
192 );
193 expect(element.props.children).toBe(undefined);
194
195 const element2 = React.cloneElement(
196 React.createElement(ComponentClass, {
197 children: 'text',
198 }),
199 {},
200 undefined,
201 );
202 expect(element2.props.children).toBe(undefined);
203 });
204
205 it('should support keys and refs', async () => {
206 let component;
207 class Parent extends React.Component {
208 xyzRef = React.createRef();
209
210 render() {
211 const clone = React.cloneElement(this.props.children, {
212 key: 'xyz',
213 ref: this.xyzRef,
214 });
215 expect(clone.key).toBe('xyz');
216 expect(clone.props.ref).toBe(this.xyzRef);
217 return <div>{clone}</div>;
218 }
219 }
220
221 class Grandparent extends React.Component {
222 parentRef = React.createRef();
223
224 componentDidMount() {
225 component = this;
226 }
227
228 render() {
229 return (
230 <Parent ref={this.parentRef}>
231 <span key="abc" />
232 </Parent>
233 );
234 }
235 }
236
237 const root = ReactDOMClient.createRoot(document.createElement('div'));
238 await act(() => root.render(<Grandparent />));
239 expect(component.parentRef.current.xyzRef.current.tagName).toBe('SPAN');
240 });
241
242 it('should steal the ref if a new ref is specified', async () => {
243 let component;
244 class Parent extends React.Component {
245 xyzRef = React.createRef();
246
247 render() {
248 const clone = React.cloneElement(this.props.children, {
249 ref: this.xyzRef,
250 });
251 return <div>{clone}</div>;
252 }
253 }
254
255 class Grandparent extends React.Component {
256 parentRef = React.createRef();
257 childRef = React.createRef();
258
259 componentDidMount() {
260 component = this;
261 }
262
263 render() {
264 return (
265 <Parent ref={this.parentRef}>
266 <span ref={this.childRef} />
267 </Parent>
268 );
269 }
270 }
271
272 const root = ReactDOMClient.createRoot(document.createElement('div'));
273 await act(() => root.render(<Grandparent />));
274 expect(component.childRef).toEqual({current: null});
275 expect(component.parentRef.current.xyzRef.current.tagName).toBe('SPAN');
276 });
277
278 it('should overwrite props', async () => {
279 class Component extends React.Component {
280 render() {
281 expect(this.props.myprop).toBe('xyz');
282 return <div />;
283 }
284 }
285
286 const root = ReactDOMClient.createRoot(document.createElement('div'));
287 await act(() =>
288 root.render(
289 React.cloneElement(<Component myprop="abc" />, {myprop: 'xyz'}),
290 ),
291 );
292 });
293
294 it('warns for keys for arrays of elements in rest args', async () => {
295 const root = ReactDOMClient.createRoot(document.createElement('div'));
296 await act(() => {
297 root.render(React.cloneElement(<div />, null, [<div />, <div />]));
298 });
299 assertConsoleErrorDev([
300 'Each child in a list should have a unique "key" prop.\n\n' +
301 'Check the top-level render call using <div>. See https://react.dev/link/warning-keys for more information.\n' +
302 ' in div (at **)',
303 ]);
304 });
305
306 it('does not warns for arrays of elements with keys', async () => {
307 const root = ReactDOMClient.createRoot(document.createElement('div'));
308 await act(() => {
309 root.render(
310 React.cloneElement(<div />, null, [<div key="#1" />, <div key="#2" />]),
311 );
312 });
313 });
314
315 it('does not warn when the element is directly in rest args', async () => {
316 const root = ReactDOMClient.createRoot(document.createElement('div'));
317 await act(() => {
318 root.render(React.cloneElement(<div />, null, <div />, <div />));
319 });
320 });
321
322 it('does not warn when the array contains a non-element', () => {
323 React.cloneElement(<div />, null, [{}, {}]);
324 });
325
326 it('should ignore key and ref warning getters', () => {
327 const elementA = React.createElement('div');
328 const elementB = React.cloneElement(elementA, elementA.props);
329 expect(elementB.key).toBe(null);
330 expect(elementB.ref).toBe(null);
331 });
332
333 it('should ignore undefined key and ref', () => {
334 const element = React.createElement(ComponentClass, {
335 key: '12',
336 ref: '34',
337 foo: '56',
338 });
339 const props = {
340 key: undefined,
341 ref: undefined,
342 foo: 'ef',
343 };
344 const clone = React.cloneElement(element, props);
345 expect(clone.type).toBe(ComponentClass);
346 expect(clone.key).toBe('12');
347 expect(clone.props.ref).toBe('34');
348 expect(clone.ref).toBe('34');
349 assertConsoleErrorDev([
350 'Accessing element.ref was removed in React 19. ref is now a ' +
351 'regular prop. It will be removed from the JSX Element ' +
352 'type in a future release.',
353 ]);
354 expect(clone.props).toEqual({foo: 'ef', ref: '34'});
355 if (__DEV__) {
356 expect(Object.isFrozen(element)).toBe(true);
357 expect(Object.isFrozen(element.props)).toBe(true);
358 }
359 });
360
361 it('should extract null key and ref', () => {
362 const element = React.createElement(ComponentClass, {
363 key: '12',
364 ref: '34',
365 foo: '56',
366 });
367 const props = {
368 key: null,
369 ref: null,
370 foo: 'ef',
371 };
372 const clone = React.cloneElement(element, props);
373 expect(clone.type).toBe(ComponentClass);
374 expect(clone.key).toBe('null');
375 expect(clone.ref).toBe(null);
376 expect(clone.props).toEqual({foo: 'ef', ref: null});
377 if (__DEV__) {
378 expect(Object.isFrozen(element)).toBe(true);
379 expect(Object.isFrozen(element.props)).toBe(true);
380 }
381 });
382
383 it('throws an error if passed null', () => {
384 const element = null;
385 expect(() => React.cloneElement(element)).toThrow(
386 'The argument must be a React element, but you passed null.',
387 );
388 });
389
390 it('throws an error if passed undefined', () => {
391 let element;
392 expect(() => React.cloneElement(element)).toThrow(
393 'The argument must be a React element, but you passed undefined.',
394 );
395 });
396 });