-
Notifications
You must be signed in to change notification settings - Fork 1.4k
Add getByRole in the browser module #4843
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
Conversation
@@ -14,6 +14,1259 @@ | |||
* limitations under the License. |
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.
The main implementation is in the injected_script.js
file. The code was directly copied over from the compiled JS code from Playwright. I have added as many integration tests as possible to ensure that all the possible flows are accounted for. While i understand the jist of what is happening in the copied code, some of it is still a bit of a mystery as to why things are done this way. A lot of the code seems to be inherited behaviour from specifications, such as not being able to work with header
or footer
unless they're not encapsulated -- exact spec is unknown though.
Do we need to add the Playwright license and notice file in the browser module?
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 we need to add another license note, as we already have this one:
k6/internal/js/modules/k6/browser/common/js/injected_script.js
Lines 1 to 15 in 113abf5
/** | |
* Copyright (c) Microsoft Corporation. | |
* | |
* Licensed under the Apache License, Version 2.0 (the "License"); | |
* you may not use this file except in compliance with the License. | |
* You may obtain a copy of the License at | |
* | |
* http://www.apache.org/licenses/LICENSE-2.0 | |
* | |
* Unless required by applicable law or agreed to in writing, software | |
* distributed under the License is distributed on an "AS IS" BASIS, | |
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
* See the License for the specific language governing permissions and | |
* limitations under the License. | |
*/ |
f35ebc0
to
a07de3f
Compare
@@ -225,6 +1478,7 @@ class InjectedScript { | |||
css: new CSSQueryEngine(), | |||
text: new TextQueryEngine(), | |||
xpath: new XPathQueryEngine(), | |||
'internal:role': createRoleEngine(true), |
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.
Sticking with how Playwright has labelled it. There is also a selector without internal
which we could consider adding. It is so that users can query without the getByRole
API, and directly do something like: page.locator('role=button');
.
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.
Yeah, I think that would be nice, specially because I think internal:role
is particular to Playwright, while the other looks more like "the standard"?
Is there any reason why not doing so on this PR?
Does it require any changes beyond also adding that pair of keyword and engine? 🤔
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.
Is there any reason why not doing so on this PR?
Mainly because I wanted to narrow the focus to working with the dedicated APIs that we have implemented getByRole
instead of allowing for the manual creation of role selectors. It can get quite complicated to manually create a role selector with options. So it's an ease of use thing mainly.
Does it require any changes beyond also adding that pair of keyword and engine? 🤔
That's basically it, add another case for role
without internal:
.
I'd like to see how this is used and whether users ask us to open up to a manual way of writing role selectors.
e684a3b
to
7919b29
Compare
@@ -0,0 +1,775 @@ | |||
package tests |
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.
Tests are failing on Windows for an unknown reason. I can run the same test in a script file and it works on Windows. I'm looking into 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.
WINDOWS! WHY ART THOU SO ANNOYING! I WANT TO LOVE YOU BUT I CAN'T 😭
I tried to run some other tests which work with page.locator
and they don't work! These tests are in keyboard_test.go
, and at the top of the file there is this comment:
// practically none of this work on windows
//go:build !windows
I will do the same for the new tests 😢
a766b35
to
e172850
Compare
@@ -1002,6 +1002,58 @@ func (f *Frame) getAttribute(selector, name string, opts *FrameBaseOptions) (str | |||
return s, true, nil | |||
} | |||
|
|||
// Locator creates and returns a new locator for this frame. |
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 GoDoc sentence looks like a leftover from copy-pasting 🤔
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.
Updated in f490d70
@@ -1002,6 +1002,58 @@ func (f *Frame) getAttribute(selector, name string, opts *FrameBaseOptions) (str | |||
return s, true, nil | |||
} | |||
|
|||
// Locator creates and returns a new locator for this frame. | |||
func (f *Frame) GetByRole(role string, opts *GetByRoleOptions) *Locator { | |||
f.log.Debugf("Frame:Locator", "fid:%s furl:%q role:%q opts:%+v", f.ID(), f.url(""), role, opts) |
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.
Should we refer to the method name instead, to keep it consistent with the other methods? 🤔
f.log.Debugf("Frame:Locator", "fid:%s furl:%q role:%q opts:%+v", f.ID(), f.URL(), role, opts) | |
f.log.Debugf("Frame:GetByRole", "fid:%s furl:%q role:%q opts:%+v", f.ID(), f.URL(), role, opts) |
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.
Updated in f490d70
if opts.Checked != nil { | ||
properties["checked"] = fmt.Sprintf("%v", *opts.Checked) | ||
} | ||
if opts.Disabled != nil { | ||
properties["disabled"] = fmt.Sprintf("%v", *opts.Disabled) | ||
} | ||
if opts.Selected != nil { | ||
properties["selected"] = fmt.Sprintf("%v", *opts.Selected) | ||
} | ||
if opts.Expanded != nil { | ||
properties["expanded"] = fmt.Sprintf("%v", *opts.Expanded) | ||
} | ||
if opts.IncludeHidden != nil { | ||
properties["include-hidden"] = fmt.Sprintf("%v", *opts.IncludeHidden) |
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 not using strconv.FormatBool
instead of string formatting here? It's slightly more idiomatic, and perhaps even a bit faster, I guess. But maybe there's a reason not to.
PS: Same, whenever it applies, with the rest of the opts parsed on this 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.
Yep, good spot. Changed in a098ac3
func stringPtr(s string) *string { | ||
return &s | ||
} | ||
|
||
func boolPtr(b bool) *bool { | ||
return &b | ||
} | ||
|
||
func int64Ptr(i int64) *int64 { | ||
return &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.
I think nowadays, thanks to generics, this can be simplified into a single generic method, like:
func ptr[T any](v T) *T {
return &v
}
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.
Thanks! Great idea. Updated in dfcd80f
74171be
to
be4b038
Compare
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 focused on the easy-to-review parts of this PR :-] I'll continue reviewing the rest tomorrow.
@@ -14,6 +14,1259 @@ | |||
* limitations under the License. |
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 we need to add another license note, as we already have this one:
k6/internal/js/modules/k6/browser/common/js/injected_script.js
Lines 1 to 15 in 113abf5
/** | |
* Copyright (c) Microsoft Corporation. | |
* | |
* Licensed under the Apache License, Version 2.0 (the "License"); | |
* you may not use this file except in compliance with the License. | |
* You may obtain a copy of the License at | |
* | |
* http://www.apache.org/licenses/LICENSE-2.0 | |
* | |
* Unless required by applicable law or agreed to in writing, software | |
* distributed under the License is distributed on an "AS IS" BASIS, | |
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
* See the License for the specific language governing permissions and | |
* limitations under the License. | |
*/ |
bb77825
to
d12e198
Compare
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 played a bit with it and everything seems to work great 👍 (plus the tests are covering a lot of use cases 🚀)
My main concern is about injected_script.js
that is already big and will only grow bigger and bigger with the upcoming PRs.
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.
Could we split this file into multiple files and try to follow the Playwright structure as much as possible (same file names)? I think this would help with reviewing and also maintaining if any changes happen in Playwright in the future.
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 JS version of PW's injected script is what is compiled from the type script injected scripts. I can see the benefit of splitting it if the files were easy to compare against PW, but it's something that seems to be available when installing it locally using node. Essentially, i feel it might be extra work to split the code into separate files, without the ability to be able to compare them against PW's changes, and so no real benefit.
If we split them and store them as separate files, we would also need to perform multiple requests to chrome to inject them one at a time during runtime, unless we had a job during CI/CD which combined them into a single file -- which is just another moving part, that could go wrong.
I toyed with the idea of copying the whole injected script from PW into k6 (not just take the parts we need to get the functionality working for the features we're implementing), so that it would make importing changes easier, but that didn't seem correct. At least with the selective approach has meant that I've been forced to understand what I've copied to help me get a good enough idea of what it is doing instead of no idea at all.
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.
Ah, I looked only at the file name and didn't notice the extension was different. Thanks for the detailed answer.
I think it could still be worth it if we copy a lot more because JS and TS are fairly easy to compare (looking at the domUtils.ts file for example). But for now, it's true that, as we copy only what we need, it makes it harder to compare. Also, if we decide to do it, it should be its own PR so that's fine for this PR 👍
// This tests all the options, and different attributes (such as explicit | ||
// aria attributes vs the text value of an element) that can be used in | ||
// the DOM with the same role. | ||
t.Run("edge_cases", func(t *testing.T) { |
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.
We are not testing the exact
attribute here, I think it would be nice to add one or two test for this use case.
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.
Good spot! Added in a18bbd4
d12e198
to
4bcbad5
Compare
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.
LGTM!
I basically copied the JS compiled injected script from PW, and pasted it into k6's injected script. It seems to work with basic role based selectors.
These tests include options and working with different attributes under-the-hood to do the same thing with a single option or role value.
It seems to be an issue with locator based APIs in integration tests, they just don't work. Locator based tests work perfectly when written and ran from a js test script.
The role based selector engine needs to work with ' and not " when it comes to the name option. This is automatically done for the user in the mapping layer, but in the integration test we need to ensure that we explicitly use ' and not ".
a18bbd4
to
e8e574c
Compare
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.
LGTM!
What?
This PR adds a long awaited feature which is
page.getByRole
. It makes working with selectors slightly simpler. MoregetBy*
APIs are in the works.Why?
It is a lot simpler to work with
getByRole
instead of XPath or CSS selectors in most cases.Checklist
make check
) and all pass.Checklist: Documentation (only for k6 maintainers and if relevant)
Please do not merge this PR until the following items are filled out.
Related PR(s)/Issue(s)
#4764
#4320
#4248