main
js 1,035 lines 34.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 * @jest-environment ./scripts/jest/ReactDOMServerIntegrationEnvironment
9 */
10
11 'use strict';
12
13 const ReactDOMServerIntegrationUtils = require('./utils/ReactDOMServerIntegrationTestUtils');
14
15 const TEXT_NODE_TYPE = 3;
16
17 let React;
18 let ReactDOM;
19 let ReactDOMClient;
20 let ReactDOMServer;
21 let assertConsoleErrorDev;
22
23 function initModules() {
24 jest.resetModules();
25 React = require('react');
26 ReactDOM = require('react-dom');
27 ReactDOMClient = require('react-dom/client');
28 ReactDOMServer = require('react-dom/server');
29 assertConsoleErrorDev = require('internal-test-utils').assertConsoleErrorDev;
30
31 // Make them available to the helpers.
32 return {
33 ReactDOMClient,
34 ReactDOMServer,
35 };
36 }
37
38 const {
39 resetModules,
40 itRenders,
41 itThrowsWhenRendering,
42 serverRender,
43 streamRender,
44 clientCleanRender,
45 clientRenderOnServerString,
46 } = ReactDOMServerIntegrationUtils(initModules);
47
48 describe('ReactDOMServerIntegration', () => {
49 beforeEach(() => {
50 resetModules();
51 });
52
53 afterEach(() => {
54 // TODO: This is a hack because expectErrors does not restore mock,
55 // however fixing it requires a major refactor to all these tests.
56 if (console.error.mockClear) {
57 console.error.mockRestore();
58 }
59 });
60
61 describe('elements and children', function () {
62 function expectNode(node, type, value) {
63 expect(node).not.toBe(null);
64 expect(node.nodeType).toBe(type);
65 expect(node.nodeValue).toMatch(value);
66 }
67
68 function expectTextNode(node, text) {
69 expectNode(node, TEXT_NODE_TYPE, text);
70 }
71
72 describe('text children', function () {
73 itRenders('a div with text', async render => {
74 const e = await render(<div>Text</div>);
75 expect(e.tagName).toBe('DIV');
76 expect(e.childNodes.length).toBe(1);
77 expectNode(e.firstChild, TEXT_NODE_TYPE, 'Text');
78 });
79
80 itRenders('a div with text with flanking whitespace', async render => {
81 // prettier-ignore
82 const e = await render(<div> Text </div>);
83 expect(e.childNodes.length).toBe(1);
84 expectNode(e.childNodes[0], TEXT_NODE_TYPE, ' Text ');
85 });
86
87 itRenders('a div with an empty text child', async render => {
88 const e = await render(<div>{''}</div>);
89 expect(e.childNodes.length).toBe(0);
90 });
91
92 itRenders('a div with multiple empty text children', async render => {
93 const e = await render(
94 <div>
95 {''}
96 {''}
97 {''}
98 </div>,
99 );
100 expect(e.childNodes.length).toBe(0);
101 expect(e.textContent).toBe('');
102 });
103
104 itRenders('a div with multiple whitespace children', async render => {
105 // prettier-ignore
106 const e = await render(<div>{' '}{' '}{' '}</div>);
107 if (
108 render === serverRender ||
109 render === clientRenderOnServerString ||
110 render === streamRender
111 ) {
112 // For plain server markup result we have comments between.
113 // If we're able to hydrate, they remain.
114 expect(e.childNodes.length).toBe(5);
115 expectTextNode(e.childNodes[0], ' ');
116 expectTextNode(e.childNodes[2], ' ');
117 expectTextNode(e.childNodes[4], ' ');
118 } else {
119 expect(e.childNodes.length).toBe(3);
120 expectTextNode(e.childNodes[0], ' ');
121 expectTextNode(e.childNodes[1], ' ');
122 expectTextNode(e.childNodes[2], ' ');
123 }
124 });
125
126 itRenders('a div with text sibling to a node', async render => {
127 const e = await render(
128 <div>
129 Text<span>More Text</span>
130 </div>,
131 );
132 expect(e.childNodes.length).toBe(2);
133 const spanNode = e.childNodes[1];
134 expectTextNode(e.childNodes[0], 'Text');
135 expect(spanNode.tagName).toBe('SPAN');
136 expect(spanNode.childNodes.length).toBe(1);
137 expectNode(spanNode.firstChild, TEXT_NODE_TYPE, 'More Text');
138 });
139
140 itRenders('a non-standard element with text', async render => {
141 // This test suite generally assumes that we get exactly
142 // the same warnings (or none) for all scenarios including
143 // SSR + innerHTML, hydration, and client-side rendering.
144 // However this particular warning fires only when creating
145 // DOM nodes on the client side. We force it to fire early
146 // so that it gets deduplicated later, and doesn't fail the test.
147 ReactDOM.flushSync(() => {
148 const root = ReactDOMClient.createRoot(document.createElement('div'));
149 root.render(<nonstandard />);
150 });
151 assertConsoleErrorDev([
152 'The tag <nonstandard> is unrecognized in this browser. ' +
153 'If you meant to render a React component, start its name with an uppercase letter.\n' +
154 ' in nonstandard (at **)',
155 ]);
156
157 const e = await render(<nonstandard>Text</nonstandard>);
158 expect(e.tagName).toBe('NONSTANDARD');
159 expect(e.childNodes.length).toBe(1);
160 expectNode(e.firstChild, TEXT_NODE_TYPE, 'Text');
161 });
162
163 itRenders('a custom element with text', async render => {
164 const e = await render(<custom-element>Text</custom-element>);
165 expect(e.tagName).toBe('CUSTOM-ELEMENT');
166 expect(e.childNodes.length).toBe(1);
167 expectNode(e.firstChild, TEXT_NODE_TYPE, 'Text');
168 });
169
170 itRenders('a leading blank child with a text sibling', async render => {
171 const e = await render(<div>{''}foo</div>);
172 expect(e.childNodes.length).toBe(1);
173 expectTextNode(e.childNodes[0], 'foo');
174 });
175
176 itRenders('a trailing blank child with a text sibling', async render => {
177 const e = await render(<div>foo{''}</div>);
178 expect(e.childNodes.length).toBe(1);
179 expectTextNode(e.childNodes[0], 'foo');
180 });
181
182 itRenders('an element with two text children', async render => {
183 const e = await render(
184 <div>
185 {'foo'}
186 {'bar'}
187 </div>,
188 );
189 if (
190 render === serverRender ||
191 render === clientRenderOnServerString ||
192 render === streamRender
193 ) {
194 // In the server render output there's a comment between them.
195 expect(e.childNodes.length).toBe(3);
196 expectTextNode(e.childNodes[0], 'foo');
197 expectTextNode(e.childNodes[2], 'bar');
198 } else {
199 expect(e.childNodes.length).toBe(2);
200 expectTextNode(e.childNodes[0], 'foo');
201 expectTextNode(e.childNodes[1], 'bar');
202 }
203 });
204
205 itRenders(
206 'a component returning text node between two text nodes',
207 async render => {
208 const B = () => 'b';
209 const e = await render(
210 <div>
211 {'a'}
212 <B />
213 {'c'}
214 </div>,
215 );
216 if (
217 render === serverRender ||
218 render === clientRenderOnServerString ||
219 render === streamRender
220 ) {
221 // In the server render output there's a comment between them.
222 expect(e.childNodes.length).toBe(5);
223 expectTextNode(e.childNodes[0], 'a');
224 expectTextNode(e.childNodes[2], 'b');
225 expectTextNode(e.childNodes[4], 'c');
226 } else {
227 expect(e.childNodes.length).toBe(3);
228 expectTextNode(e.childNodes[0], 'a');
229 expectTextNode(e.childNodes[1], 'b');
230 expectTextNode(e.childNodes[2], 'c');
231 }
232 },
233 );
234
235 itRenders('a tree with sibling host and text nodes', async render => {
236 class X extends React.Component {
237 render() {
238 return [null, [<Y key="1" />], false];
239 }
240 }
241
242 function Y() {
243 return [<Z key="1" />, ['c']];
244 }
245
246 function Z() {
247 return null;
248 }
249
250 const e = await render(
251 <div>
252 {[['a'], 'b']}
253 <div>
254 <X key="1" />d
255 </div>
256 e
257 </div>,
258 );
259 if (
260 render === serverRender ||
261 render === streamRender ||
262 render === clientRenderOnServerString
263 ) {
264 // In the server render output there's comments between text nodes.
265 expect(e.childNodes.length).toBe(5);
266 expectTextNode(e.childNodes[0], 'a');
267 expectTextNode(e.childNodes[2], 'b');
268 expect(e.childNodes[3].childNodes.length).toBe(3);
269 expectTextNode(e.childNodes[3].childNodes[0], 'c');
270 expectTextNode(e.childNodes[3].childNodes[2], 'd');
271 expectTextNode(e.childNodes[4], 'e');
272 } else {
273 expect(e.childNodes.length).toBe(4);
274 expectTextNode(e.childNodes[0], 'a');
275 expectTextNode(e.childNodes[1], 'b');
276 expect(e.childNodes[2].childNodes.length).toBe(2);
277 expectTextNode(e.childNodes[2].childNodes[0], 'c');
278 expectTextNode(e.childNodes[2].childNodes[1], 'd');
279 expectTextNode(e.childNodes[3], 'e');
280 }
281 });
282 });
283
284 describe('number children', function () {
285 itRenders('a number as single child', async render => {
286 const e = await render(<div>{3}</div>);
287 expect(e.textContent).toBe('3');
288 });
289
290 // zero is falsey, so it could look like no children if the code isn't careful.
291 itRenders('zero as single child', async render => {
292 const e = await render(<div>{0}</div>);
293 expect(e.textContent).toBe('0');
294 });
295
296 itRenders('an element with number and text children', async render => {
297 const e = await render(
298 <div>
299 {'foo'}
300 {40}
301 </div>,
302 );
303 // with Fiber, there are just two text nodes.
304 if (
305 render === serverRender ||
306 render === clientRenderOnServerString ||
307 render === streamRender
308 ) {
309 // In the server markup there's a comment between.
310 expect(e.childNodes.length).toBe(3);
311 expectTextNode(e.childNodes[0], 'foo');
312 expectTextNode(e.childNodes[2], '40');
313 } else {
314 expect(e.childNodes.length).toBe(2);
315 expectTextNode(e.childNodes[0], 'foo');
316 expectTextNode(e.childNodes[1], '40');
317 }
318 });
319 });
320
321 describe('null, false, and undefined children', function () {
322 itRenders('null single child as blank', async render => {
323 const e = await render(<div>{null}</div>);
324 expect(e.childNodes.length).toBe(0);
325 });
326
327 itRenders('false single child as blank', async render => {
328 const e = await render(<div>{false}</div>);
329 expect(e.childNodes.length).toBe(0);
330 });
331
332 itRenders('undefined single child as blank', async render => {
333 const e = await render(<div>{undefined}</div>);
334 expect(e.childNodes.length).toBe(0);
335 });
336
337 itRenders('a null component children as empty', async render => {
338 const NullComponent = () => null;
339 const e = await render(
340 <div>
341 <NullComponent />
342 </div>,
343 );
344 expect(e.childNodes.length).toBe(0);
345 });
346
347 itRenders('null children as blank', async render => {
348 const e = await render(<div>{null}foo</div>);
349 expect(e.childNodes.length).toBe(1);
350 expectTextNode(e.childNodes[0], 'foo');
351 });
352
353 itRenders('false children as blank', async render => {
354 const e = await render(<div>{false}foo</div>);
355 expect(e.childNodes.length).toBe(1);
356 expectTextNode(e.childNodes[0], 'foo');
357 });
358
359 itRenders('null and false children together as blank', async render => {
360 const e = await render(
361 <div>
362 {false}
363 {null}foo{null}
364 {false}
365 </div>,
366 );
367 expect(e.childNodes.length).toBe(1);
368 expectTextNode(e.childNodes[0], 'foo');
369 });
370
371 itRenders('only null and false children as blank', async render => {
372 const e = await render(
373 <div>
374 {false}
375 {null}
376 {null}
377 {false}
378 </div>,
379 );
380 expect(e.childNodes.length).toBe(0);
381 });
382 });
383
384 describe('elements with implicit namespaces', function () {
385 itRenders('an svg element', async render => {
386 const e = await render(<svg />);
387 expect(e.childNodes.length).toBe(0);
388 expect(e.tagName).toBe('svg');
389 expect(e.namespaceURI).toBe('http://www.w3.org/2000/svg');
390 });
391
392 itRenders('svg child element with an attribute', async render => {
393 const e = await render(<svg viewBox="0 0 0 0" />);
394 expect(e.childNodes.length).toBe(0);
395 expect(e.tagName).toBe('svg');
396 expect(e.namespaceURI).toBe('http://www.w3.org/2000/svg');
397 expect(e.getAttribute('viewBox')).toBe('0 0 0 0');
398 });
399
400 itRenders(
401 'svg child element with a namespace attribute',
402 async render => {
403 let e = await render(
404 <svg>
405 <image xlinkHref="http://i.imgur.com/w7GCRPb.png" />
406 </svg>,
407 );
408 e = e.firstChild;
409 expect(e.childNodes.length).toBe(0);
410 expect(e.tagName).toBe('image');
411 expect(e.namespaceURI).toBe('http://www.w3.org/2000/svg');
412 expect(e.getAttributeNS('http://www.w3.org/1999/xlink', 'href')).toBe(
413 'http://i.imgur.com/w7GCRPb.png',
414 );
415 },
416 );
417
418 itRenders('svg child element with a badly cased alias', async render => {
419 let e = await render(
420 <svg>
421 <image xlinkhref="http://i.imgur.com/w7GCRPb.png" />
422 </svg>,
423 1,
424 );
425 e = e.firstChild;
426 expect(e.hasAttributeNS('http://www.w3.org/1999/xlink', 'href')).toBe(
427 false,
428 );
429 expect(e.getAttribute('xlinkhref')).toBe(
430 'http://i.imgur.com/w7GCRPb.png',
431 );
432 });
433
434 itRenders('svg element with a tabIndex attribute', async render => {
435 const e = await render(<svg tabIndex="1" />);
436 expect(e.tabIndex).toBe(1);
437 });
438
439 itRenders(
440 'svg element with a badly cased tabIndex attribute',
441 async render => {
442 const e = await render(<svg tabindex="1" />, 1);
443 expect(e.tabIndex).toBe(1);
444 },
445 );
446
447 itRenders('svg element with a mixed case name', async render => {
448 let e = await render(
449 <svg>
450 <filter>
451 <feMorphology />
452 </filter>
453 </svg>,
454 );
455 e = e.firstChild.firstChild;
456 expect(e.childNodes.length).toBe(0);
457 expect(e.tagName).toBe('feMorphology');
458 expect(e.namespaceURI).toBe('http://www.w3.org/2000/svg');
459 });
460
461 itRenders('a math element', async render => {
462 const e = await render(<math />);
463 expect(e.childNodes.length).toBe(0);
464 expect(e.tagName).toBe('math');
465 expect(e.namespaceURI).toBe('http://www.w3.org/1998/Math/MathML');
466 });
467 });
468 // specially wrapped components
469 // (see the big switch near the beginning ofReactDOMComponent.mountComponent)
470 itRenders('an img', async render => {
471 const e = await render(<img />);
472 expect(e.childNodes.length).toBe(0);
473 expect(e.nextSibling).toBe(null);
474 expect(e.tagName).toBe('IMG');
475 });
476
477 itRenders('a button', async render => {
478 const e = await render(<button />);
479 expect(e.childNodes.length).toBe(0);
480 expect(e.nextSibling).toBe(null);
481 expect(e.tagName).toBe('BUTTON');
482 });
483
484 itRenders('a div with dangerouslySetInnerHTML number', async render => {
485 // Put dangerouslySetInnerHTML one level deeper because otherwise
486 // hydrating from a bad markup would cause a mismatch (since we don't
487 // patch dangerouslySetInnerHTML as text content).
488 const e = (
489 await render(
490 <div>
491 <span dangerouslySetInnerHTML={{__html: 0}} />
492 </div>,
493 )
494 ).firstChild;
495 expect(e.childNodes.length).toBe(1);
496 expect(e.firstChild.nodeType).toBe(TEXT_NODE_TYPE);
497 expect(e.textContent).toBe('0');
498 });
499
500 itRenders('a div with dangerouslySetInnerHTML boolean', async render => {
501 // Put dangerouslySetInnerHTML one level deeper because otherwise
502 // hydrating from a bad markup would cause a mismatch (since we don't
503 // patch dangerouslySetInnerHTML as text content).
504 const e = (
505 await render(
506 <div>
507 <span dangerouslySetInnerHTML={{__html: false}} />
508 </div>,
509 )
510 ).firstChild;
511 expect(e.childNodes.length).toBe(1);
512 expect(e.firstChild.nodeType).toBe(TEXT_NODE_TYPE);
513 expect(e.firstChild.data).toBe('false');
514 });
515
516 itRenders(
517 'a div with dangerouslySetInnerHTML text string',
518 async render => {
519 // Put dangerouslySetInnerHTML one level deeper because otherwise
520 // hydrating from a bad markup would cause a mismatch (since we don't
521 // patch dangerouslySetInnerHTML as text content).
522 const e = (
523 await render(
524 <div>
525 <span dangerouslySetInnerHTML={{__html: 'hello'}} />
526 </div>,
527 )
528 ).firstChild;
529 expect(e.childNodes.length).toBe(1);
530 expect(e.firstChild.nodeType).toBe(TEXT_NODE_TYPE);
531 expect(e.textContent).toBe('hello');
532 },
533 );
534
535 itRenders(
536 'a div with dangerouslySetInnerHTML element string',
537 async render => {
538 const e = await render(
539 <div dangerouslySetInnerHTML={{__html: "<span id='child'/>"}} />,
540 );
541 expect(e.childNodes.length).toBe(1);
542 expect(e.firstChild.tagName).toBe('SPAN');
543 expect(e.firstChild.getAttribute('id')).toBe('child');
544 expect(e.firstChild.childNodes.length).toBe(0);
545 },
546 );
547
548 itRenders('a div with dangerouslySetInnerHTML object', async render => {
549 const obj = {
550 toString() {
551 return "<span id='child'/>";
552 },
553 };
554 const e = await render(<div dangerouslySetInnerHTML={{__html: obj}} />);
555 expect(e.childNodes.length).toBe(1);
556 expect(e.firstChild.tagName).toBe('SPAN');
557 expect(e.firstChild.getAttribute('id')).toBe('child');
558 expect(e.firstChild.childNodes.length).toBe(0);
559 });
560
561 itRenders(
562 'a div with dangerouslySetInnerHTML set to null',
563 async render => {
564 const e = await render(
565 <div dangerouslySetInnerHTML={{__html: null}} />,
566 );
567 expect(e.childNodes.length).toBe(0);
568 },
569 );
570
571 itRenders(
572 'a div with dangerouslySetInnerHTML set to undefined',
573 async render => {
574 const e = await render(
575 <div dangerouslySetInnerHTML={{__html: undefined}} />,
576 );
577 expect(e.childNodes.length).toBe(0);
578 },
579 );
580
581 itRenders('a noscript with children', async render => {
582 const e = await render(
583 <noscript>
584 <div>Enable JavaScript to run this app.</div>
585 </noscript>,
586 );
587 if (render === clientCleanRender) {
588 // On the client we ignore the contents of a noscript
589 expect(e.childNodes.length).toBe(0);
590 } else {
591 // On the server or when hydrating the content should be correct
592 expect(e.childNodes.length).toBe(1);
593 expect(e.firstChild.textContent).toBe(
594 '<div>Enable JavaScript to run this app.</div>',
595 );
596 }
597 });
598
599 describe('newline-eating elements', function () {
600 itRenders(
601 'a newline-eating tag with content not starting with \\n',
602 async render => {
603 const e = await render(<pre>Hello</pre>);
604 expect(e.textContent).toBe('Hello');
605 },
606 );
607 itRenders(
608 'a newline-eating tag with content starting with \\n',
609 async render => {
610 const e = await render(<pre>{'\nHello'}</pre>);
611 expect(e.textContent).toBe('\nHello');
612 },
613 );
614 itRenders('a normal tag with content starting with \\n', async render => {
615 const e = await render(<div>{'\nHello'}</div>);
616 expect(e.textContent).toBe('\nHello');
617 });
618 });
619
620 describe('different component implementations', function () {
621 function checkFooDiv(e) {
622 expect(e.childNodes.length).toBe(1);
623 expectNode(e.firstChild, TEXT_NODE_TYPE, 'foo');
624 }
625
626 itRenders('stateless components', async render => {
627 const FunctionComponent = () => <div>foo</div>;
628 checkFooDiv(await render(<FunctionComponent />));
629 });
630
631 itRenders('ES6 class components', async render => {
632 class ClassComponent extends React.Component {
633 render() {
634 return <div>foo</div>;
635 }
636 }
637 checkFooDiv(await render(<ClassComponent />));
638 });
639
640 itThrowsWhenRendering(
641 'factory components',
642 async render => {
643 const FactoryComponent = () => {
644 return {
645 render: function () {
646 return <div>foo</div>;
647 },
648 };
649 };
650 await render(<FactoryComponent />, 1);
651 },
652 'Objects are not valid as a React child (found: object with keys {render})',
653 );
654 });
655
656 describe('component hierarchies', function () {
657 itRenders('single child hierarchies of components', async render => {
658 const Component = props => <div>{props.children}</div>;
659 let e = await render(
660 <Component>
661 <Component>
662 <Component>
663 <Component />
664 </Component>
665 </Component>
666 </Component>,
667 );
668 for (let i = 0; i < 3; i++) {
669 expect(e.tagName).toBe('DIV');
670 expect(e.childNodes.length).toBe(1);
671 e = e.firstChild;
672 }
673 expect(e.tagName).toBe('DIV');
674 expect(e.childNodes.length).toBe(0);
675 });
676
677 itRenders('multi-child hierarchies of components', async render => {
678 const Component = props => <div>{props.children}</div>;
679 const e = await render(
680 <Component>
681 <Component>
682 <Component />
683 <Component />
684 </Component>
685 <Component>
686 <Component />
687 <Component />
688 </Component>
689 </Component>,
690 );
691 expect(e.tagName).toBe('DIV');
692 expect(e.childNodes.length).toBe(2);
693 for (let i = 0; i < 2; i++) {
694 const child = e.childNodes[i];
695 expect(child.tagName).toBe('DIV');
696 expect(child.childNodes.length).toBe(2);
697 for (let j = 0; j < 2; j++) {
698 const grandchild = child.childNodes[j];
699 expect(grandchild.tagName).toBe('DIV');
700 expect(grandchild.childNodes.length).toBe(0);
701 }
702 }
703 });
704
705 itRenders('a div with a child', async render => {
706 const e = await render(
707 <div id="parent">
708 <div id="child" />
709 </div>,
710 );
711 expect(e.id).toBe('parent');
712 expect(e.childNodes.length).toBe(1);
713 expect(e.childNodes[0].id).toBe('child');
714 expect(e.childNodes[0].childNodes.length).toBe(0);
715 });
716
717 itRenders('a div with multiple children', async render => {
718 const e = await render(
719 <div id="parent">
720 <div id="child1" />
721 <div id="child2" />
722 </div>,
723 );
724 expect(e.id).toBe('parent');
725 expect(e.childNodes.length).toBe(2);
726 expect(e.childNodes[0].id).toBe('child1');
727 expect(e.childNodes[0].childNodes.length).toBe(0);
728 expect(e.childNodes[1].id).toBe('child2');
729 expect(e.childNodes[1].childNodes.length).toBe(0);
730 });
731
732 itRenders(
733 'a div with multiple children separated by whitespace',
734 async render => {
735 const e = await render(
736 <div id="parent">
737 <div id="child1" /> <div id="child2" />
738 </div>,
739 );
740 expect(e.id).toBe('parent');
741 expect(e.childNodes.length).toBe(3);
742 const child1 = e.childNodes[0];
743 const textNode = e.childNodes[1];
744 const child2 = e.childNodes[2];
745 expect(child1.id).toBe('child1');
746 expect(child1.childNodes.length).toBe(0);
747 expectTextNode(textNode, ' ');
748 expect(child2.id).toBe('child2');
749 expect(child2.childNodes.length).toBe(0);
750 },
751 );
752
753 itRenders(
754 'a div with a single child surrounded by whitespace',
755 async render => {
756 // prettier-ignore
757 const e = await render(<div id="parent"> <div id="child" /> </div>);
758 expect(e.childNodes.length).toBe(3);
759 const textNode1 = e.childNodes[0];
760 const child = e.childNodes[1];
761 const textNode2 = e.childNodes[2];
762 expect(e.id).toBe('parent');
763 expectTextNode(textNode1, ' ');
764 expect(child.id).toBe('child');
765 expect(child.childNodes.length).toBe(0);
766 expectTextNode(textNode2, ' ');
767 },
768 );
769
770 itRenders('a composite with multiple children', async render => {
771 const Component = props => props.children;
772 const e = await render(
773 <Component>{['a', 'b', [undefined], [[false, 'c']]]}</Component>,
774 );
775
776 const parent = e.parentNode;
777 if (
778 render === serverRender ||
779 render === clientRenderOnServerString ||
780 render === streamRender
781 ) {
782 // For plain server markup result we have comments between.
783 // If we're able to hydrate, they remain.
784 expect(parent.childNodes.length).toBe(5);
785 expectTextNode(parent.childNodes[0], 'a');
786 expectTextNode(parent.childNodes[2], 'b');
787 expectTextNode(parent.childNodes[4], 'c');
788 } else {
789 expect(parent.childNodes.length).toBe(3);
790 expectTextNode(parent.childNodes[0], 'a');
791 expectTextNode(parent.childNodes[1], 'b');
792 expectTextNode(parent.childNodes[2], 'c');
793 }
794 });
795 });
796
797 describe('escaping >, <, and &', function () {
798 itRenders('>,<, and & as single child', async render => {
799 const e = await render(<div>{'<span>Text&quot;</span>'}</div>);
800 expect(e.childNodes.length).toBe(1);
801 expectNode(e.firstChild, TEXT_NODE_TYPE, '<span>Text&quot;</span>');
802 });
803
804 itRenders('>,<, and & as multiple children', async render => {
805 const e = await render(
806 <div>
807 {'<span>Text1&quot;</span>'}
808 {'<span>Text2&quot;</span>'}
809 </div>,
810 );
811 if (
812 render === serverRender ||
813 render === clientRenderOnServerString ||
814 render === streamRender
815 ) {
816 expect(e.childNodes.length).toBe(3);
817 expectTextNode(e.childNodes[0], '<span>Text1&quot;</span>');
818 expectTextNode(e.childNodes[2], '<span>Text2&quot;</span>');
819 } else {
820 expect(e.childNodes.length).toBe(2);
821 expectTextNode(e.childNodes[0], '<span>Text1&quot;</span>');
822 expectTextNode(e.childNodes[1], '<span>Text2&quot;</span>');
823 }
824 });
825 });
826
827 describe('carriage return and null character', () => {
828 // HTML parsing normalizes CR and CRLF to LF.
829 // It also ignores null character.
830 // https://www.w3.org/TR/html5/single-page.html#preprocessing-the-input-stream
831 // If we have a mismatch, it might be caused by that (and should not be reported).
832 // We won't be patching up in this case as that matches our past behavior.
833
834 itRenders(
835 'an element with one text child with special characters',
836 async render => {
837 const e = await render(<div>{'foo\rbar\r\nbaz\nqux\u0000'}</div>);
838 if (
839 render === serverRender ||
840 render === streamRender ||
841 render === clientRenderOnServerString
842 ) {
843 expect(e.childNodes.length).toBe(1);
844 // Everything becomes LF when parsed from server HTML or hydrated.
845 // Null character is ignored.
846 expectNode(e.childNodes[0], TEXT_NODE_TYPE, 'foo\nbar\nbaz\nqux');
847 } else {
848 expect(e.childNodes.length).toBe(1);
849 // Client rendering uses JS value with CR.
850 // Null character stays.
851
852 expectNode(
853 e.childNodes[0],
854 TEXT_NODE_TYPE,
855 'foo\rbar\r\nbaz\nqux\u0000',
856 );
857 }
858 },
859 );
860
861 itRenders(
862 'an element with two text children with special characters',
863 async render => {
864 const e = await render(
865 <div>
866 {'foo\rbar'}
867 {'\r\nbaz\nqux\u0000'}
868 </div>,
869 );
870 if (
871 render === serverRender ||
872 render === streamRender ||
873 render === clientRenderOnServerString
874 ) {
875 // We have three nodes because there is a comment between them.
876 expect(e.childNodes.length).toBe(3);
877 // Everything becomes LF when parsed from server HTML or hydrated.
878 // Null character is ignored.
879 expectNode(e.childNodes[0], TEXT_NODE_TYPE, 'foo\nbar');
880 expectNode(e.childNodes[2], TEXT_NODE_TYPE, '\nbaz\nqux');
881 } else if (render === clientRenderOnServerString) {
882 // We have three nodes because there is a comment between them.
883 expect(e.childNodes.length).toBe(3);
884 // Hydration uses JS value with CR and null character.
885
886 expectNode(e.childNodes[0], TEXT_NODE_TYPE, 'foo\rbar');
887 expectNode(e.childNodes[2], TEXT_NODE_TYPE, '\r\nbaz\nqux\u0000');
888 } else {
889 expect(e.childNodes.length).toBe(2);
890 // Client rendering uses JS value with CR and null character.
891 expectNode(e.childNodes[0], TEXT_NODE_TYPE, 'foo\rbar');
892 expectNode(e.childNodes[1], TEXT_NODE_TYPE, '\r\nbaz\nqux\u0000');
893 }
894 },
895 );
896
897 itRenders(
898 'an element with an attribute value with special characters',
899 async render => {
900 const e = await render(<a title={'foo\rbar\r\nbaz\nqux\u0000'} />);
901 if (
902 render === serverRender ||
903 render === streamRender ||
904 render === clientRenderOnServerString
905 ) {
906 // Everything becomes LF when parsed from server HTML.
907 // Null character in an attribute becomes the replacement character.
908 // Hydration also ends up with LF because we don't patch up attributes.
909 expect(e.title).toBe('foo\nbar\nbaz\nqux\uFFFD');
910 } else {
911 // Client rendering uses JS value with CR and null character.
912 expect(e.title).toBe('foo\rbar\r\nbaz\nqux\u0000');
913 }
914 },
915 );
916 });
917
918 describe('components that render nullish', function () {
919 itRenders('a function returning null', async render => {
920 const NullComponent = () => null;
921 await render(<NullComponent />);
922 });
923
924 itRenders('a class returning null', async render => {
925 class NullComponent extends React.Component {
926 render() {
927 return null;
928 }
929 }
930 await render(<NullComponent />);
931 });
932
933 itRenders('a function returning undefined', async render => {
934 const UndefinedComponent = () => undefined;
935 await render(<UndefinedComponent />);
936 });
937
938 itRenders('a class returning undefined', async render => {
939 class UndefinedComponent extends React.Component {
940 render() {
941 return undefined;
942 }
943 }
944 await render(<UndefinedComponent />);
945 });
946 });
947
948 describe('components that throw errors', function () {
949 itThrowsWhenRendering(
950 'a function returning an object',
951 async render => {
952 const ObjectComponent = () => ({x: 123});
953 await render(<ObjectComponent />, 1);
954 },
955 'Objects are not valid as a React child (found: object with keys {x}).' +
956 (__DEV__
957 ? ' If you meant to render a collection of children, use ' +
958 'an array instead.'
959 : ''),
960 );
961
962 itThrowsWhenRendering(
963 'a class returning an object',
964 async render => {
965 class ObjectComponent extends React.Component {
966 render() {
967 return {x: 123};
968 }
969 }
970 await render(<ObjectComponent />, 1);
971 },
972 'Objects are not valid as a React child (found: object with keys {x}).' +
973 (__DEV__
974 ? ' If you meant to render a collection of children, use ' +
975 'an array instead.'
976 : ''),
977 );
978
979 itThrowsWhenRendering(
980 'top-level object',
981 async render => {
982 await render({x: 123});
983 },
984 'Objects are not valid as a React child (found: object with keys {x}).' +
985 (__DEV__
986 ? ' If you meant to render a collection of children, use ' +
987 'an array instead.'
988 : ''),
989 );
990 });
991
992 describe('badly-typed elements', function () {
993 itThrowsWhenRendering(
994 'object',
995 async render => {
996 let EmptyComponent = {};
997 EmptyComponent = <EmptyComponent />;
998 await render(EmptyComponent);
999 },
1000 'Element type is invalid: expected a string (for built-in components) or a class/function ' +
1001 '(for composite components) but got: object.' +
1002 (__DEV__
1003 ? " You likely forgot to export your component from the file it's defined in, " +
1004 'or you might have mixed up default and named imports.'
1005 : ''),
1006 );
1007
1008 itThrowsWhenRendering(
1009 'null',
1010 async render => {
1011 let NullComponent = null;
1012 NullComponent = <NullComponent />;
1013 await render(NullComponent);
1014 },
1015 'Element type is invalid: expected a string (for built-in components) or a class/function ' +
1016 '(for composite components) but got: null',
1017 );
1018
1019 itThrowsWhenRendering(
1020 'undefined',
1021 async render => {
1022 let UndefinedComponent = undefined;
1023 UndefinedComponent = <UndefinedComponent />;
1024 await render(UndefinedComponent);
1025 },
1026 'Element type is invalid: expected a string (for built-in components) or a class/function ' +
1027 '(for composite components) but got: undefined.' +
1028 (__DEV__
1029 ? " You likely forgot to export your component from the file it's defined in, " +
1030 'or you might have mixed up default and named imports.'
1031 : ''),
1032 );
1033 });
1034 });
1035 });