Skip to content

Commit cd63ef7

Browse files
authored
Add simulateEventDispatch to test ReactDOMEventListener (facebook#28079)
## Overview For events, the browser will yield to microtasks between calling event handers, allowing time to flush work inbetween. For example, in the browser, this code will log the flushes between events: ```js <body onclick="console.log('body'); Promise.resolve().then(() => console.log('flush body'));"> <div onclick="console.log('div'); Promise.resolve().then(() => console.log('flush div'));"> hi </div> </body> // Logs div flush div body flush body ``` [Sandbox](https://codesandbox.io/s/eloquent-noether-mw2cjg?file=/index.html) The problem is, `dispatchEvent` (either in the browser, or JSDOM) does not yield to microtasks. Which means, this code will log the flushes after the events: ```js const target = document.getElementsByTagName("div")[0]; const nativeEvent = document.createEvent("Event"); nativeEvent.initEvent("click", true, true); target.dispatchEvent(nativeEvent); // Logs div body flush div flush body ``` ## The problem This mostly isn't a problem because React attaches event handler at the root, and calls the event handlers on components via the synthetic event system. We handle flushing between calling event handlers as needed. However, if you're mixing capture and bubbling events, or using multiple roots, then the problem of not flushing microtasks between events can come into play. This was found when converting a test to `createRoot` in facebook#28050 (comment), and that test is an example of where this is an issue with nested roots. Here's a sandox for [discrete](https://codesandbox.io/p/sandbox/red-http-2wg8k5) and [continuous](https://codesandbox.io/p/sandbox/gracious-voice-6r7tsc?file=%2Fsrc%2Findex.js%3A25%2C28) events, showing how the test should behave. The existing test, when switched to `createRoot` matches the browser behavior for continuous events, but not discrete. Continuous events should be batched, and discrete should flush individually. ## The fix This PR implements the fix suggested by @sebmarkbage, to manually traverse the path up from the element and dispatch events, yielding between each call.
1 parent 04b5992 commit cd63ef7

File tree

4 files changed

+1102
-23
lines changed

4 files changed

+1102
-23
lines changed

packages/internal-test-utils/ReactInternalTestUtils.js

+38
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import * as SchedulerMock from 'scheduler/unstable_mock';
99
import {diff} from 'jest-diff';
1010
import {equals} from '@jest/expect-utils';
1111
import enqueueTask from './enqueueTask';
12+
import simulateBrowserEventDispatch from './simulateBrowserEventDispatch';
1213

1314
export {act} from './internalAct';
1415

@@ -264,3 +265,40 @@ ${diff(expectedLog, actualLog)}
264265
Error.captureStackTrace(error, assertLog);
265266
throw error;
266267
}
268+
269+
// Simulates dispatching events, waiting for microtasks in between.
270+
// This matches the browser behavior, which will flush microtasks
271+
// between each event handler. This will allow discrete events to
272+
// flush between events across different event handlers.
273+
export async function simulateEventDispatch(
274+
node: Node,
275+
eventType: string,
276+
): Promise<void> {
277+
// Ensure the node is in the document.
278+
for (let current = node; current; current = current.parentNode) {
279+
if (current === document) {
280+
break;
281+
} else if (current.parentNode == null) {
282+
return;
283+
}
284+
}
285+
286+
const customEvent = new Event(eventType, {
287+
bubbles: true,
288+
});
289+
290+
Object.defineProperty(customEvent, 'target', {
291+
// Override the target to the node on which we dispatched the event.
292+
value: node,
293+
});
294+
295+
const impl = Object.getOwnPropertySymbols(node)[0];
296+
const oldDispatch = node[impl].dispatchEvent;
297+
try {
298+
node[impl].dispatchEvent = simulateBrowserEventDispatch;
299+
300+
await node.dispatchEvent(customEvent);
301+
} finally {
302+
node[impl].dispatchEvent = oldDispatch;
303+
}
304+
}

0 commit comments

Comments
 (0)