'Python playwright: wait for arbitrary DOM state

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?



Solution 1:[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)')

Solution 2:[2]

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.

Solution 3:[3]

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

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2 Kayce Basques
Solution 3 Alexey Romanov