0

I've been tooling around with Playwright on Python (v1.16) and thought I'd migrate some of our Cypress tests as an experiment. However, I've run into several snags all related to the same problem: I need to wait for some arbitrary DOM state and don't know how.

I know Playwright can wait for a handful of element states such as visible or detached, but I want something that waits for other DOM states too, like text to become present, element attributes to have a certain value, or HTML structure within an element to change, etc.

For example, in one test I have a progress indicator that is updated a second or so after a form is completed. I want to assert that the progress percent arrives at an expected value, but in order to do that I need to wait for the progress element's text to change accordingly. I don't see how to do this without writing my own polling scheme.

In another case I have a Submit button that was built in this "really cool" dynamic way that uses a .disabled CSS class to indicate to the user they can't submit the form (the button also has its click handlers dynamically applied/removed to enforce the disabled state). Playwright doesn't know anything about this and thinks the button is ready to be clicked immediately after calling click() because all the actionability checks pass right away. I need to wait for the disabled class to be removed from the button before calling click(). How can I do this?

I know I can make new selectors for elements that include the DOM bits that I want to wait for (e.g. add a condition text=5% to my progress indicator selector), but I don't like this because if it fails, I get a generic "we couldn't find the selector" error instead of something more assertion-like such as "Failed waiting for the element's text to contain 5%". I also use page objects to store my selectors and it would really suck if I had to make a new selector for each assertion on each element.

Is there an idiomatic way to wait for arbitrary DOM states that doesn't involve duplicating/modifying selectors?

user2859458
  • 2,285
  • 1
  • 17
  • 27

3 Answers3

1

@playwright/test uses (and extends) the expect assertion library. Have a look at the async matchers.

According to the docs:

Playwright also extends it with convenience async matchers that will wait until the expected condition is met.

About the clicking on a button needing to wait until the .disabled class is removed, that one is simple I think. Just create a locator for the button to not have the .disabled class. That way Playwright's .click() action will wait until there is a button without the .disabled class in the DOM, before clicking it. Something like:

await page.click('button:not(.disabled)')

refactoreric
  • 276
  • 1
  • 2
  • I've extended the answer with some information how you could auto-wait for the button to not have the .disabled class. It's using the CSS :not() pseudo class. I haven't tested the code but hope it helps. – refactoreric Nov 20 '21 at 09:24
  • 1
    Hmm and then I realize this is about Python. The auto-waiting for the button without disabled class should work there too. But the expect assertion library auto-waiting is not available. For waiting for the progress to reach a certain value, Kayce's solution (with some modifications) looks more fitting. Maybe the Playwright Python maintainers would know an even better way. You could ask on https://aka.ms/playwright-slack for someone to answer here. – refactoreric Nov 20 '21 at 12:12
0

In another case I have a Submit button that was built in this "really cool" dynamic way that uses a .disabled CSS class to indicate to the user they can't submit the form (the button also has its click handlers dynamically applied/removed to enforce the disabled state). Playwright doesn't know anything about this and thinks the button is ready to be clicked immediately after calling click() because all the actionability checks pass right away. I need to wait for the disabled class to be removed from the button before calling click(). How can I do this?

This feels like a hack and is highly unlikely to be anything close to idiomatic (I'm also ramping up on Playwright so I don't even know what idiomatic is)... What if you were to evaluate some JS in the page's context so that the element emits an event when the desired state is reached?

expression = """el => {
  const config = {attributes: true};
  const callback = (mutationsList, observer) => {
    for (const mutation of mutationsList) {
      const {type, attributeName, target} = mutation;
      if (type === 'attributes' && attributeName === 'class') {
        if (!target.classList.contains('.disabled')) {
          const event = new Event('myevent');
          target.dispatchEvent(event);
        }
      }
    }
  };
  const observer = new MutationObserver(callback);
  observer.observe(el, config);
}
"""
selector = '#submit'
page.eval_on_selector(selector, expression)
with page.expect_event('myevent') as event_info:
  page.click(selector)

Note that I haven't tested this code so it may need tweaking.

Kayce Basques
  • 23,849
  • 11
  • 86
  • 120
0

I want something that waits for other DOM states too, like text to become present, element attributes to have a certain value, or HTML structure within an element to change, etc.

If I understand correctly, page.wait_for_selector covers the first 2 cases (and the third one depending on the exact change you want) and locator.wait_for should cover the remaining cases of the third one and other more complex DOM states if you can write down a locator for them.

I need to wait for the disabled class to be removed from the button before calling click(). How can I do this?

This example should be something like

page.wait_for('button:text("Submit") :not(.disabled)')
Alexey Romanov
  • 167,066
  • 35
  • 309
  • 487