main
js 444 lines 13.4 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
14 let React;
15 let ReactDOMClient;
16 let assertConsoleErrorDev;
17 let assertConsoleWarnDev;
18
19 // NOTE: This module tests the old, "classic" JSX runtime, React.createElement.
20 // Do not use JSX syntax in this module; call React.createElement directly.
21 describe('ReactCreateElement', () => {
22 let ComponentClass;
23
24 beforeEach(() => {
25 jest.resetModules();
26
27 ({
28 act,
29 assertConsoleErrorDev,
30 assertConsoleWarnDev,
31 } = require('internal-test-utils'));
32
33 React = require('react');
34 ReactDOMClient = require('react-dom/client');
35 ComponentClass = class extends React.Component {
36 render() {
37 return React.createElement('div');
38 }
39 };
40 });
41
42 it('returns a complete element according to spec', () => {
43 const element = React.createElement(ComponentClass);
44 expect(element.type).toBe(ComponentClass);
45 expect(element.key).toBe(null);
46 expect(element.ref).toBe(null);
47 if (__DEV__) {
48 expect(Object.isFrozen(element)).toBe(true);
49 expect(Object.isFrozen(element.props)).toBe(true);
50 }
51 expect(element.props).toEqual({});
52 });
53
54 it('should warn when `key` is being accessed on composite element', async () => {
55 class Child extends React.Component {
56 render() {
57 return React.createElement('div', null, this.props.key);
58 }
59 }
60 class Parent extends React.Component {
61 render() {
62 return React.createElement(
63 'div',
64 null,
65 React.createElement(Child, {key: '0'}),
66 React.createElement(Child, {key: '1'}),
67 React.createElement(Child, {key: '2'}),
68 );
69 }
70 }
71 const root = ReactDOMClient.createRoot(document.createElement('div'));
72 await act(() => {
73 root.render(React.createElement(Parent));
74 });
75 assertConsoleErrorDev([
76 'Child: `key` is not a prop. Trying to access it will result ' +
77 'in `undefined` being returned. If you need to access the same ' +
78 'value within the child component, you should pass it as a different ' +
79 'prop. (https://react.dev/link/special-props)\n' +
80 ' in Parent (at **)',
81 ]);
82 });
83
84 it('should warn when `key` is being accessed on a host element', () => {
85 const element = React.createElement('div', {key: '3'});
86 void element.props.key;
87 assertConsoleErrorDev([
88 'div: `key` is not a prop. Trying to access it will result ' +
89 'in `undefined` being returned. If you need to access the same ' +
90 'value within the child component, you should pass it as a different ' +
91 'prop. (https://react.dev/link/special-props)',
92 ]);
93 });
94
95 it('allows a string to be passed as the type', () => {
96 const element = React.createElement('div');
97 expect(element.type).toBe('div');
98 expect(element.key).toBe(null);
99 expect(element.ref).toBe(null);
100 if (__DEV__) {
101 expect(Object.isFrozen(element)).toBe(true);
102 expect(Object.isFrozen(element.props)).toBe(true);
103 }
104 expect(element.props).toEqual({});
105 });
106
107 it('returns an immutable element', () => {
108 const element = React.createElement(ComponentClass);
109 if (__DEV__) {
110 expect(() => (element.type = 'div')).toThrow();
111 } else {
112 expect(() => (element.type = 'div')).not.toThrow();
113 }
114 });
115
116 it('does not reuse the original config object', () => {
117 const config = {foo: 1};
118 const element = React.createElement(ComponentClass, config);
119 expect(element.props.foo).toBe(1);
120 config.foo = 2;
121 expect(element.props.foo).toBe(1);
122 });
123
124 it('does not fail if config has no prototype', () => {
125 const config = Object.create(null, {foo: {value: 1, enumerable: true}});
126 const element = React.createElement(ComponentClass, config);
127 expect(element.props.foo).toBe(1);
128 });
129
130 it('extracts key from the rest of the props', () => {
131 const element = React.createElement(ComponentClass, {
132 key: '12',
133 foo: '56',
134 });
135 expect(element.type).toBe(ComponentClass);
136 expect(element.key).toBe('12');
137 const expectation = {foo: '56'};
138 Object.freeze(expectation);
139 expect(element.props).toEqual(expectation);
140 });
141
142 it('does not extract ref from the rest of the props', () => {
143 const ref = React.createRef();
144 const element = React.createElement(ComponentClass, {
145 key: '12',
146 ref: ref,
147 foo: '56',
148 });
149 expect(element.type).toBe(ComponentClass);
150 expect(element.ref).toBe(ref);
151 assertConsoleErrorDev([
152 'Accessing element.ref was removed in React 19. ref is now a ' +
153 'regular prop. It will be removed from the JSX Element ' +
154 'type in a future release.',
155 ]);
156 const expectation = {foo: '56', ref};
157 Object.freeze(expectation);
158 expect(element.props).toEqual(expectation);
159 });
160
161 it('extracts null key', () => {
162 const element = React.createElement(ComponentClass, {
163 key: null,
164 foo: '12',
165 });
166 expect(element.type).toBe(ComponentClass);
167 expect(element.key).toBe('null');
168 if (__DEV__) {
169 expect(Object.isFrozen(element)).toBe(true);
170 expect(Object.isFrozen(element.props)).toBe(true);
171 }
172 expect(element.props).toEqual({foo: '12'});
173 });
174
175 it('ignores undefined key and ref', () => {
176 const props = {
177 foo: '56',
178 key: undefined,
179 ref: undefined,
180 };
181 const element = React.createElement(ComponentClass, props);
182 expect(element.type).toBe(ComponentClass);
183 expect(element.key).toBe(null);
184 expect(element.ref).toBe(null);
185 if (__DEV__) {
186 expect(Object.isFrozen(element)).toBe(true);
187 expect(Object.isFrozen(element.props)).toBe(true);
188 }
189 expect(element.props).toEqual({foo: '56'});
190 });
191
192 it('ignores key and ref warning getters', () => {
193 const elementA = React.createElement('div');
194 const elementB = React.createElement('div', elementA.props);
195 expect(elementB.key).toBe(null);
196 expect(elementB.ref).toBe(null);
197 });
198
199 it('coerces the key to a string', () => {
200 const element = React.createElement(ComponentClass, {
201 key: 12,
202 foo: '56',
203 });
204 expect(element.type).toBe(ComponentClass);
205 expect(element.key).toBe('12');
206 expect(element.ref).toBe(null);
207 if (__DEV__) {
208 expect(Object.isFrozen(element)).toBe(true);
209 expect(Object.isFrozen(element.props)).toBe(true);
210 }
211 expect(element.props).toEqual({foo: '56'});
212 });
213
214 it('preserves the owner on the element', async () => {
215 let element;
216 let instance;
217
218 class Wrapper extends React.Component {
219 componentDidMount() {
220 instance = this;
221 }
222 render() {
223 element = React.createElement(ComponentClass);
224 return element;
225 }
226 }
227 const root = ReactDOMClient.createRoot(document.createElement('div'));
228 await act(() => root.render(React.createElement(Wrapper)));
229 if (__DEV__) {
230 expect(element._owner.stateNode).toBe(instance);
231 } else {
232 expect('_owner' in element).toBe(false);
233 }
234 });
235
236 it('merges an additional argument onto the children prop', () => {
237 const a = 1;
238 const element = React.createElement(
239 ComponentClass,
240 {
241 children: 'text',
242 },
243 a,
244 );
245 expect(element.props.children).toBe(a);
246 });
247
248 it('does not override children if no rest args are provided', () => {
249 const element = React.createElement(ComponentClass, {
250 children: 'text',
251 });
252 expect(element.props.children).toBe('text');
253 });
254
255 it('overrides children if null is provided as an argument', () => {
256 const element = React.createElement(
257 ComponentClass,
258 {
259 children: 'text',
260 },
261 null,
262 );
263 expect(element.props.children).toBe(null);
264 });
265
266 it('merges rest arguments onto the children prop in an array', () => {
267 const a = 1;
268 const b = 2;
269 const c = 3;
270 const element = React.createElement(ComponentClass, null, a, b, c);
271 expect(element.props.children).toEqual([1, 2, 3]);
272 });
273
274 it('allows static methods to be called using the type property', () => {
275 class StaticMethodComponentClass extends React.Component {
276 render() {
277 return React.createElement('div');
278 }
279 }
280 StaticMethodComponentClass.someStaticMethod = () => 'someReturnValue';
281
282 const element = React.createElement(StaticMethodComponentClass);
283 expect(element.type.someStaticMethod()).toBe('someReturnValue');
284 });
285
286 it('is indistinguishable from a plain object', () => {
287 const element = React.createElement('div', {className: 'foo'});
288 const object = {};
289 expect(element.constructor).toBe(object.constructor);
290 });
291
292 it('should use default prop value when removing a prop', async () => {
293 class Component extends React.Component {
294 render() {
295 return React.createElement('span');
296 }
297 }
298 Component.defaultProps = {fruit: 'persimmon'};
299
300 const container = document.createElement('div');
301 const root = ReactDOMClient.createRoot(container);
302
303 const ref = React.createRef();
304 await act(() => {
305 root.render(React.createElement(Component, {ref, fruit: 'mango'}));
306 });
307 const instance = ref.current;
308 expect(instance.props.fruit).toBe('mango');
309
310 await act(() => {
311 root.render(React.createElement(Component));
312 });
313 expect(instance.props.fruit).toBe('persimmon');
314 });
315
316 it('should normalize props with default values', async () => {
317 let instance;
318 class Component extends React.Component {
319 componentDidMount() {
320 instance = this;
321 }
322 render() {
323 return React.createElement('span', null, this.props.prop);
324 }
325 }
326 Component.defaultProps = {prop: 'testKey'};
327
328 const root = ReactDOMClient.createRoot(document.createElement('div'));
329 await act(() => {
330 root.render(React.createElement(Component));
331 });
332 expect(instance.props.prop).toBe('testKey');
333
334 await act(() => {
335 root.render(React.createElement(Component, {prop: null}));
336 });
337 expect(instance.props.prop).toBe(null);
338 });
339
340 it('throws when changing a prop (in dev) after element creation', async () => {
341 class Outer extends React.Component {
342 render() {
343 const el = React.createElement('div', {className: 'moo'});
344
345 if (__DEV__) {
346 expect(function () {
347 el.props.className = 'quack';
348 }).toThrow();
349 expect(el.props.className).toBe('moo');
350 } else {
351 el.props.className = 'quack';
352 expect(el.props.className).toBe('quack');
353 }
354
355 return el;
356 }
357 }
358
359 const container = document.createElement('div');
360 const root = ReactDOMClient.createRoot(container);
361
362 await act(() => {
363 root.render(React.createElement(Outer, {color: 'orange'}));
364 });
365 if (__DEV__) {
366 expect(container.firstChild.className).toBe('moo');
367 } else {
368 expect(container.firstChild.className).toBe('quack');
369 }
370 });
371
372 it('throws when adding a prop (in dev) after element creation', async () => {
373 const container = document.createElement('div');
374 class Outer extends React.Component {
375 render() {
376 const el = React.createElement('div', null, this.props.sound);
377
378 if (__DEV__) {
379 expect(function () {
380 el.props.className = 'quack';
381 }).toThrow();
382 expect(el.props.className).toBe(undefined);
383 } else {
384 el.props.className = 'quack';
385 expect(el.props.className).toBe('quack');
386 }
387
388 return el;
389 }
390 }
391 Outer.defaultProps = {sound: 'meow'};
392 const root = ReactDOMClient.createRoot(container);
393 await act(() => {
394 root.render(React.createElement(Outer));
395 });
396 expect(container.firstChild.textContent).toBe('meow');
397 if (__DEV__) {
398 expect(container.firstChild.className).toBe('');
399 } else {
400 expect(container.firstChild.className).toBe('quack');
401 }
402 });
403
404 it('does not warn for NaN props', async () => {
405 let test;
406 class Test extends React.Component {
407 componentDidMount() {
408 test = this;
409 }
410 render() {
411 return React.createElement('div');
412 }
413 }
414 const root = ReactDOMClient.createRoot(document.createElement('div'));
415 await act(() => {
416 root.render(React.createElement(Test, {value: +undefined}));
417 });
418 expect(test.props.value).toBeNaN();
419 });
420
421 it('warns if outdated JSX transform is detected', async () => {
422 // Warns if __self is detected, because that's only passed by a compiler
423 React.createElement('div', {className: 'foo', __self: this});
424 assertConsoleWarnDev([
425 'Your app (or one of its dependencies) is using an outdated JSX ' +
426 'transform. Update to the modern JSX transform for ' +
427 'faster performance: https://react.dev/link/new-jsx-transform',
428 ]);
429
430 // Only warns the first time. Subsequent elements don't warn.
431 React.createElement('div', {className: 'foo', __self: this});
432 });
433
434 it('do not warn about outdated JSX transform if `key` is present', () => {
435 // When a static "key" prop is defined _after_ a spread, the modern JSX
436 // transform outputs `createElement` instead of `jsx`. (This is because with
437 // `jsx`, a spread key always takes precedence over a static key, regardless
438 // of the order, whereas `createElement` respects the order.)
439 //
440 // To avoid a false positive warning, we skip the warning whenever a `key`
441 // prop is present.
442 React.createElement('div', {key: 'foo', __self: this});
443 });
444 });