/** * 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'; // polyfill missing JSDOM support class ToggleEvent extends Event { constructor(type, eventInit) { super(type, eventInit); this.newState = eventInit.newState; this.oldState = eventInit.oldState; } } describe('SimpleEventPlugin', function () { let React; let ReactDOMClient; let Scheduler; let act; let onClick; let container; let assertLog; let waitForAll; async function expectClickThru(element) { await act(() => { element.click(); }); expect(onClick).toHaveBeenCalledTimes(1); } function expectNoClickThru(element) { element.click(); expect(onClick).toHaveBeenCalledTimes(0); } async function mounted(element) { container = document.createElement('div'); document.body.appendChild(container); const root = ReactDOMClient.createRoot(container); await act(() => { root.render(element); }); element = container.firstChild; return element; } beforeEach(function () { jest.resetModules(); React = require('react'); ReactDOMClient = require('react-dom/client'); Scheduler = require('scheduler'); const InternalTestUtils = require('internal-test-utils'); assertLog = InternalTestUtils.assertLog; waitForAll = InternalTestUtils.waitForAll; act = InternalTestUtils.act; onClick = jest.fn(); }); afterEach(() => { if (container && document.body.contains(container)) { document.body.removeChild(container); container = null; } }); it('A non-interactive tags click when disabled', async function () { const element =
; await expectClickThru(await mounted(element)); }); it('A non-interactive tags clicks bubble when disabled', async function () { const element = await mounted(
, ); const child = element.firstChild; child.click(); expect(onClick).toHaveBeenCalledTimes(1); }); it('does not register a click when clicking a child of a disabled element', async function () { const element = await mounted( , ); const child = element.querySelector('span'); child.click(); expect(onClick).toHaveBeenCalledTimes(0); }); it('triggers click events for children of disabled elements', async function () { const element = await mounted( , ); const child = element.querySelector('span'); child.click(); expect(onClick).toHaveBeenCalledTimes(1); }); it('triggers parent captured click events when target is a child of a disabled elements', async function () { const element = await mounted(
, ); const child = element.querySelector('span'); child.click(); expect(onClick).toHaveBeenCalledTimes(1); }); it('triggers captured click events for children of disabled elements', async function () { const element = await mounted( , ); const child = element.querySelector('span'); child.click(); expect(onClick).toHaveBeenCalledTimes(1); }); describe.each(['button', 'input', 'select', 'textarea'])( '%s', function (tagName) { it('should forward clicks when it starts out not disabled', async () => { const element = React.createElement(tagName, { onClick: onClick, }); await expectClickThru(await mounted(element)); }); it('should not forward clicks when it starts out disabled', async () => { const element = React.createElement(tagName, { onClick: onClick, disabled: true, }); await expectNoClickThru(await mounted(element)); }); it('should forward clicks when it becomes not disabled', async () => { container = document.createElement('div'); document.body.appendChild(container); const root = ReactDOMClient.createRoot(container); await act(() => { root.render( React.createElement(tagName, {onClick: onClick, disabled: true}), ); }); await act(() => { root.render(React.createElement(tagName, {onClick: onClick})); }); const element = container.firstChild; await expectClickThru(element); }); it('should not forward clicks when it becomes disabled', async () => { container = document.createElement('div'); document.body.appendChild(container); const root = ReactDOMClient.createRoot(container); await act(() => { root.render(React.createElement(tagName, {onClick: onClick})); }); await act(() => { root.render( React.createElement(tagName, {onClick: onClick, disabled: true}), ); }); const element = container.firstChild; expectNoClickThru(element); }); it('should work correctly if the listener is changed', async () => { container = document.createElement('div'); document.body.appendChild(container); const root = ReactDOMClient.createRoot(container); await act(() => { root.render( React.createElement(tagName, {onClick: onClick, disabled: true}), ); }); await act(() => { root.render( React.createElement(tagName, {onClick: onClick, disabled: false}), ); }); const element = container.firstChild; await expectClickThru(element); }); }, ); it('batches updates that occur as a result of a nested event dispatch', async () => { container = document.createElement('div'); document.body.appendChild(container); let button; class Button extends React.Component { state = {count: 0}; increment = () => this.setState(state => ({ count: state.count + 1, })); componentDidUpdate() { Scheduler.log(`didUpdate - Count: ${this.state.count}`); } render() { return ( ); } } function click() { button.dispatchEvent( new MouseEvent('click', {bubbles: true, cancelable: true}), ); } const root = ReactDOMClient.createRoot(container); await act(() => { root.render( ); } } // Initial mount root.render(
popover content
, ); }); const target = container.querySelector('#popover'); target.dispatchEvent( new ToggleEvent('toggle', { bubbles: false, cancelable: true, oldState: 'closed', newState: 'open', }), ); expect(onToggle).toHaveBeenCalledTimes(1); let event = onToggle.mock.calls[0][0]; expect(event).toEqual( expect.objectContaining({ oldState: 'closed', newState: 'open', }), ); target.dispatchEvent( new ToggleEvent('toggle', { bubbles: false, cancelable: true, oldState: 'open', newState: 'closed', }), ); expect(onToggle).toHaveBeenCalledTimes(2); event = onToggle.mock.calls[1][0]; expect(event).toEqual( expect.objectContaining({ oldState: 'open', newState: 'closed', }), ); }); it('dispatches synthetic toggle events when
is used', async () => { // This test just replays browser behavior. // The real test would be if browsers dispatch ToggleEvent on
. // This case only exists because MDN claims
doesn't receive ToggleEvent. // However, Chrome dispatches ToggleEvent on
and the spec confirms that behavior: https://html.spec.whatwg.org/multipage/indices.html#event-toggle container = document.createElement('div'); const onToggle = jest.fn(); const root = ReactDOMClient.createRoot(container); await act(() => { root.render(
Summary Details
, ); }); const target = container.querySelector('#details'); target.dispatchEvent( new ToggleEvent('toggle', { bubbles: false, cancelable: true, oldState: 'closed', newState: 'open', }), ); expect(onToggle).toHaveBeenCalledTimes(1); let event = onToggle.mock.calls[0][0]; expect(event).toEqual( expect.objectContaining({ oldState: 'closed', newState: 'open', }), ); target.dispatchEvent( new ToggleEvent('toggle', { bubbles: false, cancelable: true, oldState: 'open', newState: 'closed', }), ); expect(onToggle).toHaveBeenCalledTimes(2); event = onToggle.mock.calls[1][0]; expect(event).toEqual( expect.objectContaining({ oldState: 'open', newState: 'closed', }), ); }); }); it('includes the submitter in submit events', async function () { container = document.createElement('div'); const onSubmit = jest.fn(event => { event.preventDefault(); }); const root = ReactDOMClient.createRoot(container); await act(() => { root.render(
, ); }); const submitter = container.querySelector('#submitter'); const submitEvent = new SubmitEvent('submit', { bubbles: true, cancelable: true, submitter: submitter, }); await act(() => { submitter.dispatchEvent(submitEvent); }); expect(onSubmit).toHaveBeenCalledTimes(1); const event = onSubmit.mock.calls[0][0]; expect(event.submitter).toBe(submitter); }); });