text: /**
   * Copyright (c) Facebook, Inc. and its affiliates.
   *
   * This source code is licensed under the MIT license found in the
   * LICENSE file in the root directory of this source tree.
   *
   * @emails react-core
   */
  
  'use strict';
  
  let React = require('react');
  let ReactDOM = require('react-dom');
  let TestUtils = require('react-dom/test-utils');
  let ReactFeatureFlags;
  let Scheduler;
  
  const setUntrackedChecked = Object.getOwnPropertyDescriptor(
    HTMLInputElement.prototype,
    'checked',
  ).set;
  
  const setUntrackedValue = Object.getOwnPropertyDescriptor(
    HTMLInputElement.prototype,
    'value',
  ).set;
  
  const setUntrackedTextareaValue = Object.getOwnPropertyDescriptor(
    HTMLTextAreaElement.prototype,
    'value',
  ).set;
  
  describe('ChangeEventPlugin', () => {
    let container;
  
    beforeEach(() => {
      ReactFeatureFlags = require('shared/ReactFeatureFlags');
      // TODO pull this into helper method, reduce repetition.
      // mock the browser APIs which are used in schedule:
      // - calling 'window.postMessage' should actually fire postmessage handlers
      const originalAddEventListener = global.addEventListener;
      let postMessageCallback;
      global.addEventListener = function(eventName, callback, useCapture) {
        if (eventName === 'message') {
          postMessageCallback = callback;
        } else {
          originalAddEventListener(eventName, callback, useCapture);
        }
      };
      global.postMessage = function(messageKey, targetOrigin) {
        const postMessageEvent = {source: window, data: messageKey};
        if (postMessageCallback) {
          postMessageCallback(postMessageEvent);
        }
      };
      jest.resetModules();
      container = document.createElement('div');
      document.body.appendChild(container);
    });
  
    afterEach(() => {
      document.body.removeChild(container);
      container = null;
    });
  
    // We try to avoid firing "duplicate" React change events.
    // However, to tell which events are "duplicates" and should be ignored,
    // we are tracking the "current" input value, and only respect events
    // that occur after it changes. In most of these tests, we verify that we
    // keep track of the "current" value and only fire events when it changes.
    // See https://github.com/facebook/react/pull/5746.
  
    it('should consider initial text value to be current', () => {
      let called = 0;
  
      function cb(e) {
        called++;
        expect(e.type).toBe('change');
      }
  
      const node = ReactDOM.render(
        <input type="text" onChange={cb} defaultValue="foo" />,
        container,
      );
      node.dispatchEvent(new Event('input', {bubbles: true, cancelable: true}));
      node.dispatchEvent(new Event('change', {bubbles: true, cancelable: true}));
  
      if (ReactFeatureFlags.disableInputAttributeSyncing) {
        // TODO: figure out why. This might be a bug.
        expect(called).toBe(1);
      } else {
        // There should be no React change events because the value stayed the same.
        expect(called).toBe(0);
      }
    });
  
    it('should consider initial checkbox checked=true to be current', () => {
      let called = 0;
  
      function cb(e) {
        called++;
        expect(e.type).toBe('change');
      }
  
      const node = ReactDOM.render(
        <input type="checkbox" onChange={cb} defaultChecked={true} />,
        container,
      );
  
      // Secretly, set `checked` to false, so that dispatching the `click` will
      // make it `true` again. Thus, at the time of the event, React should not
      // consider it a change from the initial `true` value.
      setUntrackedChecked.call(node, false);
      node.dispatchEvent(
        new MouseEvent('click', {bubbles: true, cancelable: true}),
      );
      // There should be no React change events because the value stayed the same.
      expect(called).toBe(0);
    });
  
    it('should consider initial checkbox checked=false to be current', () => {
      let called = 0;
  
      function cb(e) {
        called++;
        expect(e.type).toBe('change');
      }
  
      const node = ReactDOM.render(
        <input type="checkbox" onChange={cb} defaultChecked={false} />,
        container,
      );
  
      // Secretly, set `checked` to true, so that dispatching the `click` will
      // make it `false` again. Thus, at the time of the event, React should not
      // consider it a change from the initial `false` value.
      setUntrackedChecked.call(node, true);
      node.dispatchEvent(
        new MouseEvent('click', {bubbles: true, cancelable: true}),
      );
      // There should be no React change events because the value stayed the same.
      expect(called).toBe(0);
    });
  
    it('should fire change for checkbox input', () => {
      let called = 0;
  
      function cb(e) {
        called++;
        expect(e.type).toBe('change');
      }
  
      const node = ReactDOM.render(
        <input type="checkbox" onChange={cb} />,
        container,
      );
  
      expect(node.checked).toBe(false);
      node.dispatchEvent(
        new MouseEvent('click', {bubbles: true, cancelable: true}),
      );
      // Note: unlike with text input events, dispatching `click` actually
      // toggles the checkbox and updates its `checked` value.
      expect(node.checked).toBe(true);
      expect(called).toBe(1);
  
      expect(node.checked).toBe(true);
      node.dispatchEvent(
        new MouseEvent('click', {bubbles: true, cancelable: true}),
      );
      expect(node.checked).toBe(false);
      expect(called).toBe(2);
    });
  
    it('should not fire change setting the value programmatically', () => {
      let called = 0;
  
      function cb(e) {
        called++;
        expect(e.type).toBe('change');
      }
  
      const input = ReactDOM.render(
        <input type="text" defaultValue="foo" onChange={cb} />,
        container,
      );
  
      // Set it programmatically.
      input.value = 'bar';
      // Even if a DOM input event fires, React sees that the real input value now
      // ('bar') is the same as the "current" one we already recorded.
      input.dispatchEvent(new Event('input', {bubbles: true, cancelable: true}));
      expect(input.value).toBe('bar');
      // In this case we don't expect to get a React event.
      expect(called).toBe(0);
  
      // However, we can simulate user typing by calling the underlying setter.
      setUntrackedValue.call(input, 'foo');
      // Now, when the event fires, the real input value ('foo') differs from the
      // "current" one we previously recorded ('bar').
      input.dispatchEvent(new Event('input', {bubbles: true, cancelable: true}));
      expect(input.value).toBe('foo');
      // In this case React should fire an event for it.
      expect(called).toBe(1);
  
      // Verify again that extra events without real changes are ignored.
      input.dispatchEvent(new Event('input', {bubbles: true, cancelable: true}));
      expect(called).toBe(1);
    });
  
    it('should not distinguish equal string and number values', () => {
      let called = 0;
  
      function cb(e) {
        called++;
        expect(e.type).toBe('change');
      }
  
      const input = ReactDOM.render(
        <input type="text" defaultValue="42" onChange={cb} />,
        container,
      );
  
      // When we set `value` as a property, React updates the "current" value
      // that it tracks internally. The "current" value is later used to determine
      // whether a change event is a duplicate or not.
      // Even though we set value to a number, we still shouldn't get a change
      // event because as a string, it's equal to the initial value ('42').
      input.value = 42;
      input.dispatchEvent(new Event('input', {bubbles: true, cancelable: true}));
      expect(input.value).toBe('42');
      expect(called).toBe(0);
    });
  
    // See a similar input test above for a detailed description of why.
    it('should not fire change when setting checked programmatically', () => {
      let called = 0;
  
      function cb(e) {
        called++;
        expect(e.type).toBe('change');
      }
  
      const input = ReactDOM.render(
        <input type="checkbox" onChange={cb} defaultChecked={false} />,
        container,
      );
  
      // Set the value, updating the "current" value that React tracks to true.
      input.checked = true;
      // Under the hood, uncheck the box so that the click will "check" it again.
      setUntrackedChecked.call(input, false);
      input.click();
      expect(input.checked).toBe(true);
      // We don't expect a React event because at the time of the click, the real
      // checked value (true) was the same as the last recorded "current" value
      // (also true).
      expect(called).toBe(0);
  
      // However, simulating a normal click should fire a React event because the
      // real value (false) would have changed from the last tracked value (true).
      input.click();
      expect(called).toBe(1);
    });
  
    it('should unmount', () => {
      const input = ReactDOM.render(<input />, container);
  
      ReactDOM.unmountComponentAtNode(container);
    });
  
    it('should only fire change for checked radio button once', () => {
      let called = 0;
  
      function cb(e) {
        called++;
        expect(e.type).toBe('change');
      }
  
      const input = ReactDOM.render(
        <input type="radio" onChange={cb} />,
        container,
      );
  
      setUntrackedChecked.call(input, true);
      input.dispatchEvent(new Event('click', {bubbles: true, cancelable: true}));
      input.dispatchEvent(new Event('click', {bubbles: true, cancelable: true}));
      expect(called).toBe(1);
    });
  
    it('should track radio button cousins in a group', () => {
      let called1 = 0;
      let called2 = 0;
  
      function cb1(e) {
        called1++;
        expect(e.type).toBe('change');
      }
  
      function cb2(e) {
        called2++;
        expect(e.type).toBe('change');
      }
  
      const div = ReactDOM.render(
        <div>
          <input type="radio" name="group" onChange={cb1} />
          <input type="radio" name="group" onChange={cb2} />
        </div>,
        container,
      );
      const option1 = div.childNodes[0];
      const option2 = div.childNodes[1];
  
      // Select first option.
      option1.click();
      expect(called1).toBe(1);
      expect(called2).toBe(0);
  
      // Select second option.
      option2.click();
      expect(called1).toBe(1);
      expect(called2).toBe(1);
  
      // Select the first option.
      // It should receive the React change event again.
      option1.click();
      expect(called1).toBe(2);
      expect(called2).toBe(1);
    });
  
    it('should deduplicate input value change events', () => {
      let called = 0;
  
      function cb(e) {
        called++;
        expect(e.type).toBe('change');
      }
  
      let input;
      ['text', 'number', 'range'].forEach(type => {
        called = 0;
        input = ReactDOM.render(<input type={type} onChange={cb} />, container);
        // Should be ignored (no change):
        input.dispatchEvent(
          new Event('change', {bubbles: true, cancelable: true}),
        );
        setUntrackedValue.call(input, '42');
        input.dispatchEvent(
          new Event('change', {bubbles: true, cancelable: true}),
        );
        // Should be ignored (no change):
        input.dispatchEvent(
          new Event('change', {bubbles: true, cancelable: true}),
        );
        expect(called).toBe(1);
        ReactDOM.unmountComponentAtNode(container);
  
        called = 0;
        input = ReactDOM.render(<input type={type} onChange={cb} />, container);
        // Should be ignored (no change):
        input.dispatchEvent(
          new Event('input', {bubbles: true, cancelable: true}),
        );
        setUntrackedValue.call(input, '42');
        input.dispatchEvent(
          new Event('input', {bubbles: true, cancelable: true}),
        );
        // Should be ignored (no change):
        input.dispatchEvent(
          new Event('input', {bubbles: true, cancelable: true}),
        );
        expect(called).toBe(1);
        ReactDOM.unmountComponentAtNode(container);
  
        called = 0;
        input = ReactDOM.render(<input type={type} onChange={cb} />, container);
        // Should be ignored (no change):
        input.dispatchEvent(
          new Event('change', {bubbles: true, cancelable: true}),
        );
        setUntrackedValue.call(input, '42');
        input.dispatchEvent(
          new Event('input', {bubbles: true, cancelable: true}),
        );
        // Should be ignored (no change):
        input.dispatchEvent(
          new Event('change', {bubbles: true, cancelable: true}),
        );
        expect(called).toBe(1);
        ReactDOM.unmountComponentAtNode(container);
      });
    });
  
    it('should listen for both change and input events when supported', () => {
      let called = 0;
  
      function cb(e) {
        called++;
        expect(e.type).toBe('change');
      }
  
      const input = ReactDOM.render(
        <input type="range" onChange={cb} />,
        container,
      );
  
      setUntrackedValue.call(input, 10);
      input.dispatchEvent(new Event('input', {bubbles: true, cancelable: true}));
  
      setUntrackedValue.call(input, 20);
      input.dispatchEvent(new Event('change', {bubbles: true, cancelable: true}));
  
      expect(called).toBe(2);
    });
  
    it('should only fire events when the value changes for range inputs', () => {
      let called = 0;
  
      function cb(e) {
        called++;
        expect(e.type).toBe('change');
      }
  
      const input = ReactDOM.render(
        <input type="range" onChange={cb} />,
        container,
      );
      setUntrackedValue.call(input, '40');
      input.dispatchEvent(new Event('input', {bubbles: true, cancelable: true}));
      input.dispatchEvent(new Event('change', {bubbles: true, cancelable: true}));
  
      setUntrackedValue.call(input, 'foo');
      input.dispatchEvent(new Event('input', {bubbles: true, cancelable: true}));
      input.dispatchEvent(new Event('change', {bubbles: true, cancelable: true}));
  
      expect(called).toBe(2);
    });
  
    it('does not crash for nodes with custom value property', () => {
      let originalCreateElement;
      // https://github.com/facebook/react/issues/10196
      try {
        originalCreateElement = document.createElement;
        document.createElement = function() {
          const node = originalCreateElement.apply(this, arguments);
          Object.defineProperty(node, 'value', {
            get() {},
            set() {},
          });
          return node;
        };
        const div = document.createElement('div');
        // Mount
        const node = ReactDOM.render(<input type="text" />, div);
        // Update
        ReactDOM.render(<input type="text" />, div);
        // Change
        node.dispatchEvent(
          new Event('change', {bubbles: true, cancelable: true}),
        );
        // Unmount
        ReactDOM.unmountComponentAtNode(div);
      } finally {
        document.createElement = originalCreateElement;
      }
    });
  
    describe('concurrent mode', () => {
      beforeEach(() => {
        jest.resetModules();
        ReactFeatureFlags = require('shared/ReactFeatureFlags');
  
        React = require('react');
        ReactDOM = require('react-dom');
        TestUtils = require('react-dom/test-utils');
        Scheduler = require('scheduler');
      });
  
      // @gate experimental
      it('text input', () => {
        const root = ReactDOM.unstable_createRoot(container);
        let input;
  
        class ControlledInput extends React.Component {
          state = {value: 'initial'};
          onChange = event => this.setState({value: event.target.value});
          render() {
            Scheduler.unstable_yieldValue(`render: ${this.state.value}`);
            const controlledValue =
              this.state.value === 'changed' ? 'changed [!]' : this.state.value;
            return (
              <input
                ref={el => (input = el)}
                type="text"
                value={controlledValue}
                onChange={this.onChange}
              />
            );
          }
        }
  
        // Initial mount. Test that this is async.
        root.render(<ControlledInput />);
        // Should not have flushed yet.
        expect(Scheduler).toHaveYielded([]);
        expect(input).toBe(undefined);
        // Flush callbacks.
        expect(Scheduler).toFlushAndYield(['render: initial']);
        expect(input.value).toBe('initial');
  
        // Trigger a change event.
        setUntrackedValue.call(input, 'changed');
        input.dispatchEvent(
          new Event('input', {bubbles: true, cancelable: true}),
        );
        // Change should synchronously flush
        expect(Scheduler).toHaveYielded(['render: changed']);
        // Value should be the controlled value, not the original one
        expect(input.value).toBe('changed [!]');
      });
  
      // @gate experimental
      it('checkbox input', () => {
        const root = ReactDOM.unstable_createRoot(container);
        let input;
  
        class ControlledInput extends React.Component {
          state = {checked: false};
          onChange = event => {
            this.setState({checked: event.target.checked});
          };
          render() {
            Scheduler.unstable_yieldValue(`render: ${this.state.checked}`);
            const controlledValue = this.props.reverse
              ? !this.state.checked
              : this.state.checked;
            return (
              <input
                ref={el => (input = el)}
                type="checkbox"
                checked={controlledValue}
                onChange={this.onChange}
              />
            );
          }
        }
  
        // Initial mount. Test that this is async.
        root.render(<ControlledInput reverse={false} />);
        // Should not have flushed yet.
        expect(Scheduler).toHaveYielded([]);
        expect(input).toBe(undefined);
        // Flush callbacks.
        expect(Scheduler).toFlushAndYield(['render: false']);
        expect(input.checked).toBe(false);
  
        // Trigger a change event.
        input.dispatchEvent(
          new MouseEvent('click', {bubbles: true, cancelable: true}),
        );
        // Change should synchronously flush
        expect(Scheduler).toHaveYielded(['render: true']);
        expect(input.checked).toBe(true);
  
        // Now let's make sure we're using the controlled value.
        root.render(<ControlledInput reverse={true} />);
        expect(Scheduler).toFlushAndYield(['render: true']);
  
        // Trigger another change event.
        input.dispatchEvent(
          new MouseEvent('click', {bubbles: true, cancelable: true}),
        );
        // Change should synchronously flush
        expect(Scheduler).toHaveYielded(['render: true']);
        expect(input.checked).toBe(false);
      });
  
      // @gate experimental
      it('textarea', () => {
        const root = ReactDOM.unstable_createRoot(container);
        let textarea;
  
        class ControlledTextarea extends React.Component {
          state = {value: 'initial'};
          onChange = event => this.setState({value: event.target.value});
          render() {
            Scheduler.unstable_yieldValue(`render: ${this.state.value}`);
            const controlledValue =
              this.state.value === 'changed' ? 'changed [!]' : this.state.value;
            return (
              <textarea
                ref={el => (textarea = el)}
                type="text"
                value={controlledValue}
                onChange={this.onChange}
              />
            );
          }
        }
  
        // Initial mount. Test that this is async.
        root.render(<ControlledTextarea />);
        // Should not have flushed yet.
        expect(Scheduler).toHaveYielded([]);
        expect(textarea).toBe(undefined);
        // Flush callbacks.
        expect(Scheduler).toFlushAndYield(['render: initial']);
        expect(textarea.value).toBe('initial');
  
        // Trigger a change event.
        setUntrackedTextareaValue.call(textarea, 'changed');
        textarea.dispatchEvent(
          new Event('input', {bubbles: true, cancelable: true}),
        );
        // Change should synchronously flush
        expect(Scheduler).toHaveYielded(['render: changed']);
        // Value should be the controlled value, not the original one
        expect(textarea.value).toBe('changed [!]');
      });
  
      // @gate experimental
      it('parent of input', () => {
        const root = ReactDOM.unstable_createRoot(container);
        let input;
  
        class ControlledInput extends React.Component {
          state = {value: 'initial'};
          onChange = event => this.setState({value: event.target.value});
          render() {
            Scheduler.unstable_yieldValue(`render: ${this.state.value}`);
            const controlledValue =
              this.state.value === 'changed' ? 'changed [!]' : this.state.value;
            return (
              <div onChange={this.onChange}>
                <input
                  ref={el => (input = el)}
                  type="text"
                  value={controlledValue}
                  onChange={() => {
                    // Does nothing. Parent handler is responsible for updating.
                  }}
                />
              </div>
            );
          }
        }
  
        // Initial mount. Test that this is async.
        root.render(<ControlledInput />);
        // Should not have flushed yet.
        expect(Scheduler).toHaveYielded([]);
        expect(input).toBe(undefined);
        // Flush callbacks.
        expect(Scheduler).toFlushAndYield(['render: initial']);
        expect(input.value).toBe('initial');
  
        // Trigger a change event.
        setUntrackedValue.call(input, 'changed');
        input.dispatchEvent(
          new Event('input', {bubbles: true, cancelable: true}),
        );
        // Change should synchronously flush
        expect(Scheduler).toHaveYielded(['render: changed']);
        // Value should be the controlled value, not the original one
        expect(input.value).toBe('changed [!]');
      });
  
      // @gate experimental
      it('is async for non-input events', () => {
        const root = ReactDOM.unstable_createRoot(container);
        let input;
  
        class ControlledInput extends React.Component {
          state = {value: 'initial'};
          onChange = event => this.setState({value: event.target.value});
          reset = () => {
            this.setState({value: ''});
          };
          render() {
            Scheduler.unstable_yieldValue(`render: ${this.state.value}`);
            const controlledValue =
              this.state.value === 'changed' ? 'changed [!]' : this.state.value;
            return (
              <input
                ref={el => (input = el)}
                type="text"
                value={controlledValue}
                onChange={this.onChange}
                onClick={this.reset}
              />
            );
          }
        }
  
        // Initial mount. Test that this is async.
        root.render(<ControlledInput />);
        // Should not have flushed yet.
        expect(Scheduler).toHaveYielded([]);
        expect(input).toBe(undefined);
        // Flush callbacks.
        expect(Scheduler).toFlushAndYield(['render: initial']);
        expect(input.value).toBe('initial');
  
        // Trigger a click event
        input.dispatchEvent(
          new Event('click', {bubbles: true, cancelable: true}),
        );
        // Nothing should have changed
        expect(Scheduler).toHaveYielded([]);
        expect(input.value).toBe('initial');
  
        // Flush callbacks.
        // Now the click update has flushed.
        expect(Scheduler).toFlushAndYield(['render: ']);
        expect(input.value).toBe('');
      });
  
      // @gate experimental
      it('mouse enter/leave should be user-blocking but not discrete', async () => {
        // This is currently behind a feature flag
        jest.resetModules();
        React = require('react');
        ReactDOM = require('react-dom');
        TestUtils = require('react-dom/test-utils');
        Scheduler = require('scheduler');
  
        const {act} = TestUtils;
        const {useState} = React;
  
        const root = ReactDOM.unstable_createRoot(container);
  
        const target = React.createRef(null);
        function Foo() {
          const [isHover, setHover] = useState(false);
          return (
            <div
              ref={target}
              onMouseEnter={() => setHover(true)}
              onMouseLeave={() => setHover(false)}>
              {isHover ? 'hovered' : 'not hovered'}
            </div>
          );
        }
  
        await act(async () => {
          root.render(<Foo />);
        });
        expect(container.textContent).toEqual('not hovered');
  
        await act(async () => {
          const mouseOverEvent = document.createEvent('MouseEvents');
          mouseOverEvent.initEvent('mouseover', true, true);
          target.current.dispatchEvent(mouseOverEvent);
  
          // 3s should be enough to expire the updates
          Scheduler.unstable_advanceTime(3000);
          expect(Scheduler).toFlushExpired([]);
          expect(container.textContent).toEqual('hovered');
        });
      });
    });
  });
  