Skip to content

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

Merged
merged 24 commits into from
Jul 9, 2025
Merged

Conversation

ankur22
Copy link
Contributor

@ankur22 ankur22 commented Jun 9, 2025

What?

This PR adds a long awaited feature which is page.getByRole. It makes working with selectors slightly simpler. More getBy* 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

  • I have performed a self-review of my code.
  • I have commented on my code, particularly in hard-to-understand areas.
  • I have added tests for my changes.
  • I have run linter and tests locally (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.

  • I have added the correct milestone and labels to the PR.
  • I have updated the release notes: link
  • I have updated or added an issue to the k6-documentation: grafana/k6-docs#NUMBER if applicable
  • I have updated or added an issue to the TypeScript definitions: grafana/k6-DefinitelyTyped#NUMBER if applicable

Related PR(s)/Issue(s)

#4764
#4320
#4248

@ankur22 ankur22 requested a review from a team as a code owner June 9, 2025 21:23
@ankur22 ankur22 requested review from inancgumus and joanlopez and removed request for a team June 9, 2025 21:23
@@ -14,6 +14,1259 @@
* limitations under the License.
Copy link
Contributor Author

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?

Copy link
Contributor

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:

/**
* 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.
*/

@ankur22 ankur22 force-pushed the poc/role-based-selector-engine branch from f35ebc0 to a07de3f Compare June 9, 2025 21:31
@@ -225,6 +1478,7 @@ class InjectedScript {
css: new CSSQueryEngine(),
text: new TextQueryEngine(),
xpath: new XPathQueryEngine(),
'internal:role': createRoleEngine(true),
Copy link
Contributor Author

@ankur22 ankur22 Jun 9, 2025

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');.

Copy link
Contributor

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? 🤔

Copy link
Contributor Author

@ankur22 ankur22 Jul 9, 2025

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.

@ankur22 ankur22 force-pushed the poc/role-based-selector-engine branch 2 times, most recently from e684a3b to 7919b29 Compare June 9, 2025 21:42
@@ -0,0 +1,775 @@
package tests
Copy link
Contributor Author

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 😕

Copy link
Contributor Author

@ankur22 ankur22 Jun 10, 2025

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 😢

@ankur22 ankur22 force-pushed the poc/role-based-selector-engine branch from a766b35 to e172850 Compare June 10, 2025 14:19
@ankur22 ankur22 added this to the v1.1.0 milestone Jun 10, 2025
@@ -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.
Copy link
Contributor

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 🤔

Copy link
Contributor Author

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)
Copy link
Contributor

@joanlopez joanlopez Jun 16, 2025

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? 🤔

Suggested change
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)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated in f490d70

Comment on lines 1015 to 1028
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)
Copy link
Contributor

@joanlopez joanlopez Jun 16, 2025

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.

Copy link
Contributor Author

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

Comment on lines 15 to 25
func stringPtr(s string) *string {
return &s
}

func boolPtr(b bool) *bool {
return &b
}

func int64Ptr(i int64) *int64 {
return &i
}
Copy link
Contributor

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
}

Copy link
Contributor Author

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

@ankur22 ankur22 force-pushed the poc/role-based-selector-engine branch from 74171be to be4b038 Compare June 16, 2025 15:23
@ankur22 ankur22 requested a review from joanlopez June 16, 2025 15:52
Copy link
Contributor

@inancgumus inancgumus left a 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.
Copy link
Contributor

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:

/**
* 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.
*/

@joanlopez joanlopez modified the milestones: v1.1.0, v1.2.0 Jun 25, 2025
@ankur22 ankur22 force-pushed the poc/role-based-selector-engine branch from bb77825 to d12e198 Compare June 25, 2025 19:32
Copy link
Contributor

@AgnesToulet AgnesToulet left a 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.

Copy link
Contributor

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.

Copy link
Contributor Author

@ankur22 ankur22 Jul 7, 2025

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.

Copy link
Contributor

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) {
Copy link
Contributor

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.

Copy link
Contributor Author

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

@ankur22 ankur22 force-pushed the poc/role-based-selector-engine branch from d12e198 to 4bcbad5 Compare July 7, 2025 13:18
Copy link
Contributor

@AgnesToulet AgnesToulet left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM!

ankur22 added 4 commits July 8, 2025 10:10
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.
ankur22 added 20 commits July 8, 2025 10:11
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 ".
@ankur22 ankur22 force-pushed the poc/role-based-selector-engine branch from a18bbd4 to e8e574c Compare July 8, 2025 09:11
Copy link
Contributor

@joanlopez joanlopez left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM!

@ankur22 ankur22 merged commit faff94a into master Jul 9, 2025
76 of 84 checks passed
@ankur22 ankur22 deleted the poc/role-based-selector-engine branch July 9, 2025 14:42
@mstoykov mstoykov added the documentation-needed A PR which will need a separate PR for documentation label Aug 4, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
documentation-needed A PR which will need a separate PR for documentation
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants