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. const ReactDOMServerIntegrationUtils = require('./utils/ReactDOMServerIntegrationTestUtils');
    
  13. 
    
  14. let React;
    
  15. let ReactDOM;
    
  16. let ReactDOMServer;
    
  17. let ReactTestUtils;
    
  18. 
    
  19. function initModules() {
    
  20.   // Reset warning cache.
    
  21.   jest.resetModules();
    
  22.   React = require('react');
    
  23.   ReactDOM = require('react-dom');
    
  24.   ReactDOMServer = require('react-dom/server');
    
  25.   ReactTestUtils = require('react-dom/test-utils');
    
  26. 
    
  27.   // Make them available to the helpers.
    
  28.   return {
    
  29.     ReactDOM,
    
  30.     ReactDOMServer,
    
  31.     ReactTestUtils,
    
  32.   };
    
  33. }
    
  34. 
    
  35. const {resetModules, itClientRenders, renderIntoDom, serverRender} =
    
  36.   ReactDOMServerIntegrationUtils(initModules);
    
  37. 
    
  38. describe('ReactDOMServerIntegrationUserInteraction', () => {
    
  39.   let ControlledInput, ControlledTextArea, ControlledCheckbox, ControlledSelect;
    
  40. 
    
  41.   beforeEach(() => {
    
  42.     resetModules();
    
  43.     ControlledInput = class extends React.Component {
    
  44.       static defaultProps = {
    
  45.         type: 'text',
    
  46.         initialValue: 'Hello',
    
  47.       };
    
  48.       constructor() {
    
  49.         super(...arguments);
    
  50.         this.state = {value: this.props.initialValue};
    
  51.       }
    
  52.       handleChange(event) {
    
  53.         if (this.props.onChange) {
    
  54.           this.props.onChange(event);
    
  55.         }
    
  56.         this.setState({value: event.target.value});
    
  57.       }
    
  58.       render() {
    
  59.         return (
    
  60.           <input
    
  61.             type={this.props.type}
    
  62.             value={this.state.value}
    
  63.             onChange={this.handleChange.bind(this)}
    
  64.           />
    
  65.         );
    
  66.       }
    
  67.     };
    
  68.     ControlledTextArea = class extends React.Component {
    
  69.       constructor() {
    
  70.         super();
    
  71.         this.state = {value: 'Hello'};
    
  72.       }
    
  73.       handleChange(event) {
    
  74.         if (this.props.onChange) {
    
  75.           this.props.onChange(event);
    
  76.         }
    
  77.         this.setState({value: event.target.value});
    
  78.       }
    
  79.       render() {
    
  80.         return (
    
  81.           <textarea
    
  82.             value={this.state.value}
    
  83.             onChange={this.handleChange.bind(this)}
    
  84.           />
    
  85.         );
    
  86.       }
    
  87.     };
    
  88.     ControlledCheckbox = class extends React.Component {
    
  89.       constructor() {
    
  90.         super();
    
  91.         this.state = {value: true};
    
  92.       }
    
  93.       handleChange(event) {
    
  94.         if (this.props.onChange) {
    
  95.           this.props.onChange(event);
    
  96.         }
    
  97.         this.setState({value: event.target.checked});
    
  98.       }
    
  99.       render() {
    
  100.         return (
    
  101.           <input
    
  102.             type="checkbox"
    
  103.             checked={this.state.value}
    
  104.             onChange={this.handleChange.bind(this)}
    
  105.           />
    
  106.         );
    
  107.       }
    
  108.     };
    
  109.     ControlledSelect = class extends React.Component {
    
  110.       constructor() {
    
  111.         super();
    
  112.         this.state = {value: 'Hello'};
    
  113.       }
    
  114.       handleChange(event) {
    
  115.         if (this.props.onChange) {
    
  116.           this.props.onChange(event);
    
  117.         }
    
  118.         this.setState({value: event.target.value});
    
  119.       }
    
  120.       render() {
    
  121.         return (
    
  122.           <select
    
  123.             value={this.state.value}
    
  124.             onChange={this.handleChange.bind(this)}>
    
  125.             <option key="1" value="Hello">
    
  126.               Hello
    
  127.             </option>
    
  128.             <option key="2" value="Goodbye">
    
  129.               Goodbye
    
  130.             </option>
    
  131.           </select>
    
  132.         );
    
  133.       }
    
  134.     };
    
  135.   });
    
  136. 
    
  137.   describe('user interaction with controlled inputs', function () {
    
  138.     itClientRenders('a controlled text input', async render => {
    
  139.       const setUntrackedValue = Object.getOwnPropertyDescriptor(
    
  140.         HTMLInputElement.prototype,
    
  141.         'value',
    
  142.       ).set;
    
  143. 
    
  144.       let changeCount = 0;
    
  145.       const e = await render(
    
  146.         <ControlledInput onChange={() => changeCount++} />,
    
  147.       );
    
  148.       const container = e.parentNode;
    
  149.       document.body.appendChild(container);
    
  150. 
    
  151.       try {
    
  152.         expect(changeCount).toBe(0);
    
  153.         expect(e.value).toBe('Hello');
    
  154. 
    
  155.         // simulate a user typing.
    
  156.         setUntrackedValue.call(e, 'Goodbye');
    
  157.         e.dispatchEvent(new Event('input', {bubbles: true, cancelable: false}));
    
  158. 
    
  159.         expect(changeCount).toBe(1);
    
  160.         expect(e.value).toBe('Goodbye');
    
  161.       } finally {
    
  162.         document.body.removeChild(container);
    
  163.       }
    
  164.     });
    
  165. 
    
  166.     itClientRenders('a controlled textarea', async render => {
    
  167.       const setUntrackedValue = Object.getOwnPropertyDescriptor(
    
  168.         HTMLTextAreaElement.prototype,
    
  169.         'value',
    
  170.       ).set;
    
  171. 
    
  172.       let changeCount = 0;
    
  173.       const e = await render(
    
  174.         <ControlledTextArea onChange={() => changeCount++} />,
    
  175.       );
    
  176.       const container = e.parentNode;
    
  177.       document.body.appendChild(container);
    
  178. 
    
  179.       try {
    
  180.         expect(changeCount).toBe(0);
    
  181.         expect(e.value).toBe('Hello');
    
  182. 
    
  183.         // simulate a user typing.
    
  184.         setUntrackedValue.call(e, 'Goodbye');
    
  185.         e.dispatchEvent(new Event('input', {bubbles: true, cancelable: false}));
    
  186. 
    
  187.         expect(changeCount).toBe(1);
    
  188.         expect(e.value).toBe('Goodbye');
    
  189.       } finally {
    
  190.         document.body.removeChild(container);
    
  191.       }
    
  192.     });
    
  193. 
    
  194.     itClientRenders('a controlled checkbox', async render => {
    
  195.       let changeCount = 0;
    
  196.       const e = await render(
    
  197.         <ControlledCheckbox onChange={() => changeCount++} />,
    
  198.       );
    
  199.       const container = e.parentNode;
    
  200.       document.body.appendChild(container);
    
  201. 
    
  202.       try {
    
  203.         expect(changeCount).toBe(0);
    
  204.         expect(e.checked).toBe(true);
    
  205. 
    
  206.         // simulate a user clicking.
    
  207.         e.click();
    
  208. 
    
  209.         expect(changeCount).toBe(1);
    
  210.         expect(e.checked).toBe(false);
    
  211.       } finally {
    
  212.         document.body.removeChild(container);
    
  213.       }
    
  214.     });
    
  215. 
    
  216.     itClientRenders('a controlled select', async render => {
    
  217.       const setUntrackedValue = Object.getOwnPropertyDescriptor(
    
  218.         HTMLSelectElement.prototype,
    
  219.         'value',
    
  220.       ).set;
    
  221. 
    
  222.       let changeCount = 0;
    
  223.       const e = await render(
    
  224.         <ControlledSelect onChange={() => changeCount++} />,
    
  225.       );
    
  226.       const container = e.parentNode;
    
  227.       document.body.appendChild(container);
    
  228. 
    
  229.       try {
    
  230.         expect(changeCount).toBe(0);
    
  231.         expect(e.value).toBe('Hello');
    
  232. 
    
  233.         // simulate a user typing.
    
  234.         setUntrackedValue.call(e, 'Goodbye');
    
  235.         e.dispatchEvent(
    
  236.           new Event('change', {bubbles: true, cancelable: false}),
    
  237.         );
    
  238. 
    
  239.         expect(changeCount).toBe(1);
    
  240.         expect(e.value).toBe('Goodbye');
    
  241.       } finally {
    
  242.         document.body.removeChild(container);
    
  243.       }
    
  244.     });
    
  245.   });
    
  246. 
    
  247.   describe('user interaction with inputs before client render', function () {
    
  248.     // renders the element and changes the value **before** the client
    
  249.     // code has a chance to render; this simulates what happens when a
    
  250.     // user starts to interact with a server-rendered form before
    
  251.     // ReactDOM.render is called. the client render should NOT blow away
    
  252.     // the changes the user has made.
    
  253.     const testUserInteractionBeforeClientRender = async (
    
  254.       element,
    
  255.       initialValue = 'Hello',
    
  256.       changedValue = 'Goodbye',
    
  257.       valueKey = 'value',
    
  258.     ) => {
    
  259.       const field = await serverRender(element);
    
  260.       expect(field[valueKey]).toBe(initialValue);
    
  261. 
    
  262.       // simulate a user typing in the field **before** client-side reconnect happens.
    
  263.       field[valueKey] = changedValue;
    
  264. 
    
  265.       resetModules();
    
  266.       // client render on top of the server markup.
    
  267.       const clientField = await renderIntoDom(element, field.parentNode, true);
    
  268.       // verify that the input field was not replaced.
    
  269.       // Note that we cannot use expect(clientField).toBe(field) because
    
  270.       // of jest bug #1772
    
  271.       expect(clientField === field).toBe(true);
    
  272.       // confirm that the client render has not changed what the user typed.
    
  273.       expect(clientField[valueKey]).toBe(changedValue);
    
  274.     };
    
  275. 
    
  276.     it('should not blow away user-entered text on successful reconnect to an uncontrolled input', () =>
    
  277.       testUserInteractionBeforeClientRender(<input defaultValue="Hello" />));
    
  278. 
    
  279.     it('should not blow away user-entered text on successful reconnect to a controlled input', async () => {
    
  280.       let changeCount = 0;
    
  281.       await testUserInteractionBeforeClientRender(
    
  282.         <ControlledInput onChange={() => changeCount++} />,
    
  283.       );
    
  284.       // note that there's a strong argument to be made that the DOM revival
    
  285.       // algorithm should notice that the user has changed the value and fire
    
  286.       // an onChange. however, it does not now, so that's what this tests.
    
  287.       expect(changeCount).toBe(0);
    
  288.     });
    
  289. 
    
  290.     it('should not blow away user-interaction on successful reconnect to an uncontrolled range input', () =>
    
  291.       testUserInteractionBeforeClientRender(
    
  292.         <input type="text" defaultValue="0.5" />,
    
  293.         '0.5',
    
  294.         '1',
    
  295.       ));
    
  296. 
    
  297.     it('should not blow away user-interaction on successful reconnect to a controlled range input', async () => {
    
  298.       let changeCount = 0;
    
  299.       await testUserInteractionBeforeClientRender(
    
  300.         <ControlledInput
    
  301.           type="range"
    
  302.           initialValue="0.25"
    
  303.           onChange={() => changeCount++}
    
  304.         />,
    
  305.         '0.25',
    
  306.         '1',
    
  307.       );
    
  308.       expect(changeCount).toBe(0);
    
  309.     });
    
  310. 
    
  311.     it('should not blow away user-entered text on successful reconnect to an uncontrolled checkbox', () =>
    
  312.       testUserInteractionBeforeClientRender(
    
  313.         <input type="checkbox" defaultChecked={true} />,
    
  314.         true,
    
  315.         false,
    
  316.         'checked',
    
  317.       ));
    
  318. 
    
  319.     it('should not blow away user-entered text on successful reconnect to a controlled checkbox', async () => {
    
  320.       let changeCount = 0;
    
  321.       await testUserInteractionBeforeClientRender(
    
  322.         <ControlledCheckbox onChange={() => changeCount++} />,
    
  323.         true,
    
  324.         false,
    
  325.         'checked',
    
  326.       );
    
  327.       expect(changeCount).toBe(0);
    
  328.     });
    
  329. 
    
  330.     // skipping this test because React 15 does the wrong thing. it blows
    
  331.     // away the user's typing in the textarea.
    
  332.     xit('should not blow away user-entered text on successful reconnect to an uncontrolled textarea', () =>
    
  333.       testUserInteractionBeforeClientRender(<textarea defaultValue="Hello" />));
    
  334. 
    
  335.     // skipping this test because React 15 does the wrong thing. it blows
    
  336.     // away the user's typing in the textarea.
    
  337.     xit('should not blow away user-entered text on successful reconnect to a controlled textarea', async () => {
    
  338.       let changeCount = 0;
    
  339.       await testUserInteractionBeforeClientRender(
    
  340.         <ControlledTextArea onChange={() => changeCount++} />,
    
  341.       );
    
  342.       expect(changeCount).toBe(0);
    
  343.     });
    
  344. 
    
  345.     it('should not blow away user-selected value on successful reconnect to an uncontrolled select', () =>
    
  346.       testUserInteractionBeforeClientRender(
    
  347.         <select defaultValue="Hello">
    
  348.           <option key="1" value="Hello">
    
  349.             Hello
    
  350.           </option>
    
  351.           <option key="2" value="Goodbye">
    
  352.             Goodbye
    
  353.           </option>
    
  354.         </select>,
    
  355.       ));
    
  356. 
    
  357.     it('should not blow away user-selected value on successful reconnect to an controlled select', async () => {
    
  358.       let changeCount = 0;
    
  359.       await testUserInteractionBeforeClientRender(
    
  360.         <ControlledSelect onChange={() => changeCount++} />,
    
  361.       );
    
  362.       expect(changeCount).toBe(0);
    
  363.     });
    
  364.   });
    
  365. });