main
js 768 lines 21 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 = React.PropTypes;
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 withoutStack: true,
226 });
227 });
228
229 it('warns for keys for arrays of elements with owner info', () => {
230 class InnerComponent extends React.Component {
231 render() {
232 return <Component>{this.props.childSet}</Component>;
233 }
234 }
235
236 class ComponentWrapper extends React.Component {
237 render() {
238 return <InnerComponent childSet={[<Component />, <Component />]} />;
239 }
240 }
241
242 expect(() =>
243 ReactTestUtils.renderIntoDocument(<ComponentWrapper />)
244 ).toErrorDev(
245 'Each child in a list should have a unique "key" prop.' +
246 '\n\nCheck the render method of `InnerComponent`. ' +
247 'It was passed a child from ComponentWrapper. ',
248 {withoutStack: true}
249 );
250 });
251
252 it('does not warn for arrays of elements with keys', () => {
253 ReactTestUtils.renderIntoDocument(
254 <Component>{[<Component key="#1" />, <Component key="#2" />]}</Component>
255 );
256 });
257
258 it('does not warn for iterable elements with keys', () => {
259 const iterable = {
260 '@@iterator': function () {
261 let i = 0;
262 return {
263 next: function () {
264 const done = ++i > 2;
265 return {
266 value: done ? undefined : <Component key={'#' + i} />,
267 done: done,
268 };
269 },
270 };
271 },
272 };
273
274 ReactTestUtils.renderIntoDocument(<Component>{iterable}</Component>);
275 });
276
277 it('does not warn for numeric keys in entry iterable as a child', () => {
278 const iterable = {
279 '@@iterator': function () {
280 let i = 0;
281 return {
282 next: function () {
283 const done = ++i > 2;
284 return {value: done ? undefined : [i, <Component />], done: done};
285 },
286 };
287 },
288 };
289 iterable.entries = iterable['@@iterator'];
290
291 ReactTestUtils.renderIntoDocument(<Component>{iterable}</Component>);
292 });
293
294 it('does not warn when the element is directly as children', () => {
295 ReactTestUtils.renderIntoDocument(
296 <Component>
297 <Component />
298 <Component />
299 </Component>
300 );
301 });
302
303 it('does not warn when the child array contains non-elements', () => {
304 void (<Component>{[{}, {}]}</Component>);
305 });
306
307 it('should give context for PropType errors in nested components.', () => {
308 // In this test, we're making sure that if a proptype error is found in a
309 // component, we give a small hint as to which parent instantiated that
310 // component as per warnings about key usage in ReactElementValidator.
311 function MyComp({color}) {
312 return <div>My color is {color}</div>;
313 }
314 MyComp.propTypes = {
315 color: PropTypes.string,
316 };
317 class ParentComp extends React.Component {
318 render() {
319 return <MyComp color={123} />;
320 }
321 }
322 expect(() => ReactTestUtils.renderIntoDocument(<ParentComp />)).toErrorDev(
323 'Warning: Failed prop type: ' +
324 'Invalid prop `color` of type `number` supplied to `MyComp`, ' +
325 'expected `string`.',
326 {withoutStack: true}
327 );
328 });
329
330 it('gives a helpful error when passing null, undefined, or boolean', () => {
331 const Undefined = undefined;
332 const Null = null;
333 const True = true;
334 const Div = 'div';
335 expect(() => void (<Undefined />)).toErrorDev(
336 'Warning: React.jsx: type is invalid -- expected a string ' +
337 '(for built-in components) or a class/function (for composite ' +
338 'components) but got: undefined. You likely forgot to export your ' +
339 "component from the file it's defined in, or you might have mixed up " +
340 'default and named imports.' +
341 (process.env.BABEL_ENV === 'development'
342 ? '\n\nCheck your code at **.'
343 : ''),
344 {withoutStack: true}
345 );
346 expect(() => void (<Null />)).toErrorDev(
347 'Warning: React.jsx: type is invalid -- expected a string ' +
348 '(for built-in components) or a class/function (for composite ' +
349 'components) but got: null.' +
350 (process.env.BABEL_ENV === 'development'
351 ? '\n\nCheck your code at **.'
352 : ''),
353 {withoutStack: true}
354 );
355 expect(() => void (<True />)).toErrorDev(
356 'Warning: React.jsx: type is invalid -- expected a string ' +
357 '(for built-in components) or a class/function (for composite ' +
358 'components) but got: boolean.' +
359 (process.env.BABEL_ENV === 'development'
360 ? '\n\nCheck your code at **.'
361 : ''),
362 {withoutStack: true}
363 );
364 // No error expected
365 void (<Div />);
366 });
367
368 it('should check default prop values', () => {
369 RequiredPropComponent.defaultProps = {prop: null};
370
371 expect(() =>
372 ReactTestUtils.renderIntoDocument(<RequiredPropComponent />)
373 ).toErrorDev(
374 'Warning: Failed prop type: Required prop `prop` was not specified in ' +
375 '`RequiredPropComponent`.',
376 {withoutStack: true}
377 );
378 });
379
380 it('should warn on invalid prop types', () => {
381 // Since there is no prevalidation step for ES6 classes, there is no hook
382 // for us to issue a warning earlier than element creation when the error
383 // actually occurs. Since this step is skipped in production, we should just
384 // warn instead of throwing for this case.
385 class NullPropTypeComponent extends React.Component {
386 render() {
387 return <span>{this.props.prop}</span>;
388 }
389 }
390 NullPropTypeComponent.propTypes = {
391 prop: null,
392 };
393 expect(() =>
394 ReactTestUtils.renderIntoDocument(<NullPropTypeComponent />)
395 ).toErrorDev(
396 'NullPropTypeComponent: prop type `prop` is invalid; it must be a ' +
397 'function, usually from the `prop-types` package,',
398 {withoutStack: true}
399 );
400 });
401
402 xit('should warn on invalid context types', () => {
403 class NullContextTypeComponent extends React.Component {
404 render() {
405 return <span>{this.props.prop}</span>;
406 }
407 }
408 NullContextTypeComponent.contextTypes = {
409 prop: null,
410 };
411 expect(() =>
412 ReactTestUtils.renderIntoDocument(<NullContextTypeComponent />)
413 ).toErrorDev(
414 'NullContextTypeComponent: type `prop` is invalid; it must ' +
415 'be a function, usually from the `prop-types` package,'
416 );
417 });
418
419 it('should warn if getDefaultProps is specified on the class', () => {
420 class GetDefaultPropsComponent extends React.Component {
421 render() {
422 return <span>{this.props.prop}</span>;
423 }
424 }
425 GetDefaultPropsComponent.getDefaultProps = () => ({
426 prop: 'foo',
427 });
428 expect(() =>
429 ReactTestUtils.renderIntoDocument(<GetDefaultPropsComponent />)
430 ).toErrorDev(
431 'getDefaultProps is only used on classic React.createClass definitions.' +
432 ' Use a static property named `defaultProps` instead.',
433 {withoutStack: true}
434 );
435 });
436
437 it('should warn if component declares PropTypes instead of propTypes', () => {
438 class MisspelledPropTypesComponent extends React.Component {
439 render() {
440 return <span>{this.props.prop}</span>;
441 }
442 }
443 MisspelledPropTypesComponent.PropTypes = {
444 prop: PropTypes.string,
445 };
446 expect(() =>
447 ReactTestUtils.renderIntoDocument(
448 <MisspelledPropTypesComponent prop="hi" />
449 )
450 ).toErrorDev(
451 'Warning: Component MisspelledPropTypesComponent declared `PropTypes` ' +
452 'instead of `propTypes`. Did you misspell the property assignment?',
453 {withoutStack: true}
454 );
455 });
456
457 // Not supported.
458 xit('warns for fragments with illegal attributes', () => {
459 class Foo extends React.Component {
460 render() {
461 return <React.Fragment a={1}>hello</React.Fragment>;
462 }
463 }
464
465 expect(() => ReactTestUtils.renderIntoDocument(<Foo />)).toErrorDev(
466 'Invalid prop `a` supplied to `React.Fragment`. React.Fragment ' +
467 'can only have `key` and `children` props.'
468 );
469 });
470
471 // Not supported.
472 xit('warns for fragments with refs', () => {
473 class Foo extends React.Component {
474 render() {
475 return (
476 <React.Fragment
477 ref={bar => {
478 this.foo = bar;
479 }}>
480 hello
481 </React.Fragment>
482 );
483 }
484 }
485
486 expect(() => ReactTestUtils.renderIntoDocument(<Foo />)).toErrorDev(
487 'Invalid attribute `ref` supplied to `React.Fragment`.'
488 );
489 });
490
491 // Not supported.
492 xit('does not warn for fragments of multiple elements without keys', () => {
493 ReactTestUtils.renderIntoDocument(
494 <>
495 <span>1</span>
496 <span>2</span>
497 </>
498 );
499 });
500
501 // Not supported.
502 xit('warns for fragments of multiple elements with same key', () => {
503 expect(() =>
504 ReactTestUtils.renderIntoDocument(
505 <>
506 <span key="a">1</span>
507 <span key="a">2</span>
508 <span key="b">3</span>
509 </>
510 )
511 ).toErrorDev('Encountered two children with the same key, `a`.', {
512 withoutStack: true,
513 });
514 });
515
516 // Not supported.
517 xit('does not call lazy initializers eagerly', () => {
518 let didCall = false;
519 const Lazy = React.lazy(() => {
520 didCall = true;
521 return {then() {}};
522 });
523 <Lazy />;
524 expect(didCall).toBe(false);
525 });
526
527 it('supports classic refs', () => {
528 class Foo extends React.Component {
529 render() {
530 return <div className="foo" ref="inner" />;
531 }
532 }
533 const container = document.createElement('div');
534 const instance = ReactDOM.render(<Foo />, container);
535 expect(instance.refs.inner.className).toBe('foo');
536 });
537
538 it('should support refs on owned components', () => {
539 const innerObj = {};
540 const outerObj = {};
541
542 class Wrapper extends React.Component {
543 getObject = () => {
544 return this.props.object;
545 };
546
547 render() {
548 return <div>{this.props.children}</div>;
549 }
550 }
551
552 class Component extends React.Component {
553 render() {
554 const inner = <Wrapper object={innerObj} ref="inner" />;
555 const outer = (
556 <Wrapper object={outerObj} ref="outer">
557 {inner}
558 </Wrapper>
559 );
560 return outer;
561 }
562
563 componentDidMount() {
564 expect(this.refs.inner.getObject()).toEqual(innerObj);
565 expect(this.refs.outer.getObject()).toEqual(outerObj);
566 }
567 }
568
569 ReactTestUtils.renderIntoDocument(<Component />);
570 });
571
572 it('should support callback-style refs', () => {
573 const innerObj = {};
574 const outerObj = {};
575
576 class Wrapper extends React.Component {
577 getObject = () => {
578 return this.props.object;
579 };
580
581 render() {
582 return <div>{this.props.children}</div>;
583 }
584 }
585
586 let mounted = false;
587
588 class Component extends React.Component {
589 render() {
590 const inner = (
591 <Wrapper object={innerObj} ref={c => (this.innerRef = c)} />
592 );
593 const outer = (
594 <Wrapper object={outerObj} ref={c => (this.outerRef = c)}>
595 {inner}
596 </Wrapper>
597 );
598 return outer;
599 }
600
601 componentDidMount() {
602 expect(this.innerRef.getObject()).toEqual(innerObj);
603 expect(this.outerRef.getObject()).toEqual(outerObj);
604 mounted = true;
605 }
606 }
607
608 ReactTestUtils.renderIntoDocument(<Component />);
609 expect(mounted).toBe(true);
610 });
611
612 // Not supported.
613 xit('should support object-style refs', () => {
614 const innerObj = {};
615 const outerObj = {};
616
617 class Wrapper extends React.Component {
618 getObject = () => {
619 return this.props.object;
620 };
621
622 render() {
623 return <div>{this.props.children}</div>;
624 }
625 }
626
627 let mounted = false;
628
629 class Component extends React.Component {
630 constructor() {
631 super();
632 this.innerRef = React.createRef();
633 this.outerRef = React.createRef();
634 }
635 render() {
636 const inner = <Wrapper object={innerObj} ref={this.innerRef} />;
637 const outer = (
638 <Wrapper object={outerObj} ref={this.outerRef}>
639 {inner}
640 </Wrapper>
641 );
642 return outer;
643 }
644
645 componentDidMount() {
646 expect(this.innerRef.current.getObject()).toEqual(innerObj);
647 expect(this.outerRef.current.getObject()).toEqual(outerObj);
648 mounted = true;
649 }
650 }
651
652 ReactTestUtils.renderIntoDocument(<Component />);
653 expect(mounted).toBe(true);
654 });
655
656 it('should support new-style refs with mixed-up owners', () => {
657 class Wrapper extends React.Component {
658 getTitle = () => {
659 return this.props.title;
660 };
661
662 render() {
663 return this.props.getContent();
664 }
665 }
666
667 let mounted = false;
668
669 class Component extends React.Component {
670 getInner = () => {
671 // (With old-style refs, it's impossible to get a ref to this div
672 // because Wrapper is the current owner when this function is called.)
673 return <div className="inner" ref={c => (this.innerRef = c)} />;
674 };
675
676 render() {
677 return (
678 <Wrapper
679 title="wrapper"
680 ref={c => (this.wrapperRef = c)}
681 getContent={this.getInner}
682 />
683 );
684 }
685
686 componentDidMount() {
687 // Check .props.title to make sure we got the right elements back
688 expect(this.wrapperRef.getTitle()).toBe('wrapper');
689 expect(this.innerRef.className).toBe('inner');
690 mounted = true;
691 }
692 }
693
694 ReactTestUtils.renderIntoDocument(<Component />);
695 expect(mounted).toBe(true);
696 });
697
698 it('should warn when `key` is being accessed on composite element', () => {
699 const container = document.createElement('div');
700 class Child extends React.Component {
701 render() {
702 return <div> {this.props.key} </div>;
703 }
704 }
705 class Parent extends React.Component {
706 render() {
707 return (
708 <div>
709 <Child key="0" />
710 <Child key="1" />
711 <Child key="2" />
712 </div>
713 );
714 }
715 }
716 expect(() => ReactDOM.render(<Parent />, container)).toErrorDev(
717 'Child: `key` is not a prop. Trying to access it will result ' +
718 'in `undefined` being returned. If you need to access the same ' +
719 'value within the child component, you should pass it as a different ' +
720 'prop. (https://reactjs.org/link/special-props)',
721 {withoutStack: true}
722 );
723 });
724
725 it('should warn when `ref` is being accessed', () => {
726 const container = document.createElement('div');
727 class Child extends React.Component {
728 render() {
729 return <div> {this.props.ref} </div>;
730 }
731 }
732 class Parent extends React.Component {
733 render() {
734 return (
735 <div>
736 <Child ref="childElement" />
737 </div>
738 );
739 }
740 }
741 expect(() => ReactDOM.render(<Parent />, container)).toErrorDev(
742 'Child: `ref` is not a prop. Trying to access it will result ' +
743 'in `undefined` being returned. If you need to access the same ' +
744 'value within the child component, you should pass it as a different ' +
745 'prop. (https://reactjs.org/link/special-props)',
746 {withoutStack: true}
747 );
748 });
749
750 // Note: no warning before 16.
751 it('should NOT warn when owner and self are different for string refs', () => {
752 class ClassWithRenderProp extends React.Component {
753 render() {
754 return this.props.children();
755 }
756 }
757
758 class ClassParent extends React.Component {
759 render() {
760 return (
761 <ClassWithRenderProp>{() => <div ref="myRef" />}</ClassWithRenderProp>
762 );
763 }
764 }
765
766 const container = document.createElement('div');
767 ReactDOM.render(<ClassParent />, container);
768 });