main
js 1,074 lines 31 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 * @jest-environment node
9 */
10
11 'use strict';
12
13 let React;
14 let ReactDOMServer;
15 let PropTypes;
16 let ReactSharedInternals;
17 let assertConsoleErrorDev;
18
19 describe('ReactDOMServer', () => {
20 beforeEach(() => {
21 jest.resetModules();
22 React = require('react');
23 PropTypes = require('prop-types');
24 ReactDOMServer = require('react-dom/server');
25 assertConsoleErrorDev =
26 require('internal-test-utils').assertConsoleErrorDev;
27 ReactSharedInternals =
28 React.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE;
29 });
30
31 describe('renderToString', () => {
32 it('should generate simple markup', () => {
33 const response = ReactDOMServer.renderToString(<span>hello world</span>);
34 expect(response).toMatch(new RegExp('<span' + '>hello world</span>'));
35 });
36
37 it('should generate simple markup for self-closing tags', () => {
38 const response = ReactDOMServer.renderToString(<img />);
39 expect(response).toMatch(new RegExp('<img' + '/>'));
40 });
41
42 it('should generate comment markup for component returns null', () => {
43 class NullComponent extends React.Component {
44 render() {
45 return null;
46 }
47 }
48
49 const response = ReactDOMServer.renderToString(<NullComponent />);
50 expect(response).toBe('');
51 });
52
53 // TODO: Test that listeners are not registered onto any document/container.
54
55 it('should render composite components', () => {
56 class Parent extends React.Component {
57 render() {
58 return (
59 <div>
60 <Child name="child" />
61 </div>
62 );
63 }
64 }
65
66 class Child extends React.Component {
67 render() {
68 return <span>My name is {this.props.name}</span>;
69 }
70 }
71
72 const response = ReactDOMServer.renderToString(<Parent />);
73 expect(response).toMatch(
74 new RegExp(
75 '<div>' +
76 '<span' +
77 '>' +
78 'My name is <!-- -->child' +
79 '</span>' +
80 '</div>',
81 ),
82 );
83 });
84
85 it('should only execute certain lifecycle methods', () => {
86 function runTest() {
87 const lifecycle = [];
88
89 class TestComponent extends React.Component {
90 constructor(props) {
91 super(props);
92 lifecycle.push('getInitialState');
93 this.state = {name: 'TestComponent'};
94 }
95
96 UNSAFE_componentWillMount() {
97 lifecycle.push('componentWillMount');
98 }
99
100 componentDidMount() {
101 lifecycle.push('componentDidMount');
102 }
103
104 render() {
105 lifecycle.push('render');
106 return <span>Component name: {this.state.name}</span>;
107 }
108
109 UNSAFE_componentWillUpdate() {
110 lifecycle.push('componentWillUpdate');
111 }
112
113 componentDidUpdate() {
114 lifecycle.push('componentDidUpdate');
115 }
116
117 shouldComponentUpdate() {
118 lifecycle.push('shouldComponentUpdate');
119 }
120
121 UNSAFE_componentWillReceiveProps() {
122 lifecycle.push('componentWillReceiveProps');
123 }
124
125 componentWillUnmount() {
126 lifecycle.push('componentWillUnmount');
127 }
128 }
129
130 const response = ReactDOMServer.renderToString(<TestComponent />);
131
132 expect(response).toMatch(
133 new RegExp(
134 '<span>' + 'Component name: <!-- -->TestComponent' + '</span>',
135 ),
136 );
137 expect(lifecycle).toEqual([
138 'getInitialState',
139 'componentWillMount',
140 'render',
141 ]);
142 }
143
144 runTest();
145 });
146
147 it('should throw with silly args', () => {
148 expect(
149 ReactDOMServer.renderToString.bind(ReactDOMServer, {x: 123}),
150 ).toThrow(
151 'Objects are not valid as a React child (found: object with keys {x})',
152 );
153 });
154
155 it('should throw prop mapping error for an <iframe /> with invalid props', () => {
156 expect(() => {
157 ReactDOMServer.renderToString(<iframe style="border:none;" />);
158 }).toThrow(
159 'The `style` prop expects a mapping from style properties to values, not ' +
160 "a string. For example, style={{marginRight: spacing + 'em'}} when using JSX.",
161 );
162 });
163
164 it('should not crash on poisoned hasOwnProperty', () => {
165 const html = ReactDOMServer.renderToString(
166 <div hasOwnProperty="poison">
167 <span unknown="test" />
168 </div>,
169 );
170 assertConsoleErrorDev([
171 'React does not recognize the `hasOwnProperty` prop on a DOM element. ' +
172 'If you intentionally want it to appear in the DOM as a custom attribute, ' +
173 'spell it as lowercase `hasownproperty` instead. ' +
174 'If you accidentally passed it from a parent component, remove it from the DOM element.\n' +
175 ' in div (at **)',
176 ]);
177 expect(html).toContain('<span unknown="test">');
178 });
179 });
180
181 describe('renderToStaticMarkup', () => {
182 it('should not put checksum and React ID on components', () => {
183 class NestedComponent extends React.Component {
184 render() {
185 return <div>inner text</div>;
186 }
187 }
188
189 class TestComponent extends React.Component {
190 render() {
191 return (
192 <span>
193 <NestedComponent />
194 </span>
195 );
196 }
197 }
198
199 const response = ReactDOMServer.renderToStaticMarkup(<TestComponent />);
200
201 expect(response).toBe('<span><div>inner text</div></span>');
202 });
203
204 it('should not put checksum and React ID on text components', () => {
205 class TestComponent extends React.Component {
206 render() {
207 return (
208 <span>
209 {'hello'} {'world'}
210 </span>
211 );
212 }
213 }
214
215 const response = ReactDOMServer.renderToStaticMarkup(<TestComponent />);
216
217 expect(response).toBe('<span>hello world</span>');
218 });
219
220 it('should not use comments for empty nodes', () => {
221 class TestComponent extends React.Component {
222 render() {
223 return null;
224 }
225 }
226
227 const response = ReactDOMServer.renderToStaticMarkup(<TestComponent />);
228
229 expect(response).toBe('');
230 });
231
232 it('should only execute certain lifecycle methods', () => {
233 function runTest() {
234 const lifecycle = [];
235
236 class TestComponent extends React.Component {
237 constructor(props) {
238 super(props);
239 lifecycle.push('getInitialState');
240 this.state = {name: 'TestComponent'};
241 }
242
243 UNSAFE_componentWillMount() {
244 lifecycle.push('componentWillMount');
245 }
246
247 componentDidMount() {
248 lifecycle.push('componentDidMount');
249 }
250
251 render() {
252 lifecycle.push('render');
253 return <span>Component name: {this.state.name}</span>;
254 }
255
256 UNSAFE_componentWillUpdate() {
257 lifecycle.push('componentWillUpdate');
258 }
259
260 componentDidUpdate() {
261 lifecycle.push('componentDidUpdate');
262 }
263
264 shouldComponentUpdate() {
265 lifecycle.push('shouldComponentUpdate');
266 }
267
268 UNSAFE_componentWillReceiveProps() {
269 lifecycle.push('componentWillReceiveProps');
270 }
271
272 componentWillUnmount() {
273 lifecycle.push('componentWillUnmount');
274 }
275 }
276
277 const response = ReactDOMServer.renderToStaticMarkup(<TestComponent />);
278
279 expect(response).toBe('<span>Component name: TestComponent</span>');
280 expect(lifecycle).toEqual([
281 'getInitialState',
282 'componentWillMount',
283 'render',
284 ]);
285 }
286
287 runTest();
288 });
289
290 it('should throw with silly args', () => {
291 expect(
292 ReactDOMServer.renderToStaticMarkup.bind(ReactDOMServer, {x: 123}),
293 ).toThrow(
294 'Objects are not valid as a React child (found: object with keys {x})',
295 );
296 });
297
298 it('allows setState in componentWillMount without using DOM', () => {
299 class Component extends React.Component {
300 UNSAFE_componentWillMount() {
301 this.setState({text: 'hello, world'});
302 }
303
304 render() {
305 return <div>{this.state.text}</div>;
306 }
307 }
308 const markup = ReactDOMServer.renderToStaticMarkup(<Component />);
309 expect(markup).toContain('hello, world');
310 });
311
312 it('allows setState in componentWillMount with custom constructor', () => {
313 class Component extends React.Component {
314 constructor() {
315 super();
316 this.state = {text: 'default state'};
317 }
318
319 UNSAFE_componentWillMount() {
320 this.setState({text: 'hello, world'});
321 }
322
323 render() {
324 return <div>{this.state.text}</div>;
325 }
326 }
327 const markup = ReactDOMServer.renderToStaticMarkup(<Component />);
328 expect(markup).toContain('hello, world');
329 });
330
331 it('renders with props when using custom constructor', () => {
332 class Component extends React.Component {
333 constructor() {
334 super();
335 }
336
337 render() {
338 return <div>{this.props.text}</div>;
339 }
340 }
341
342 const markup = ReactDOMServer.renderToStaticMarkup(
343 <Component text="hello, world" />,
344 );
345 expect(markup).toContain('hello, world');
346 });
347
348 // @gate !disableLegacyContext
349 it('renders with context when using custom constructor', () => {
350 class Component extends React.Component {
351 constructor() {
352 super();
353 }
354
355 render() {
356 return <div>{this.context.text}</div>;
357 }
358 }
359
360 Component.contextTypes = {
361 text: PropTypes.string.isRequired,
362 };
363
364 class ContextProvider extends React.Component {
365 getChildContext() {
366 return {
367 text: 'hello, world',
368 };
369 }
370
371 render() {
372 return this.props.children;
373 }
374 }
375
376 ContextProvider.childContextTypes = {
377 text: PropTypes.string,
378 };
379
380 const markup = ReactDOMServer.renderToStaticMarkup(
381 <ContextProvider>
382 <Component />
383 </ContextProvider>,
384 );
385 assertConsoleErrorDev([
386 'ContextProvider uses the legacy childContextTypes API which will soon be removed. ' +
387 'Use React.createContext() instead. (https://react.dev/link/legacy-context)\n' +
388 ' in ContextProvider (at **)',
389 'Component uses the legacy contextTypes API which will soon be removed. ' +
390 'Use React.createContext() with static contextType instead. (https://react.dev/link/legacy-context)\n' +
391 ' in Component (at **)',
392 ]);
393 expect(markup).toContain('hello, world');
394 });
395
396 it('renders with new context API', () => {
397 const Context = React.createContext(0);
398
399 function Consumer(props) {
400 return (
401 <Context.Consumer>{value => 'Result: ' + value}</Context.Consumer>
402 );
403 }
404
405 const Indirection = React.Fragment;
406
407 function App(props) {
408 return (
409 <Context.Provider value={props.value}>
410 <Context.Provider value={2}>
411 <Consumer />
412 </Context.Provider>
413 <Indirection>
414 <Indirection>
415 <Consumer />
416 <Context.Provider value={3}>
417 <Consumer />
418 </Context.Provider>
419 </Indirection>
420 </Indirection>
421 <Consumer />
422 </Context.Provider>
423 );
424 }
425
426 const markup = ReactDOMServer.renderToStaticMarkup(<App value={1} />);
427 // Extract the numbers rendered by the consumers
428 const results = markup.match(/\d+/g).map(Number);
429 expect(results).toEqual([2, 1, 3, 1]);
430 });
431
432 it('renders with dispatcher.readContext mechanism', () => {
433 const Context = React.createContext(0);
434
435 function readContext(context) {
436 return ReactSharedInternals.H.readContext(context);
437 }
438
439 function Consumer(props) {
440 return 'Result: ' + readContext(Context);
441 }
442
443 const Indirection = React.Fragment;
444
445 function App(props) {
446 return (
447 <Context.Provider value={props.value}>
448 <Context.Provider value={2}>
449 <Consumer />
450 </Context.Provider>
451 <Indirection>
452 <Indirection>
453 <Consumer />
454 <Context.Provider value={3}>
455 <Consumer />
456 </Context.Provider>
457 </Indirection>
458 </Indirection>
459 <Consumer />
460 </Context.Provider>
461 );
462 }
463
464 const markup = ReactDOMServer.renderToStaticMarkup(<App value={1} />);
465 // Extract the numbers rendered by the consumers
466 const results = markup.match(/\d+/g).map(Number);
467 expect(results).toEqual([2, 1, 3, 1]);
468 });
469
470 it('renders context API, reentrancy', () => {
471 const Context = React.createContext(0);
472
473 function Consumer(props) {
474 return (
475 <Context.Consumer>{value => 'Result: ' + value}</Context.Consumer>
476 );
477 }
478
479 let reentrantMarkup;
480 function Reentrant() {
481 reentrantMarkup = ReactDOMServer.renderToStaticMarkup(
482 <App value={1} reentrant={false} />,
483 );
484 return null;
485 }
486
487 const Indirection = React.Fragment;
488
489 function App(props) {
490 return (
491 <Context.Provider value={props.value}>
492 {props.reentrant && <Reentrant />}
493 <Context.Provider value={2}>
494 <Consumer />
495 </Context.Provider>
496 <Indirection>
497 <Indirection>
498 <Consumer />
499 <Context.Provider value={3}>
500 <Consumer />
501 </Context.Provider>
502 </Indirection>
503 </Indirection>
504 <Consumer />
505 </Context.Provider>
506 );
507 }
508
509 const markup = ReactDOMServer.renderToStaticMarkup(
510 <App value={1} reentrant={true} />,
511 );
512 // Extract the numbers rendered by the consumers
513 const results = markup.match(/\d+/g).map(Number);
514 const reentrantResults = reentrantMarkup.match(/\d+/g).map(Number);
515 expect(results).toEqual([2, 1, 3, 1]);
516 expect(reentrantResults).toEqual([2, 1, 3, 1]);
517 });
518
519 it('renders components with different batching strategies', () => {
520 class StaticComponent extends React.Component {
521 render() {
522 const staticContent = ReactDOMServer.renderToStaticMarkup(
523 <div>
524 <img src="foo-bar.jpg" />
525 </div>,
526 );
527 return <div dangerouslySetInnerHTML={{__html: staticContent}} />;
528 }
529 }
530
531 class Component extends React.Component {
532 UNSAFE_componentWillMount() {
533 this.setState({text: 'hello, world'});
534 }
535
536 render() {
537 return <div>{this.state.text}</div>;
538 }
539 }
540
541 expect(
542 ReactDOMServer.renderToString.bind(
543 ReactDOMServer,
544 <div>
545 <StaticComponent />
546 <Component />
547 </div>,
548 ),
549 ).not.toThrow();
550 });
551
552 it('renders synchronously resolved lazy component', () => {
553 const LazyFoo = React.lazy(() => ({
554 then(resolve) {
555 resolve({
556 default: function Foo({id}) {
557 return <div id={id}>lazy</div>;
558 },
559 });
560 },
561 }));
562
563 expect(ReactDOMServer.renderToStaticMarkup(<LazyFoo id="foo" />)).toEqual(
564 '<div id="foo">lazy</div>',
565 );
566 });
567
568 it('throws error from synchronously rejected lazy component', () => {
569 const LazyFoo = React.lazy(() => ({
570 then(resolve, reject) {
571 reject(new Error('Bad lazy'));
572 },
573 }));
574
575 expect(() => ReactDOMServer.renderToStaticMarkup(<LazyFoo />)).toThrow(
576 'Bad lazy',
577 );
578 });
579
580 it('aborts synchronously any suspended tasks and renders their fallbacks', () => {
581 const promise = new Promise(res => {});
582 function Suspender() {
583 throw promise;
584 }
585 const response = ReactDOMServer.renderToStaticMarkup(
586 <React.Suspense fallback={'fallback'}>
587 <Suspender />
588 </React.Suspense>,
589 );
590 expect(response).toEqual('fallback');
591 });
592 });
593
594 it('warns with a no-op when an async setState is triggered', () => {
595 class Foo extends React.Component {
596 UNSAFE_componentWillMount() {
597 this.setState({text: 'hello'});
598 setTimeout(() => {
599 this.setState({text: 'error'});
600 });
601 }
602 render() {
603 return <div onClick={() => {}}>{this.state.text}</div>;
604 }
605 }
606
607 ReactDOMServer.renderToString(<Foo />);
608 jest.runOnlyPendingTimers();
609 assertConsoleErrorDev([
610 'Can only update a mounting component. ' +
611 'This usually means you called setState() outside componentWillMount() on the server. ' +
612 'This is a no-op.\n' +
613 '\n' +
614 'Please check the code for the Foo component.',
615 ]);
616
617 const markup = ReactDOMServer.renderToStaticMarkup(<Foo />);
618 expect(markup).toBe('<div>hello</div>');
619 // No additional warnings are expected
620 jest.runOnlyPendingTimers();
621 });
622
623 it('warns with a no-op when an async forceUpdate is triggered', () => {
624 class Baz extends React.Component {
625 UNSAFE_componentWillMount() {
626 this.forceUpdate();
627 setTimeout(() => {
628 this.forceUpdate();
629 });
630 }
631
632 render() {
633 return <div onClick={() => {}} />;
634 }
635 }
636
637 ReactDOMServer.renderToString(<Baz />);
638 jest.runOnlyPendingTimers();
639 assertConsoleErrorDev([
640 'Can only update a mounting component. ' +
641 'This usually means you called forceUpdate() outside componentWillMount() on the server. ' +
642 'This is a no-op.\n' +
643 '\n' +
644 'Please check the code for the Baz component.',
645 ]);
646 const markup = ReactDOMServer.renderToStaticMarkup(<Baz />);
647 expect(markup).toBe('<div></div>');
648 });
649
650 it('does not get confused by throwing null', () => {
651 function Bad() {
652 // eslint-disable-next-line no-throw-literal
653 throw null;
654 }
655
656 let didError;
657 let error;
658 try {
659 ReactDOMServer.renderToString(<Bad />);
660 } catch (err) {
661 didError = true;
662 error = err;
663 }
664 expect(didError).toBe(true);
665 expect(error).toBe(null);
666 });
667
668 it('does not get confused by throwing undefined', () => {
669 function Bad() {
670 // eslint-disable-next-line no-throw-literal
671 throw undefined;
672 }
673
674 let didError;
675 let error;
676 try {
677 ReactDOMServer.renderToString(<Bad />);
678 } catch (err) {
679 didError = true;
680 error = err;
681 }
682 expect(didError).toBe(true);
683 expect(error).toBe(undefined);
684 });
685
686 it('does not get confused by throwing a primitive', () => {
687 function Bad() {
688 // eslint-disable-next-line no-throw-literal
689 throw 'foo';
690 }
691
692 let didError;
693 let error;
694 try {
695 ReactDOMServer.renderToString(<Bad />);
696 } catch (err) {
697 didError = true;
698 error = err;
699 }
700 expect(didError).toBe(true);
701 expect(error).toBe('foo');
702 });
703
704 it('should throw (in dev) when children are mutated during render', () => {
705 function Wrapper(props) {
706 props.children[1] = <p key={1} />; // Mutation is illegal
707 return <div>{props.children}</div>;
708 }
709 if (__DEV__) {
710 expect(() => {
711 ReactDOMServer.renderToStaticMarkup(
712 <Wrapper>
713 <span key={0} />
714 <span key={1} />
715 <span key={2} />
716 </Wrapper>,
717 );
718 }).toThrow(/Cannot assign to read only property.*/);
719 } else {
720 expect(
721 ReactDOMServer.renderToStaticMarkup(
722 <Wrapper>
723 <span key={0} />
724 <span key={1} />
725 <span key={2} />
726 </Wrapper>,
727 ),
728 ).toContain('<p>');
729 }
730 });
731
732 it('warns about lowercase html but not in svg tags', () => {
733 function CompositeG(props) {
734 // Make sure namespace passes through composites
735 return <g>{props.children}</g>;
736 }
737 ReactDOMServer.renderToStaticMarkup(
738 <div>
739 <inPUT />
740 <svg>
741 <CompositeG>
742 <linearGradient />
743 <foreignObject>
744 {/* back to HTML */}
745 <iFrame />
746 </foreignObject>
747 </CompositeG>
748 </svg>
749 </div>,
750 );
751 assertConsoleErrorDev([
752 '<inPUT /> is using incorrect casing. ' +
753 'Use PascalCase for React components, ' +
754 'or lowercase for HTML elements.\n' +
755 ' in inPUT (at **)',
756 // linearGradient doesn't warn
757 '<iFrame /> is using incorrect casing. ' +
758 'Use PascalCase for React components, ' +
759 'or lowercase for HTML elements.\n' +
760 ' in iFrame (at **)',
761 ]);
762 });
763
764 it('should warn about contentEditable and children', () => {
765 ReactDOMServer.renderToString(<div contentEditable={true} children="" />);
766 assertConsoleErrorDev([
767 'A component is `contentEditable` and contains `children` ' +
768 'managed by React. It is now your responsibility to guarantee that ' +
769 'none of those nodes are unexpectedly modified or duplicated. This ' +
770 'is probably not intentional.\n' +
771 ' in div (at **)',
772 ]);
773 });
774
775 it('should warn when server rendering a class with a render method that does not extend React.Component', () => {
776 class ClassWithRenderNotExtended {
777 render() {
778 return <div />;
779 }
780 }
781
782 expect(() =>
783 ReactDOMServer.renderToString(<ClassWithRenderNotExtended />),
784 ).toThrow(TypeError);
785 assertConsoleErrorDev([
786 'The <ClassWithRenderNotExtended /> component appears to have a render method, ' +
787 "but doesn't extend React.Component. This is likely to cause errors. " +
788 'Change ClassWithRenderNotExtended to extend React.Component instead.\n' +
789 ' in ClassWithRenderNotExtended (at **)',
790 ]);
791
792 // Test deduplication
793 expect(() => {
794 ReactDOMServer.renderToString(<ClassWithRenderNotExtended />);
795 }).toThrow(TypeError);
796 });
797
798 // We're just testing importing, not using it.
799 // It is important because even isomorphic components may import it.
800 it('can import react-dom in Node environment', () => {
801 if (
802 typeof requestAnimationFrame !== 'undefined' ||
803 global.hasOwnProperty('requestAnimationFrame') ||
804 typeof requestIdleCallback !== 'undefined' ||
805 global.hasOwnProperty('requestIdleCallback') ||
806 typeof window !== 'undefined' ||
807 global.hasOwnProperty('window')
808 ) {
809 // Don't remove this. This test is specifically checking
810 // what happens when they *don't* exist. It's useless otherwise.
811 throw new Error('Expected this test to run in a Node environment.');
812 }
813 jest.resetModules();
814 expect(() => {
815 require('react-dom');
816 }).not.toThrow();
817 });
818
819 it('includes a useful stack in warnings', () => {
820 function A() {
821 return null;
822 }
823
824 function B() {
825 return (
826 <font>
827 <C>
828 <span ariaTypo="no" />
829 </C>
830 </font>
831 );
832 }
833
834 class C extends React.Component {
835 render() {
836 return <b>{this.props.children}</b>;
837 }
838 }
839
840 function Child() {
841 return [<A key="1" />, <B key="2" />, <span ariaTypo2="no" key="3" />];
842 }
843
844 function App() {
845 return (
846 <div>
847 <section />
848 <span>
849 <Child />
850 </span>
851 </div>
852 );
853 }
854
855 ReactDOMServer.renderToString(<App />);
856 assertConsoleErrorDev([
857 'Invalid ARIA attribute `ariaTypo`. ARIA attributes follow the pattern aria-* and must be lowercase.\n' +
858 ' in span (at **)\n' +
859 ' in B (at **)\n' +
860 ' in Child (at **)\n' +
861 ' in App (at **)',
862 'Invalid ARIA attribute `ariaTypo2`. ARIA attributes follow the pattern aria-* and must be lowercase.\n' +
863 ' in span (at **)\n' +
864 ' in Child (at **)\n' +
865 ' in App (at **)',
866 ]);
867 });
868
869 it('reports stacks with re-entrant renderToString() calls', () => {
870 function Child2(props) {
871 return <span ariaTypo3="no">{props.children}</span>;
872 }
873
874 function App2() {
875 return (
876 <Child2>
877 {ReactDOMServer.renderToString(<blink ariaTypo2="no" />)}
878 </Child2>
879 );
880 }
881
882 function Child() {
883 return (
884 <span ariaTypo4="no">{ReactDOMServer.renderToString(<App2 />)}</span>
885 );
886 }
887
888 function App() {
889 return (
890 <div>
891 <span ariaTypo="no" />
892 <Child />
893 <font ariaTypo5="no" />
894 </div>
895 );
896 }
897
898 ReactDOMServer.renderToString(<App />);
899 assertConsoleErrorDev([
900 // ReactDOMServer(App > div > span)
901 'Invalid ARIA attribute `ariaTypo`. ARIA attributes follow the pattern aria-* and must be lowercase.\n' +
902 ' in span (at **)\n' +
903 ' in App (at **)',
904 // ReactDOMServer(App > div > Child) >>> ReactDOMServer(App2) >>> ReactDOMServer(blink)
905 'Invalid ARIA attribute `ariaTypo2`. ARIA attributes follow the pattern aria-* and must be lowercase.\n' +
906 ' in blink (at **)\n' +
907 ' in App2 (at **)\n' +
908 ' in Child (at **)\n' +
909 ' in App (at **)',
910 // ReactDOMServer(App > div > Child) >>> ReactDOMServer(App2 > Child2 > span)
911 'Invalid ARIA attribute `ariaTypo3`. ARIA attributes follow the pattern aria-* and must be lowercase.\n' +
912 ' in span (at **)\n' +
913 ' in Child2 (at **)\n' +
914 ' in App2 (at **)\n' +
915 ' in Child (at **)\n' +
916 ' in App (at **)',
917 // ReactDOMServer(App > div > Child > span)
918 'Invalid ARIA attribute `ariaTypo4`. ARIA attributes follow the pattern aria-* and must be lowercase.\n' +
919 ' in span (at **)\n' +
920 ' in Child (at **)\n' +
921 ' in App (at **)',
922 // ReactDOMServer(App > div > font)
923 'Invalid ARIA attribute `ariaTypo5`. ARIA attributes follow the pattern aria-* and must be lowercase.\n' +
924 ' in font (at **)\n' +
925 ' in App (at **)',
926 ]);
927 });
928
929 it('should warn if an invalid contextType is defined', () => {
930 const Context = React.createContext();
931 class ComponentA extends React.Component {
932 static contextType = Context.Consumer;
933 render() {
934 return <div />;
935 }
936 }
937
938 ReactDOMServer.renderToString(<ComponentA />);
939 assertConsoleErrorDev([
940 'ComponentA defines an invalid contextType. ' +
941 'contextType should point to the Context object returned by React.createContext(). ' +
942 'Did you accidentally pass the Context.Consumer instead?\n' +
943 ' in ComponentA (at **)',
944 ]);
945
946 // Warnings should be deduped by component type
947 ReactDOMServer.renderToString(<ComponentA />);
948
949 class ComponentB extends React.Component {
950 static contextType = Context.Provider;
951 render() {
952 return <div />;
953 }
954 }
955 // Does not warn because Context === Context.Provider.
956 ReactDOMServer.renderToString(<ComponentB />);
957 });
958
959 it('should not warn when class contextType is null', () => {
960 class Foo extends React.Component {
961 static contextType = null; // Handy for conditional declaration
962 render() {
963 return this.context.hello.world;
964 }
965 }
966
967 expect(() => {
968 ReactDOMServer.renderToString(<Foo />);
969 }).toThrow("Cannot read properties of undefined (reading 'world')");
970 });
971
972 it('should warn when class contextType is undefined', () => {
973 class Foo extends React.Component {
974 // This commonly happens with circular deps
975 // https://github.com/facebook/react/issues/13969
976 static contextType = undefined;
977 render() {
978 return this.context.hello.world;
979 }
980 }
981
982 expect(() => {
983 ReactDOMServer.renderToString(<Foo />);
984 }).toThrow("Cannot read properties of undefined (reading 'world')");
985 assertConsoleErrorDev([
986 'Foo defines an invalid contextType. ' +
987 'contextType should point to the Context object returned by React.createContext(). ' +
988 'However, it is set to undefined. ' +
989 'This can be caused by a typo or by mixing up named and default imports. ' +
990 'This can also happen due to a circular dependency, ' +
991 'so try moving the createContext() call to a separate file.\n' +
992 ' in Foo (at **)',
993 ]);
994 });
995
996 it('should warn when class contextType is an object', () => {
997 class Foo extends React.Component {
998 // Can happen due to a typo
999 static contextType = {
1000 x: 42,
1001 y: 'hello',
1002 };
1003 render() {
1004 return this.context.hello.world;
1005 }
1006 }
1007
1008 expect(() => {
1009 ReactDOMServer.renderToString(<Foo />);
1010 }).toThrow("Cannot read properties of undefined (reading 'hello')");
1011 assertConsoleErrorDev([
1012 'Foo defines an invalid contextType. ' +
1013 'contextType should point to the Context object returned by React.createContext(). ' +
1014 'However, it is set to an object with keys {x, y}.\n' +
1015 ' in Foo (at **)',
1016 ]);
1017 });
1018
1019 it('should warn when class contextType is a primitive', () => {
1020 class Foo extends React.Component {
1021 static contextType = 'foo';
1022 render() {
1023 return this.context.hello.world;
1024 }
1025 }
1026
1027 expect(() => {
1028 ReactDOMServer.renderToString(<Foo />);
1029 }).toThrow("Cannot read properties of undefined (reading 'world')");
1030 assertConsoleErrorDev([
1031 'Foo defines an invalid contextType. ' +
1032 'contextType should point to the Context object returned by React.createContext(). ' +
1033 'However, it is set to a string.\n' +
1034 ' in Foo (at **)',
1035 ]);
1036 });
1037
1038 describe('custom element server rendering', () => {
1039 it('String properties should be server rendered for custom elements', () => {
1040 const output = ReactDOMServer.renderToString(
1041 <my-custom-element foo="bar" />,
1042 );
1043 expect(output).toBe(`<my-custom-element foo="bar"></my-custom-element>`);
1044 });
1045
1046 it('Number properties should be server rendered for custom elements', () => {
1047 const output = ReactDOMServer.renderToString(
1048 <my-custom-element foo={5} />,
1049 );
1050 expect(output).toBe(`<my-custom-element foo="5"></my-custom-element>`);
1051 });
1052
1053 it('Object properties should not be server rendered for custom elements', () => {
1054 const output = ReactDOMServer.renderToString(
1055 <my-custom-element foo={{foo: 'bar'}} />,
1056 );
1057 expect(output).toBe(`<my-custom-element></my-custom-element>`);
1058 });
1059
1060 it('Array properties should not be server rendered for custom elements', () => {
1061 const output = ReactDOMServer.renderToString(
1062 <my-custom-element foo={['foo', 'bar']} />,
1063 );
1064 expect(output).toBe(`<my-custom-element></my-custom-element>`);
1065 });
1066
1067 it('Function properties should not be server rendered for custom elements', () => {
1068 const output = ReactDOMServer.renderToString(
1069 <my-custom-element foo={() => console.log('bar')} />,
1070 );
1071 expect(output).toBe(`<my-custom-element></my-custom-element>`);
1072 });
1073 });
1074 });