diff --git a/packages/react-dom/src/__tests__/ReactDOMNestedEvents-test.js b/packages/react-dom/src/__tests__/ReactDOMNestedEvents-test.js
index 6c632cd31e..1fac2c6ed1 100644
--- a/packages/react-dom/src/__tests__/ReactDOMNestedEvents-test.js
+++ b/packages/react-dom/src/__tests__/ReactDOMNestedEvents-test.js
@@ -11,6 +11,7 @@
describe('ReactDOMNestedEvents', () => {
let React;
+ let ReactDOM;
let ReactDOMClient;
let Scheduler;
let act;
@@ -19,13 +20,14 @@ describe('ReactDOMNestedEvents', () => {
beforeEach(() => {
jest.resetModules();
React = require('react');
+ ReactDOM = require('react-dom');
ReactDOMClient = require('react-dom/client');
Scheduler = require('scheduler');
act = require('jest-react').act;
useState = React.useState;
});
- test('nested event dispatches should not cause updates to flush', async () => {
+ it('nested event dispatches should not cause updates to flush', async () => {
const buttonRef = React.createRef(null);
function App() {
const [isClicked, setIsClicked] = useState(false);
@@ -72,4 +74,160 @@ describe('ReactDOMNestedEvents', () => {
]);
expect(buttonRef.current.innerHTML).toEqual('Clicked: true, Focused: true');
});
+
+ it('custom events inside a discrete event is batched with sync updates and flushed synchronously', async () => {
+ const buttonRef = React.createRef(null);
+ function App() {
+ const [isClicked, setIsClicked] = useState(false);
+ const [isCustom, setIsCustom] = useState(false);
+ const onClick = () => {
+ setIsClicked(true);
+ buttonRef.current.dispatchEvent(new Event('custom'));
+ };
+ const onCustomEvent = () => {
+ setIsCustom(true);
+ };
+ React.useEffect(() => {
+ buttonRef.current.addEventListener('click', onClick);
+ buttonRef.current.addEventListener('custom', onCustomEvent);
+ }, []);
+ Scheduler.unstable_yieldValue(`render: ${isClicked} / ${isCustom}`);
+ return ;
+ }
+
+ const container = document.createElement('div');
+ document.body.appendChild(container);
+ const root = ReactDOMClient.createRoot(container);
+
+ await act(async () => {
+ root.render();
+ });
+ expect(Scheduler).toHaveYielded(['render: false / false']);
+
+ await act(async () => {
+ buttonRef.current.click();
+ });
+ expect(Scheduler).toHaveYielded(['render: true / true']);
+ });
+
+ // This just exists to assert the Jest behavior. Once it has the expected behavior,
+ // this and related mocks can be deleted in this file
+ it('jest does not have the right `window.event` in queueMicrotask', () => {
+ const btn = document.createElement('button');
+ const customEvent = new CustomEvent('custom');
+ btn.addEventListener('custom', () => {
+ queueMicrotask(() => {
+ // This should be 'click'
+ expect(window.event).toBe(undefined);
+ });
+ });
+ btn.addEventListener('click', () => {
+ btn.dispatchEvent(customEvent);
+ });
+
+ btn.click();
+ });
+
+ it('custom events inside a discrete event flushes synchronously', async () => {
+ const buttonRef = React.createRef(null);
+
+ // Mock the browser behavior on setting `window.event` in microtasks
+ let outterEvent;
+
+ function App() {
+ const [isCustom, setIsCustom] = useState(false);
+ const onClick = () => {
+ outterEvent = window.event;
+ const customEvent = new Event('custom');
+ buttonRef.current.dispatchEvent(customEvent);
+ queueMicrotask(() => {
+ window.event = undefined;
+ });
+ };
+ const onCustomEvent = () => {
+ // In the microtask, `window.event` should be the outter click event
+ queueMicrotask(() => {
+ window.event = outterEvent;
+ });
+ setIsCustom(true);
+ };
+ React.useEffect(() => {
+ buttonRef.current.addEventListener('custom', onCustomEvent);
+ buttonRef.current.addEventListener('click', onClick);
+ }, []);
+ Scheduler.unstable_yieldValue(`render: ${isCustom}`);
+ return ;
+ }
+
+ const container = document.createElement('div');
+ document.body.appendChild(container);
+ const root = ReactDOMClient.createRoot(container);
+
+ await act(async () => {
+ root.render();
+ });
+ expect(Scheduler).toHaveYielded(['render: false']);
+
+ await act(async () => {
+ buttonRef.current.click();
+ queueMicrotask(() => {
+ Scheduler.unstable_yieldValue('Sync');
+ });
+ Scheduler.unstable_scheduleCallback(
+ Scheduler.unstable_ImmediatePriority,
+ () => {
+ Scheduler.unstable_yieldValue('Immediate');
+ },
+ );
+ });
+ // The custom update should happens synchronously before yielding 'Immediate'
+ expect(Scheduler).toHaveYielded(['render: true', 'Sync', 'Immediate']);
+ });
+
+ it('custom events inside other events keeps the default priority', async () => {
+ const buttonRef = React.createRef(null);
+
+ function App() {
+ const [isCustom, setIsCustom] = useState(false);
+ const onClick = () => {
+ buttonRef.current.dispatchEvent(new Event('custom'));
+ };
+ const onCustomEvent = () => {
+ setIsCustom(true);
+ };
+ React.useEffect(() => {
+ buttonRef.current.addEventListener('custom', onCustomEvent);
+ buttonRef.current.addEventListener('mouseover', onClick);
+ }, []);
+ Scheduler.unstable_yieldValue(`render: ${isCustom}`);
+ return ;
+ }
+
+ const container = document.createElement('div');
+ document.body.appendChild(container);
+ const root = ReactDOMClient.createRoot(container);
+
+ await act(async () => {
+ root.render();
+ });
+ expect(Scheduler).toHaveYielded(['render: false']);
+
+ await act(async () => {
+ buttonRef.current.dispatchEvent(new Event('mouseover', {bubbles: true}));
+ Scheduler.unstable_scheduleCallback(
+ Scheduler.unstable_UserBlockingPriority,
+ () => {
+ Scheduler.unstable_yieldValue('Continuous');
+ },
+ );
+ Scheduler.unstable_scheduleCallback(
+ Scheduler.unstable_NormalPriority,
+ () => {
+ Scheduler.unstable_yieldValue('Default');
+ },
+ );
+ });
+ // The custom update should happens with the default priority
+ expect(Scheduler).toHaveYielded(['Continuous', 'render: true', 'Default']);
+ });
});