Waiting for Network Events
Playwright auto-waits for elements to become actionable, visible, enabled, stable, but it does not automatically know that a click should be followed by a specific network round trip finishing before the next assertion runs. page.waitForResponse(), page.waitForRequest(), and page.waitForLoadState('networkidle') give a test explicit control to pause until a particular request or response, or the absence of network activity, actually occurs.
Cricket analogy: It is like a run-out appeal where the umpire doesn't just guess the batsman is out, they explicitly wait for the third umpire's replay confirmation before raising the finger, even though everything else on the field looks settled.
waitForResponse and waitForRequest
page.waitForResponse(urlOrPredicate) resolves its promise as soon as a matching response arrives, but it should be started before the action that triggers it, typically wrapped together with the triggering click inside Promise.all, otherwise there's a race where a fast response can fire before the waiter is even listening, causing the wait to hang until the timeout. page.waitForRequest() works the same way but resolves on the outgoing request rather than the incoming response.
Cricket analogy: It is like a slip fielder who must already be crouched and watching before the ball is bowled, if they only start reacting after hearing the bat's edge, the catch chance has already passed them by.
// Start the wait BEFORE triggering the action, using Promise.all to avoid a race
const [response] = await Promise.all([
page.waitForResponse(
(res) => res.url().includes('/api/save') && res.status() === 200
),
page.click('#save-button'),
]);
expect(response.ok()).toBeTruthy();
await expect(page.getByText('Saved successfully')).toBeVisible();
networkidle and Its Pitfalls
page.waitForLoadState('networkidle') resolves once there have been no more than zero active network connections for at least 500 milliseconds, which sounds convenient but is explicitly discouraged in Playwright's own documentation for pages with long-polling, WebSocket connections, analytics beacons, or streaming responses, where 'idle' network activity may never truly occur or may take far longer than the actual UI update the test cares about.
Cricket analogy: It is like waiting for a stadium to go completely silent before assuming a match has ended, when in truth a chatty crowd near the boundary rope might never fully quiet down even though the game finished overs ago.
Prefer waiting for a specific, meaningful signal, a particular response, a selector becoming visible, or a network request count, over the broad and often unreliable waitForLoadState('networkidle'). Playwright's team specifically recommends against relying on networkidle for pages with persistent connections like WebSockets or polling.
Listening to All Traffic with page.on('response')
page.on('request', handler) and page.on('response', handler) register event listeners that fire for every single network exchange on the page, which is useful for debugging a flaky test by logging all traffic, or for asserting on request counts, such as verifying an expensive analytics call only fires once per page load instead of on every re-render.
Cricket analogy: It is like a match analyst reviewing the complete ball-by-ball commentary log after a game rather than just the highlights reel, catching every single delivery including the ones that seemed unremarkable at the time.
Use response.request().resourceType() (values like 'xhr', 'fetch', 'document', 'image') to filter a broad page.on('response') listener down to just the API calls you care about, ignoring images, fonts, and other static assets in the log.
- Playwright auto-waits for element actionability, not for arbitrary network round trips.
- waitForResponse()/waitForRequest() must be started before the triggering action, typically via Promise.all.
- networkidle waits for zero connections for 500ms, and can hang or over-wait on polling/streaming pages.
- Prefer waiting for a specific response or selector over the broad networkidle signal.
- page.on('request'/'response') listens to all traffic, useful for debugging and counting calls.
- resourceType() filters broad traffic listeners down to relevant request types like xhr or fetch.
- Racing a wait against its trigger action is a common source of flaky, intermittently-hanging tests.
Practice what you learned
1. Why should page.waitForResponse() typically be wrapped with the triggering action inside Promise.all?
2. Why does Playwright's documentation discourage relying on waitForLoadState('networkidle') for many pages?
3. What does page.on('response') let a test do that waitForResponse() does not?
4. Which method would you use to filter a broad page.on('response') listener down to only API calls?
Was this page helpful?
You May Also Like
Network Interception with page.route()
Learn how Playwright's page.route() and context.route() intercept, inspect, modify, fulfill, and abort network requests before they reach the real server.
Mocking API Responses
Use route.fulfill(), routeFromHAR, and route.fetch() to return deterministic, realistic API responses for fast, reliable UI tests.
API Testing with the Request Context
Use Playwright's APIRequestContext to make real HTTP calls, assert on responses, and seed data for faster, more focused tests.