-
Notifications
You must be signed in to change notification settings - Fork 4.3k
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
E2E tests: Introduce insertBlock() util #7013
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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.
When and why does this occur? Sounds like a legitimate bug that we're pretending doesn't exist with the workaround here. 🙈
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.
My understanding1 is that the "bug" is that we're treating
click()
as if it weren't a very low-level API.Here's what
click()
does:Input.dispatchMouseEvent( { x, y, type: 'mouseMoved' } )
via the DevTools protocolInput.dispatchMouseEvent( { type: 'mousePressed' } )
via the DevTools protocoloptions.delay
is specified, waits for that amount of timeInput.dispatchMouseEvent( { type: 'mouseReleased' } )
via the DevTools protocolThe promise returned by
click()
is resolved when step 5 is completed. Each step takes a few milliseconds while Puppeteer waits for the DevTools protocol to send back an ACK message.In other words,
click()
returns a promise that resolves after Chrome processes ~5 WebSocket messages. This may be slow or fast, but its speed depends on how busy Chrome's DevTools protocol server is and not on what our web application is doing.We're treating
click()
as if it werejQuery.click()
—a method that can be blocked when the web application is painting a frame or executing a script. But this is not true.We ordinarily don't see any problems to do with using this low-level API because, normally, our UI responds to a click in less time than Chrome takes to process ~5 messages. But the inserter is fairly complex.
I measured this just now on my local machine and it took about 65 ms to render the inserter and have its search field focused:
I then measured that
click()
takes anywhere from 40 to 80 ms by inserting the following into the E2E test:These times are very similar to each other, and so I'm convinced that this is why we were seeing intermittent test failures.
With all that in mind, some possible solutions:
click()
action and before continuing with the test. This is what I opted for.delay
option when calling click, e.g.page.click( '.edit-post-header [aria-label="Add block"]', { delay: 30 } )
. I wasn't comfortable with this solution since it could still lead to an intermittent failure if the application is slow enough for whatever reason. It could also make our tests take longer to run.1 Please note that Gutenberg is my first experience in using Puppeteer and this is all based on the hour or so that I spent digging into the source code and API docs. I could not find documentation which explains exactly how all of this works. If we want to learn more, my next step would be asking around in the DevTools protocol mailing list.
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.
Aside: @pento just noted that we likely end up with a list block because the search field appears while
code
is being typed. Note that if you searchde
then the first result is the list block.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.
Wow, thanks for the thorough investigation and explanation @noisysocks ! Definitely helps shed light on what's going on here, and explains one of the most common issues that's been encountered with Puppeteer, which is race conditions between interactions and their expected effects. It's alluded to briefly in their documentation. Their suggestion is to define the expected
waitFor*
with the interaction in aPromise.all
.With your explanation, my hunch is that we'd still potentially have issues if the inserter, for whatever reason, started being rendered very quickly, since there might be the possibility that it's already rendered by the time we call
waitForSelector
in the tests? Thus it would hang forever becausewaitForSelector
does not resolve itself if the selector predicate is already matched on the page.Would something like this be an improvement then?
To my general motivation for leaving a comment: I just want to make sure that the UI is not being rendered after some delay, where a test which otherwise would fail because it moves too fast is subject to the same breakage for a particularly fast user (or a user on a slow computer). That doesn't seem to be what's happening here. Though it's unfortunate we have to author tests in such a way where we're "waiting" for selectors merely due to the mechanics of the DevTools protocol messaging.
I'd say we don't need to pursue this as a solution for testing in particular, and I don't think it'd be very bulletproof anyways, but that 65ms to render the Inserter is, in general, unacceptable, and should be improved anyways.
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 shouldn't be an issue because, according to the docs,
waitForSelector
will resolve immediately if the selector already exists.The
Promise.all
example that they have in the docs is for the very specific case where aclick()
results in page navigation. In this case, it's important to callwaitForNavigation()
before the navigation occurs.I actually did some more experimenting after writing my comment above because I wasn't satisfied with the seemingly scary proposition that we have to call
waitFor*
after everyclick()
action.It turns out that it's not necessary to do that because Chrome usually buffers events that occur while the UI is blocked due to a slow script. That is, it's normally safe to call
click()
ortype()
immediately after rendering a slow component because Chrome will wait for the frame to render before doing the clicking or typing. You can see this for yourself by repeatedly clicking on the inserter toggle: no matter how fast you do it, an even amount of clicks will result in it being closed.The reason that we have to
waitForSelector
in this particular case is becausetype()
relies on having the search<input>
selected and Chrome doesn't focus the input until the next frame. You can see that behaviour in the screenshot I posted above: note that there's a single frame where the inserter has rendered but the input has no blue focus ring around it.To confirm this I created this weird test HTML page. 'Click me' burns CPU cycles for 500 ms and then renders a
div
. Thediv
contains another button and an auto-focused text field.Note that:
tl;dr: Use
waitForSelector()
when you are relying on something that takes more than one frame, e.g. a network request, anautofocus
, asetTimeout(..., 0)
I wonder if async rendering will help with this when it's enabled in React.
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 is super helpful information, as I was starting to find myself sliding into the assumption to use
waitForSelector
on basically every interaction, as I had seen enough unexpected cases where it was needed to find it most reliable.