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 React;
    
  13. let ReactDOM;
    
  14. let ReactDOMServer;
    
  15. 
    
  16. function getTestDocument(markup) {
    
  17.   const doc = document.implementation.createHTMLDocument('');
    
  18.   doc.open();
    
  19.   doc.write(
    
  20.     markup ||
    
  21.       '<!doctype html><html><meta charset=utf-8><title>test doc</title>',
    
  22.   );
    
  23.   doc.close();
    
  24.   return doc;
    
  25. }
    
  26. 
    
  27. describe('rendering React components at document', () => {
    
  28.   beforeEach(() => {
    
  29.     jest.resetModules();
    
  30. 
    
  31.     React = require('react');
    
  32.     ReactDOM = require('react-dom');
    
  33.     ReactDOMServer = require('react-dom/server');
    
  34.   });
    
  35. 
    
  36.   describe('with new explicit hydration API', () => {
    
  37.     it('should be able to adopt server markup', () => {
    
  38.       class Root extends React.Component {
    
  39.         render() {
    
  40.           return (
    
  41.             <html>
    
  42.               <head>
    
  43.                 <title>Hello World</title>
    
  44.               </head>
    
  45.               <body>{'Hello ' + this.props.hello}</body>
    
  46.             </html>
    
  47.           );
    
  48.         }
    
  49.       }
    
  50. 
    
  51.       const markup = ReactDOMServer.renderToString(<Root hello="world" />);
    
  52.       expect(markup).not.toContain('DOCTYPE');
    
  53.       const testDocument = getTestDocument(markup);
    
  54.       const body = testDocument.body;
    
  55. 
    
  56.       ReactDOM.hydrate(<Root hello="world" />, testDocument);
    
  57.       expect(testDocument.body.innerHTML).toBe('Hello world');
    
  58. 
    
  59.       ReactDOM.hydrate(<Root hello="moon" />, testDocument);
    
  60.       expect(testDocument.body.innerHTML).toBe('Hello moon');
    
  61. 
    
  62.       expect(body === testDocument.body).toBe(true);
    
  63.     });
    
  64. 
    
  65.     // @gate enableHostSingletons
    
  66.     it('should be able to unmount component from document node, but leaves singleton nodes intact', () => {
    
  67.       class Root extends React.Component {
    
  68.         render() {
    
  69.           return (
    
  70.             <html>
    
  71.               <head>
    
  72.                 <title>Hello World</title>
    
  73.               </head>
    
  74.               <body>Hello world</body>
    
  75.             </html>
    
  76.           );
    
  77.         }
    
  78.       }
    
  79. 
    
  80.       const markup = ReactDOMServer.renderToString(<Root />);
    
  81.       const testDocument = getTestDocument(markup);
    
  82.       ReactDOM.hydrate(<Root />, testDocument);
    
  83.       expect(testDocument.body.innerHTML).toBe('Hello world');
    
  84. 
    
  85.       const originalDocEl = testDocument.documentElement;
    
  86.       const originalHead = testDocument.head;
    
  87.       const originalBody = testDocument.body;
    
  88. 
    
  89.       // When we unmount everything is removed except the singleton nodes of html, head, and body
    
  90.       ReactDOM.unmountComponentAtNode(testDocument);
    
  91.       expect(testDocument.firstChild).toBe(originalDocEl);
    
  92.       expect(testDocument.head).toBe(originalHead);
    
  93.       expect(testDocument.body).toBe(originalBody);
    
  94.       expect(originalBody.firstChild).toEqual(null);
    
  95.       expect(originalHead.firstChild).toEqual(null);
    
  96.     });
    
  97. 
    
  98.     // @gate !enableHostSingletons
    
  99.     it('should be able to unmount component from document node', () => {
    
  100.       class Root extends React.Component {
    
  101.         render() {
    
  102.           return (
    
  103.             <html>
    
  104.               <head>
    
  105.                 <title>Hello World</title>
    
  106.               </head>
    
  107.               <body>Hello world</body>
    
  108.             </html>
    
  109.           );
    
  110.         }
    
  111.       }
    
  112. 
    
  113.       const markup = ReactDOMServer.renderToString(<Root />);
    
  114.       const testDocument = getTestDocument(markup);
    
  115.       ReactDOM.hydrate(<Root />, testDocument);
    
  116.       expect(testDocument.body.innerHTML).toBe('Hello world');
    
  117. 
    
  118.       // When we unmount everything is removed except the persistent nodes of html, head, and body
    
  119.       ReactDOM.unmountComponentAtNode(testDocument);
    
  120.       expect(testDocument.firstChild).toBe(null);
    
  121.     });
    
  122. 
    
  123.     it('should not be able to switch root constructors', () => {
    
  124.       class Component extends React.Component {
    
  125.         render() {
    
  126.           return (
    
  127.             <html>
    
  128.               <head>
    
  129.                 <title>Hello World</title>
    
  130.               </head>
    
  131.               <body>Hello world</body>
    
  132.             </html>
    
  133.           );
    
  134.         }
    
  135.       }
    
  136. 
    
  137.       class Component2 extends React.Component {
    
  138.         render() {
    
  139.           return (
    
  140.             <html>
    
  141.               <head>
    
  142.                 <title>Hello World</title>
    
  143.               </head>
    
  144.               <body>Goodbye world</body>
    
  145.             </html>
    
  146.           );
    
  147.         }
    
  148.       }
    
  149. 
    
  150.       const markup = ReactDOMServer.renderToString(<Component />);
    
  151.       const testDocument = getTestDocument(markup);
    
  152. 
    
  153.       ReactDOM.hydrate(<Component />, testDocument);
    
  154. 
    
  155.       expect(testDocument.body.innerHTML).toBe('Hello world');
    
  156. 
    
  157.       // This works but is probably a bad idea.
    
  158.       ReactDOM.hydrate(<Component2 />, testDocument);
    
  159. 
    
  160.       expect(testDocument.body.innerHTML).toBe('Goodbye world');
    
  161.     });
    
  162. 
    
  163.     it('should be able to mount into document', () => {
    
  164.       class Component extends React.Component {
    
  165.         render() {
    
  166.           return (
    
  167.             <html>
    
  168.               <head>
    
  169.                 <title>Hello World</title>
    
  170.               </head>
    
  171.               <body>{this.props.text}</body>
    
  172.             </html>
    
  173.           );
    
  174.         }
    
  175.       }
    
  176. 
    
  177.       const markup = ReactDOMServer.renderToString(
    
  178.         <Component text="Hello world" />,
    
  179.       );
    
  180.       const testDocument = getTestDocument(markup);
    
  181. 
    
  182.       ReactDOM.hydrate(<Component text="Hello world" />, testDocument);
    
  183. 
    
  184.       expect(testDocument.body.innerHTML).toBe('Hello world');
    
  185.     });
    
  186. 
    
  187.     it('cannot render over an existing text child at the root', () => {
    
  188.       const container = document.createElement('div');
    
  189.       container.textContent = 'potato';
    
  190.       expect(() => ReactDOM.hydrate(<div>parsnip</div>, container)).toErrorDev(
    
  191.         'Expected server HTML to contain a matching <div> in <div>.',
    
  192.       );
    
  193.       // This creates an unfortunate double text case.
    
  194.       expect(container.textContent).toBe('potatoparsnip');
    
  195.     });
    
  196. 
    
  197.     it('renders over an existing nested text child without throwing', () => {
    
  198.       const container = document.createElement('div');
    
  199.       const wrapper = document.createElement('div');
    
  200.       wrapper.textContent = 'potato';
    
  201.       container.appendChild(wrapper);
    
  202.       expect(() =>
    
  203.         ReactDOM.hydrate(
    
  204.           <div>
    
  205.             <div>parsnip</div>
    
  206.           </div>,
    
  207.           container,
    
  208.         ),
    
  209.       ).toErrorDev(
    
  210.         'Expected server HTML to contain a matching <div> in <div>.',
    
  211.       );
    
  212.       expect(container.textContent).toBe('parsnip');
    
  213.     });
    
  214. 
    
  215.     it('should give helpful errors on state desync', () => {
    
  216.       class Component extends React.Component {
    
  217.         render() {
    
  218.           return (
    
  219.             <html>
    
  220.               <head>
    
  221.                 <title>Hello World</title>
    
  222.               </head>
    
  223.               <body>{this.props.text}</body>
    
  224.             </html>
    
  225.           );
    
  226.         }
    
  227.       }
    
  228. 
    
  229.       const markup = ReactDOMServer.renderToString(
    
  230.         <Component text="Goodbye world" />,
    
  231.       );
    
  232.       const testDocument = getTestDocument(markup);
    
  233. 
    
  234.       expect(() =>
    
  235.         ReactDOM.hydrate(<Component text="Hello world" />, testDocument),
    
  236.       ).toErrorDev('Warning: Text content did not match.');
    
  237.       expect(testDocument.body.innerHTML).toBe('Hello world');
    
  238.     });
    
  239. 
    
  240.     it('should render w/ no markup to full document', () => {
    
  241.       const testDocument = getTestDocument();
    
  242. 
    
  243.       class Component extends React.Component {
    
  244.         render() {
    
  245.           return (
    
  246.             <html>
    
  247.               <head>
    
  248.                 <title>Hello World</title>
    
  249.               </head>
    
  250.               <body>{this.props.text}</body>
    
  251.             </html>
    
  252.           );
    
  253.         }
    
  254.       }
    
  255. 
    
  256.       if (gate(flags => flags.enableFloat)) {
    
  257.         // with float the title no longer is a hydration mismatch so we get an error on the body mismatch
    
  258.         expect(() =>
    
  259.           ReactDOM.hydrate(<Component text="Hello world" />, testDocument),
    
  260.         ).toErrorDev(
    
  261.           'Expected server HTML to contain a matching text node for "Hello world" in <body>',
    
  262.         );
    
  263.       } else {
    
  264.         // getTestDocument() has an extra <meta> that we didn't render.
    
  265.         expect(() =>
    
  266.           ReactDOM.hydrate(<Component text="Hello world" />, testDocument),
    
  267.         ).toErrorDev(
    
  268.           'Did not expect server HTML to contain a <meta> in <head>.',
    
  269.         );
    
  270.       }
    
  271.       expect(testDocument.body.innerHTML).toBe('Hello world');
    
  272.     });
    
  273. 
    
  274.     it('supports findDOMNode on full-page components', () => {
    
  275.       const tree = (
    
  276.         <html>
    
  277.           <head>
    
  278.             <title>Hello World</title>
    
  279.           </head>
    
  280.           <body>Hello world</body>
    
  281.         </html>
    
  282.       );
    
  283. 
    
  284.       const markup = ReactDOMServer.renderToString(tree);
    
  285.       const testDocument = getTestDocument(markup);
    
  286.       const component = ReactDOM.hydrate(tree, testDocument);
    
  287.       expect(testDocument.body.innerHTML).toBe('Hello world');
    
  288.       expect(ReactDOM.findDOMNode(component).tagName).toBe('HTML');
    
  289.     });
    
  290.   });
    
  291. });