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 ReactDOMClient;
    
  15. let Scheduler;
    
  16. let act;
    
  17. let waitForAll;
    
  18. let waitForDiscrete;
    
  19. let assertLog;
    
  20. 
    
  21. const setUntrackedChecked = Object.getOwnPropertyDescriptor(
    
  22.   HTMLInputElement.prototype,
    
  23.   'checked',
    
  24. ).set;
    
  25. 
    
  26. const setUntrackedValue = Object.getOwnPropertyDescriptor(
    
  27.   HTMLInputElement.prototype,
    
  28.   'value',
    
  29. ).set;
    
  30. 
    
  31. const setUntrackedTextareaValue = Object.getOwnPropertyDescriptor(
    
  32.   HTMLTextAreaElement.prototype,
    
  33.   'value',
    
  34. ).set;
    
  35. 
    
  36. describe('ChangeEventPlugin', () => {
    
  37.   let container;
    
  38. 
    
  39.   beforeEach(() => {
    
  40.     jest.resetModules();
    
  41.     // TODO pull this into helper method, reduce repetition.
    
  42.     // mock the browser APIs which are used in schedule:
    
  43.     // - calling 'window.postMessage' should actually fire postmessage handlers
    
  44.     const originalAddEventListener = global.addEventListener;
    
  45.     let postMessageCallback;
    
  46.     global.addEventListener = function (eventName, callback, useCapture) {
    
  47.       if (eventName === 'message') {
    
  48.         postMessageCallback = callback;
    
  49.       } else {
    
  50.         originalAddEventListener(eventName, callback, useCapture);
    
  51.       }
    
  52.     };
    
  53.     global.postMessage = function (messageKey, targetOrigin) {
    
  54.       const postMessageEvent = {source: window, data: messageKey};
    
  55.       if (postMessageCallback) {
    
  56.         postMessageCallback(postMessageEvent);
    
  57.       }
    
  58.     };
    
  59.     React = require('react');
    
  60.     ReactDOM = require('react-dom');
    
  61.     ReactDOMClient = require('react-dom/client');
    
  62.     act = require('internal-test-utils').act;
    
  63.     Scheduler = require('scheduler');
    
  64. 
    
  65.     const InternalTestUtils = require('internal-test-utils');
    
  66.     waitForAll = InternalTestUtils.waitForAll;
    
  67.     waitForDiscrete = InternalTestUtils.waitForDiscrete;
    
  68.     assertLog = InternalTestUtils.assertLog;
    
  69. 
    
  70.     container = document.createElement('div');
    
  71.     document.body.appendChild(container);
    
  72.   });
    
  73. 
    
  74.   afterEach(() => {
    
  75.     document.body.removeChild(container);
    
  76.     container = null;
    
  77.   });
    
  78. 
    
  79.   // We try to avoid firing "duplicate" React change events.
    
  80.   // However, to tell which events are "duplicates" and should be ignored,
    
  81.   // we are tracking the "current" input value, and only respect events
    
  82.   // that occur after it changes. In most of these tests, we verify that we
    
  83.   // keep track of the "current" value and only fire events when it changes.
    
  84.   // See https://github.com/facebook/react/pull/5746.
    
  85. 
    
  86.   it('should consider initial text value to be current', () => {
    
  87.     let called = 0;
    
  88. 
    
  89.     function cb(e) {
    
  90.       called++;
    
  91.       expect(e.type).toBe('change');
    
  92.     }
    
  93. 
    
  94.     const node = ReactDOM.render(
    
  95.       <input type="text" onChange={cb} defaultValue="foo" />,
    
  96.       container,
    
  97.     );
    
  98.     node.dispatchEvent(new Event('input', {bubbles: true, cancelable: true}));
    
  99.     node.dispatchEvent(new Event('change', {bubbles: true, cancelable: true}));
    
  100. 
    
  101.     // There should be no React change events because the value stayed the same.
    
  102.     expect(called).toBe(0);
    
  103.   });
    
  104. 
    
  105.   it('should consider initial text value to be current (capture)', () => {
    
  106.     let called = 0;
    
  107. 
    
  108.     function cb(e) {
    
  109.       called++;
    
  110.       expect(e.type).toBe('change');
    
  111.     }
    
  112. 
    
  113.     const node = ReactDOM.render(
    
  114.       <input type="text" onChangeCapture={cb} defaultValue="foo" />,
    
  115.       container,
    
  116.     );
    
  117.     node.dispatchEvent(new Event('input', {bubbles: true, cancelable: true}));
    
  118.     node.dispatchEvent(new Event('change', {bubbles: true, cancelable: true}));
    
  119. 
    
  120.     // There should be no React change events because the value stayed the same.
    
  121.     expect(called).toBe(0);
    
  122.   });
    
  123. 
    
  124.   it('should not invoke a change event for textarea same value', () => {
    
  125.     let called = 0;
    
  126. 
    
  127.     function cb(e) {
    
  128.       called++;
    
  129.       expect(e.type).toBe('change');
    
  130.     }
    
  131. 
    
  132.     const node = ReactDOM.render(
    
  133.       <textarea onChange={cb} defaultValue="initial" />,
    
  134.       container,
    
  135.     );
    
  136.     node.dispatchEvent(new Event('input', {bubbles: true, cancelable: true}));
    
  137.     node.dispatchEvent(new Event('change', {bubbles: true, cancelable: true}));
    
  138.     // There should be no React change events because the value stayed the same.
    
  139.     expect(called).toBe(0);
    
  140.   });
    
  141. 
    
  142.   it('should not invoke a change event for textarea same value (capture)', () => {
    
  143.     let called = 0;
    
  144. 
    
  145.     function cb(e) {
    
  146.       called++;
    
  147.       expect(e.type).toBe('change');
    
  148.     }
    
  149. 
    
  150.     const node = ReactDOM.render(
    
  151.       <textarea onChangeCapture={cb} defaultValue="initial" />,
    
  152.       container,
    
  153.     );
    
  154.     node.dispatchEvent(new Event('input', {bubbles: true, cancelable: true}));
    
  155.     node.dispatchEvent(new Event('change', {bubbles: true, cancelable: true}));
    
  156.     // There should be no React change events because the value stayed the same.
    
  157.     expect(called).toBe(0);
    
  158.   });
    
  159. 
    
  160.   it('should consider initial checkbox checked=true to be current', () => {
    
  161.     let called = 0;
    
  162. 
    
  163.     function cb(e) {
    
  164.       called++;
    
  165.       expect(e.type).toBe('change');
    
  166.     }
    
  167. 
    
  168.     const node = ReactDOM.render(
    
  169.       <input type="checkbox" onChange={cb} defaultChecked={true} />,
    
  170.       container,
    
  171.     );
    
  172. 
    
  173.     // Secretly, set `checked` to false, so that dispatching the `click` will
    
  174.     // make it `true` again. Thus, at the time of the event, React should not
    
  175.     // consider it a change from the initial `true` value.
    
  176.     setUntrackedChecked.call(node, false);
    
  177.     node.dispatchEvent(
    
  178.       new MouseEvent('click', {bubbles: true, cancelable: true}),
    
  179.     );
    
  180.     // There should be no React change events because the value stayed the same.
    
  181.     expect(called).toBe(0);
    
  182.   });
    
  183. 
    
  184.   it('should consider initial checkbox checked=false to be current', () => {
    
  185.     let called = 0;
    
  186. 
    
  187.     function cb(e) {
    
  188.       called++;
    
  189.       expect(e.type).toBe('change');
    
  190.     }
    
  191. 
    
  192.     const node = ReactDOM.render(
    
  193.       <input type="checkbox" onChange={cb} defaultChecked={false} />,
    
  194.       container,
    
  195.     );
    
  196. 
    
  197.     // Secretly, set `checked` to true, so that dispatching the `click` will
    
  198.     // make it `false` again. Thus, at the time of the event, React should not
    
  199.     // consider it a change from the initial `false` value.
    
  200.     setUntrackedChecked.call(node, true);
    
  201.     node.dispatchEvent(
    
  202.       new MouseEvent('click', {bubbles: true, cancelable: true}),
    
  203.     );
    
  204.     // There should be no React change events because the value stayed the same.
    
  205.     expect(called).toBe(0);
    
  206.   });
    
  207. 
    
  208.   it('should fire change for checkbox input', () => {
    
  209.     let called = 0;
    
  210. 
    
  211.     function cb(e) {
    
  212.       called++;
    
  213.       expect(e.type).toBe('change');
    
  214.     }
    
  215. 
    
  216.     const node = ReactDOM.render(
    
  217.       <input type="checkbox" onChange={cb} />,
    
  218.       container,
    
  219.     );
    
  220. 
    
  221.     expect(node.checked).toBe(false);
    
  222.     node.dispatchEvent(
    
  223.       new MouseEvent('click', {bubbles: true, cancelable: true}),
    
  224.     );
    
  225.     // Note: unlike with text input events, dispatching `click` actually
    
  226.     // toggles the checkbox and updates its `checked` value.
    
  227.     expect(node.checked).toBe(true);
    
  228.     expect(called).toBe(1);
    
  229. 
    
  230.     expect(node.checked).toBe(true);
    
  231.     node.dispatchEvent(
    
  232.       new MouseEvent('click', {bubbles: true, cancelable: true}),
    
  233.     );
    
  234.     expect(node.checked).toBe(false);
    
  235.     expect(called).toBe(2);
    
  236.   });
    
  237. 
    
  238.   it('should not fire change setting the value programmatically', () => {
    
  239.     let called = 0;
    
  240. 
    
  241.     function cb(e) {
    
  242.       called++;
    
  243.       expect(e.type).toBe('change');
    
  244.     }
    
  245. 
    
  246.     const input = ReactDOM.render(
    
  247.       <input type="text" defaultValue="foo" onChange={cb} />,
    
  248.       container,
    
  249.     );
    
  250. 
    
  251.     // Set it programmatically.
    
  252.     input.value = 'bar';
    
  253.     // Even if a DOM input event fires, React sees that the real input value now
    
  254.     // ('bar') is the same as the "current" one we already recorded.
    
  255.     input.dispatchEvent(new Event('input', {bubbles: true, cancelable: true}));
    
  256.     expect(input.value).toBe('bar');
    
  257.     // In this case we don't expect to get a React event.
    
  258.     expect(called).toBe(0);
    
  259. 
    
  260.     // However, we can simulate user typing by calling the underlying setter.
    
  261.     setUntrackedValue.call(input, 'foo');
    
  262.     // Now, when the event fires, the real input value ('foo') differs from the
    
  263.     // "current" one we previously recorded ('bar').
    
  264.     input.dispatchEvent(new Event('input', {bubbles: true, cancelable: true}));
    
  265.     expect(input.value).toBe('foo');
    
  266.     // In this case React should fire an event for it.
    
  267.     expect(called).toBe(1);
    
  268. 
    
  269.     // Verify again that extra events without real changes are ignored.
    
  270.     input.dispatchEvent(new Event('input', {bubbles: true, cancelable: true}));
    
  271.     expect(called).toBe(1);
    
  272.   });
    
  273. 
    
  274.   it('should not distinguish equal string and number values', () => {
    
  275.     let called = 0;
    
  276. 
    
  277.     function cb(e) {
    
  278.       called++;
    
  279.       expect(e.type).toBe('change');
    
  280.     }
    
  281. 
    
  282.     const input = ReactDOM.render(
    
  283.       <input type="text" defaultValue="42" onChange={cb} />,
    
  284.       container,
    
  285.     );
    
  286. 
    
  287.     // When we set `value` as a property, React updates the "current" value
    
  288.     // that it tracks internally. The "current" value is later used to determine
    
  289.     // whether a change event is a duplicate or not.
    
  290.     // Even though we set value to a number, we still shouldn't get a change
    
  291.     // event because as a string, it's equal to the initial value ('42').
    
  292.     input.value = 42;
    
  293.     input.dispatchEvent(new Event('input', {bubbles: true, cancelable: true}));
    
  294.     expect(input.value).toBe('42');
    
  295.     expect(called).toBe(0);
    
  296.   });
    
  297. 
    
  298.   // See a similar input test above for a detailed description of why.
    
  299.   it('should not fire change when setting checked programmatically', () => {
    
  300.     let called = 0;
    
  301. 
    
  302.     function cb(e) {
    
  303.       called++;
    
  304.       expect(e.type).toBe('change');
    
  305.     }
    
  306. 
    
  307.     const input = ReactDOM.render(
    
  308.       <input type="checkbox" onChange={cb} defaultChecked={false} />,
    
  309.       container,
    
  310.     );
    
  311. 
    
  312.     // Set the value, updating the "current" value that React tracks to true.
    
  313.     input.checked = true;
    
  314.     // Under the hood, uncheck the box so that the click will "check" it again.
    
  315.     setUntrackedChecked.call(input, false);
    
  316.     input.click();
    
  317.     expect(input.checked).toBe(true);
    
  318.     // We don't expect a React event because at the time of the click, the real
    
  319.     // checked value (true) was the same as the last recorded "current" value
    
  320.     // (also true).
    
  321.     expect(called).toBe(0);
    
  322. 
    
  323.     // However, simulating a normal click should fire a React event because the
    
  324.     // real value (false) would have changed from the last tracked value (true).
    
  325.     input.click();
    
  326.     expect(called).toBe(1);
    
  327.   });
    
  328. 
    
  329.   it('should unmount', () => {
    
  330.     const input = ReactDOM.render(<input />, container);
    
  331. 
    
  332.     ReactDOM.unmountComponentAtNode(container);
    
  333.   });
    
  334. 
    
  335.   it('should only fire change for checked radio button once', () => {
    
  336.     let called = 0;
    
  337. 
    
  338.     function cb(e) {
    
  339.       called++;
    
  340.       expect(e.type).toBe('change');
    
  341.     }
    
  342. 
    
  343.     const input = ReactDOM.render(
    
  344.       <input type="radio" onChange={cb} />,
    
  345.       container,
    
  346.     );
    
  347. 
    
  348.     setUntrackedChecked.call(input, true);
    
  349.     input.dispatchEvent(new Event('click', {bubbles: true, cancelable: true}));
    
  350.     input.dispatchEvent(new Event('click', {bubbles: true, cancelable: true}));
    
  351.     expect(called).toBe(1);
    
  352.   });
    
  353. 
    
  354.   it('should track radio button cousins in a group', () => {
    
  355.     let called1 = 0;
    
  356.     let called2 = 0;
    
  357. 
    
  358.     function cb1(e) {
    
  359.       called1++;
    
  360.       expect(e.type).toBe('change');
    
  361.     }
    
  362. 
    
  363.     function cb2(e) {
    
  364.       called2++;
    
  365.       expect(e.type).toBe('change');
    
  366.     }
    
  367. 
    
  368.     const div = ReactDOM.render(
    
  369.       <div>
    
  370.         <input type="radio" name="group" onChange={cb1} />
    
  371.         <input type="radio" name="group" onChange={cb2} />
    
  372.       </div>,
    
  373.       container,
    
  374.     );
    
  375.     const option1 = div.childNodes[0];
    
  376.     const option2 = div.childNodes[1];
    
  377. 
    
  378.     // Select first option.
    
  379.     option1.click();
    
  380.     expect(called1).toBe(1);
    
  381.     expect(called2).toBe(0);
    
  382. 
    
  383.     // Select second option.
    
  384.     option2.click();
    
  385.     expect(called1).toBe(1);
    
  386.     expect(called2).toBe(1);
    
  387. 
    
  388.     // Select the first option.
    
  389.     // It should receive the React change event again.
    
  390.     option1.click();
    
  391.     expect(called1).toBe(2);
    
  392.     expect(called2).toBe(1);
    
  393.   });
    
  394. 
    
  395.   it('should deduplicate input value change events', () => {
    
  396.     let called = 0;
    
  397. 
    
  398.     function cb(e) {
    
  399.       called++;
    
  400.       expect(e.type).toBe('change');
    
  401.     }
    
  402. 
    
  403.     let input;
    
  404.     ['text', 'number', 'range'].forEach(type => {
    
  405.       called = 0;
    
  406.       input = ReactDOM.render(<input type={type} onChange={cb} />, container);
    
  407.       // Should be ignored (no change):
    
  408.       input.dispatchEvent(
    
  409.         new Event('change', {bubbles: true, cancelable: true}),
    
  410.       );
    
  411.       setUntrackedValue.call(input, '42');
    
  412.       input.dispatchEvent(
    
  413.         new Event('change', {bubbles: true, cancelable: true}),
    
  414.       );
    
  415.       // Should be ignored (no change):
    
  416.       input.dispatchEvent(
    
  417.         new Event('change', {bubbles: true, cancelable: true}),
    
  418.       );
    
  419.       expect(called).toBe(1);
    
  420.       ReactDOM.unmountComponentAtNode(container);
    
  421. 
    
  422.       called = 0;
    
  423.       input = ReactDOM.render(<input type={type} onChange={cb} />, container);
    
  424.       // Should be ignored (no change):
    
  425.       input.dispatchEvent(
    
  426.         new Event('input', {bubbles: true, cancelable: true}),
    
  427.       );
    
  428.       setUntrackedValue.call(input, '42');
    
  429.       input.dispatchEvent(
    
  430.         new Event('input', {bubbles: true, cancelable: true}),
    
  431.       );
    
  432.       // Should be ignored (no change):
    
  433.       input.dispatchEvent(
    
  434.         new Event('input', {bubbles: true, cancelable: true}),
    
  435.       );
    
  436.       expect(called).toBe(1);
    
  437.       ReactDOM.unmountComponentAtNode(container);
    
  438. 
    
  439.       called = 0;
    
  440.       input = ReactDOM.render(<input type={type} onChange={cb} />, container);
    
  441.       // Should be ignored (no change):
    
  442.       input.dispatchEvent(
    
  443.         new Event('change', {bubbles: true, cancelable: true}),
    
  444.       );
    
  445.       setUntrackedValue.call(input, '42');
    
  446.       input.dispatchEvent(
    
  447.         new Event('input', {bubbles: true, cancelable: true}),
    
  448.       );
    
  449.       // Should be ignored (no change):
    
  450.       input.dispatchEvent(
    
  451.         new Event('change', {bubbles: true, cancelable: true}),
    
  452.       );
    
  453.       expect(called).toBe(1);
    
  454.       ReactDOM.unmountComponentAtNode(container);
    
  455.     });
    
  456.   });
    
  457. 
    
  458.   it('should listen for both change and input events when supported', () => {
    
  459.     let called = 0;
    
  460. 
    
  461.     function cb(e) {
    
  462.       called++;
    
  463.       expect(e.type).toBe('change');
    
  464.     }
    
  465. 
    
  466.     const input = ReactDOM.render(
    
  467.       <input type="range" onChange={cb} />,
    
  468.       container,
    
  469.     );
    
  470. 
    
  471.     setUntrackedValue.call(input, 10);
    
  472.     input.dispatchEvent(new Event('input', {bubbles: true, cancelable: true}));
    
  473. 
    
  474.     setUntrackedValue.call(input, 20);
    
  475.     input.dispatchEvent(new Event('change', {bubbles: true, cancelable: true}));
    
  476. 
    
  477.     expect(called).toBe(2);
    
  478.   });
    
  479. 
    
  480.   it('should only fire events when the value changes for range inputs', () => {
    
  481.     let called = 0;
    
  482. 
    
  483.     function cb(e) {
    
  484.       called++;
    
  485.       expect(e.type).toBe('change');
    
  486.     }
    
  487. 
    
  488.     const input = ReactDOM.render(
    
  489.       <input type="range" onChange={cb} />,
    
  490.       container,
    
  491.     );
    
  492.     setUntrackedValue.call(input, '40');
    
  493.     input.dispatchEvent(new Event('input', {bubbles: true, cancelable: true}));
    
  494.     input.dispatchEvent(new Event('change', {bubbles: true, cancelable: true}));
    
  495. 
    
  496.     setUntrackedValue.call(input, 'foo');
    
  497.     input.dispatchEvent(new Event('input', {bubbles: true, cancelable: true}));
    
  498.     input.dispatchEvent(new Event('change', {bubbles: true, cancelable: true}));
    
  499. 
    
  500.     expect(called).toBe(2);
    
  501.   });
    
  502. 
    
  503.   it('does not crash for nodes with custom value property', () => {
    
  504.     let originalCreateElement;
    
  505.     // https://github.com/facebook/react/issues/10196
    
  506.     try {
    
  507.       originalCreateElement = document.createElement;
    
  508.       document.createElement = function () {
    
  509.         const node = originalCreateElement.apply(this, arguments);
    
  510.         Object.defineProperty(node, 'value', {
    
  511.           get() {},
    
  512.           set() {},
    
  513.         });
    
  514.         return node;
    
  515.       };
    
  516.       const div = document.createElement('div');
    
  517.       // Mount
    
  518.       const node = ReactDOM.render(<input type="text" />, div);
    
  519.       // Update
    
  520.       ReactDOM.render(<input type="text" />, div);
    
  521.       // Change
    
  522.       node.dispatchEvent(
    
  523.         new Event('change', {bubbles: true, cancelable: true}),
    
  524.       );
    
  525.       // Unmount
    
  526.       ReactDOM.unmountComponentAtNode(div);
    
  527.     } finally {
    
  528.       document.createElement = originalCreateElement;
    
  529.     }
    
  530.   });
    
  531. 
    
  532.   describe('concurrent mode', () => {
    
  533.     it('text input', async () => {
    
  534.       const root = ReactDOMClient.createRoot(container);
    
  535.       let input;
    
  536. 
    
  537.       class ControlledInput extends React.Component {
    
  538.         state = {value: 'initial'};
    
  539.         onChange = event => this.setState({value: event.target.value});
    
  540.         render() {
    
  541.           Scheduler.log(`render: ${this.state.value}`);
    
  542.           const controlledValue =
    
  543.             this.state.value === 'changed' ? 'changed [!]' : this.state.value;
    
  544.           return (
    
  545.             <input
    
  546.               ref={el => (input = el)}
    
  547.               type="text"
    
  548.               value={controlledValue}
    
  549.               onChange={this.onChange}
    
  550.             />
    
  551.           );
    
  552.         }
    
  553.       }
    
  554. 
    
  555.       // Initial mount. Test that this is async.
    
  556.       root.render(<ControlledInput />);
    
  557.       // Should not have flushed yet.
    
  558.       assertLog([]);
    
  559.       expect(input).toBe(undefined);
    
  560.       // Flush callbacks.
    
  561.       await waitForAll(['render: initial']);
    
  562.       expect(input.value).toBe('initial');
    
  563. 
    
  564.       // Trigger a change event.
    
  565.       setUntrackedValue.call(input, 'changed');
    
  566.       input.dispatchEvent(
    
  567.         new Event('input', {bubbles: true, cancelable: true}),
    
  568.       );
    
  569.       // Change should synchronously flush
    
  570.       assertLog(['render: changed']);
    
  571.       // Value should be the controlled value, not the original one
    
  572.       expect(input.value).toBe('changed [!]');
    
  573.     });
    
  574. 
    
  575.     it('checkbox input', async () => {
    
  576.       const root = ReactDOMClient.createRoot(container);
    
  577.       let input;
    
  578. 
    
  579.       class ControlledInput extends React.Component {
    
  580.         state = {checked: false};
    
  581.         onChange = event => {
    
  582.           this.setState({checked: event.target.checked});
    
  583.         };
    
  584.         render() {
    
  585.           Scheduler.log(`render: ${this.state.checked}`);
    
  586.           const controlledValue = this.props.reverse
    
  587.             ? !this.state.checked
    
  588.             : this.state.checked;
    
  589.           return (
    
  590.             <input
    
  591.               ref={el => (input = el)}
    
  592.               type="checkbox"
    
  593.               checked={controlledValue}
    
  594.               onChange={this.onChange}
    
  595.             />
    
  596.           );
    
  597.         }
    
  598.       }
    
  599. 
    
  600.       // Initial mount. Test that this is async.
    
  601.       root.render(<ControlledInput reverse={false} />);
    
  602.       // Should not have flushed yet.
    
  603.       assertLog([]);
    
  604.       expect(input).toBe(undefined);
    
  605.       // Flush callbacks.
    
  606.       await waitForAll(['render: false']);
    
  607.       expect(input.checked).toBe(false);
    
  608. 
    
  609.       // Trigger a change event.
    
  610.       input.dispatchEvent(
    
  611.         new MouseEvent('click', {bubbles: true, cancelable: true}),
    
  612.       );
    
  613.       // Change should synchronously flush
    
  614.       assertLog(['render: true']);
    
  615.       expect(input.checked).toBe(true);
    
  616. 
    
  617.       // Now let's make sure we're using the controlled value.
    
  618.       root.render(<ControlledInput reverse={true} />);
    
  619.       await waitForAll(['render: true']);
    
  620. 
    
  621.       // Trigger another change event.
    
  622.       input.dispatchEvent(
    
  623.         new MouseEvent('click', {bubbles: true, cancelable: true}),
    
  624.       );
    
  625.       // Change should synchronously flush
    
  626.       assertLog(['render: true']);
    
  627.       expect(input.checked).toBe(false);
    
  628.     });
    
  629. 
    
  630.     it('textarea', async () => {
    
  631.       const root = ReactDOMClient.createRoot(container);
    
  632.       let textarea;
    
  633. 
    
  634.       class ControlledTextarea extends React.Component {
    
  635.         state = {value: 'initial'};
    
  636.         onChange = event => this.setState({value: event.target.value});
    
  637.         render() {
    
  638.           Scheduler.log(`render: ${this.state.value}`);
    
  639.           const controlledValue =
    
  640.             this.state.value === 'changed' ? 'changed [!]' : this.state.value;
    
  641.           return (
    
  642.             <textarea
    
  643.               ref={el => (textarea = el)}
    
  644.               type="text"
    
  645.               value={controlledValue}
    
  646.               onChange={this.onChange}
    
  647.             />
    
  648.           );
    
  649.         }
    
  650.       }
    
  651. 
    
  652.       // Initial mount. Test that this is async.
    
  653.       root.render(<ControlledTextarea />);
    
  654.       // Should not have flushed yet.
    
  655.       assertLog([]);
    
  656.       expect(textarea).toBe(undefined);
    
  657.       // Flush callbacks.
    
  658.       await waitForAll(['render: initial']);
    
  659.       expect(textarea.value).toBe('initial');
    
  660. 
    
  661.       // Trigger a change event.
    
  662.       setUntrackedTextareaValue.call(textarea, 'changed');
    
  663.       textarea.dispatchEvent(
    
  664.         new Event('input', {bubbles: true, cancelable: true}),
    
  665.       );
    
  666.       // Change should synchronously flush
    
  667.       assertLog(['render: changed']);
    
  668.       // Value should be the controlled value, not the original one
    
  669.       expect(textarea.value).toBe('changed [!]');
    
  670.     });
    
  671. 
    
  672.     it('parent of input', async () => {
    
  673.       const root = ReactDOMClient.createRoot(container);
    
  674.       let input;
    
  675. 
    
  676.       class ControlledInput extends React.Component {
    
  677.         state = {value: 'initial'};
    
  678.         onChange = event => this.setState({value: event.target.value});
    
  679.         render() {
    
  680.           Scheduler.log(`render: ${this.state.value}`);
    
  681.           const controlledValue =
    
  682.             this.state.value === 'changed' ? 'changed [!]' : this.state.value;
    
  683.           return (
    
  684.             <div onChange={this.onChange}>
    
  685.               <input
    
  686.                 ref={el => (input = el)}
    
  687.                 type="text"
    
  688.                 value={controlledValue}
    
  689.                 onChange={() => {
    
  690.                   // Does nothing. Parent handler is responsible for updating.
    
  691.                 }}
    
  692.               />
    
  693.             </div>
    
  694.           );
    
  695.         }
    
  696.       }
    
  697. 
    
  698.       // Initial mount. Test that this is async.
    
  699.       root.render(<ControlledInput />);
    
  700.       // Should not have flushed yet.
    
  701.       assertLog([]);
    
  702.       expect(input).toBe(undefined);
    
  703.       // Flush callbacks.
    
  704.       await waitForAll(['render: initial']);
    
  705.       expect(input.value).toBe('initial');
    
  706. 
    
  707.       // Trigger a change event.
    
  708.       setUntrackedValue.call(input, 'changed');
    
  709.       input.dispatchEvent(
    
  710.         new Event('input', {bubbles: true, cancelable: true}),
    
  711.       );
    
  712.       // Change should synchronously flush
    
  713.       assertLog(['render: changed']);
    
  714.       // Value should be the controlled value, not the original one
    
  715.       expect(input.value).toBe('changed [!]');
    
  716.     });
    
  717. 
    
  718.     it('is sync for non-input events', async () => {
    
  719.       const root = ReactDOMClient.createRoot(container);
    
  720.       let input;
    
  721. 
    
  722.       class ControlledInput extends React.Component {
    
  723.         state = {value: 'initial'};
    
  724.         onChange = event => this.setState({value: event.target.value});
    
  725.         reset = () => {
    
  726.           this.setState({value: ''});
    
  727.         };
    
  728.         render() {
    
  729.           Scheduler.log(`render: ${this.state.value}`);
    
  730.           const controlledValue =
    
  731.             this.state.value === 'changed' ? 'changed [!]' : this.state.value;
    
  732.           return (
    
  733.             <input
    
  734.               ref={el => (input = el)}
    
  735.               type="text"
    
  736.               value={controlledValue}
    
  737.               onChange={this.onChange}
    
  738.               onClick={this.reset}
    
  739.             />
    
  740.           );
    
  741.         }
    
  742.       }
    
  743. 
    
  744.       // Initial mount. Test that this is async.
    
  745.       root.render(<ControlledInput />);
    
  746.       // Should not have flushed yet.
    
  747.       assertLog([]);
    
  748.       expect(input).toBe(undefined);
    
  749.       // Flush callbacks.
    
  750.       await waitForAll(['render: initial']);
    
  751.       expect(input.value).toBe('initial');
    
  752. 
    
  753.       // Trigger a click event
    
  754.       input.dispatchEvent(
    
  755.         new Event('click', {bubbles: true, cancelable: true}),
    
  756.       );
    
  757. 
    
  758.       // Flush microtask queue.
    
  759.       await waitForDiscrete(['render: ']);
    
  760.       expect(input.value).toBe('');
    
  761.     });
    
  762. 
    
  763.     it('mouse enter/leave should be user-blocking but not discrete', async () => {
    
  764.       const {useState} = React;
    
  765. 
    
  766.       const root = ReactDOMClient.createRoot(container);
    
  767. 
    
  768.       const target = React.createRef(null);
    
  769.       function Foo() {
    
  770.         const [isHover, setHover] = useState(false);
    
  771.         return (
    
  772.           <div
    
  773.             ref={target}
    
  774.             onMouseEnter={() => setHover(true)}
    
  775.             onMouseLeave={() => setHover(false)}>
    
  776.             {isHover ? 'hovered' : 'not hovered'}
    
  777.           </div>
    
  778.         );
    
  779.       }
    
  780. 
    
  781.       await act(() => {
    
  782.         root.render(<Foo />);
    
  783.       });
    
  784.       expect(container.textContent).toEqual('not hovered');
    
  785. 
    
  786.       await act(() => {
    
  787.         const mouseOverEvent = document.createEvent('MouseEvents');
    
  788.         mouseOverEvent.initEvent('mouseover', true, true);
    
  789.         target.current.dispatchEvent(mouseOverEvent);
    
  790. 
    
  791.         // Flush discrete updates
    
  792.         ReactDOM.flushSync();
    
  793.         // Since mouse enter/leave is not discrete, should not have updated yet
    
  794.         expect(container.textContent).toEqual('not hovered');
    
  795.       });
    
  796.       expect(container.textContent).toEqual('hovered');
    
  797.     });
    
  798.   });
    
  799. });