Skip to content

Commit

Permalink
Add simulateEventDispatch to test ReactDOMEventListener (facebook#28079)
Browse files Browse the repository at this point in the history
## 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.
  • Loading branch information
rickhanlonii authored and AndyPengc12 committed Apr 15, 2024
1 parent 52901c3 commit a984e67
Show file tree
Hide file tree
Showing 4 changed files with 1,102 additions and 23 deletions.
38 changes: 38 additions & 0 deletions packages/internal-test-utils/ReactInternalTestUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import * as SchedulerMock from 'scheduler/unstable_mock';
import {diff} from 'jest-diff';
import {equals} from '@jest/expect-utils';
import enqueueTask from './enqueueTask';
import simulateBrowserEventDispatch from './simulateBrowserEventDispatch';

export {act} from './internalAct';

Expand Down Expand Up @@ -264,3 +265,40 @@ ${diff(expectedLog, actualLog)}
Error.captureStackTrace(error, assertLog);
throw error;
}

// Simulates dispatching events, waiting for microtasks in between.
// This matches the browser behavior, which will flush microtasks
// between each event handler. This will allow discrete events to
// flush between events across different event handlers.
export async function simulateEventDispatch(
node: Node,
eventType: string,
): Promise<void> {
// Ensure the node is in the document.
for (let current = node; current; current = current.parentNode) {
if (current === document) {
break;
} else if (current.parentNode == null) {
return;
}
}

const customEvent = new Event(eventType, {
bubbles: true,
});

Object.defineProperty(customEvent, 'target', {
// Override the target to the node on which we dispatched the event.
value: node,
});

const impl = Object.getOwnPropertySymbols(node)[0];
const oldDispatch = node[impl].dispatchEvent;
try {
node[impl].dispatchEvent = simulateBrowserEventDispatch;

await node.dispatchEvent(customEvent);
} finally {
node[impl].dispatchEvent = oldDispatch;
}
}
Loading

0 comments on commit a984e67

Please sign in to comment.