-
Notifications
You must be signed in to change notification settings - Fork 47.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add simulateEventDispatch to test ReactDOMEventListener #28079
Add simulateEventDispatch to test ReactDOMEventListener #28079
Conversation
Comparing: 952aa74...b9ed282 Critical size changesIncludes critical production bundles, as well as any change greater than 2%:
Significant size changesIncludes any change greater than 0.2%: Expand to show
|
(can we call it |
@sophiebits renamed to |
I think the ideal implementation of this would accept an array of array of logs, with the logs we expect between each yield, and asserting that they're logged. Calling |
// By the time we leave the handler, the second update is flushed. | ||
// | ||
// Since this is a discrete event, the previous update is already done. | ||
if (gate(flags => flags.enableLegacyFBSupport)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think this was the right fix, I think the issue is we're not dispatching up to the document, because the other tests are failing for the same reason. I'll look deeper and figure out the fix
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the issue here isn't the document.
I'm not sure how this works exactly if it's two listeners at the document or just one. I think it's actually two listeners at the document. So we should flush between them.
The issue is that dispatchAndWaitForDiscrete
waits between nodes but it doesn't wait between events on the same node.
I wonder if there's a way to get to the internal list of events in JSDOM. Otherwise, you might have to patch EventTarget.prototype.addEventListener
so you can gather your own list and iterate through it and wait between each one.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks like you can get access to internals through the symbol:
const impl = Object.getOwnPropertySymbols(x)[0];
x[impl]._eventListeners
They should've called it DO_NOT_USE.
You could potentially fork https://github.com/jsdom/jsdom/blob/2f8a7302a43fff92f244d5f3426367a8eb2b8896/lib/jsdom/living/events/EventTarget-impl.js#L114 and make it an async function.
// Bubbling phase | ||
// Walk the path from the element to the document and dispatch | ||
// the event on each node, flushing microtasks in between. | ||
for (let current = node; current; current = current.parentNode) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Technically if you remove or move a node during this phase it'll get the events.
So really this should create a snapshot of all parent nodes into an array and then dispatch to each of those. That way if they change the DOM during the bubbling phase, all events still happen (on disconnected nodes).
I wish I knew less about the DOM.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, if this doesn't end up with a parentNode that is in a document, it shouldn't dispatch anything because it means the target was detached before dispatching.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed and added a test.
Object.defineProperty(customEvent, 'eventPhase', { | ||
// Avoid going through the capture/bubbling phases, | ||
// since we're doing it manually. | ||
value: Event.AT_TARGET, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do you need to set this if you're dispatching to each target without bubbling anyway? Shouldn't this always be AT_TARGET
anyway?
Ideally it should only be AT_TARGET
when node === current
and otherwise the phase. You could potentially set it to CAPTURING_PHASE
and add an event listener in the end of the capturing phase and then in that listener switch to BUBBLING_PHASE
.
Not sure this matters though.
The phases will be wrong anyway since it'll be capture/bubble at each node instead of all capture and then all bubbles.
); | ||
} | ||
|
||
const container = document.createElement('div'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This container is not added to the document body so it shouldn't get any events dispatched to it.
The test probably intended to add it to the body like the others.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed and added a test.
2ae431c
to
6b73d5d
Compare
@sebmarkbage updated to fork This also means we don't need to manually dispatch events up the tree, so the capture phase isn't duplicated. I've renamed the function |
Updates React from 2bc7d336a to ba5e6a832. ### React upstream changes - facebook/react#28283 - facebook/react#28280 - facebook/react#28079 - facebook/react#28233 - facebook/react#28276 - facebook/react#28272 - facebook/react#28265 - facebook/react#28259 - facebook/react#28153 - facebook/react#28246 - facebook/react#28218 - facebook/react#28263 - facebook/react#28257 - facebook/react#28261 - facebook/react#28262 - facebook/react#28260 - facebook/react#28258 - facebook/react#27864 - facebook/react#28254 - facebook/react#28219 - facebook/react#28248 - facebook/react#28216 - facebook/react#28249 - facebook/react#28241 - facebook/react#28243 - facebook/react#28253 - facebook/react#28256 - facebook/react#28236 - facebook/react#28237 - facebook/react#28242 - facebook/react#28251 - facebook/react#28252 Closes NEXT-2411
## 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.
## 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 #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. DiffTrain build for commit cd63ef7.
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:
Sandbox
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: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 #28050 (comment), and that test is an example of where this is an issue with nested roots.Here's a sandox for discrete and continuous 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.