Categories
Playwright

Consistently waiting for network responses in Playwright

One of the neat features I like about Playwright is how easily it is to wait for network responses that are triggered by actions like clicking an element in a browser. This is the way a lot of modern web applications work so it’s important to be able to handle this.

But I noticed the way I was writing code for this example scenario was problematic and that it could result in non-deterministic (flaky) test results. For example:

  test('can wait for network responses when clicking', async ({ page }) => {
    await page.goto('https://webdriverjsdemo.github.io/dynamic/')
    await page.click('#show')
    await page.waitForResponse('https://my-json-server.typicode.com/webdriverjsdemo/webdriverjsdemo.github.io/posts')
    await expect(page.locator('#content')).toHaveText('[ { "id": 1, "title": "Post 1" }, { "id": 2, "title": "Post 2" }, { "id": 3, "title": "Post 3" } ]')
  })

I noticed in the example above there can be a race condition between Playwright clicking and waiting for the response, resulting in the waitForResponse to timeout as though it never responded when in fact it did but just before the click finished!

Thankfully Playwright makes it easy to handle these scenarios in a promise wrapper they suggest via their documentation:

// Note that Promise.all prevents a race condition
// between clicking and waiting for the response.
const [response] = await Promise.all([
  // Waits for the next response with the specified url
  page.waitForResponse('https://example.com/resource'),
  // Triggers the response
  page.click('button.triggers-response'),
]);

We can use the Promise.all call in our test like so, noting that there’s no awaits on the calls within Promise.all:

test('can wait for network responses when clicking', async ({ page }) => {
    await page.goto('https://webdriverjsdemo.github.io/dynamic/')
    await Promise.all([
      page.waitForResponse('https://my-json-server.typicode.com/webdriverjsdemo/webdriverjsdemo.github.io/posts'),
      page.click('#show')
    ])
    await expect(page.locator('#content')).toHaveText('[ { "id": 1, "title": "Post 1" }, { "id": 2, "title": "Post 2" }, { "id": 3, "title": "Post 3" } ]')
  })

Whilst this works well, I find it a bit harder to write and remember not to just call these sequentially, so if we’re going to clicking things and waiting for responses a lot we can move it into a shared function like so:

export async function clickAndWait (page: Page, locator: string, expectResponseURL: string) {
  const [response] = await Promise.all([
    page.waitForResponse(expectResponseURL),
    page.click(locator)
  ])
  return response
}

This way our test becomes simpler and easier to read again:

test('can wait for network responses when clicking', async ({ page }) => {
    await page.goto('https://webdriverjsdemo.github.io/dynamic/')
    await clickAndWait(page, '#show', 'https://my-json-server.typicode.com/webdriverjsdemo/webdriverjsdemo.github.io/posts')
    await expect(page.locator('#content')).toHaveText('[ { "id": 1, "title": "Post 1" }, { "id": 2, "title": "Post 2" }, { "id": 3, "title": "Post 3" } ]')
  })

Have you had to use this feature? I hope this helps if you’ve been having problems with page.waitForResponse like me.

Leave a Reply

Your email address will not be published.