(Updated: )

Leveraging Network Interception with Playwright for End-to-End Monitoring

Share on social

If you're using Playwright, either on its own or for synthetic monitoring with Checkly, you might have heard about writing Playwright scripts in a user-first manner. This approach focuses on interacting with the UI as a user would, such as clicking buttons or submitting forms, and then waiting for the UI to reflect the changes. However, sometimes you need to intercept and analyze the network layer to verify that your UI is getting the right responses from its supporting API.

This is just a bit different from a pure API check, since we’re loading a page and then looking for the responses that page gets from a particular endpoint.

Example Project

To demonstrate network interception, I created a simple example project. This project involves a website running on my localhost. When you click the submit button, the site starts polling an API endpoint at "api/submit." The responses typically include "in progress" repeatedly until finally returning "success" and a name, which is then rendered in the UI. This polling method is common for user interactions that initiate longer computations on the server side. My server responds with a very simple JSON object.

server.js
const http = require('http');

const hostname = 'localhost';
const port = 3000;

const server = http.createServer((req, res) => {
  res.statusCode = 200;
  res.setHeader('Content-Type', 'application/json');
  const response = {
    heading: 'stefan'
  };
  res.end(JSON.stringify(response));
});

server.listen(port, hostname, () => {
  console.log(`Server running at http://${hostname}:${port}/`);
});

In debugging my example project, it can be useful to directly examine the responses the page is getting from the API endpoint. That’s where we’ll use the Playwright page.waitForResponse method.

Writing a Playwright Check

To write a Playwright Check for this scenario, we need to check the network layer to ensure the API is responding correctly. Here's a quick overview of the case:

polling.spec.ts
import { test, expect } from '@playwright/test';

test('assert a specific API response', async ({ page }) => {
  await page.goto('<http://localhost:3000>');

  // Helper function to wait for the fulfilled response
  async function getFulfilledResponse() {
    const response = await page.waitForResponse(async (response) => {
      if (!response.url().includes('api/submit')) return false;
      const body = await response.json();
      return body.status === 'success';
    });
    return response.json();
  }

  // Click the submit button
  await page.click('button#submit');

  // Get the fulfilled response
  const responseBody = await getFulfilledResponse();

  // Log the response body to verify
  console.log(responseBody);

  // Web first assertion to check if the correct headline is visible
  await expect(page.getByRole('heading', { name: "stefan"})).toBeVisible();
});

  1. Set Up the Test Case: We start by preparing a Playwright test in a polling.spec.ts file. The case, to assert a specific API response, will use a helper function, getFulfilledResponse, to implement network monitoring. After clicking the submit button, we'll check the response body and log it to verify the correct response.
  2. Implement Network Monitoring: The helper function, getFulfilledResponse, needs to be implemented to intercept the network layer, wait for the correct request URLs, check and parse the response, and return the response object once it includes "success" and a name.

Intercepting Network Requests

To intercept network requests, we use the page.waitForResponse method in Playwright. This method matches responses based on specific criteria. However, by default, it returns the first matching response, which is not suitable for our polling scenario. Instead, we need to:

  1. Filter Responses: Include a condition to filter out unwanted response URLs. If the response URL doesn't include "api/submit," we return false, instructing waitForResponse to continue listening for more responses.
  2. Check Response Bodies: Pass a function to waitForResponse that evaluates the response body. We check if the response body status is "success" and return true if it matches, ensuring we get the final successful response.

Testing the Implementation

We log the responses to verify that the correct one is captured. Once confirmed, we ensure our checklist is complete:

  • Waited for the correct request and response URLs.
  • Checked for the correct response.
  • Return the response object.

Next, we update our page check to use the returned response. We adjust our web-first assertion to check if the heading includes the name from the response body.

Running the Check

Finally, we run the test. This should pass, confirming that the heading includes the expected name, and multiple API calls are made as expected.

Conclusion

When your checks require waiting for specific responses, Playwright's waitForResponse method is highly useful. For scenarios where you need to inspect response bodies, passing a function to waitForResponse can help. Remember, always prioritize UI-first interaction to avoid flaky checks. Network monitoring should be used judiciously, especially for cases where the frontend relies on repeated API calls. This approach ensures your UI is rendering the correct data, providing a robust monitoring strategy.

See it in action

To see response monitoring in action, check out Stefan’s video on demonstrating this technique:

If you're using Playwright for end-to-end testing, consider checking out Checkly for global monitoring. Thanks for reading, and I look forward to sharing more Playwright tips soon!

Share on social