Click a button. An API call fires. The UI updates. Simple, right? But your test does not know when the API call finishes. If you check the UI too early, the data is not there yet. If you add a sleep(3000), that is fragile and slow. Playwright gives you proper tools to wait for specific API calls.
import { test, expect } from '@playwright/test';
test('wait for search API to respond', async ({ page }) => {
await page.goto('/shopping');
// Set up the wait BEFORE triggering the action
const responsePromise = page.waitForResponse(
(response) =>
response.url().includes('/api/products') &&
response.status() === 200
);
// Trigger the API call
await page.getByPlaceholder('Search products').fill('laptop');
await page.keyboard.press('Enter');
// Wait for the response
const response = await responsePromise;
const data = await response.json();
// Now assert on the response data
expect(data.length).toBeGreaterThan(0);
expect(data[0].name.toLowerCase()).toContain('laptop');
});Always create the waitForResponse promise BEFORE the action that triggers the request. Otherwise you have a race condition -- the response might arrive before your wait is set up.
test('verify transfer request payload', async ({ page }) => {
await page.goto('/banking/transfer');
// Wait for the POST request
const requestPromise = page.waitForRequest(
(request) =>
request.url().includes('/api/transfer') &&
request.method() === 'POST'
);
// Fill and submit the form
await page.getByLabel('From Account').selectOption('ACC001');
await page.getByLabel('To Account').selectOption('ACC002');
await page.getByLabel('Amount').fill('5000');
await page.getByRole('button', { name: 'Transfer' }).click();
// Verify the request body
const request = await requestPromise;
const postData = request.postDataJSON();
expect(postData.from).toBe('ACC001');
expect(postData.to).toBe('ACC002');
expect(postData.amount).toBe(5000);
});// Instead of a predicate function, you can pass a URL string or regex
const response = await page.waitForResponse('**/api/products');
const request = await page.waitForRequest(/\/api\/transfer/);
// With timeout -- fail fast if the API is not called
const response2 = await page.waitForResponse('**/api/products', {
timeout: 5000,
});test('verify add-to-cart sends correct data', async ({ page }) => {
await page.goto('/shopping');
// Wait for both the request and response
const [request, response] = await Promise.all([
page.waitForRequest('**/api/cart'),
page.waitForResponse('**/api/cart'),
// This click triggers both
page.getByRole('button', { name: 'Add to Cart' }).first().click(),
]);
// Assert on request
expect(request.method()).toBe('POST');
const body = request.postDataJSON();
expect(body.productId).toBeDefined();
expect(body.quantity).toBe(1);
// Assert on response
expect(response.status()).toBe(200);
const responseBody = await response.json();
expect(responseBody.cartTotal).toBeGreaterThan(0);
});Q: Why is waitForResponse better than page.waitForTimeout() after clicking a button?
A: waitForResponse waits for the exact event you care about -- the API finishing. waitForTimeout waits a fixed time regardless. If the API responds in 50ms, waitForTimeout(3000) wastes 2950ms. If the API takes 5 seconds, waitForTimeout(3000) checks too early and fails. waitForResponse is both faster and more reliable.
Key Point: waitForResponse() waits for the API to respond. waitForRequest() verifies what was sent. Always set them up BEFORE the triggering action.