main
js 767 lines 21.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 // These tests are based on ReactJSXElement-test,
11 // ReactJSXElementValidator-test, ReactComponent-test,
12 // and ReactElementJSX-test.
13
14 jest.mock('react/jsx-runtime', () => require('./jsx-runtime'), {virtual: true});
15 jest.mock('react/jsx-dev-runtime', () => require('./jsx-dev-runtime'), {
16 virtual: true,
17 });
18
19 let React = require('react');
20 let ReactDOM = require('react-dom');
21 let ReactTestUtils = {
22 renderIntoDocument(el) {
23 const container = document.createElement('div');
24 return ReactDOM.render(el, container);
25 },
26 };
27 let PropTypes = require('prop-types');
28 let Component = class Component extends React.Component {
29 render() {
30 return <div />;
31 }
32 };
33 let RequiredPropComponent = class extends React.Component {
34 render() {
35 return <span>{this.props.prop}</span>;
36 }
37 };
38 RequiredPropComponent.displayName = 'RequiredPropComponent';
39 RequiredPropComponent.propTypes = {prop: PropTypes.string.isRequired};
40
41 it('works', () => {
42 const container = document.createElement('div');
43 ReactDOM.render(<h1>hello</h1>, container);
44 expect(container.textContent).toBe('hello');
45 });
46
47 it('returns a complete element according to spec', () => {
48 const element = <Component />;
49 expect(element.type).toBe(Component);
50 expect(element.key).toBe(null);
51 expect(element.ref).toBe(null);
52 const expectation = {};
53 Object.freeze(expectation);
54 expect(element.props).toEqual(expectation);
55 });
56
57 it('allows a lower-case to be passed as the string type', () => {
58 const element = <div />;
59 expect(element.type).toBe('div');
60 expect(element.key).toBe(null);
61 expect(element.ref).toBe(null);
62 const expectation = {};
63 Object.freeze(expectation);
64 expect(element.props).toEqual(expectation);
65 });
66
67 it('allows a string to be passed as the type', () => {
68 const TagName = 'div';
69 const element = <TagName />;
70 expect(element.type).toBe('div');
71 expect(element.key).toBe(null);
72 expect(element.ref).toBe(null);
73 const expectation = {};
74 Object.freeze(expectation);
75 expect(element.props).toEqual(expectation);
76 });
77
78 it('returns an immutable element', () => {
79 const element = <Component />;
80 if (process.env.NODE_ENV === 'development') {
81 expect(() => (element.type = 'div')).toThrow();
82 } else {
83 expect(() => (element.type = 'div')).not.toThrow();
84 }
85 });
86
87 it('does not reuse the object that is spread into props', () => {
88 const config = {foo: 1};
89 const element = <Component {...config} />;
90 expect(element.props.foo).toBe(1);
91 config.foo = 2;
92 expect(element.props.foo).toBe(1);
93 });
94
95 it('extracts key and ref from the rest of the props', () => {
96 const element = <Component key="12" ref="34" foo="56" />;
97 expect(element.type).toBe(Component);
98 expect(element.key).toBe('12');
99 expect(element.ref).toBe('34');
100 const expectation = {foo: '56'};
101 Object.freeze(expectation);
102 expect(element.props).toEqual(expectation);
103 });
104
105 it('coerces the key to a string', () => {
106 const element = <Component key={12} foo="56" />;
107 expect(element.type).toBe(Component);
108 expect(element.key).toBe('12');
109 expect(element.ref).toBe(null);
110 const expectation = {foo: '56'};
111 Object.freeze(expectation);
112 expect(element.props).toEqual(expectation);
113 });
114
115 it('merges JSX children onto the children prop', () => {
116 const a = 1;
117 const element = <Component children="text">{a}</Component>;
118 expect(element.props.children).toBe(a);
119 });
120
121 it('does not override children if no JSX children are provided', () => {
122 const element = <Component children="text" />;
123 expect(element.props.children).toBe('text');
124 });
125
126 it('overrides children if null is provided as a JSX child', () => {
127 const element = <Component children="text">{null}</Component>;
128 expect(element.props.children).toBe(null);
129 });
130
131 it('overrides children if undefined is provided as an argument', () => {
132 const element = <Component children="text">{undefined}</Component>;
133 expect(element.props.children).toBe(undefined);
134
135 const element2 = React.cloneElement(
136 <Component children="text" />,
137 {},
138 undefined
139 );
140 expect(element2.props.children).toBe(undefined);
141 });
142
143 it('merges JSX children onto the children prop in an array', () => {
144 const a = 1;
145 const b = 2;
146 const c = 3;
147 const element = (
148 <Component>
149 {a}
150 {b}
151 {c}
152 </Component>
153 );
154 expect(element.props.children).toEqual([1, 2, 3]);
155 });
156
157 it('allows static methods to be called using the type property', () => {
158 class StaticMethodComponent {
159 static someStaticMethod() {
160 return 'someReturnValue';
161 }
162 render() {
163 return <div />;
164 }
165 }
166
167 const element = <StaticMethodComponent />;
168 expect(element.type.someStaticMethod()).toBe('someReturnValue');
169 });
170
171 it('identifies valid elements', () => {
172 expect(React.isValidElement(<div />)).toEqual(true);
173 expect(React.isValidElement(<Component />)).toEqual(true);
174
175 expect(React.isValidElement(null)).toEqual(false);
176 expect(React.isValidElement(true)).toEqual(false);
177 expect(React.isValidElement({})).toEqual(false);
178 expect(React.isValidElement('string')).toEqual(false);
179 expect(React.isValidElement(Component)).toEqual(false);
180 expect(React.isValidElement({type: 'div', props: {}})).toEqual(false);
181 });
182
183 it('is indistinguishable from a plain object', () => {
184 const element = <div className="foo" />;
185 const object = {};
186 expect(element.constructor).toBe(object.constructor);
187 });
188
189 it('should use default prop value when removing a prop', () => {
190 Component.defaultProps = {fruit: 'persimmon'};
191
192 const container = document.createElement('div');
193 const instance = ReactDOM.render(<Component fruit="mango" />, container);
194 expect(instance.props.fruit).toBe('mango');
195
196 ReactDOM.render(<Component />, container);
197 expect(instance.props.fruit).toBe('persimmon');
198 });
199
200 it('should normalize props with default values', () => {
201 class NormalizingComponent extends React.Component {
202 render() {
203 return <span>{this.props.prop}</span>;
204 }
205 }
206 NormalizingComponent.defaultProps = {prop: 'testKey'};
207
208 const container = document.createElement('div');
209 const instance = ReactDOM.render(<NormalizingComponent />, container);
210 expect(instance.props.prop).toBe('testKey');
211
212 const inst2 = ReactDOM.render(
213 <NormalizingComponent prop={null} />,
214 container
215 );
216 expect(inst2.props.prop).toBe(null);
217 });
218
219 it('warns for keys for arrays of elements in children position', () => {
220 expect(() =>
221 ReactTestUtils.renderIntoDocument(
222 <Component>{[<Component />, <Component />]}</Component>
223 )
224 ).toErrorDev('Each child in a list should have a unique "key" prop.');
225 });
226
227 it('warns for keys for arrays of elements with owner info', () => {
228 class InnerComponent extends React.Component {
229 render() {
230 return <Component>{this.props.childSet}</Component>;
231 }
232 }
233
234 class ComponentWrapper extends React.Component {
235 render() {
236 return <InnerComponent childSet={[<Component />, <Component />]} />;
237 }
238 }
239
240 expect(() =>
241 ReactTestUtils.renderIntoDocument(<ComponentWrapper />)
242 ).toErrorDev(
243 'Each child in a list should have a unique "key" prop.' +
244 '\n\nCheck the render method of `InnerComponent`. ' +
245 'It was passed a child from ComponentWrapper. '
246 );
247 });
248
249 it('does not warn for arrays of elements with keys', () => {
250 ReactTestUtils.renderIntoDocument(
251 <Component>{[<Component key="#1" />, <Component key="#2" />]}</Component>
252 );
253 });
254
255 it('does not warn for iterable elements with keys', () => {
256 const iterable = {
257 '@@iterator': function () {
258 let i = 0;
259 return {
260 next: function () {
261 const done = ++i > 2;
262 return {
263 value: done ? undefined : <Component key={'#' + i} />,
264 done: done,
265 };
266 },
267 };
268 },
269 };
270
271 ReactTestUtils.renderIntoDocument(<Component>{iterable}</Component>);
272 });
273
274 it('does not warn for numeric keys in entry iterable as a child', () => {
275 const iterable = {
276 '@@iterator': function () {
277 let i = 0;
278 return {
279 next: function () {
280 const done = ++i > 2;
281 return {value: done ? undefined : [i, <Component />], done: done};
282 },
283 };
284 },
285 };
286 iterable.entries = iterable['@@iterator'];
287
288 ReactTestUtils.renderIntoDocument(<Component>{iterable}</Component>);
289 });
290
291 it('does not warn when the element is directly as children', () => {
292 ReactTestUtils.renderIntoDocument(
293 <Component>
294 <Component />
295 <Component />
296 </Component>
297 );
298 });
299
300 it('does not warn when the child array contains non-elements', () => {
301 void (<Component>{[{}, {}]}</Component>);
302 });
303
304 it('should give context for PropType errors in nested components.', () => {
305 // In this test, we're making sure that if a proptype error is found in a
306 // component, we give a small hint as to which parent instantiated that
307 // component as per warnings about key usage in ReactElementValidator.
308 function MyComp({color}) {
309 return <div>My color is {color}</div>;
310 }
311 MyComp.propTypes = {
312 color: PropTypes.string,
313 };
314 class ParentComp extends React.Component {
315 render() {
316 return <MyComp color={123} />;
317 }
318 }
319 expect(() => ReactTestUtils.renderIntoDocument(<ParentComp />)).toErrorDev(
320 'Warning: Failed prop type: ' +
321 'Invalid prop `color` of type `number` supplied to `MyComp`, ' +
322 'expected `string`.\n' +
323 ' in MyComp (at **)\n' +
324 ' in ParentComp (at **)'
325 );
326 });
327
328 it('gives a helpful error when passing null, undefined, or boolean', () => {
329 const Undefined = undefined;
330 const Null = null;
331 const True = true;
332 const Div = 'div';
333 expect(() => void (<Undefined />)).toErrorDev(
334 'Warning: React.jsx: type is invalid -- expected a string ' +
335 '(for built-in components) or a class/function (for composite ' +
336 'components) but got: undefined. You likely forgot to export your ' +
337 "component from the file it's defined in, or you might have mixed up " +
338 'default and named imports.' +
339 (process.env.BABEL_ENV === 'development'
340 ? '\n\nCheck your code at **.'
341 : ''),
342 {withoutStack: true}
343 );
344 expect(() => void (<Null />)).toErrorDev(
345 'Warning: React.jsx: type is invalid -- expected a string ' +
346 '(for built-in components) or a class/function (for composite ' +
347 'components) but got: null.' +
348 (process.env.BABEL_ENV === 'development'
349 ? '\n\nCheck your code at **.'
350 : ''),
351 {withoutStack: true}
352 );
353 expect(() => void (<True />)).toErrorDev(
354 'Warning: React.jsx: type is invalid -- expected a string ' +
355 '(for built-in components) or a class/function (for composite ' +
356 'components) but got: boolean.' +
357 (process.env.BABEL_ENV === 'development'
358 ? '\n\nCheck your code at **.'
359 : ''),
360 {withoutStack: true}
361 );
362 // No error expected
363 void (<Div />);
364 });
365
366 it('should check default prop values', () => {
367 RequiredPropComponent.defaultProps = {prop: null};
368
369 expect(() =>
370 ReactTestUtils.renderIntoDocument(<RequiredPropComponent />)
371 ).toErrorDev(
372 'Warning: Failed prop type: The prop `prop` is marked as required in ' +
373 '`RequiredPropComponent`, but its value is `null`.\n' +
374 ' in RequiredPropComponent (at **)'
375 );
376 });
377
378 it('should warn on invalid prop types', () => {
379 // Since there is no prevalidation step for ES6 classes, there is no hook
380 // for us to issue a warning earlier than element creation when the error
381 // actually occurs. Since this step is skipped in production, we should just
382 // warn instead of throwing for this case.
383 class NullPropTypeComponent extends React.Component {
384 render() {
385 return <span>{this.props.prop}</span>;
386 }
387 }
388 NullPropTypeComponent.propTypes = {
389 prop: null,
390 };
391 expect(() =>
392 ReactTestUtils.renderIntoDocument(<NullPropTypeComponent />)
393 ).toErrorDev(
394 'NullPropTypeComponent: prop type `prop` is invalid; it must be a ' +
395 'function, usually from the `prop-types` package,'
396 );
397 });
398
399 it('should warn on invalid context types', () => {
400 class NullContextTypeComponent extends React.Component {
401 render() {
402 return <span>{this.props.prop}</span>;
403 }
404 }
405 NullContextTypeComponent.contextTypes = {
406 prop: null,
407 };
408 expect(() =>
409 ReactTestUtils.renderIntoDocument(<NullContextTypeComponent />)
410 ).toErrorDev(
411 'NullContextTypeComponent: context type `prop` is invalid; it must ' +
412 'be a function, usually from the `prop-types` package,'
413 );
414 });
415
416 it('should warn if getDefaultProps is specified on the class', () => {
417 class GetDefaultPropsComponent extends React.Component {
418 render() {
419 return <span>{this.props.prop}</span>;
420 }
421 }
422 GetDefaultPropsComponent.getDefaultProps = () => ({
423 prop: 'foo',
424 });
425 expect(() =>
426 ReactTestUtils.renderIntoDocument(<GetDefaultPropsComponent />)
427 ).toErrorDev(
428 'getDefaultProps is only used on classic React.createClass definitions.' +
429 ' Use a static property named `defaultProps` instead.',
430 {withoutStack: true}
431 );
432 });
433
434 it('should warn if component declares PropTypes instead of propTypes', () => {
435 class MisspelledPropTypesComponent extends React.Component {
436 render() {
437 return <span>{this.props.prop}</span>;
438 }
439 }
440 MisspelledPropTypesComponent.PropTypes = {
441 prop: PropTypes.string,
442 };
443 expect(() =>
444 ReactTestUtils.renderIntoDocument(
445 <MisspelledPropTypesComponent prop="hi" />
446 )
447 ).toErrorDev(
448 'Warning: Component MisspelledPropTypesComponent declared `PropTypes` ' +
449 'instead of `propTypes`. Did you misspell the property assignment?',
450 {withoutStack: true}
451 );
452 });
453
454 it('warns for fragments with illegal attributes', () => {
455 class Foo extends React.Component {
456 render() {
457 return <React.Fragment a={1}>hello</React.Fragment>;
458 }
459 }
460
461 expect(() => ReactTestUtils.renderIntoDocument(<Foo />)).toErrorDev(
462 'Invalid prop `a` supplied to `React.Fragment`. React.Fragment ' +
463 'can only have `key` and `children` props.'
464 );
465 });
466
467 it('warns for fragments with refs', () => {
468 class Foo extends React.Component {
469 render() {
470 return (
471 <React.Fragment
472 ref={bar => {
473 this.foo = bar;
474 }}>
475 hello
476 </React.Fragment>
477 );
478 }
479 }
480
481 expect(() => ReactTestUtils.renderIntoDocument(<Foo />)).toErrorDev(
482 'Invalid attribute `ref` supplied to `React.Fragment`.'
483 );
484 });
485
486 it('does not warn for fragments of multiple elements without keys', () => {
487 ReactTestUtils.renderIntoDocument(
488 <>
489 <span>1</span>
490 <span>2</span>
491 </>
492 );
493 });
494
495 it('warns for fragments of multiple elements with same key', () => {
496 expect(() =>
497 ReactTestUtils.renderIntoDocument(
498 <>
499 <span key="a">1</span>
500 <span key="a">2</span>
501 <span key="b">3</span>
502 </>
503 )
504 ).toErrorDev('Encountered two children with the same key, `a`.', {
505 withoutStack: true,
506 });
507 });
508
509 it('does not call lazy initializers eagerly', () => {
510 let didCall = false;
511 const Lazy = React.lazy(() => {
512 didCall = true;
513 return {then() {}};
514 });
515 <Lazy />;
516 expect(didCall).toBe(false);
517 });
518
519 it('supports classic refs', () => {
520 class Foo extends React.Component {
521 render() {
522 return <div className="foo" ref="inner" />;
523 }
524 }
525 const container = document.createElement('div');
526 const instance = ReactDOM.render(<Foo />, container);
527 expect(instance.refs.inner.className).toBe('foo');
528 });
529
530 it('should support refs on owned components', () => {
531 const innerObj = {};
532 const outerObj = {};
533
534 class Wrapper extends React.Component {
535 getObject = () => {
536 return this.props.object;
537 };
538
539 render() {
540 return <div>{this.props.children}</div>;
541 }
542 }
543
544 class Component extends React.Component {
545 render() {
546 const inner = <Wrapper object={innerObj} ref="inner" />;
547 const outer = (
548 <Wrapper object={outerObj} ref="outer">
549 {inner}
550 </Wrapper>
551 );
552 return outer;
553 }
554
555 componentDidMount() {
556 expect(this.refs.inner.getObject()).toEqual(innerObj);
557 expect(this.refs.outer.getObject()).toEqual(outerObj);
558 }
559 }
560
561 ReactTestUtils.renderIntoDocument(<Component />);
562 });
563
564 it('should support callback-style refs', () => {
565 const innerObj = {};
566 const outerObj = {};
567
568 class Wrapper extends React.Component {
569 getObject = () => {
570 return this.props.object;
571 };
572
573 render() {
574 return <div>{this.props.children}</div>;
575 }
576 }
577
578 let mounted = false;
579
580 class Component extends React.Component {
581 render() {
582 const inner = (
583 <Wrapper object={innerObj} ref={c => (this.innerRef = c)} />
584 );
585 const outer = (
586 <Wrapper object={outerObj} ref={c => (this.outerRef = c)}>
587 {inner}
588 </Wrapper>
589 );
590 return outer;
591 }
592
593 componentDidMount() {
594 expect(this.innerRef.getObject()).toEqual(innerObj);
595 expect(this.outerRef.getObject()).toEqual(outerObj);
596 mounted = true;
597 }
598 }
599
600 ReactTestUtils.renderIntoDocument(<Component />);
601 expect(mounted).toBe(true);
602 });
603
604 it('should support object-style refs', () => {
605 const innerObj = {};
606 const outerObj = {};
607
608 class Wrapper extends React.Component {
609 getObject = () => {
610 return this.props.object;
611 };
612
613 render() {
614 return <div>{this.props.children}</div>;
615 }
616 }
617
618 let mounted = false;
619
620 class Component extends React.Component {
621 constructor() {
622 super();
623 this.innerRef = React.createRef();
624 this.outerRef = React.createRef();
625 }
626 render() {
627 const inner = <Wrapper object={innerObj} ref={this.innerRef} />;
628 const outer = (
629 <Wrapper object={outerObj} ref={this.outerRef}>
630 {inner}
631 </Wrapper>
632 );
633 return outer;
634 }
635
636 componentDidMount() {
637 expect(this.innerRef.current.getObject()).toEqual(innerObj);
638 expect(this.outerRef.current.getObject()).toEqual(outerObj);
639 mounted = true;
640 }
641 }
642
643 ReactTestUtils.renderIntoDocument(<Component />);
644 expect(mounted).toBe(true);
645 });
646
647 it('should support new-style refs with mixed-up owners', () => {
648 class Wrapper extends React.Component {
649 getTitle = () => {
650 return this.props.title;
651 };
652
653 render() {
654 return this.props.getContent();
655 }
656 }
657
658 let mounted = false;
659
660 class Component extends React.Component {
661 getInner = () => {
662 // (With old-style refs, it's impossible to get a ref to this div
663 // because Wrapper is the current owner when this function is called.)
664 return <div className="inner" ref={c => (this.innerRef = c)} />;
665 };
666
667 render() {
668 return (
669 <Wrapper
670 title="wrapper"
671 ref={c => (this.wrapperRef = c)}
672 getContent={this.getInner}
673 />
674 );
675 }
676
677 componentDidMount() {
678 // Check .props.title to make sure we got the right elements back
679 expect(this.wrapperRef.getTitle()).toBe('wrapper');
680 expect(this.innerRef.className).toBe('inner');
681 mounted = true;
682 }
683 }
684
685 ReactTestUtils.renderIntoDocument(<Component />);
686 expect(mounted).toBe(true);
687 });
688
689 it('should warn when `key` is being accessed on composite element', () => {
690 const container = document.createElement('div');
691 class Child extends React.Component {
692 render() {
693 return <div> {this.props.key} </div>;
694 }
695 }
696 class Parent extends React.Component {
697 render() {
698 return (
699 <div>
700 <Child key="0" />
701 <Child key="1" />
702 <Child key="2" />
703 </div>
704 );
705 }
706 }
707 expect(() => ReactDOM.render(<Parent />, container)).toErrorDev(
708 'Child: `key` is not a prop. Trying to access it will result ' +
709 'in `undefined` being returned. If you need to access the same ' +
710 'value within the child component, you should pass it as a different ' +
711 'prop. (https://reactjs.org/link/special-props)'
712 );
713 });
714
715 it('should warn when `ref` is being accessed', () => {
716 const container = document.createElement('div');
717 class Child extends React.Component {
718 render() {
719 return <div> {this.props.ref} </div>;
720 }
721 }
722 class Parent extends React.Component {
723 render() {
724 return (
725 <div>
726 <Child ref="childElement" />
727 </div>
728 );
729 }
730 }
731 expect(() => ReactDOM.render(<Parent />, container)).toErrorDev(
732 'Child: `ref` is not a prop. Trying to access it will result ' +
733 'in `undefined` being returned. If you need to access the same ' +
734 'value within the child component, you should pass it as a different ' +
735 'prop. (https://reactjs.org/link/special-props)'
736 );
737 });
738
739 it('should warn when owner and self are different for string refs', () => {
740 class ClassWithRenderProp extends React.Component {
741 render() {
742 return this.props.children();
743 }
744 }
745
746 class ClassParent extends React.Component {
747 render() {
748 return (
749 <ClassWithRenderProp>{() => <div ref="myRef" />}</ClassWithRenderProp>
750 );
751 }
752 }
753
754 const container = document.createElement('div');
755 if (process.env.BABEL_ENV === 'development') {
756 expect(() => ReactDOM.render(<ClassParent />, container)).toErrorDev([
757 'Warning: Component "ClassWithRenderProp" contains the string ref "myRef". ' +
758 'Support for string refs will be removed in a future major release. ' +
759 'This case cannot be automatically converted to an arrow function. ' +
760 'We ask you to manually fix this case by using useRef() or createRef() instead. ' +
761 'Learn more about using refs safely here: ' +
762 'https://reactjs.org/link/strict-mode-string-ref',
763 ]);
764 } else {
765 ReactDOM.render(<ClassParent />, container);
766 }
767 });