Skip to content
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

[Feature] selector improvements proposals #2370

Closed
dgozman opened this issue May 27, 2020 · 10 comments
Closed

[Feature] selector improvements proposals #2370

dgozman opened this issue May 27, 2020 · 10 comments

Comments

@dgozman
Copy link
Contributor

dgozman commented May 27, 2020

waitForSelectors

There are quite a few requests for more advanced logic to retrieve elements matching selectors. For example:

  • wait for at least one element matching selector and get all matching elements;
  • wait for at least one element matching selector to be visible and get it.

Proposal:

page.waitForSelectors(selector, countOrPredicate, { state?: 'attached' | 'visible' | 'hidden', filter? }): Promise<ElementHandle[]>

This method will query all elements matching selector, filter them by them by visibility state, then filter by the filter predicate, and then resolve with remaining elements if there are not less than countOrPredicate of them when it's a number or the predicate returns true.

Examples:

  • page.waitForSelectors('button', 2) - at least two buttons.
  • page.waitForSelectors('button', buttons => buttons.length >= myButtonsCount) - at least myButtonsCount buttons;
  • page.waitForSelectors('button', 1, { state: 'visible' }) - at least one visible button;
  • page.waitForSelectors('button', 3, { state: 'visible', filter: b => !b.disabled }) - at least three visible and enabled buttons.

selectors.visible

There are requests to match not the first element, but the first visible one. This is more flaky and the preferred way is to use a stable selector the uniquely identifies the visible target element. That said, maybe it makes sense to introduce a wrapper that takes a regular selector:

selectors.visible('css=div')

Questions:

  • How does this interact with waitForSelector() state option that could be visible | hidden | attached | detached?

selectors.index

This could simplify selecting a particular item from the list.

Examples:

  • page.click(selectors.index('.my-list-item', 3)) - click at the third list item;
  • page.waitForSelector(selectors.index('button', 2)) - wait until there are at least two buttons.
@pavelfeldman
Copy link
Member

pavelfeldman commented May 29, 2020

  • waitForSelectorAll() - wait and return all
  • waitForSelectorAll({ min: 5 }) - wait for at least 5 and return all
  • waitForSelectorAll({ min: 3, filter: el => !el.disabled })

If we are adding filter, it would make sense for waitForSelector as well.

@yael118
Copy link

yael118 commented Oct 14, 2020

Hi, I would like to know when these capabilities will be implemented or a different solution was decided to the issues presented here

@torkjels
Copy link

torkjels commented Oct 23, 2020

Just adding my use case:
We have some tests that need to click a button, but the text displayed on this button is randomly set to either A or B. So we'd love a selector that would search for two things, but return the first match.
Something along the lines of

page.click(['text=A', 'text=B'])

or

page.click(await waitForSelectors(["text=A", "text=B"]))

Right now our workaround for this is

await page
      .click("text=A")
      .catch(() => page.click("text=B"));

This works, but unfortunately it's pretty slow because the selector inside catch will only run when the first selector has timed out.

@thernstig
Copy link
Contributor

thernstig commented Oct 23, 2020

@torkjels
Copy link

@thernstig that's a good suggestion, however as both Promises are started, they will both fulfill if they can when using that method. So let's say you have a button with text that is sometimes "A" and sometimes "B", and pressing that button reveals a new button somewhere else on the page that has a the text "B". Using Promise.any() or Promise.race() would click both those buttons, even if you only wanted to press the first one.

@arjunattam
Copy link
Contributor

Hi @torkjels, while we align on a solution here, sharing a snippet with waitForSelector + Promise.race. Would this work for your needs?

// returns ElementHandle to either A or B (whichever is found first)
const element = await Promise.race(page.waitForSelector('text=A'), page.waitForSelector('text=B'));
await element.click();

@thernstig
Copy link
Contributor

Still showing strong support for this feature. We find that in about 20-30% of test cases we want to get lists of things (tds, trs, tabs etc.) to count them or do other things.

@dgozman
Copy link
Contributor Author

dgozman commented Mar 31, 2021

Thank you everyone for sharing your opinion! Below is a list of common problems and possible solutions for Playwright v1.10. I think that most of the problems are addressed now, so closing this issue. Overall, please refer to selectors documentation for all the possibilities.


Problem: Wait for at least one element matching selector and get all matching elements.

Solution: Use the following snippet.

await page.waitForSelector(selector);
const all = await page.$$(selector);

Problem: Wait for at least one element matching selector to be visible and get it.

Solution: Use the :visible pseudo-class, read more here.

const element = await page.waitForSelector('button:visible');

Problem: Click at the third list item.

Solution: Use the :nth-match() pseudo-class, read more here.

await page.click(':nth-match(.my-list-item, 3)');

Problem: Wait until there are at least two buttons.

Solution: Use the :nth-match() pseudo-class, read more here.

await page.waitForSelector(':nth-match(button, 2)');

Problem: Click on either "A" or "B" button.

Solution: Use the :is() pseudo-class, read more here.

await page.click(':is(button:has-text("A"), button:has-text("B"))');

@dgozman dgozman closed this as completed Mar 31, 2021
@thernstig
Copy link
Contributor

Problem: Wait until there are at least two buttons.

Solution: Use the :nth-match() pseudo-class, read more here.

await page.waitForSelector(':nth-match(button, 2)');

@dgozman let's say you have a section where you know X buttons will load almost instantly, but you do not know the amount beforehand. Just that they should almost render at the same time. Is there a way to wait for them all to have been loaded? E.g. like waitForLoadState or something else?

The buttons are rendered as part of a user action where a user clicks a checkbox, and then X buttons are rendered "instantly" (no network request or anything though).

@dgozman
Copy link
Contributor Author

dgozman commented Apr 19, 2021

The buttons are rendered as part of a user action where a user clicks a checkbox, and then X buttons are rendered "instantly" (no network request or anything though).

If all the buttons are rendered together, just use waitForSelector() to wait for one of them, and all others will be there. If there is some delay between rendering the buttons, you need some kind of signal from the application, e.g. some attribute "done-rendering-buttons", or you have to know what to wait for, e.g. 5 buttons.

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

No branches or pull requests

6 participants