main
js 636 lines 19.1 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 PropTypes;
13 let React;
14 let ReactDOM;
15 let ReactDOMClient;
16 let assertConsoleErrorDev;
17 let assertConsoleWarnDev;
18
19 describe('ReactES6Class', () => {
20 let container;
21 let root;
22 const freeze = function (expectation) {
23 Object.freeze(expectation);
24 return expectation;
25 };
26 let Inner;
27 let attachedListener = null;
28 let renderedName = null;
29
30 beforeEach(() => {
31 PropTypes = require('prop-types');
32 React = require('react');
33 ReactDOM = require('react-dom');
34 ReactDOMClient = require('react-dom/client');
35 ({
36 assertConsoleErrorDev,
37 assertConsoleWarnDev,
38 } = require('internal-test-utils'));
39 container = document.createElement('div');
40 root = ReactDOMClient.createRoot(container);
41 attachedListener = null;
42 renderedName = null;
43 Inner = class extends React.Component {
44 getName() {
45 return this.props.name;
46 }
47 render() {
48 attachedListener = this.props.onClick;
49 renderedName = this.props.name;
50 return <div className={this.props.name} />;
51 }
52 };
53 });
54
55 function runTest(element, expectedTag, expectedClassName) {
56 ReactDOM.flushSync(() => root.render(element));
57 expect(container.firstChild).not.toBeNull();
58 expect(container.firstChild.tagName).toBe(expectedTag);
59 expect(container.firstChild.className).toBe(expectedClassName);
60 }
61
62 it('preserves the name of the class for use in error messages', () => {
63 class Foo extends React.Component {}
64 expect(Foo.name).toBe('Foo');
65 });
66
67 it('throws if no render function is defined', () => {
68 class Foo extends React.Component {}
69 const caughtErrors = [];
70 function errorHandler(event) {
71 event.preventDefault();
72 caughtErrors.push(event.error);
73 }
74 window.addEventListener('error', errorHandler);
75 try {
76 ReactDOM.flushSync(() => root.render(<Foo />));
77 assertConsoleErrorDev([
78 // A failed component renders twice in DEV in concurrent mode
79 'No `render` method found on the Foo instance: ' +
80 'you may have forgotten to define `render`.\n' +
81 ' in Foo (at **)',
82 'No `render` method found on the Foo instance: ' +
83 'you may have forgotten to define `render`.\n' +
84 ' in Foo (at **)',
85 ]);
86 } finally {
87 window.removeEventListener('error', errorHandler);
88 }
89 expect(caughtErrors).toEqual([
90 expect.objectContaining({
91 message: expect.stringContaining('is not a function'),
92 }),
93 ]);
94 });
95
96 it('renders a simple stateless component with prop', () => {
97 class Foo extends React.Component {
98 render() {
99 return <Inner name={this.props.bar} />;
100 }
101 }
102 runTest(<Foo bar="foo" />, 'DIV', 'foo');
103 runTest(<Foo bar="bar" />, 'DIV', 'bar');
104 });
105
106 it('renders based on state using initial values in this.props', () => {
107 class Foo extends React.Component {
108 constructor(props) {
109 super(props);
110 this.state = {bar: this.props.initialValue};
111 }
112 render() {
113 return <span className={this.state.bar} />;
114 }
115 }
116 runTest(<Foo initialValue="foo" />, 'SPAN', 'foo');
117 });
118
119 it('renders based on state using props in the constructor', () => {
120 class Foo extends React.Component {
121 constructor(props) {
122 super(props);
123 this.state = {bar: props.initialValue};
124 }
125 changeState() {
126 this.setState({bar: 'bar'});
127 }
128 render() {
129 if (this.state.bar === 'foo') {
130 return <div className="foo" />;
131 }
132 return <span className={this.state.bar} />;
133 }
134 }
135 const ref = React.createRef();
136 runTest(<Foo initialValue="foo" ref={ref} />, 'DIV', 'foo');
137 ReactDOM.flushSync(() => ref.current.changeState());
138 runTest(<Foo />, 'SPAN', 'bar');
139 });
140
141 it('sets initial state with value returned by static getDerivedStateFromProps', () => {
142 class Foo extends React.Component {
143 state = {};
144 static getDerivedStateFromProps(nextProps, prevState) {
145 return {
146 foo: nextProps.foo,
147 bar: 'bar',
148 };
149 }
150 render() {
151 return <div className={`${this.state.foo} ${this.state.bar}`} />;
152 }
153 }
154 runTest(<Foo foo="foo" />, 'DIV', 'foo bar');
155 });
156
157 it('warns if getDerivedStateFromProps is not static', () => {
158 class Foo extends React.Component {
159 getDerivedStateFromProps() {
160 return {};
161 }
162 render() {
163 return <div />;
164 }
165 }
166 ReactDOM.flushSync(() => root.render(<Foo foo="foo" />));
167 assertConsoleErrorDev([
168 'Foo: getDerivedStateFromProps() is defined as an instance method ' +
169 'and will be ignored. Instead, declare it as a static method.\n' +
170 ' in Foo (at **)',
171 ]);
172 });
173
174 it('warns if getDerivedStateFromError is not static', () => {
175 class Foo extends React.Component {
176 getDerivedStateFromError() {
177 return {};
178 }
179 render() {
180 return <div />;
181 }
182 }
183 ReactDOM.flushSync(() => root.render(<Foo foo="foo" />));
184 assertConsoleErrorDev([
185 'Foo: getDerivedStateFromError() is defined as an instance method ' +
186 'and will be ignored. Instead, declare it as a static method.\n' +
187 ' in Foo (at **)',
188 ]);
189 });
190
191 it('warns if getSnapshotBeforeUpdate is static', () => {
192 class Foo extends React.Component {
193 static getSnapshotBeforeUpdate() {}
194 render() {
195 return <div />;
196 }
197 }
198 ReactDOM.flushSync(() => root.render(<Foo foo="foo" />));
199 assertConsoleErrorDev([
200 'Foo: getSnapshotBeforeUpdate() is defined as a static method ' +
201 'and will be ignored. Instead, declare it as an instance method.\n' +
202 ' in Foo (at **)',
203 ]);
204 });
205
206 it('warns if state not initialized before static getDerivedStateFromProps', () => {
207 class Foo extends React.Component {
208 static getDerivedStateFromProps(nextProps, prevState) {
209 return {
210 foo: nextProps.foo,
211 bar: 'bar',
212 };
213 }
214 render() {
215 return <div className={`${this.state.foo} ${this.state.bar}`} />;
216 }
217 }
218 ReactDOM.flushSync(() => root.render(<Foo foo="foo" />));
219 assertConsoleErrorDev([
220 '`Foo` uses `getDerivedStateFromProps` but its initial state is ' +
221 'undefined. This is not recommended. Instead, define the initial state by ' +
222 'assigning an object to `this.state` in the constructor of `Foo`. ' +
223 'This ensures that `getDerivedStateFromProps` arguments have a consistent shape.\n' +
224 ' in Foo (at **)',
225 ]);
226 });
227
228 it('updates initial state with values returned by static getDerivedStateFromProps', () => {
229 class Foo extends React.Component {
230 state = {
231 foo: 'foo',
232 bar: 'bar',
233 };
234 static getDerivedStateFromProps(nextProps, prevState) {
235 return {
236 foo: `not-${prevState.foo}`,
237 };
238 }
239 render() {
240 return <div className={`${this.state.foo} ${this.state.bar}`} />;
241 }
242 }
243 runTest(<Foo />, 'DIV', 'not-foo bar');
244 });
245
246 it('renders updated state with values returned by static getDerivedStateFromProps', () => {
247 class Foo extends React.Component {
248 state = {
249 value: 'initial',
250 };
251 static getDerivedStateFromProps(nextProps, prevState) {
252 if (nextProps.update) {
253 return {
254 value: 'updated',
255 };
256 }
257 return null;
258 }
259 render() {
260 return <div className={this.state.value} />;
261 }
262 }
263 runTest(<Foo update={false} />, 'DIV', 'initial');
264 runTest(<Foo update={true} />, 'DIV', 'updated');
265 });
266
267 if (!require('shared/ReactFeatureFlags').disableLegacyContext) {
268 it('renders based on context in the constructor', () => {
269 class Foo extends React.Component {
270 constructor(props, context) {
271 super(props, context);
272 this.state = {tag: context.tag, className: this.context.className};
273 }
274
275 render() {
276 const Tag = this.state.tag;
277 return <Tag className={this.state.className} />;
278 }
279 }
280
281 Foo.contextTypes = {
282 tag: PropTypes.string,
283 className: PropTypes.string,
284 };
285
286 class Outer extends React.Component {
287 getChildContext() {
288 return {tag: 'span', className: 'foo'};
289 }
290
291 render() {
292 return <Foo />;
293 }
294 }
295
296 Outer.childContextTypes = {
297 tag: PropTypes.string,
298 className: PropTypes.string,
299 };
300 runTest(<Outer />, 'SPAN', 'foo');
301
302 assertConsoleErrorDev([
303 'Outer uses the legacy childContextTypes API which will soon be removed. ' +
304 'Use React.createContext() instead. (https://react.dev/link/legacy-context)\n' +
305 ' in Outer (at **)',
306 'Foo uses the legacy contextTypes API which will soon be removed. ' +
307 'Use React.createContext() with static contextType instead. (https://react.dev/link/legacy-context)\n' +
308 ' in Outer (at **)',
309 ]);
310 });
311 }
312
313 it('renders only once when setting state in componentWillMount', () => {
314 let renderCount = 0;
315 class Foo extends React.Component {
316 constructor(props) {
317 super(props);
318 this.state = {bar: props.initialValue};
319 }
320 UNSAFE_componentWillMount() {
321 this.setState({bar: 'bar'});
322 }
323 render() {
324 renderCount++;
325 return <span className={this.state.bar} />;
326 }
327 }
328 runTest(<Foo initialValue="foo" />, 'SPAN', 'bar');
329 expect(renderCount).toBe(1);
330 });
331
332 it('should warn with non-object in the initial state property', () => {
333 [['an array'], 'a string', 1234].forEach(function (state) {
334 class Foo extends React.Component {
335 constructor() {
336 super();
337 this.state = state;
338 }
339 render() {
340 return <span />;
341 }
342 }
343 runTest(<Foo />, 'SPAN', '');
344 assertConsoleErrorDev([
345 'Foo.state: must be set to an object or null\n in Foo (at **)',
346 ]);
347 });
348 });
349
350 it('should render with null in the initial state property', () => {
351 class Foo extends React.Component {
352 constructor() {
353 super();
354 this.state = null;
355 }
356 render() {
357 return <span />;
358 }
359 }
360 runTest(<Foo />, 'SPAN', '');
361 });
362
363 it('setState through an event handler', () => {
364 class Foo extends React.Component {
365 constructor(props) {
366 super(props);
367 this.state = {bar: props.initialValue};
368 }
369 handleClick() {
370 this.setState({bar: 'bar'});
371 }
372 render() {
373 return (
374 <Inner name={this.state.bar} onClick={this.handleClick.bind(this)} />
375 );
376 }
377 }
378 runTest(<Foo initialValue="foo" />, 'DIV', 'foo');
379
380 ReactDOM.flushSync(() => attachedListener());
381 expect(renderedName).toBe('bar');
382 });
383
384 it('should not implicitly bind event handlers', () => {
385 class Foo extends React.Component {
386 constructor(props) {
387 super(props);
388 this.state = {bar: props.initialValue};
389 }
390 handleClick() {
391 this.setState({bar: 'bar'});
392 }
393 render() {
394 return <Inner name={this.state.bar} onClick={this.handleClick} />;
395 }
396 }
397 runTest(<Foo initialValue="foo" />, 'DIV', 'foo');
398 expect(attachedListener).toThrow();
399 });
400
401 it('renders using forceUpdate even when there is no state', () => {
402 class Foo extends React.Component {
403 constructor(props) {
404 super(props);
405 this.mutativeValue = props.initialValue;
406 }
407 handleClick() {
408 this.mutativeValue = 'bar';
409 this.forceUpdate();
410 }
411 render() {
412 return (
413 <Inner
414 name={this.mutativeValue}
415 onClick={this.handleClick.bind(this)}
416 />
417 );
418 }
419 }
420 runTest(<Foo initialValue="foo" />, 'DIV', 'foo');
421 ReactDOM.flushSync(() => attachedListener());
422 expect(renderedName).toBe('bar');
423 });
424
425 it('will call all the normal life cycle methods', () => {
426 let lifeCycles = [];
427 class Foo extends React.Component {
428 constructor() {
429 super();
430 this.state = {};
431 }
432 UNSAFE_componentWillMount() {
433 lifeCycles.push('will-mount');
434 }
435 componentDidMount() {
436 lifeCycles.push('did-mount');
437 }
438 UNSAFE_componentWillReceiveProps(nextProps) {
439 lifeCycles.push('receive-props', nextProps);
440 }
441 shouldComponentUpdate(nextProps, nextState) {
442 lifeCycles.push('should-update', nextProps, nextState);
443 return true;
444 }
445 UNSAFE_componentWillUpdate(nextProps, nextState) {
446 lifeCycles.push('will-update', nextProps, nextState);
447 }
448 componentDidUpdate(prevProps, prevState) {
449 lifeCycles.push('did-update', prevProps, prevState);
450 }
451 componentWillUnmount() {
452 lifeCycles.push('will-unmount');
453 }
454 render() {
455 return <span className={this.props.value} />;
456 }
457 }
458 runTest(<Foo value="foo" />, 'SPAN', 'foo');
459 expect(lifeCycles).toEqual(['will-mount', 'did-mount']);
460 lifeCycles = []; // reset
461 runTest(<Foo value="bar" />, 'SPAN', 'bar');
462 // prettier-ignore
463 expect(lifeCycles).toEqual([
464 'receive-props', freeze({value: 'bar'}),
465 'should-update', freeze({value: 'bar'}), {},
466 'will-update', freeze({value: 'bar'}), {},
467 'did-update', freeze({value: 'foo'}), {},
468 ]);
469 lifeCycles = []; // reset
470 ReactDOM.flushSync(() => root.unmount());
471 expect(lifeCycles).toEqual(['will-unmount']);
472 });
473
474 if (!require('shared/ReactFeatureFlags').disableLegacyContext) {
475 it('warns when classic properties are defined on the instance, but does not invoke them.', () => {
476 let getDefaultPropsWasCalled = false;
477 let getInitialStateWasCalled = false;
478 class Foo extends React.Component {
479 constructor() {
480 super();
481 this.contextTypes = {};
482 this.contextType = {};
483 }
484 getInitialState() {
485 getInitialStateWasCalled = true;
486 return {};
487 }
488 getDefaultProps() {
489 getDefaultPropsWasCalled = true;
490 return {};
491 }
492 render() {
493 return <span className="foo" />;
494 }
495 }
496
497 runTest(<Foo />, 'SPAN', 'foo');
498 assertConsoleErrorDev([
499 'getInitialState was defined on Foo, a plain JavaScript class. ' +
500 'This is only supported for classes created using React.createClass. ' +
501 'Did you mean to define a state property instead?\n' +
502 ' in Foo (at **)',
503 'getDefaultProps was defined on Foo, a plain JavaScript class. ' +
504 'This is only supported for classes created using React.createClass. ' +
505 'Use a static property to define defaultProps instead.\n' +
506 ' in Foo (at **)',
507 'contextType was defined as an instance property on Foo. ' +
508 'Use a static property to define contextType instead.\n' +
509 ' in Foo (at **)',
510 'contextTypes was defined as an instance property on Foo. ' +
511 'Use a static property to define contextTypes instead.\n' +
512 ' in Foo (at **)',
513 ]);
514 expect(getInitialStateWasCalled).toBe(false);
515 expect(getDefaultPropsWasCalled).toBe(false);
516 });
517 }
518
519 it('does not warn about getInitialState() on class components if state is also defined.', () => {
520 class Foo extends React.Component {
521 state = this.getInitialState();
522 getInitialState() {
523 return {};
524 }
525 render() {
526 return <span className="foo" />;
527 }
528 }
529 runTest(<Foo />, 'SPAN', 'foo');
530 });
531
532 it('should warn when misspelling shouldComponentUpdate', () => {
533 class NamedComponent extends React.Component {
534 componentShouldUpdate() {
535 return false;
536 }
537 render() {
538 return <span className="foo" />;
539 }
540 }
541
542 runTest(<NamedComponent />, 'SPAN', 'foo');
543 assertConsoleErrorDev([
544 'NamedComponent has a method called componentShouldUpdate(). Did you ' +
545 'mean shouldComponentUpdate()? The name is phrased as a question ' +
546 'because the function is expected to return a value.\n' +
547 ' in NamedComponent (at **)',
548 ]);
549 });
550
551 it('should warn when misspelling componentWillReceiveProps', () => {
552 class NamedComponent extends React.Component {
553 componentWillRecieveProps() {
554 return false;
555 }
556 render() {
557 return <span className="foo" />;
558 }
559 }
560
561 runTest(<NamedComponent />, 'SPAN', 'foo');
562 assertConsoleErrorDev([
563 'NamedComponent has a method called componentWillRecieveProps(). Did ' +
564 'you mean componentWillReceiveProps()?\n' +
565 ' in NamedComponent (at **)',
566 ]);
567 });
568
569 it('should warn when misspelling UNSAFE_componentWillReceiveProps', () => {
570 class NamedComponent extends React.Component {
571 UNSAFE_componentWillRecieveProps() {
572 return false;
573 }
574 render() {
575 return <span className="foo" />;
576 }
577 }
578
579 runTest(<NamedComponent />, 'SPAN', 'foo');
580 assertConsoleErrorDev([
581 'NamedComponent has a method called UNSAFE_componentWillRecieveProps(). ' +
582 'Did you mean UNSAFE_componentWillReceiveProps()?\n' +
583 ' in NamedComponent (at **)',
584 ]);
585 });
586
587 it('should throw AND warn when trying to access classic APIs', () => {
588 const ref = React.createRef();
589 runTest(<Inner name="foo" ref={ref} />, 'DIV', 'foo');
590
591 expect(() => ref.current.replaceState({})).toThrow();
592 assertConsoleWarnDev([
593 'replaceState(...) is deprecated in plain JavaScript React classes. ' +
594 'Refactor your code to use setState instead (see https://github.com/facebook/react/issues/3236).',
595 ]);
596 expect(() => ref.current.isMounted()).toThrow();
597 assertConsoleWarnDev([
598 'isMounted(...) is deprecated in plain JavaScript React classes. ' +
599 'Instead, make sure to clean up subscriptions and pending requests in ' +
600 'componentWillUnmount to prevent memory leaks.',
601 ]);
602 });
603
604 if (!require('shared/ReactFeatureFlags').disableLegacyContext) {
605 it('supports this.context passed via getChildContext', () => {
606 class Bar extends React.Component {
607 render() {
608 return <div className={this.context.bar} />;
609 }
610 }
611
612 Bar.contextTypes = {bar: PropTypes.string};
613
614 class Foo extends React.Component {
615 getChildContext() {
616 return {bar: 'bar-through-context'};
617 }
618
619 render() {
620 return <Bar />;
621 }
622 }
623
624 Foo.childContextTypes = {bar: PropTypes.string};
625 runTest(<Foo />, 'DIV', 'bar-through-context');
626 assertConsoleErrorDev([
627 'Foo uses the legacy childContextTypes API which will soon be removed. ' +
628 'Use React.createContext() instead. (https://react.dev/link/legacy-context)\n' +
629 ' in Foo (at **)',
630 'Bar uses the legacy contextTypes API which will soon be removed. ' +
631 'Use React.createContext() with static contextType instead. (https://react.dev/link/legacy-context)\n' +
632 ' in Foo (at **)',
633 ]);
634 });
635 }
636 });