-
Notifications
You must be signed in to change notification settings - Fork 4.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
E2E Utils: Use frameLocator for retrieving editor canvas #54911
Conversation
@WunderBart, I just remembered this conversation #51824 (comment). Just in case, it's helpful. |
It is, thanks! Some useful context there, like the issue with replacing |
Size Change: +5.03 kB (0%) Total Size: 1.65 MB
ℹ️ View Unchanged
|
Flaky tests detected in bab65f8. 🔍 Workflow run URL: https://github.com/WordPress/gutenberg/actions/runs/6416319195
|
Thanks, @Mamaduka! I've listed the changes in the description. Hope it helps! |
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.
Awesome work! I hope that you didn't just change these files by hand because that's a lot of work 😱 . Glad to see that it's possible, and perhaps not too late to make the switch!
The .locator( ':root' )
is a bit weird but I think it's not too bad. I just have to rethink canvas
as a locator and not a frame now in my head.
if ( this.useLegacyCanvas ) { | ||
return this.page; | ||
} |
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 wonder if we still need this. If the user has to opt-in to legacy canvas then they should be able to just use page
than editor.canvas
?
This also prevents the typescript awkwardness with FrameLocator | Page
which doesn't really extend each other 😅 .
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 assumes that switching to legacy canvas is intentional, but that's not always true. Can we still return the this.page
page as a fallback?
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 might be missing something, but won't useLegacyCanvas
only be true if we call switchToLegacyCanvas
? If the user opts in to call switchToLegacyCanvas
then I guess we can expect them to use page
instead.
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.
Let's say the consumer enables the metabox plugin in the test (which forces the editor to render legacy canvas) and forgets to call switchToLegacyCanvas
; this won't work.
It would be great to hide editors' implementation details for legacy canvas vs. iframe. It shouldn't be noticeable for people writing tests or using the editor, IMO.
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 like it as well and would be more than happy to remove it! 😄 There might be an issue with some Editor utils that reference canvas (e.g. selectBlocks()
IIRC) while running against the legacy one, but it might as well just pass - will check it! 🤞
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.
Removed in fd96c75
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.
It would be great to hide editors' implementation details for legacy canvas vs. iframe. It shouldn't be noticeable for people writing tests or using the editor, IMO.
This is also a good point. Unfortunately, I don't think automatically "piercing" through iframe boundaries can be done trivially in Playwright, yet (I don't know really, I haven't given it any thoughts 😅). There's a trade-off to make here, and I would personally prefer predictable types and autocomplete for package-level utility 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.
I missed a part of this exchange - GH is not great at refreshing discussions. 😑
Anyway, I've been on edge with this one because, on one side, there's the desirable autocomplete without the type ambiguity (FrameLocator | Page
), and on the other side, there's the implementation detail or the potentially broken editor utils that depend on the canvas
reference (like the aforementioned selectBlocks()
.
As for the solution, we can't do it the old "page fallback" way because frameLocator
doesn't return null
like the frame
does – it will time out if not found. The only solution that worked for me (considering we want to hide the implementation detail) has been turning the canvas
into an async getter and evaluating the parent element by wrapping framed and unframed locators with Promise.any()
. Unfortunately, it would be still far from ideal as it also would:
- involve a hefty refactor caused by the inability to chain from
editor.canvas
(it would become(await editor.canvas()).getByRole(...
. - still mess with the canvas types.
Do you think the reported flaky tests are related? The most recent reports seem to only happen on this branch. 🤔 If we can fix it here it will be best, but I think we can also fix them in follow-ups. |
@Mamaduka, just a heads up as I've added more examples to the changes involved! 🙌 |
await editor.canvas | ||
.locator( 'role=button[name="Add default block"i]' ) | ||
.click(); |
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.
Sidenote: This is a really common pattern; I wonder if we should extract it into a utility method.
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.
await editor.canvas | |
.locator( 'role=button[name="Add default block"i]' ) | |
.click(); | |
await editor.canvas | |
.getByRole( 'button', { | |
name: "Add default block" | |
}) | |
.click(); |
Nit: I have previously been advised to prefer getByRole
wherever possible.
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.
It's probably not worth refactoring old code for this standard, especially if we plan to extract this in helper.
Both ways are fine, but using RTL-style locators is preferred.
await page.evaluate( () => { | ||
window.wp.blocks.registerBlockType( 'test/v2', { | ||
apiVersion: '2', | ||
title: 'test', | ||
} ); | ||
} ); |
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 assume there's no longer a need for a legacy canvas switch. There're some left overs, so removal is appreciated.
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.
Some tests still fail when running against iframed canvas, so I'd leave the switch. I tried migrating most of the legacy tests to iframed canvas, but it wasn't possible for some of them. For example, cancels dragging blocks from the global inserter by pressing Escape
in inserting-blocks.spec.js
does not hide the insertion point after pressing Escape, no matter what I do. It seems to be an issue with Playwright, because the tested functionality works as expected 100% of the time when testing manually.
// Wait for both iframed and non-iframed canvas and resolve once the | ||
// currently available one is ready. To make this work, we need an inner | ||
// legacy canvas selector that is unavailable directly when the canvas is | ||
// iframed. | ||
await Promise.any( [ | ||
this.page.locator( '.wp-block-post-content' ).waitFor(), | ||
this.page | ||
.frameLocator( '[name=editor-canvas]' ) | ||
.locator( 'body > *' ) | ||
.first() | ||
.waitFor(), | ||
] ); |
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 we should keep this. The missing lazy evaluation from editor.canvas
wasn't the only reason for this safeguard.
Why?
- Expectation that editor canvas is loaded when performing an action after the
createNewPost
call. Inherited from Puppeteer utils. It could count as a breaking change. - Some of the test patterns don't use locators for creating blocks. See examples below.
Examples
// Insert a block using `editor` utils. Was flaky before https://github.com/WordPress/gutenberg/pull/51824.
await admin.createNewPost();
await editor.insertBlock( { name: 'core/heading' } );
// Add blocks using writing flow "shortcut":
await admin.createNewPost();
await page.keyboard.press( 'Enter' );
await page.keyboard.type( 'Content' );
The last pattern was very common in Puppeteer but became flaky after #48286 (before #51824), so we had to switch to editor.canvas.click( 'role=button[name="Add default block"i]' )
.
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.
Believing that the entire editor is immediately ready upon invoking callingNewPost()
, especially considering editor's dynamic nature, can be a risky assumption. Instead of making such assumptions, we should directly ensure that the resources we intend to use are available at the time of reference in the context where the resource is accessed.
From the sample you shared, it seems more logical to adjust the insertBlock()
function.
Rather than having:
async function insertBlock(
this: Editor,
blockRepresentation: BlockRepresentation
) {
await this.page.evaluate( ( _blockRepresentation ) => {
//...
window.wp.data.dispatch( 'core/block-editor' ).insertBlock( block );
}, blockRepresentation );
I propose this version:
async function insertBlock(
this: Editor,
blockRepresentation: BlockRepresentation
) {
await this.page.waitForFunction( ( _blockRepresentation ) => {
if ( ! window?.wp?.data ) {
return false;
}
//...
window.wp.data.dispatch( 'core/block-editor' ).insertBlock( block );
}, blockRepresentation );
In this alternative, the function waits until the window.wp.data
resource is ready before proceeding. The immediate use of evaluate()
presupposes the availability of window.wp.data
, effectively moving the responsibility of ensuring its presence outside this function. To illustrate:
await admin.createNewPost();
await editor.insertBlock( { name: 'core/heading' } );
await this.page.reload();
await editor.insertBlock( { name: 'core/heading' } );
If the responsibility to check resource availability lies within createNewPost()
, the second insertBlock()
call could be unstable. However, if insertBlock()
self-validates that its required resources are ready, we ensure a more robust outcome.
Does that perspective make sense?
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.
Does that perspective make sense?
For me, it does. I don't feel a strong attachment to previous behavior 😄
As I mentioned, this changes the "expected" behavior of createNewPost()
, and not everyone actively follows changes to the e2e test environment.
I like your suggestion for improving the insertBlock
utility 🚀
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.
As for the second example you provided, it involves some ambiguity because we assume the cursor is in the correct spot, but we don't really know what that spot is. Instead of shifting the burden of ensuring the cursor is where it should be to createNewPost()
(which is not even the case, because we'd only wait for the editor body to be visible, which doesn't ensure cursor position!), I propose adding a step that explicitly waits for the cursor to be in the right spot:
await admin.createNewPost();
await expect(
editor.canvas.getByRole( 'textbox', { name: 'Add title' } )
).toBeFocused();
await page.keyboard.press( 'Enter' );
await page.keyboard.type( 'Content' );
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.
One DX benefit of removing this is it cleans up a little noise in the trace viewer that is caused by one of these locators timing out.
Thank you, @WunderBart! I left a couple of notes, but the changes look good in general ✅ I agree with @kevin940726. Fixing the related flaky test reports would be nice, but this can done after merging as well. |
It very well might be, @kevin940726. The locator itself is lazily evaluated but there still can be some flakiness from situations where we e.g. I'm checking the flakies now! |
Ensure cursor position before navigating
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.
Wunderful work Bart!
@kevin940726 PR-specific flakies seem to be gone. 🥳 There's one more that is not specific to this PR so I've opened a separate PR for it. |
What?
Improve E2E tests stability by referencing the editor canvas via lazily evaluated
frameLocator
instead offrame
. For example, we'll no longer need to explicitly wait for the canvas frame before callingeditor.canvas.*
:Before
After
Related: WordPress/wordpress-develop#5333 (comment)
Bonus: Like this one, I was able to switch many other tests to be run against the iframed canvas. 🎉
Some still required legacy canvas to pass, but addressing them is out of the scope of this PR.
Major changes
This migration enforces some changes to how we work with
canvas
, as it's no longer of typeFrame
:Use
canvas.locator('selector').click()
instead ofcanvas.click('selector')
.This will now be enforced, but it's also recommended as the direct
frame.click
has been deprecated. This also includes other actions likefill
,focus
,hover
, etc.Before
After
Use
editor.canvas.locator(':root').evaluate()
instead ofeditor.canvas.evaluate()
. For example:Before
After
Use
expect.poll(editor.canvas.evaluate())
instead ofeditor.canvas.waitForFunction()
.This is not an equivalent, but I'm afraid there is no better alternative ATM. There's an open request for
frameLocator.waitForFunction
we can +1, though. :)Before
After
Inner locators cannot use
frameLocator
, so at this time it's not possible to match via thehas:
property if the inner locator containsframeLocator
.Before
After
For tests that require the legacy (non-iframed) canvas, use the
editor.switchToLegacyCanvas()
utility.Before
After
Testing instructions
E2E tests should pass.