main
js 531 lines 19 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 // NOTE: We're explicitly not using JSX in this file. This is intended to test
13 // classic React.createElement without JSX.
14 // TODO: ^ the above note is a bit stale because there are tests in this file
15 // that do use JSX syntax. We should port them to React.createElement, and also
16 // confirm there's a corresponding test that uses JSX syntax.
17
18 let React;
19 let ReactDOMClient;
20 let act;
21 let assertConsoleErrorDev;
22
23 describe('ReactElementValidator', () => {
24 let ComponentClass;
25
26 beforeEach(() => {
27 jest.resetModules();
28
29 React = require('react');
30 ReactDOMClient = require('react-dom/client');
31 ({act, assertConsoleErrorDev} = require('internal-test-utils'));
32 ComponentClass = class extends React.Component {
33 render() {
34 return React.createElement('div', null, this.props.children);
35 }
36 };
37 });
38
39 it('warns for keys for arrays of elements in rest args', async () => {
40 const root = ReactDOMClient.createRoot(document.createElement('div'));
41 await act(() =>
42 root.render(
43 React.createElement(ComponentClass, null, [
44 React.createElement(ComponentClass),
45 React.createElement(ComponentClass),
46 ]),
47 ),
48 );
49 assertConsoleErrorDev([
50 'Each child in a list should have a unique "key" prop.\n\n' +
51 'Check the render method of `ComponentClass`. See https://react.dev/link/warning-keys for more information.\n' +
52 ' in ComponentClass (at **)',
53 ]);
54 });
55
56 it('warns for keys for arrays of elements with owner info', async () => {
57 class InnerClass extends React.Component {
58 render() {
59 return React.createElement(ComponentClass, null, this.props.childSet);
60 }
61 }
62
63 class ComponentWrapper extends React.Component {
64 render() {
65 return React.createElement(InnerClass, {
66 childSet: [
67 React.createElement(ComponentClass),
68 React.createElement(ComponentClass),
69 ],
70 });
71 }
72 }
73
74 const root = ReactDOMClient.createRoot(document.createElement('div'));
75 await act(() => root.render(React.createElement(ComponentWrapper)));
76 assertConsoleErrorDev([
77 'Each child in a list should have a unique "key" prop.' +
78 '\n\nCheck the render method of `ComponentClass`. ' +
79 'It was passed a child from ComponentWrapper. ' +
80 'See https://react.dev/link/warning-keys for more information.\n' +
81 ' in ComponentWrapper (at **)',
82 ]);
83 });
84
85 it('warns for keys for arrays with no owner or parent info', async () => {
86 function Anonymous({children}) {
87 return <div>{children}</div>;
88 }
89 Object.defineProperty(Anonymous, 'name', {value: undefined});
90
91 const divs = [<div />, <div />];
92
93 const root = ReactDOMClient.createRoot(document.createElement('div'));
94 await act(() => root.render(<Anonymous>{divs}</Anonymous>));
95 assertConsoleErrorDev([
96 // For owner stacks the parent being validated is the div.
97 'Each child in a list should have a unique ' +
98 '"key" prop.' +
99 '\n\nCheck the top-level render call using <div>. ' +
100 'See https://react.dev/link/warning-keys for more information.\n' +
101 ' in div (at **)',
102 ]);
103 });
104
105 it('warns for keys for arrays of elements with no owner info', async () => {
106 const divs = [<div />, <div />];
107
108 const root = ReactDOMClient.createRoot(document.createElement('div'));
109
110 await act(() => root.render(<div>{divs}</div>));
111 assertConsoleErrorDev([
112 'Each child in a list should have a unique ' +
113 '"key" prop.' +
114 '\n\nCheck the top-level render call using <div>. ' +
115 'See https://react.dev/link/warning-keys for more information.\n' +
116 ' in div (at **)',
117 ]);
118 });
119
120 it('warns for keys with component stack info', async () => {
121 function Component() {
122 return <div>{[<div />, <div />]}</div>;
123 }
124
125 function Parent(props) {
126 return React.cloneElement(props.child);
127 }
128
129 function GrandParent() {
130 return <Parent child={<Component />} />;
131 }
132
133 const root = ReactDOMClient.createRoot(document.createElement('div'));
134 await act(() => root.render(<GrandParent />));
135 assertConsoleErrorDev([
136 'Each child in a list should have a unique ' +
137 '"key" prop.\n\nCheck the render method of `Component`. See ' +
138 'https://react.dev/link/warning-keys for more information.\n' +
139 ' in div (at **)\n' +
140 ' in Component (at **)\n' +
141 ' in GrandParent (at **)',
142 ]);
143 });
144
145 it('does not warn for keys when passing children down', async () => {
146 function Wrapper(props) {
147 return (
148 <div>
149 {props.children}
150 <footer />
151 </div>
152 );
153 }
154
155 const root = ReactDOMClient.createRoot(document.createElement('div'));
156 await act(() =>
157 root.render(
158 <Wrapper>
159 <span />
160 <span />
161 </Wrapper>,
162 ),
163 );
164 });
165
166 it('warns for keys for iterables of elements in rest args', async () => {
167 const iterable = {
168 '@@iterator': function () {
169 let i = 0;
170 return {
171 next: function () {
172 const done = ++i > 2;
173 return {
174 value: done ? undefined : React.createElement(ComponentClass),
175 done: done,
176 };
177 },
178 };
179 },
180 };
181
182 const root = ReactDOMClient.createRoot(document.createElement('div'));
183 await act(() =>
184 root.render(React.createElement(ComponentClass, null, iterable)),
185 );
186 assertConsoleErrorDev([
187 'Each child in a list should have a unique "key" prop.\n\n' +
188 'Check the render method of `ComponentClass`. It was passed a child from div. ' +
189 'See https://react.dev/link/warning-keys for more information.\n' +
190 ' in ComponentClass (at **)',
191 ]);
192 });
193
194 it('does not warns for arrays of elements with keys', () => {
195 React.createElement(ComponentClass, null, [
196 React.createElement(ComponentClass, {key: '#1'}),
197 React.createElement(ComponentClass, {key: '#2'}),
198 ]);
199 });
200
201 it('does not warns for iterable elements with keys', () => {
202 const iterable = {
203 '@@iterator': function () {
204 let i = 0;
205 return {
206 next: function () {
207 const done = ++i > 2;
208 return {
209 value: done
210 ? undefined
211 : React.createElement(ComponentClass, {key: '#' + i}),
212 done: done,
213 };
214 },
215 };
216 },
217 };
218
219 React.createElement(ComponentClass, null, iterable);
220 });
221
222 it('does not warn when the element is directly in rest args', () => {
223 React.createElement(
224 ComponentClass,
225 null,
226 React.createElement(ComponentClass),
227 React.createElement(ComponentClass),
228 );
229 });
230
231 it('does not warn when the array contains a non-element', () => {
232 React.createElement(ComponentClass, null, [{}, {}]);
233 });
234
235 it('should give context for errors in nested components.', async () => {
236 function MyComp() {
237 return [React.createElement('div')];
238 }
239 function ParentComp() {
240 return React.createElement(MyComp);
241 }
242 const root = ReactDOMClient.createRoot(document.createElement('div'));
243 await act(() => root.render(React.createElement(ParentComp)));
244 assertConsoleErrorDev([
245 'Each child in a list should have a unique "key" prop.' +
246 '\n\nCheck the render method of `ParentComp`. It was passed a child from MyComp. ' +
247 'See https://react.dev/link/warning-keys for more information.\n' +
248 ' in div (at **)\n' +
249 ' in MyComp (at **)\n' +
250 ' in ParentComp (at **)',
251 ]);
252 });
253
254 it('gives a helpful error when passing invalid types', async () => {
255 function Foo() {}
256 const errors = [];
257 const root = ReactDOMClient.createRoot(document.createElement('div'), {
258 onUncaughtError(error) {
259 errors.push(error.message);
260 },
261 });
262 const cases = [
263 [
264 () => React.createElement(undefined),
265 'React.createElement: type is invalid -- expected a string ' +
266 '(for built-in components) or a class/function (for composite ' +
267 'components) but got: undefined. You likely forgot to export your ' +
268 "component from the file it's defined in, or you might have mixed up " +
269 'default and named imports.',
270 ],
271 [
272 () => React.createElement(null),
273 'React.createElement: type is invalid -- expected a string ' +
274 '(for built-in components) or a class/function (for composite ' +
275 'components) but got: null.',
276 ],
277 [
278 () => React.createElement(true),
279 'React.createElement: type is invalid -- expected a string ' +
280 '(for built-in components) or a class/function (for composite ' +
281 'components) but got: boolean.',
282 ],
283 [
284 () => React.createElement({x: 17}),
285 'React.createElement: type is invalid -- expected a string ' +
286 '(for built-in components) or a class/function (for composite ' +
287 'components) but got: object.',
288 ],
289 [
290 () => React.createElement({}),
291 'React.createElement: type is invalid -- expected a string ' +
292 '(for built-in components) or a class/function (for composite ' +
293 'components) but got: object. You likely forgot to export your ' +
294 "component from the file it's defined in, or you might have mixed up " +
295 'default and named imports.',
296 ],
297 [
298 () => React.createElement(React.createElement('div')),
299 'React.createElement: type is invalid -- expected a string ' +
300 '(for built-in components) or a class/function (for composite ' +
301 'components) but got: <div />. Did you accidentally export a JSX literal ' +
302 'instead of a component?',
303 ],
304 [
305 () => React.createElement(React.createElement(Foo)),
306 'React.createElement: type is invalid -- expected a string ' +
307 '(for built-in components) or a class/function (for composite ' +
308 'components) but got: <Foo />. Did you accidentally export a JSX literal ' +
309 'instead of a component?',
310 ],
311 [
312 () =>
313 React.createElement(
314 React.createElement(React.createContext().Consumer),
315 ),
316 'React.createElement: type is invalid -- expected a string ' +
317 '(for built-in components) or a class/function (for composite ' +
318 'components) but got: <Context.Consumer />. Did you accidentally ' +
319 'export a JSX literal instead of a component?',
320 ],
321 [
322 () => React.createElement({$$typeof: 'non-react-thing'}),
323 'React.createElement: type is invalid -- expected a string ' +
324 '(for built-in components) or a class/function (for composite ' +
325 'components) but got: object.',
326 ],
327 ];
328 for (let i = 0; i < cases.length; i++) {
329 await act(async () => root.render(cases[i][0]()));
330 }
331
332 expect(errors).toEqual(
333 __DEV__
334 ? [
335 'Element type is invalid: expected a string ' +
336 '(for built-in components) or a class/function (for composite ' +
337 'components) but got: undefined. You likely forgot to export your ' +
338 "component from the file it's defined in, or you might have mixed up " +
339 'default and named imports.',
340 'Element type is invalid: expected a string ' +
341 '(for built-in components) or a class/function (for composite ' +
342 'components) but got: null.',
343 'Element type is invalid: expected a string ' +
344 '(for built-in components) or a class/function (for composite ' +
345 'components) but got: boolean.',
346 'Element type is invalid: expected a string ' +
347 '(for built-in components) or a class/function (for composite ' +
348 'components) but got: object.',
349 'Element type is invalid: expected a string ' +
350 '(for built-in components) or a class/function (for composite ' +
351 'components) but got: object. You likely forgot to export your ' +
352 "component from the file it's defined in, or you might have mixed up " +
353 'default and named imports.',
354 'Element type is invalid: expected a string ' +
355 '(for built-in components) or a class/function (for composite ' +
356 'components) but got: <div />. Did you accidentally export a JSX literal ' +
357 'instead of a component?',
358 'Element type is invalid: expected a string ' +
359 '(for built-in components) or a class/function (for composite ' +
360 'components) but got: <Foo />. Did you accidentally export a JSX literal ' +
361 'instead of a component?',
362 'Element type is invalid: expected a string ' +
363 '(for built-in components) or a class/function (for composite ' +
364 'components) but got: <Context.Consumer />. Did you accidentally ' +
365 'export a JSX literal instead of a component?',
366 'Element type is invalid: expected a string ' +
367 '(for built-in components) or a class/function (for composite ' +
368 'components) but got: object.',
369 ]
370 : [
371 'Element type is invalid: expected a string ' +
372 '(for built-in components) or a class/function (for composite ' +
373 'components) but got: undefined.',
374 'Element type is invalid: expected a string ' +
375 '(for built-in components) or a class/function (for composite ' +
376 'components) but got: null.',
377 'Element type is invalid: expected a string ' +
378 '(for built-in components) or a class/function (for composite ' +
379 'components) but got: boolean.',
380 'Element type is invalid: expected a string ' +
381 '(for built-in components) or a class/function (for composite ' +
382 'components) but got: object.',
383 'Element type is invalid: expected a string ' +
384 '(for built-in components) or a class/function (for composite ' +
385 'components) but got: object.',
386 'Element type is invalid: expected a string ' +
387 '(for built-in components) or a class/function (for composite ' +
388 'components) but got: object.',
389 'Element type is invalid: expected a string ' +
390 '(for built-in components) or a class/function (for composite ' +
391 'components) but got: object.',
392 'Element type is invalid: expected a string ' +
393 '(for built-in components) or a class/function (for composite ' +
394 'components) but got: object.',
395 'Element type is invalid: expected a string ' +
396 '(for built-in components) or a class/function (for composite ' +
397 'components) but got: object.',
398 ],
399 );
400
401 // Should not log any additional warnings
402 React.createElement('div');
403 });
404
405 it('includes the owner name when passing null, undefined, boolean, or number', async () => {
406 function ParentComp() {
407 return React.createElement(null);
408 }
409
410 await expect(async () => {
411 const root = ReactDOMClient.createRoot(document.createElement('div'));
412 await act(() => root.render(React.createElement(ParentComp)));
413 }).rejects.toThrow(
414 'Element type is invalid: expected a string (for built-in components) ' +
415 'or a class/function (for composite components) but got: null.' +
416 (__DEV__ ? '\n\nCheck the render method of `ParentComp`.' : ''),
417 );
418 });
419
420 it('warns for fragments with illegal attributes', async () => {
421 class Foo extends React.Component {
422 render() {
423 return React.createElement(React.Fragment, {a: 1}, '123');
424 }
425 }
426
427 const root = ReactDOMClient.createRoot(document.createElement('div'));
428 await act(() => root.render(React.createElement(Foo)));
429 assertConsoleErrorDev([
430 gate('enableFragmentRefs')
431 ? 'Invalid prop `a` supplied to `React.Fragment`. React.Fragment ' +
432 'can only have `key`, `ref`, and `children` props.\n' +
433 ' in Foo (at **)'
434 : 'Invalid prop `a` supplied to `React.Fragment`. React.Fragment ' +
435 'can only have `key` and `children` props.\n' +
436 ' in Foo (at **)',
437 ]);
438 });
439
440 it('does not warn when using DOM node as children', async () => {
441 class DOMContainer extends React.Component {
442 ref;
443 render() {
444 return <div ref={n => (this.ref = n)} />;
445 }
446 componentDidMount() {
447 this.ref.appendChild(this.props.children);
448 }
449 }
450
451 const node = document.createElement('div');
452 const root = ReactDOMClient.createRoot(document.createElement('div'));
453 await act(() => {
454 // This shouldn't cause a stack overflow or any other problems (#3883)
455 root.render(<DOMContainer>{node}</DOMContainer>);
456 });
457 });
458
459 it('should not enumerate enumerable numbers (#4776)', () => {
460 /*eslint-disable no-extend-native */
461 Number.prototype['@@iterator'] = function () {
462 throw new Error('number iterator called');
463 };
464 /*eslint-enable no-extend-native */
465
466 try {
467 void (
468 <div>
469 {5}
470 {12}
471 {13}
472 </div>
473 );
474 } finally {
475 delete Number.prototype['@@iterator'];
476 }
477 });
478
479 it('does not blow up with inlined children', () => {
480 // We don't suggest this since it silences all sorts of warnings, but we
481 // shouldn't blow up either.
482
483 const child = {
484 $$typeof: (<div />).$$typeof,
485 type: 'span',
486 key: null,
487 ref: null,
488 props: {},
489 _owner: null,
490 };
491
492 void (<div>{[child]}</div>);
493 });
494
495 it('does not blow up on key warning with undefined type', () => {
496 const Foo = undefined;
497 void (<Foo>{[<div />]}</Foo>);
498 });
499
500 it('does not call lazy initializers eagerly', () => {
501 let didCall = false;
502 const Lazy = React.lazy(() => {
503 didCall = true;
504 return {then() {}};
505 });
506 React.createElement(Lazy);
507 expect(didCall).toBe(false);
508 });
509
510 it('__self and __source are treated as normal props', async () => {
511 // These used to be reserved props because the classic React.createElement
512 // runtime passed this data as props, whereas the jsxDEV() runtime passes
513 // them as separate arguments.
514 function Child({__self, __source}) {
515 return __self + __source;
516 }
517
518 const container = document.createElement('div');
519 const root = ReactDOMClient.createRoot(container);
520 // NOTE: The Babel transform treats the presence of these props as a syntax
521 // error but theoretically it doesn't have to. Using spread here to
522 // circumvent the syntax error and demonstrate that the runtime
523 // doesn't care.
524 const props = {
525 __self: 'Hello ',
526 __source: 'world!',
527 };
528 await act(() => root.render(<Child {...props} />));
529 expect(container.textContent).toBe('Hello world!');
530 });
531 });