/**
 * Copyright (c) Meta Platforms, Inc. and 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';

// Set by `yarn test-fire`.
const {disableInputAttributeSyncing} = require('shared/ReactFeatureFlags');

describe('DOMPropertyOperations', () => {
  let React;
  let ReactDOMClient;
  let act;
  let assertConsoleErrorDev;

  beforeEach(() => {
    jest.resetModules();
    React = require('react');
    ReactDOMClient = require('react-dom/client');
    ({act, assertConsoleErrorDev} = require('internal-test-utils'));
  });

  // Sets a value in a way that React doesn't see,
  // so that a subsequent "change" event will trigger the event handler.
  const setUntrackedValue = Object.getOwnPropertyDescriptor(
    HTMLInputElement.prototype,
    'value',
  ).set;
  const setUntrackedChecked = Object.getOwnPropertyDescriptor(
    HTMLInputElement.prototype,
    'checked',
  ).set;

  describe('setValueForProperty', () => {
    it('should set values as properties by default', async () => {
      const container = document.createElement('div');
      const root = ReactDOMClient.createRoot(container);
      await act(() => {
        root.render(<div title="Tip!" />);
      });
      expect(container.firstChild.title).toBe('Tip!');
    });

    it('should set values as attributes if necessary', async () => {
      const container = document.createElement('div');
      const root = ReactDOMClient.createRoot(container);
      await act(() => {
        root.render(<div role="#" />);
      });
      expect(container.firstChild.getAttribute('role')).toBe('#');
      expect(container.firstChild.role).toBeUndefined();
    });

    it('should set values as namespace attributes if necessary', async () => {
      const container = document.createElementNS(
        'http://www.w3.org/2000/svg',
        'svg',
      );
      const root = ReactDOMClient.createRoot(container);
      await act(() => {
        root.render(<image xlinkHref="about:blank" />);
      });
      expect(
        container.firstChild.getAttributeNS(
          'http://www.w3.org/1999/xlink',
          'href',
        ),
      ).toBe('about:blank');
    });

    it('should set values as boolean properties', async () => {
      const container = document.createElement('div');
      const root = ReactDOMClient.createRoot(container);
      await act(() => {
        root.render(<div disabled="disabled" />);
      });
      expect(container.firstChild.getAttribute('disabled')).toBe('');
      await act(() => {
        root.render(<div disabled={true} />);
      });
      expect(container.firstChild.getAttribute('disabled')).toBe('');
      await act(() => {
        root.render(<div disabled={false} />);
      });
      expect(container.firstChild.getAttribute('disabled')).toBe(null);
      await act(() => {
        root.render(<div disabled={true} />);
      });
      await act(() => {
        root.render(<div disabled={null} />);
      });
      expect(container.firstChild.getAttribute('disabled')).toBe(null);
      await act(() => {
        root.render(<div disabled={true} />);
      });
      await act(() => {
        root.render(<div disabled={undefined} />);
      });
      expect(container.firstChild.getAttribute('disabled')).toBe(null);
    });

    it('should convert attribute values to string first', async () => {
      // Browsers default to this behavior, but some test environments do not.
      // This ensures that we have consistent behavior.
      const obj = {
        toString: function () {
          return 'css-class';
        },
      };

      const container = document.createElement('div');
      const root = ReactDOMClient.createRoot(container);
      await act(() => {
        root.render(<div className={obj} />);
      });
      expect(container.firstChild.getAttribute('class')).toBe('css-class');
    });

    it('should not remove empty attributes for special input properties', async () => {
      const container = document.createElement('div');
      const root = ReactDOMClient.createRoot(container);
      await act(() => {
        root.render(<input value="" onChange={() => {}} />);
      });
      if (disableInputAttributeSyncing) {
        expect(container.firstChild.hasAttribute('value')).toBe(false);
      } else {
        expect(container.firstChild.getAttribute('value')).toBe('');
      }
      expect(container.firstChild.value).toBe('');
    });

    it('should not remove empty attributes for special option properties', async () => {
      const container = document.createElement('div');
      const root = ReactDOMClient.createRoot(container);
      await act(() => {
        root.render(
          <select>
            <option value="">empty</option>
            <option>filled</option>
          </select>,
        );
      });
      // Regression test for https://github.com/facebook/react/issues/6219
      expect(container.firstChild.firstChild.value).toBe('');
      expect(container.firstChild.lastChild.value).toBe('filled');
    });

    it('should remove for falsey boolean properties', async () => {
      const container = document.createElement('div');
      const root = ReactDOMClient.createRoot(container);
      await act(() => {
        root.render(<div allowFullScreen={false} />);
      });
      expect(container.firstChild.hasAttribute('allowFullScreen')).toBe(false);
    });

    it('should remove when setting custom attr to null', async () => {
      const container = document.createElement('div');
      const root = ReactDOMClient.createRoot(container);
      await act(() => {
        root.render(<div data-foo="bar" />);
      });
      expect(container.firstChild.hasAttribute('data-foo')).toBe(true);
      await act(() => {
        root.render(<div data-foo={null} />);
      });
      expect(container.firstChild.hasAttribute('data-foo')).toBe(false);
    });

    it('should set className to empty string instead of null', async () => {
      const container = document.createElement('div');
      const root = ReactDOMClient.createRoot(container);
      await act(() => {
        root.render(<div className="selected" />);
      });
      expect(container.firstChild.className).toBe('selected');
      await act(() => {
        root.render(<div className={null} />);
      });
      // className should be '', not 'null' or null (which becomes 'null' in
      // some browsers)
      expect(container.firstChild.className).toBe('');
      expect(container.firstChild.getAttribute('class')).toBe(null);
    });

    it('should remove property properly for boolean properties', async () => {
      const container = document.createElement('div');
      const root = ReactDOMClient.createRoot(container);
      await act(() => {
        root.render(<div hidden={true} />);
      });
      expect(container.firstChild.hasAttribute('hidden')).toBe(true);
      await act(() => {
        root.render(<div hidden={false} />);
      });
      expect(container.firstChild.hasAttribute('hidden')).toBe(false);
    });

    it('should always assign the value attribute for non-inputs', async () => {
      const container = document.createElement('div');
      const root = ReactDOMClient.createRoot(container);
      await act(() => {
        root.render(<progress />);
      });
      spyOnDevAndProd(container.firstChild, 'setAttribute');
      await act(() => {
        root.render(<progress value={30} />);
      });
      await act(() => {
        root.render(<progress value="30" />);
      });
      expect(container.firstChild.setAttribute).toHaveBeenCalledTimes(2);
    });

    it('should return the progress to intermediate state on null value', async () => {
      const container = document.createElement('div');
      const root = ReactDOMClient.createRoot(container);
      await act(() => {
        root.render(<progress value={30} />);
      });
      await act(() => {
        root.render(<progress value={null} />);
      });
      // Ensure we move progress back to an indeterminate state.
      // Regression test for https://github.com/facebook/react/issues/6119
      expect(container.firstChild.hasAttribute('value')).toBe(false);
    });

    it('custom element custom events lowercase', async () => {
      const oncustomevent = jest.fn();
      function Test() {
        return <my-custom-element oncustomevent={oncustomevent} />;
      }
      const container = document.createElement('div');
      const root = ReactDOMClient.createRoot(container);
      await act(() => {
        root.render(<Test />);
      });
      container
        .querySelector('my-custom-element')
        .dispatchEvent(new Event('customevent'));
      expect(oncustomevent).toHaveBeenCalledTimes(1);
    });

    it('custom element custom events uppercase', async () => {
      const oncustomevent = jest.fn();
      function Test() {
        return <my-custom-element onCustomevent={oncustomevent} />;
      }
      const container = document.createElement('div');
      const root = ReactDOMClient.createRoot(container);
      await act(() => {
        root.render(<Test />);
      });
      container
        .querySelector('my-custom-element')
        .dispatchEvent(new Event('Customevent'));
      expect(oncustomevent).toHaveBeenCalledTimes(1);
    });

    it('custom element custom event with dash in name', async () => {
      const oncustomevent = jest.fn();
      function Test() {
        return <my-custom-element oncustom-event={oncustomevent} />;
      }
      const container = document.createElement('div');
      const root = ReactDOMClient.createRoot(container);
      await act(() => {
        root.render(<Test />);
      });
      container
        .querySelector('my-custom-element')
        .dispatchEvent(new Event('custom-event'));
      expect(oncustomevent).toHaveBeenCalledTimes(1);
    });

    it('custom element remove event handler', async () => {
      const oncustomevent = jest.fn();
      function Test(props) {
        return <my-custom-element oncustomevent={props.handler} />;
      }

      const container = document.createElement('div');
      const root = ReactDOMClient.createRoot(container);
      await act(() => {
        root.render(<Test handler={oncustomevent} />);
      });
      const customElement = container.querySelector('my-custom-element');
      customElement.dispatchEvent(new Event('customevent'));
      expect(oncustomevent).toHaveBeenCalledTimes(1);

      await act(() => {
        root.render(<Test handler={false} />);
      });
      // Make sure that the second render didn't create a new element. We want
      // to make sure removeEventListener actually gets called on the same element.
      expect(customElement).toBe(customElement);
      customElement.dispatchEvent(new Event('customevent'));
      expect(oncustomevent).toHaveBeenCalledTimes(1);

      await act(() => {
        root.render(<Test handler={oncustomevent} />);
      });
      customElement.dispatchEvent(new Event('customevent'));
      expect(oncustomevent).toHaveBeenCalledTimes(2);

      const oncustomevent2 = jest.fn();
      await act(() => {
        root.render(<Test handler={oncustomevent2} />);
      });
      customElement.dispatchEvent(new Event('customevent'));
      expect(oncustomevent).toHaveBeenCalledTimes(2);
      expect(oncustomevent2).toHaveBeenCalledTimes(1);
    });

    it('custom elements shouldnt have non-functions for on* attributes treated as event listeners', async () => {
      const container = document.createElement('div');
      const root = ReactDOMClient.createRoot(container);
      await act(() => {
        root.render(
          <my-custom-element
            onstring={'hello'}
            onobj={{hello: 'world'}}
            onarray={['one', 'two']}
            ontrue={true}
            onfalse={false}
          />,
        );
      });
      const customElement = container.querySelector('my-custom-element');
      expect(customElement.getAttribute('onstring')).toBe('hello');
      expect(customElement.getAttribute('onobj')).toBe('[object Object]');
      expect(customElement.getAttribute('onarray')).toBe('one,two');
      expect(customElement.getAttribute('ontrue')).toBe('');
      expect(customElement.getAttribute('onfalse')).toBe(null);

      // Dispatch the corresponding event names to make sure that nothing crashes.
      customElement.dispatchEvent(new Event('string'));
      customElement.dispatchEvent(new Event('obj'));
      customElement.dispatchEvent(new Event('array'));
      customElement.dispatchEvent(new Event('true'));
      customElement.dispatchEvent(new Event('false'));
    });

    it('custom elements should still have onClick treated like regular elements', async () => {
      let syntheticClickEvent = null;
      const syntheticEventHandler = jest.fn(
        event => (syntheticClickEvent = event),
      );
      let nativeClickEvent = null;
      const nativeEventHandler = jest.fn(event => (nativeClickEvent = event));
      function Test() {
        return <my-custom-element onClick={syntheticEventHandler} />;
      }

      const container = document.createElement('div');
      document.body.appendChild(container);
      const root = ReactDOMClient.createRoot(container);
      await act(() => {
        root.render(<Test />);
      });

      const customElement = container.querySelector('my-custom-element');
      customElement.onclick = nativeEventHandler;
      container.querySelector('my-custom-element').click();

      expect(nativeEventHandler).toHaveBeenCalledTimes(1);
      expect(syntheticEventHandler).toHaveBeenCalledTimes(1);
      expect(syntheticClickEvent.nativeEvent).toBe(nativeClickEvent);
    });

    it('custom elements should have working onChange event listeners', async () => {
      let reactChangeEvent = null;
      const eventHandler = jest.fn(event => (reactChangeEvent = event));
      const container = document.createElement('div');
      document.body.appendChild(container);
      const root = ReactDOMClient.createRoot(container);
      await act(() => {
        root.render(<my-custom-element onChange={eventHandler} />);
      });
      const customElement = container.querySelector('my-custom-element');
      let expectedHandlerCallCount = 0;

      const changeEvent = new Event('change', {bubbles: true});
      customElement.dispatchEvent(changeEvent);
      expectedHandlerCallCount++;
      expect(eventHandler).toHaveBeenCalledTimes(expectedHandlerCallCount);
      expect(reactChangeEvent.nativeEvent).toBe(changeEvent);

      // Also make sure that removing and re-adding the event listener works
      await act(() => {
        root.render(<my-custom-element />);
      });
      customElement.dispatchEvent(new Event('change', {bubbles: true}));
      expect(eventHandler).toHaveBeenCalledTimes(expectedHandlerCallCount);
      await act(() => {
        root.render(<my-custom-element onChange={eventHandler} />);
      });
      customElement.dispatchEvent(new Event('change', {bubbles: true}));
      expectedHandlerCallCount++;
      expect(eventHandler).toHaveBeenCalledTimes(expectedHandlerCallCount);
    });

    it('custom elements should have working onInput event listeners', async () => {
      let reactInputEvent = null;
      const eventHandler = jest.fn(event => (reactInputEvent = event));
      const container = document.createElement('div');
      document.body.appendChild(container);
      const root = ReactDOMClient.createRoot(container);
      await act(() => {
        root.render(<my-custom-element onInput={eventHandler} />);
      });
      const customElement = container.querySelector('my-custom-element');
      let expectedHandlerCallCount = 0;

      const inputEvent = new Event('input', {bubbles: true});
      customElement.dispatchEvent(inputEvent);
      expectedHandlerCallCount++;
      expect(eventHandler).toHaveBeenCalledTimes(expectedHandlerCallCount);
      expect(reactInputEvent.nativeEvent).toBe(inputEvent);

      // Also make sure that removing and re-adding the event listener works
      await act(() => {
        root.render(<my-custom-element />);
      });
      customElement.dispatchEvent(new Event('input', {bubbles: true}));
      expect(eventHandler).toHaveBeenCalledTimes(expectedHandlerCallCount);
      await act(() => {
        root.render(<my-custom-element onInput={eventHandler} />);
      });
      customElement.dispatchEvent(new Event('input', {bubbles: true}));
      expectedHandlerCallCount++;
      expect(eventHandler).toHaveBeenCalledTimes(expectedHandlerCallCount);
    });

    it('custom elements should have separate onInput and onChange handling', async () => {
      const container = document.createElement('div');
      document.body.appendChild(container);
      const root = ReactDOMClient.createRoot(container);
      const inputEventHandler = jest.fn();
      const changeEventHandler = jest.fn();
      await act(() => {
        root.render(
          <my-custom-element
            onInput={inputEventHandler}
            onChange={changeEventHandler}
          />,
        );
      });
      const customElement = container.querySelector('my-custom-element');

      customElement.dispatchEvent(new Event('input', {bubbles: true}));
      expect(inputEventHandler).toHaveBeenCalledTimes(1);
      expect(changeEventHandler).toHaveBeenCalledTimes(0);

      customElement.dispatchEvent(new Event('change', {bubbles: true}));
      expect(inputEventHandler).toHaveBeenCalledTimes(1);
      expect(changeEventHandler).toHaveBeenCalledTimes(1);
    });

    it('custom elements should be able to remove and re-add custom event listeners', async () => {
      const container = document.createElement('div');
      document.body.appendChild(container);
      const root = ReactDOMClient.createRoot(container);
      const eventHandler = jest.fn();
      await act(() => {
        root.render(<my-custom-element oncustomevent={eventHandler} />);
      });

      const customElement = container.querySelector('my-custom-element');
      customElement.dispatchEvent(new Event('customevent'));
      expect(eventHandler).toHaveBeenCalledTimes(1);

      await act(() => {
        root.render(<my-custom-element />);
      });
      customElement.dispatchEvent(new Event('customevent'));
      expect(eventHandler).toHaveBeenCalledTimes(1);

      await act(() => {
        root.render(<my-custom-element oncustomevent={eventHandler} />);
      });
      customElement.dispatchEvent(new Event('customevent'));
      expect(eventHandler).toHaveBeenCalledTimes(2);
    });

    it('<input is=...> should have the same onChange/onInput/onClick behavior as <input>', async () => {
      const container = document.createElement('div');
      document.body.appendChild(container);
      const root = ReactDOMClient.createRoot(container);
      const regularOnInputHandler = jest.fn();
      const regularOnChangeHandler = jest.fn();
      const regularOnClickHandler = jest.fn();
      const customOnInputHandler = jest.fn();
      const customOnChangeHandler = jest.fn();
      const customOnClickHandler = jest.fn();
      function clearMocks() {
        regularOnInputHandler.mockClear();
        regularOnChangeHandler.mockClear();
        regularOnClickHandler.mockClear();
        customOnInputHandler.mockClear();
        customOnChangeHandler.mockClear();
        customOnClickHandler.mockClear();
      }
      await act(() => {
        root.render(
          <div>
            <input
              onInput={regularOnInputHandler}
              onChange={regularOnChangeHandler}
              onClick={regularOnClickHandler}
            />
            <input
              is="my-custom-element"
              onInput={customOnInputHandler}
              onChange={customOnChangeHandler}
              onClick={customOnClickHandler}
            />
          </div>,
        );
      });

      const regularInput = container.querySelector(
        'input:not([is=my-custom-element])',
      );
      const customInput = container.querySelector(
        'input[is=my-custom-element]',
      );
      expect(regularInput).not.toBe(customInput);

      // Typing should trigger onInput and onChange for both kinds of inputs.
      clearMocks();
      setUntrackedValue.call(regularInput, 'hello');
      regularInput.dispatchEvent(new Event('input', {bubbles: true}));
      expect(regularOnInputHandler).toHaveBeenCalledTimes(1);
      expect(regularOnChangeHandler).toHaveBeenCalledTimes(1);
      expect(regularOnClickHandler).toHaveBeenCalledTimes(0);
      setUntrackedValue.call(customInput, 'hello');
      customInput.dispatchEvent(new Event('input', {bubbles: true}));
      expect(customOnInputHandler).toHaveBeenCalledTimes(1);
      expect(customOnChangeHandler).toHaveBeenCalledTimes(1);
      expect(customOnClickHandler).toHaveBeenCalledTimes(0);

      // The native change event itself does not produce extra React events.
      clearMocks();
      regularInput.dispatchEvent(new Event('change', {bubbles: true}));
      expect(regularOnInputHandler).toHaveBeenCalledTimes(0);
      expect(regularOnChangeHandler).toHaveBeenCalledTimes(0);
      expect(regularOnClickHandler).toHaveBeenCalledTimes(0);
      customInput.dispatchEvent(new Event('change', {bubbles: true}));
      expect(customOnInputHandler).toHaveBeenCalledTimes(0);
      expect(customOnChangeHandler).toHaveBeenCalledTimes(0);
      expect(customOnClickHandler).toHaveBeenCalledTimes(0);

      // The click event is handled by both inputs.
      clearMocks();
      regularInput.dispatchEvent(new Event('click', {bubbles: true}));
      expect(regularOnInputHandler).toHaveBeenCalledTimes(0);
      expect(regularOnChangeHandler).toHaveBeenCalledTimes(0);
      expect(regularOnClickHandler).toHaveBeenCalledTimes(1);
      customInput.dispatchEvent(new Event('click', {bubbles: true}));
      expect(customOnInputHandler).toHaveBeenCalledTimes(0);
      expect(customOnChangeHandler).toHaveBeenCalledTimes(0);
      expect(customOnClickHandler).toHaveBeenCalledTimes(1);

      // Typing again should trigger onInput and onChange for both kinds of inputs.
      clearMocks();
      setUntrackedValue.call(regularInput, 'goodbye');
      regularInput.dispatchEvent(new Event('input', {bubbles: true}));
      expect(regularOnInputHandler).toHaveBeenCalledTimes(1);
      expect(regularOnChangeHandler).toHaveBeenCalledTimes(1);
      expect(regularOnClickHandler).toHaveBeenCalledTimes(0);
      setUntrackedValue.call(customInput, 'goodbye');
      customInput.dispatchEvent(new Event('input', {bubbles: true}));
      expect(customOnInputHandler).toHaveBeenCalledTimes(1);
      expect(customOnChangeHandler).toHaveBeenCalledTimes(1);
      expect(customOnClickHandler).toHaveBeenCalledTimes(0);
    });

    it('<input type=radio is=...> should have the same onChange/onInput/onClick behavior as <input type=radio>', async () => {
      const container = document.createElement('div');
      document.body.appendChild(container);
      const root = ReactDOMClient.createRoot(container);
      const regularOnInputHandler = jest.fn();
      const regularOnChangeHandler = jest.fn();
      const regularOnClickHandler = jest.fn();
      const customOnInputHandler = jest.fn();
      const customOnChangeHandler = jest.fn();
      const customOnClickHandler = jest.fn();
      function clearMocks() {
        regularOnInputHandler.mockClear();
        regularOnChangeHandler.mockClear();
        regularOnClickHandler.mockClear();
        customOnInputHandler.mockClear();
        customOnChangeHandler.mockClear();
        customOnClickHandler.mockClear();
      }
      await act(() => {
        root.render(
          <div>
            <input
              type="radio"
              onInput={regularOnInputHandler}
              onChange={regularOnChangeHandler}
              onClick={regularOnClickHandler}
            />
            <input
              is="my-custom-element"
              type="radio"
              onInput={customOnInputHandler}
              onChange={customOnChangeHandler}
              onClick={customOnClickHandler}
            />
          </div>,
        );
      });

      const regularInput = container.querySelector(
        'input:not([is=my-custom-element])',
      );
      const customInput = container.querySelector(
        'input[is=my-custom-element]',
      );
      expect(regularInput).not.toBe(customInput);

      // Clicking should trigger onClick and onChange on both inputs.
      clearMocks();
      setUntrackedChecked.call(regularInput, true);
      regularInput.dispatchEvent(new Event('click', {bubbles: true}));
      expect(regularOnInputHandler).toHaveBeenCalledTimes(0);
      expect(regularOnChangeHandler).toHaveBeenCalledTimes(1);
      expect(regularOnClickHandler).toHaveBeenCalledTimes(1);
      setUntrackedChecked.call(customInput, true);
      customInput.dispatchEvent(new Event('click', {bubbles: true}));
      expect(customOnInputHandler).toHaveBeenCalledTimes(0);
      expect(customOnChangeHandler).toHaveBeenCalledTimes(1);
      expect(customOnClickHandler).toHaveBeenCalledTimes(1);

      // The native input event only produces a React onInput event.
      clearMocks();
      regularInput.dispatchEvent(new Event('input', {bubbles: true}));
      expect(regularOnInputHandler).toHaveBeenCalledTimes(1);
      expect(regularOnChangeHandler).toHaveBeenCalledTimes(0);
      expect(regularOnClickHandler).toHaveBeenCalledTimes(0);
      customInput.dispatchEvent(new Event('input', {bubbles: true}));
      expect(customOnInputHandler).toHaveBeenCalledTimes(1);
      expect(customOnChangeHandler).toHaveBeenCalledTimes(0);
      expect(customOnClickHandler).toHaveBeenCalledTimes(0);

      // Clicking again should trigger onClick and onChange on both inputs.
      clearMocks();
      setUntrackedChecked.call(regularInput, false);
      regularInput.dispatchEvent(new Event('click', {bubbles: true}));
      expect(regularOnInputHandler).toHaveBeenCalledTimes(0);
      expect(regularOnChangeHandler).toHaveBeenCalledTimes(1);
      expect(regularOnClickHandler).toHaveBeenCalledTimes(1);
      setUntrackedChecked.call(customInput, false);
      customInput.dispatchEvent(new Event('click', {bubbles: true}));
      expect(customOnInputHandler).toHaveBeenCalledTimes(0);
      expect(customOnChangeHandler).toHaveBeenCalledTimes(1);
      expect(customOnClickHandler).toHaveBeenCalledTimes(1);
    });

    it('<select is=...> should have the same onChange/onInput/onClick behavior as <select>', async () => {
      const container = document.createElement('div');
      document.body.appendChild(container);
      const root = ReactDOMClient.createRoot(container);
      const regularOnInputHandler = jest.fn();
      const regularOnChangeHandler = jest.fn();
      const regularOnClickHandler = jest.fn();
      const customOnInputHandler = jest.fn();
      const customOnChangeHandler = jest.fn();
      const customOnClickHandler = jest.fn();
      function clearMocks() {
        regularOnInputHandler.mockClear();
        regularOnChangeHandler.mockClear();
        regularOnClickHandler.mockClear();
        customOnInputHandler.mockClear();
        customOnChangeHandler.mockClear();
        customOnClickHandler.mockClear();
      }
      await act(() => {
        root.render(
          <div>
            <select
              onInput={regularOnInputHandler}
              onChange={regularOnChangeHandler}
              onClick={regularOnClickHandler}
            />
            <select
              is="my-custom-element"
              onInput={customOnInputHandler}
              onChange={customOnChangeHandler}
              onClick={customOnClickHandler}
            />
          </div>,
        );
      });

      const regularSelect = container.querySelector(
        'select:not([is=my-custom-element])',
      );
      const customSelect = container.querySelector(
        'select[is=my-custom-element]',
      );
      expect(regularSelect).not.toBe(customSelect);

      // Clicking should only trigger onClick on both inputs.
      clearMocks();
      regularSelect.dispatchEvent(new Event('click', {bubbles: true}));
      expect(regularOnInputHandler).toHaveBeenCalledTimes(0);
      expect(regularOnChangeHandler).toHaveBeenCalledTimes(0);
      expect(regularOnClickHandler).toHaveBeenCalledTimes(1);
      customSelect.dispatchEvent(new Event('click', {bubbles: true}));
      expect(customOnInputHandler).toHaveBeenCalledTimes(0);
      expect(customOnChangeHandler).toHaveBeenCalledTimes(0);
      expect(customOnClickHandler).toHaveBeenCalledTimes(1);

      // Native input event should only trigger onInput on both inputs.
      clearMocks();
      regularSelect.dispatchEvent(new Event('input', {bubbles: true}));
      expect(regularOnInputHandler).toHaveBeenCalledTimes(1);
      expect(regularOnChangeHandler).toHaveBeenCalledTimes(0);
      expect(regularOnClickHandler).toHaveBeenCalledTimes(0);
      customSelect.dispatchEvent(new Event('input', {bubbles: true}));
      expect(customOnInputHandler).toHaveBeenCalledTimes(1);
      expect(customOnChangeHandler).toHaveBeenCalledTimes(0);
      expect(customOnClickHandler).toHaveBeenCalledTimes(0);

      // Native change event should trigger onChange.
      clearMocks();
      regularSelect.dispatchEvent(new Event('change', {bubbles: true}));
      expect(regularOnInputHandler).toHaveBeenCalledTimes(0);
      expect(regularOnChangeHandler).toHaveBeenCalledTimes(1);
      expect(regularOnClickHandler).toHaveBeenCalledTimes(0);
      customSelect.dispatchEvent(new Event('change', {bubbles: true}));
      expect(customOnInputHandler).toHaveBeenCalledTimes(0);
      expect(customOnChangeHandler).toHaveBeenCalledTimes(1);
      expect(customOnClickHandler).toHaveBeenCalledTimes(0);
    });

    it('onChange/onInput/onClick on div with various types of children', async () => {
      const container = document.createElement('div');
      document.body.appendChild(container);
      const root = ReactDOMClient.createRoot(container);
      const onChangeHandler = jest.fn();
      const onInputHandler = jest.fn();
      const onClickHandler = jest.fn();
      function clearMocks() {
        onChangeHandler.mockClear();
        onInputHandler.mockClear();
        onClickHandler.mockClear();
      }
      await act(() => {
        root.render(
          <div
            onChange={onChangeHandler}
            onInput={onInputHandler}
            onClick={onClickHandler}>
            <my-custom-element />
            <input />
            <input is="my-custom-element" />
          </div>,
        );
      });
      const customElement = container.querySelector('my-custom-element');
      const regularInput = container.querySelector(
        'input:not([is="my-custom-element"])',
      );
      const customInput = container.querySelector(
        'input[is="my-custom-element"]',
      );
      expect(regularInput).not.toBe(customInput);

      // Custom element has no special logic for input/change.
      clearMocks();
      customElement.dispatchEvent(new Event('input', {bubbles: true}));
      expect(onChangeHandler).toBeCalledTimes(0);
      expect(onInputHandler).toBeCalledTimes(1);
      expect(onClickHandler).toBeCalledTimes(0);
      customElement.dispatchEvent(new Event('change', {bubbles: true}));
      expect(onChangeHandler).toBeCalledTimes(1);
      expect(onInputHandler).toBeCalledTimes(1);
      expect(onClickHandler).toBeCalledTimes(0);
      customElement.dispatchEvent(new Event('click', {bubbles: true}));
      expect(onChangeHandler).toBeCalledTimes(1);
      expect(onInputHandler).toBeCalledTimes(1);
      expect(onClickHandler).toBeCalledTimes(1);

      // Regular input treats browser input as onChange.
      clearMocks();
      setUntrackedValue.call(regularInput, 'hello');
      regularInput.dispatchEvent(new Event('input', {bubbles: true}));
      expect(onChangeHandler).toBeCalledTimes(1);
      expect(onInputHandler).toBeCalledTimes(1);
      expect(onClickHandler).toBeCalledTimes(0);
      regularInput.dispatchEvent(new Event('change', {bubbles: true}));
      expect(onChangeHandler).toBeCalledTimes(1);
      expect(onInputHandler).toBeCalledTimes(1);
      expect(onClickHandler).toBeCalledTimes(0);
      regularInput.dispatchEvent(new Event('click', {bubbles: true}));
      expect(onChangeHandler).toBeCalledTimes(1);
      expect(onInputHandler).toBeCalledTimes(1);
      expect(onClickHandler).toBeCalledTimes(1);

      // Custom input treats browser input as onChange.
      clearMocks();
      setUntrackedValue.call(customInput, 'hello');
      customInput.dispatchEvent(new Event('input', {bubbles: true}));
      expect(onChangeHandler).toBeCalledTimes(1);
      expect(onInputHandler).toBeCalledTimes(1);
      expect(onClickHandler).toBeCalledTimes(0);
      customInput.dispatchEvent(new Event('change', {bubbles: true}));
      expect(onChangeHandler).toBeCalledTimes(1);
      expect(onInputHandler).toBeCalledTimes(1);
      expect(onClickHandler).toBeCalledTimes(0);
      customInput.dispatchEvent(new Event('click', {bubbles: true}));
      expect(onChangeHandler).toBeCalledTimes(1);
      expect(onInputHandler).toBeCalledTimes(1);
      expect(onClickHandler).toBeCalledTimes(1);
    });

    it('custom element onChange/onInput/onClick with event target input child', async () => {
      const container = document.createElement('div');
      document.body.appendChild(container);
      const root = ReactDOMClient.createRoot(container);
      const onChangeHandler = jest.fn();
      const onInputHandler = jest.fn();
      const onClickHandler = jest.fn();
      await act(() => {
        root.render(
          <my-custom-element
            onChange={onChangeHandler}
            onInput={onInputHandler}
            onClick={onClickHandler}>
            <input />
          </my-custom-element>,
        );
      });

      const input = container.querySelector('input');
      setUntrackedValue.call(input, 'hello');
      input.dispatchEvent(new Event('input', {bubbles: true}));
      // Simulated onChange from the child's input event
      // bubbles to the parent custom element.
      expect(onChangeHandler).toBeCalledTimes(1);
      expect(onInputHandler).toBeCalledTimes(1);
      expect(onClickHandler).toBeCalledTimes(0);
      // Consequently, the native change event is ignored.
      input.dispatchEvent(new Event('change', {bubbles: true}));
      expect(onChangeHandler).toBeCalledTimes(1);
      expect(onInputHandler).toBeCalledTimes(1);
      expect(onClickHandler).toBeCalledTimes(0);
      input.dispatchEvent(new Event('click', {bubbles: true}));
      expect(onChangeHandler).toBeCalledTimes(1);
      expect(onInputHandler).toBeCalledTimes(1);
      expect(onClickHandler).toBeCalledTimes(1);
    });

    it('custom element onChange/onInput/onClick with event target div child', async () => {
      const container = document.createElement('div');
      document.body.appendChild(container);
      const root = ReactDOMClient.createRoot(container);
      const onChangeHandler = jest.fn();
      const onInputHandler = jest.fn();
      const onClickHandler = jest.fn();
      await act(() => {
        root.render(
          <my-custom-element
            onChange={onChangeHandler}
            onInput={onInputHandler}
            onClick={onClickHandler}>
            <div />
          </my-custom-element>,
        );
      });

      const div = container.querySelector('div');
      div.dispatchEvent(new Event('input', {bubbles: true}));
      expect(onChangeHandler).toBeCalledTimes(0);
      expect(onInputHandler).toBeCalledTimes(1);
      expect(onClickHandler).toBeCalledTimes(0);

      div.dispatchEvent(new Event('change', {bubbles: true}));
      // React always ignores change event invoked on non-custom and non-input targets.
      // So change event emitted on a div does not propagate upwards.
      expect(onChangeHandler).toBeCalledTimes(0);
      expect(onInputHandler).toBeCalledTimes(1);
      expect(onClickHandler).toBeCalledTimes(0);

      div.dispatchEvent(new Event('click', {bubbles: true}));
      expect(onChangeHandler).toBeCalledTimes(0);
      expect(onInputHandler).toBeCalledTimes(1);
      expect(onClickHandler).toBeCalledTimes(1);
    });

    it('div onChange/onInput/onClick with event target div child', async () => {
      const container = document.createElement('div');
      document.body.appendChild(container);
      const root = ReactDOMClient.createRoot(container);
      const onChangeHandler = jest.fn();
      const onInputHandler = jest.fn();
      const onClickHandler = jest.fn();
      await act(() => {
        root.render(
          <div
            onChange={onChangeHandler}
            onInput={onInputHandler}
            onClick={onClickHandler}>
            <div />
          </div>,
        );
      });

      const div = container.querySelector('div > div');
      div.dispatchEvent(new Event('input', {bubbles: true}));
      expect(onChangeHandler).toBeCalledTimes(0);
      expect(onInputHandler).toBeCalledTimes(1);
      expect(onClickHandler).toBeCalledTimes(0);

      div.dispatchEvent(new Event('change', {bubbles: true}));
      // React always ignores change event invoked on non-custom and non-input targets.
      // So change event emitted on a div does not propagate upwards.
      expect(onChangeHandler).toBeCalledTimes(0);
      expect(onInputHandler).toBeCalledTimes(1);
      expect(onClickHandler).toBeCalledTimes(0);

      div.dispatchEvent(new Event('click', {bubbles: true}));
      expect(onChangeHandler).toBeCalledTimes(0);
      expect(onInputHandler).toBeCalledTimes(1);
      expect(onClickHandler).toBeCalledTimes(1);
    });

    it('custom element onChange/onInput/onClick with event target custom element child', async () => {
      const container = document.createElement('div');
      document.body.appendChild(container);
      const root = ReactDOMClient.createRoot(container);
      const onChangeHandler = jest.fn();
      const onInputHandler = jest.fn();
      const onClickHandler = jest.fn();
      await act(() => {
        root.render(
          <my-custom-element
            onChange={onChangeHandler}
            onInput={onInputHandler}
            onClick={onClickHandler}>
            <other-custom-element />
          </my-custom-element>,
        );
      });

      const customChild = container.querySelector('other-custom-element');
      customChild.dispatchEvent(new Event('input', {bubbles: true}));
      // There is no simulated onChange, only raw onInput is dispatched.
      expect(onChangeHandler).toBeCalledTimes(0);
      expect(onInputHandler).toBeCalledTimes(1);
      expect(onClickHandler).toBeCalledTimes(0);
      // The native change event propagates to the parent as onChange.
      customChild.dispatchEvent(new Event('change', {bubbles: true}));
      expect(onChangeHandler).toBeCalledTimes(1);
      expect(onInputHandler).toBeCalledTimes(1);
      expect(onClickHandler).toBeCalledTimes(0);
      customChild.dispatchEvent(new Event('click', {bubbles: true}));
      expect(onChangeHandler).toBeCalledTimes(1);
      expect(onInputHandler).toBeCalledTimes(1);
      expect(onClickHandler).toBeCalledTimes(1);
    });

    it('custom elements should allow custom events with capture event listeners', async () => {
      const oncustomeventCapture = jest.fn();
      const oncustomevent = jest.fn();
      function Test() {
        return (
          <my-custom-element
            oncustomeventCapture={oncustomeventCapture}
            oncustomevent={oncustomevent}>
            <div />
          </my-custom-element>
        );
      }
      const container = document.createElement('div');
      const root = ReactDOMClient.createRoot(container);
      await act(() => {
        root.render(<Test />);
      });
      container
        .querySelector('my-custom-element > div')
        .dispatchEvent(new Event('customevent', {bubbles: false}));
      expect(oncustomeventCapture).toHaveBeenCalledTimes(1);
      expect(oncustomevent).toHaveBeenCalledTimes(0);
    });

    it('innerHTML should not work on custom elements', async () => {
      const container = document.createElement('div');
      const root = ReactDOMClient.createRoot(container);
      await act(() => {
        root.render(<my-custom-element innerHTML="foo" />);
      });
      const customElement = container.querySelector('my-custom-element');
      expect(customElement.getAttribute('innerHTML')).toBe(null);
      expect(customElement.hasChildNodes()).toBe(false);

      // Render again to verify the update codepath doesn't accidentally let
      // something through.
      await act(() => {
        root.render(<my-custom-element innerHTML="bar" />);
      });
      expect(customElement.getAttribute('innerHTML')).toBe(null);
      expect(customElement.hasChildNodes()).toBe(false);
    });

    it('innerText should not work on custom elements', async () => {
      const container = document.createElement('div');
      const root = ReactDOMClient.createRoot(container);
      await act(() => {
        root.render(<my-custom-element innerText="foo" />);
      });
      const customElement = container.querySelector('my-custom-element');
      expect(customElement.getAttribute('innerText')).toBe(null);
      expect(customElement.hasChildNodes()).toBe(false);

      // Render again to verify the update codepath doesn't accidentally let
      // something through.
      await act(() => {
        root.render(<my-custom-element innerText="bar" />);
      });
      expect(customElement.getAttribute('innerText')).toBe(null);
      expect(customElement.hasChildNodes()).toBe(false);
    });

    it('textContent should not work on custom elements', async () => {
      const container = document.createElement('div');
      const root = ReactDOMClient.createRoot(container);
      await act(() => {
        root.render(<my-custom-element textContent="foo" />);
      });
      const customElement = container.querySelector('my-custom-element');
      expect(customElement.getAttribute('textContent')).toBe(null);
      expect(customElement.hasChildNodes()).toBe(false);

      // Render again to verify the update codepath doesn't accidentally let
      // something through.
      await act(() => {
        root.render(<my-custom-element textContent="bar" />);
      });
      expect(customElement.getAttribute('textContent')).toBe(null);
      expect(customElement.hasChildNodes()).toBe(false);
    });

    it('values should not be converted to booleans when assigning into custom elements', async () => {
      const container = document.createElement('div');
      document.body.appendChild(container);
      const root = ReactDOMClient.createRoot(container);
      await act(() => {
        root.render(<my-custom-element />);
      });
      const customElement = container.querySelector('my-custom-element');
      customElement.foo = null;

      // true => string
      await act(() => {
        root.render(<my-custom-element foo={true} />);
      });
      expect(customElement.foo).toBe(true);
      await act(() => {
        root.render(<my-custom-element foo="bar" />);
      });
      expect(customElement.foo).toBe('bar');

      // false => string
      await act(() => {
        root.render(<my-custom-element foo={false} />);
      });
      expect(customElement.foo).toBe(false);
      await act(() => {
        root.render(<my-custom-element foo="bar" />);
      });
      expect(customElement.foo).toBe('bar');

      // true => null
      await act(() => {
        root.render(<my-custom-element foo={true} />);
      });
      expect(customElement.foo).toBe(true);
      await act(() => {
        root.render(<my-custom-element foo={null} />);
      });
      expect(customElement.foo).toBe(null);

      // false => null
      await act(() => {
        root.render(<my-custom-element foo={false} />);
      });
      expect(customElement.foo).toBe(false);
      await act(() => {
        root.render(<my-custom-element foo={null} />);
      });
      expect(customElement.foo).toBe(null);
    });

    it('boolean props should not be stringified in attributes', async () => {
      const container = document.createElement('div');
      document.body.appendChild(container);
      const root = ReactDOMClient.createRoot(container);
      await act(() => {
        root.render(<my-custom-element foo={true} />);
      });
      const customElement = container.querySelector('my-custom-element');

      expect(customElement.getAttribute('foo')).toBe('');

      // true => false
      await act(() => {
        root.render(<my-custom-element foo={false} />);
      });

      expect(customElement.getAttribute('foo')).toBe(null);
    });

    it('custom element custom event handlers assign multiple types', async () => {
      const container = document.createElement('div');
      document.body.appendChild(container);
      const root = ReactDOMClient.createRoot(container);
      const oncustomevent = jest.fn();

      // First render with string
      await act(() => {
        root.render(<my-custom-element oncustomevent={'foo'} />);
      });
      const customelement = container.querySelector('my-custom-element');
      customelement.dispatchEvent(new Event('customevent'));
      expect(oncustomevent).toHaveBeenCalledTimes(0);
      expect(customelement.oncustomevent).toBe(undefined);
      expect(customelement.getAttribute('oncustomevent')).toBe('foo');

      // string => event listener
      await act(() => {
        root.render(<my-custom-element oncustomevent={oncustomevent} />);
      });
      customelement.dispatchEvent(new Event('customevent'));
      expect(oncustomevent).toHaveBeenCalledTimes(1);
      expect(customelement.oncustomevent).toBe(undefined);
      expect(customelement.getAttribute('oncustomevent')).toBe(null);

      // event listener => string
      await act(() => {
        root.render(<my-custom-element oncustomevent={'foo'} />);
      });
      customelement.dispatchEvent(new Event('customevent'));
      expect(oncustomevent).toHaveBeenCalledTimes(1);
      expect(customelement.oncustomevent).toBe(undefined);
      expect(customelement.getAttribute('oncustomevent')).toBe('foo');

      // string => nothing
      await act(() => {
        root.render(<my-custom-element />);
      });
      customelement.dispatchEvent(new Event('customevent'));
      expect(oncustomevent).toHaveBeenCalledTimes(1);
      expect(customelement.oncustomevent).toBe(undefined);
      expect(customelement.getAttribute('oncustomevent')).toBe(null);

      // nothing => event listener
      await act(() => {
        root.render(<my-custom-element oncustomevent={oncustomevent} />);
      });
      customelement.dispatchEvent(new Event('customevent'));
      expect(oncustomevent).toHaveBeenCalledTimes(2);
      expect(customelement.oncustomevent).toBe(undefined);
      expect(customelement.getAttribute('oncustomevent')).toBe(null);
    });

    it('custom element custom event handlers assign multiple types with setter', async () => {
      const container = document.createElement('div');
      document.body.appendChild(container);
      const root = ReactDOMClient.createRoot(container);
      const oncustomevent = jest.fn();

      // First render with nothing
      await act(() => {
        root.render(<my-custom-element />);
      });
      const customelement = container.querySelector('my-custom-element');
      // Install a setter to activate the `in` heuristic
      Object.defineProperty(customelement, 'oncustomevent', {
        set: function (x) {
          this._oncustomevent = x;
        },
        get: function () {
          return this._oncustomevent;
        },
      });
      expect(customelement.oncustomevent).toBe(undefined);

      // nothing => event listener
      await act(() => {
        root.render(<my-custom-element oncustomevent={oncustomevent} />);
      });
      customelement.dispatchEvent(new Event('customevent'));
      expect(oncustomevent).toHaveBeenCalledTimes(1);
      expect(customelement.oncustomevent).toBe(null);
      expect(customelement.getAttribute('oncustomevent')).toBe(null);

      // event listener => string
      await act(() => {
        root.render(<my-custom-element oncustomevent={'foo'} />);
      });
      customelement.dispatchEvent(new Event('customevent'));
      expect(oncustomevent).toHaveBeenCalledTimes(1);
      expect(customelement.oncustomevent).toBe('foo');
      expect(customelement.getAttribute('oncustomevent')).toBe(null);

      // string => event listener
      await act(() => {
        root.render(<my-custom-element oncustomevent={oncustomevent} />);
      });
      customelement.dispatchEvent(new Event('customevent'));
      expect(oncustomevent).toHaveBeenCalledTimes(2);
      expect(customelement.oncustomevent).toBe(null);
      expect(customelement.getAttribute('oncustomevent')).toBe(null);

      // event listener => nothing
      await act(() => {
        root.render(<my-custom-element />);
      });
      customelement.dispatchEvent(new Event('customevent'));
      expect(oncustomevent).toHaveBeenCalledTimes(2);
      expect(customelement.oncustomevent).toBe(undefined);
      expect(customelement.getAttribute('oncustomevent')).toBe(null);
    });

    it('assigning to a custom element property should not remove attributes', async () => {
      const container = document.createElement('div');
      document.body.appendChild(container);
      const root = ReactDOMClient.createRoot(container);
      await act(() => {
        root.render(<my-custom-element foo="one" />);
      });
      const customElement = container.querySelector('my-custom-element');
      expect(customElement.getAttribute('foo')).toBe('one');

      // Install a setter to activate the `in` heuristic
      Object.defineProperty(customElement, 'foo', {
        set: function (x) {
          this._foo = x;
        },
        get: function () {
          return this._foo;
        },
      });
      await act(() => {
        root.render(<my-custom-element foo="two" />);
      });
      expect(customElement.foo).toBe('two');
      expect(customElement.getAttribute('foo')).toBe('one');
    });

    it('custom element properties should accept functions', async () => {
      const container = document.createElement('div');
      document.body.appendChild(container);
      const root = ReactDOMClient.createRoot(container);
      await act(() => {
        root.render(<my-custom-element />);
      });
      const customElement = container.querySelector('my-custom-element');

      // Install a setter to activate the `in` heuristic
      Object.defineProperty(customElement, 'foo', {
        set: function (x) {
          this._foo = x;
        },
        get: function () {
          return this._foo;
        },
      });
      function myFunction() {
        return 'this is myFunction';
      }
      await act(() => {
        root.render(<my-custom-element foo={myFunction} />);
      });
      expect(customElement.foo).toBe(myFunction);

      // Also remove and re-add the property for good measure
      await act(() => {
        root.render(<my-custom-element />);
      });
      expect(customElement.foo).toBe(undefined);
      await act(() => {
        root.render(<my-custom-element foo={myFunction} />);
      });
      expect(customElement.foo).toBe(myFunction);
    });

    it('switching between null and undefined should update a property', async () => {
      const container = document.createElement('div');
      document.body.appendChild(container);
      const root = ReactDOMClient.createRoot(container);
      await act(() => {
        root.render(<my-custom-element foo={undefined} />);
      });
      const customElement = container.querySelector('my-custom-element');
      customElement.foo = undefined;

      await act(() => {
        root.render(<my-custom-element foo={null} />);
      });
      expect(customElement.foo).toBe(null);

      await act(() => {
        root.render(<my-custom-element foo={undefined} />);
      });
      expect(customElement.foo).toBe(undefined);
    });

    it('warns when using popoverTarget={HTMLElement}', async () => {
      const popoverTarget = document.createElement('div');
      const container = document.createElement('div');
      const root = ReactDOMClient.createRoot(container);

      await act(() => {
        root.render(
          <button key="one" popoverTarget={popoverTarget}>
            Toggle popover
          </button>,
        );
      });

      assertConsoleErrorDev([
        'The `popoverTarget` prop expects the ID of an Element as a string. Received HTMLDivElement {} instead.\n' +
          '    in button (at **)',
      ]);

      // Dedupe warning
      await act(() => {
        root.render(
          <button key="two" popoverTarget={popoverTarget}>
            Toggle popover
          </button>,
        );
      });
    });
  });

  describe('deleteValueForProperty', () => {
    it('should remove attributes for normal properties', async () => {
      const container = document.createElement('div');
      const root = ReactDOMClient.createRoot(container);
      await act(() => {
        root.render(<div title="foo" />);
      });
      expect(container.firstChild.getAttribute('title')).toBe('foo');
      await act(() => {
        root.render(<div />);
      });
      expect(container.firstChild.getAttribute('title')).toBe(null);
    });

    it('should not remove attributes for special properties', async () => {
      const container = document.createElement('div');
      const root = ReactDOMClient.createRoot(container);
      await act(() => {
        root.render(
          <input type="text" value="foo" onChange={function () {}} />,
        );
      });
      if (disableInputAttributeSyncing) {
        expect(container.firstChild.hasAttribute('value')).toBe(false);
      } else {
        expect(container.firstChild.getAttribute('value')).toBe('foo');
      }
      expect(container.firstChild.value).toBe('foo');
      await act(() => {
        root.render(<input type="text" onChange={function () {}} />);
      });
      assertConsoleErrorDev([
        'A component is changing a controlled input to be uncontrolled. ' +
          'This is likely caused by the value changing from a defined to undefined, ' +
          'which should not happen. Decide between using a controlled or uncontrolled ' +
          'input element for the lifetime of the component. ' +
          'More info: https://react.dev/link/controlled-components\n' +
          '    in input (at **)',
      ]);
      if (disableInputAttributeSyncing) {
        expect(container.firstChild.hasAttribute('value')).toBe(false);
      } else {
        expect(container.firstChild.getAttribute('value')).toBe('foo');
      }
      expect(container.firstChild.value).toBe('foo');
    });

    it('should not remove attributes for custom component tag', async () => {
      const container = document.createElement('div');
      const root = ReactDOMClient.createRoot(container);
      await act(() => {
        root.render(<my-icon size="5px" />);
      });
      expect(container.firstChild.getAttribute('size')).toBe('5px');
    });

    it('custom elements should remove by setting undefined to restore defaults', async () => {
      const container = document.createElement('div');
      document.body.appendChild(container);
      const root = ReactDOMClient.createRoot(container);
      await act(() => {
        root.render(<my-custom-element />);
      });
      const customElement = container.querySelector('my-custom-element');

      // Non-setter but existing property to active the `in` heuristic
      customElement.raw = 1;

      // Install a setter to activate the `in` heuristic
      Object.defineProperty(customElement, 'object', {
        set: function (value = null) {
          this._object = value;
        },
        get: function () {
          return this._object;
        },
      });

      Object.defineProperty(customElement, 'string', {
        set: function (value = '') {
          this._string = value;
        },
        get: function () {
          return this._string;
        },
      });

      const obj = {};
      await act(() => {
        root.render(<my-custom-element raw={2} object={obj} string="hi" />);
      });
      expect(customElement.raw).toBe(2);
      expect(customElement.object).toBe(obj);
      expect(customElement.string).toBe('hi');

      // Removing the properties should reset to defaults by passing undefined
      await act(() => {
        root.render(<my-custom-element />);
      });
      expect(customElement.raw).toBe(undefined);
      expect(customElement.object).toBe(null);
      expect(customElement.string).toBe('');
    });
  });
});