djangopytest-djangoplaywright-python

Playwright + Django: how to wait for events


In my tests, I have to wait for an event to trigger before continuing with test assertions, but I can't figure out how to make Playwright wait for that event. It seems like Playwright can't see the event.

Simple example with a django page: clicking the button fires an event boop that changes the background color of the document.

Template event.html:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Playwright Events</title>
</head>
<body>
  <button>Click me</button>
  <script>
    const btn = document.querySelector("button");
    btn.addEventListener("click", (e) => document.dispatchEvent(new Event("boop")));
    document.addEventListener("boop", (e) => { document.body.style.backgroundColor = "darkcyan"; });
  </script>
</body>
</html>

URL conf:

urlpatterns = [
    path('', TemplateView.as_view(template_name="event.html"))
]

The test:

def test(page, live_server):
    page.goto(live_server.url)
    page.wait_for_timeout(500)
    btn = page.get_by_text('Click me')
    with page.expect_event("boop", timeout=1000):
        btn.click()
    page.wait_for_timeout(500)

When running the test in headed mode, you can see the background color changing - meaning that the event boop was fired. But the test still fails because expect_event times out:
playwright._impl._api_types.TimeoutError: Timeout 1000ms exceeded while waiting for event "boop"

I must be doing something wrong, but I can't figure out what.

Found this similar post, but it is not about playwright-python with django.


Solution

  • It's not that explicit in the documentation but events refer to playwright events, not HTML events.

    There is no support at the moment for the functionality you describe, you could emulate it as follows though:

    class ExpectedHTMLEvent:
        def __init__(self, page: sync_api.Page, html_event: str, timeout_ms: float):
            self.page = page
            self.html_event = html_event
            self.timeout_ms = timeout_ms
            self.fn_name = f"setFlag{''.join(random.choices(string.ascii_lowercase, k=6))}"
            self._flag = False
            self._end = None
    
        def _flag_setter(self):
            self._flag = True
    
        def __enter__(self) -> None:
            self.page.expose_function(self.fn_name, self._flag_setter)
            opts = "{once: true}"
            self.page.evaluate(
                f"document.addEventListener('{self.html_event}', async () => await window.{self.fn_name}(), {opts})"
            )
            self._end = datetime.now() + timedelta(milliseconds=self.timeout_ms)
    
        def __exit__(self, __exc_type, __exc_value, __traceback) -> None:
            while datetime.now() < self._end:
                if self._flag:
                    return
                time.sleep(0.1)
    
            raise TimeoutError(f"'{self.html_event}' was never observed")
    
    
    with sync_playwright() as p:
        browser = p.chromium.launch()
        page = browser.new_page()
        page.goto('http://example.com/')
    
        with ExpectedHTMLEvent(page, "boop", 2_000):
            page.evaluate("() => document.dispatchEvent(new Event('boop'))")
    
        with ExpectedHTMLEvent(page, "not boop", 2_000):
            page.evaluate("() => document.dispatchEvent(new Event('boop'))")
    
    TimeoutError: 'not boop' was never observed