Skip to content

Conversation

inancgumus
Copy link
Contributor

@inancgumus inancgumus commented Aug 29, 2025

What?

  • Implements Locator.Filter to get a new locator with a selector that filters sub-elements from a set of elements described by another locator on which the Filter method is called.
  • Adds a locator_filter.js example E2E test.

Why?

  • To give users more flexibility when working with locators.
  • To make migrating from PW to k6-browser easier.

Use

Filter matches Playwright's Filter method behavior. We currently only support HasText and HasNotText filters.

Given an HTML:

<ul>
  <li>
    <h3>Product 1</h3>
    <button onclick="console.log('clicked 1')">Add to cart 1</button>
  </li>
  <li>
    <h3>Product 2</h3>
    <button onclick="console.log('clicked 2')">Add to cart 2</button>
  </li>
</ul>

Filter the 2nd item using hasText:

await p.getByRole('listitem')
  .filter({ hasText: 'Product 2' })
  .first()

Filter the 1st item using hasNotText and a regex:

await p.getByRole('listitem')
  .filter({ hasNotText: /Product 2/ })
  .first()

See this document to find more details.

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)

Closes #5033.

@inancgumus inancgumus self-assigned this Aug 29, 2025
@inancgumus inancgumus added this to the v1.3.0 milestone Aug 29, 2025
@inancgumus inancgumus force-pushed the add/locator-filter branch 4 times, most recently from 79dce26 to 6519119 Compare September 2, 2025 22:07
@inancgumus inancgumus force-pushed the add/locator-filter branch 5 times, most recently from 503392f to 330386c Compare September 3, 2025 02:07
@inancgumus inancgumus marked this pull request as ready for review September 3, 2025 12:45
@inancgumus inancgumus requested a review from a team as a code owner September 3, 2025 12:45
@inancgumus inancgumus requested review from mstoykov and ankur22 and removed request for a team September 3, 2025 12:45
This will be used by the injected_script.js and selectors.go later on to
select the elements with a specified text.
The has and has-not engines will use this to evaluate selectors.
This engine selects elements that have the specified text.
It detects the internal selector and prepares it for the injected
script. We use TrimQuotes to get rid of the quotes around the selector
so that the selector engines can work with the selector.

This allow using chainable locator strings:

"listitem >> internal:has-text='Product 2'"

Which this feature requires.
LocatorFilterOptions embed LocatorOptions, as their options are the same
except the `visible` option, which we'll soon add. Their options are
similar because `filter` is just a chain operation for locators.

See:
- https://playwright.dev/docs/api/class-locator#locator-filter
- https://playwright.dev/docs/api/class-page#page-locator
We currently only use `LocatorOptions`, but the code is ready to use the
`view` option
(https://playwright.dev/docs/api/class-locator#locator-filter).
@@ -460,13 +495,13 @@ func (l *Locator) innerText(opts *FrameInnerTextOptions) (string, error) {
// Last will return the last child of the element matching the locator's
// selector.
func (l *Locator) Last() *Locator {
return NewLocator(l.ctx, l.selector+" >> nth=-1", l.frame, l.log)
return NewLocator(l.ctx, nil, l.selector+" >> nth=-1", l.frame, l.log)
Copy link
Contributor Author

@inancgumus inancgumus Sep 3, 2025

Choose a reason for hiding this comment

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

We don't pass the options here, otherwise, we'd duplicate options when iterating on elements. For example:

page.getByRole('listitem').filter({ hasText: 'foo' }).last()

Would produce:

internal:role=listitem >> internal:has-text=foo >> nth=42 >> internal:has-text=foo

Now, it produces (matching the PW behavior):

internal:role=listitem >> internal:has-text=foo >> nth=42

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Implement locator.filter
1 participant