QA How-To
How to Use Playwright waitForResponse (2026)
Learn playwright waitForResponse with reliable TypeScript patterns for API matching, response validation, GraphQL calls, timeouts, and flake-free tests.
18 min read | 3,591 words
TL;DR
Create page.waitForResponse before clicking, match one meaningful response with a narrow predicate, trigger the action, then await and inspect the returned Response. Pair network checks with a web-first UI assertion so the test proves both the service contract and the user outcome.
Key Takeaways
- Create the waitForResponse promise before the action that can trigger the request.
- Match the URL, HTTP method, and operation identity so unrelated traffic cannot satisfy the wait.
- Use Response for status, headers, and body checks, then assert the user-visible result as well.
- Parse URLs and request payloads when query strings or GraphQL endpoints make string matching ambiguous.
- Treat a wait timeout as diagnostic evidence, not a reason to add fixed sleeps.
- Use route interception for controlled behavior and waitForResponse for observing real browser traffic.
The playwright waitForResponse method waits for one browser response that matches a URL, regular expression, or predicate, then returns a Playwright Response object. The reliable pattern is simple: register the wait before the triggering action, match the request narrowly, await the response, and assert the user-visible result.
This guide explains the timing model, TypeScript patterns, response inspection, GraphQL matching, failure diagnosis, and design decisions that keep network-aware tests deterministic. The examples use Playwright Test and public APIs available in current Playwright releases.
TL;DR
import { test, expect } from '@playwright/test';
test('saves a profile after the API succeeds', async ({ page }) => {
await page.goto('/profile');
const responsePromise = page.waitForResponse(response =>
new URL(response.url()).pathname === '/api/profile' &&
response.request().method() === 'PUT',
);
await page.getByRole('button', { name: 'Save profile' }).click();
const response = await responsePromise;
expect(response.status()).toBe(200);
await expect(page.getByRole('status')).toHaveText('Profile saved');
});
| Need | Best Playwright signal |
|---|---|
| Observe a completed browser request | page.waitForResponse |
| Inspect a request before a response exists | page.waitForRequest |
| Replace, modify, or abort traffic | page.route or browserContext.route |
| Test an API directly without the page | APIRequestContext |
| Prove the interface updated | Locator web-first assertion |
The listener must exist before the click. A fixed timeout does not create that guarantee.
1. What playwright waitForResponse Actually Does
page.waitForResponse accepts a string, regular expression, or predicate and resolves with the first matching Response. A response event occurs when the browser receives response status and headers. You can then read the body through response.json(), response.text(), or response.body().
The signature is conceptually:
const response = await page.waitForResponse(
urlOrPredicate,
{ timeout: 10_000 },
);
The predicate receives a Response and may inspect its URL, status, headers, and originating Request. That request link is important because two calls can use the same endpoint with different methods or bodies.
waitForResponse is an event wait. It does not send a request, retry a failed service, or make the page ready by itself. It observes traffic initiated by the page. If no future response matches, it times out.
A direct URL string is useful for one exact endpoint:
const responsePromise = page.waitForResponse(
'https://app.example.test/api/session',
);
When the browser context has a baseURL, a relative path string is resolved with the URL constructor:
// use.baseURL is http://127.0.0.1:3000
const responsePromise = page.waitForResponse('/api/session');
Predicates are usually safer for applications with query parameters, several environments, or repeated endpoints. They let the test express why this particular response matters.
The method belongs to Page, so it observes responses associated with that page. For suite-wide diagnostics across pages, BrowserContext response events may be more appropriate. For one user action, keep the wait close to the action so ownership remains obvious.
2. Set Up Playwright Network Tests in TypeScript
A small configuration gives examples a stable base URL, trace evidence, and deliberate timeouts:
// playwright.config.ts
import { defineConfig } from '@playwright/test';
export default defineConfig({
testDir: './tests',
timeout: 30_000,
expect: {
timeout: 5_000,
},
use: {
baseURL: process.env.BASE_URL ?? 'http://127.0.0.1:3000',
trace: 'on-first-retry',
screenshot: 'only-on-failure',
},
webServer: process.env.BASE_URL
? undefined
: {
command: 'npm run start',
url: 'http://127.0.0.1:3000',
reuseExistingServer: !process.env.CI,
timeout: 120_000,
},
});
Keep test timeout, assertion timeout, and response-wait timeout conceptually separate. The test timeout limits the whole test. The expect timeout controls retrying web assertions. The waitForResponse timeout limits only the event wait and defaults to the page or context default timeout when not overridden.
A focused test imports both test and expect:
// tests/orders.spec.ts
import { test, expect } from '@playwright/test';
test('submits an order', async ({ page }) => {
await page.goto('/checkout');
const responsePromise = page.waitForResponse(response =>
new URL(response.url()).pathname === '/api/orders' &&
response.request().method() === 'POST',
);
await page.getByRole('button', { name: 'Place order' }).click();
const response = await responsePromise;
expect(response.ok()).toBe(true);
await expect(page.getByRole('heading', { name: 'Order confirmed' }))
.toBeVisible();
});
Use unique application data, especially when tests run in parallel. Matching by an order ID, account ID, or operation name is more reliable than assuming only one test can reach an endpoint.
For broader setup patterns, see the Playwright TypeScript setup guide and Playwright configuration best practices.
3. Register the Wait Before the Triggering Action
The most important ordering rule is to create the response promise first without awaiting it. Then perform the action and await the saved promise.
Correct:
const responsePromise = page.waitForResponse(response =>
response.url().includes('/api/preferences') &&
response.request().method() === 'PATCH',
);
await page.getByRole('button', { name: 'Save preferences' }).click();
const response = await responsePromise;
Incorrect:
await page.getByRole('button', { name: 'Save preferences' }).click();
const response = await page.waitForResponse(response =>
response.url().includes('/api/preferences'),
);
The second version can miss a fast response because the click may complete after the browser has already emitted it. The later listener cannot recover a past event.
Also avoid awaiting the wait before the action:
// Deadlock until timeout because the click never runs.
const response = await page.waitForResponse(
response => response.url().includes('/api/preferences'),
);
await page.getByRole('button', { name: 'Save preferences' }).click();
Promise.all can express the same ordering because array expressions are evaluated before Promise.all awaits them:
const [response] = await Promise.all([
page.waitForResponse(response =>
response.url().includes('/api/preferences'),
),
page.getByRole('button', { name: 'Save preferences' }).click(),
]);
Both styles are valid. The named promise pattern is often easier to debug and extend. It separates registration, trigger, and observation into visible steps.
Do not add page.waitForTimeout before or after the click. A sleep delays execution but does not guarantee the right listener ordering, endpoint, response body, or UI state. Event registration removes the race directly.
4. Match URL, Method, Status, and Request Identity
Broad matching is a common source of false passes. An application may call /api/orders on initial load, during polling, and after checkout. Match enough dimensions to identify the operation.
const responsePromise = page.waitForResponse(response => {
const url = new URL(response.url());
return url.pathname === '/api/orders' &&
response.request().method() === 'POST' &&
response.status() === 201;
});
Whether status belongs in the predicate is a design choice. If the test should fail immediately when the endpoint returns 500, matching only 201 causes a timeout that hides the actual response. Often it is better to match request identity first, then assert status separately:
const responsePromise = page.waitForResponse(response => {
const url = new URL(response.url());
return url.pathname === '/api/orders' &&
response.request().method() === 'POST';
});
await page.getByRole('button', { name: 'Place order' }).click();
const response = await responsePromise;
expect(
response.status(),
'POST /api/orders should create an order',
).toBe(201);
Parse URLs instead of using endsWith when query strings are possible:
const responsePromise = page.waitForResponse(response => {
const url = new URL(response.url());
return url.pathname === '/api/search' &&
url.searchParams.get('q') === 'playwright' &&
url.searchParams.get('page') === '2';
});
Inspect request bodies when the endpoint alone is not unique:
const responsePromise = page.waitForResponse(response => {
const request = response.request();
if (request.method() !== 'POST') return false;
const url = new URL(response.url());
if (url.pathname !== '/api/events') return false;
const data = request.postDataJSON() as { type?: string } | null;
return data?.type === 'PROFILE_UPDATED';
});
Keep predicates side-effect free. They may run against many responses, so do not add assertions, mutation, or expensive logging inside them.
5. Use playwright waitForResponse to Validate Status, Headers, and JSON
Once the promise resolves, the returned Response provides the evidence needed for focused contract checks.
type CreateOrderResponse = {
id: string;
status: 'confirmed' | 'pending';
total: number;
};
const responsePromise = page.waitForResponse(response =>
new URL(response.url()).pathname === '/api/orders' &&
response.request().method() === 'POST',
);
await page.getByRole('button', { name: 'Place order' }).click();
const response = await responsePromise;
expect(response.status()).toBe(201);
expect(await response.headerValue('content-type'))
.toContain('application/json');
const body = await response.json() as CreateOrderResponse;
expect(body.id).toMatch(/^ord_/);
expect(body.status).toBe('confirmed');
expect(body.total).toBeGreaterThan(0);
response.ok() is true for status codes in the 200 through 299 range. It is convenient when any successful response is acceptable:
expect(response.ok()).toBe(true);
Use an exact status when 200, 201, 202, or 204 has specific meaning. A generic ok check would not detect a service that accidentally changed from asynchronous acceptance to synchronous creation.
Avoid reading JSON from a 204 response or from content that is not JSON. Check status and content type first. For text, use response.text():
const text = await response.text();
expect(text).toContain('export ready');
For binary data, response.body() returns a Buffer:
const bytes = await response.body();
expect(bytes.byteLength).toBeGreaterThan(0);
Do not turn every browser test into a complete schema validator. Validate fields that drive the scenario, and keep exhaustive service contracts at the API or contract-test layer. Browser checks should explain a user risk.
6. Pair Network Assertions With Web-First UI Assertions
A successful response does not prove the interface handled it correctly. A visible success message does not prove the intended request was sent. Strong end-to-end tests often assert both.
test('renames a workspace', async ({ page }) => {
await page.goto('/settings/workspace');
await page.getByLabel('Workspace name').fill('Quality Guild');
const responsePromise = page.waitForResponse(response =>
new URL(response.url()).pathname === '/api/workspace' &&
response.request().method() === 'PATCH',
);
await page.getByRole('button', { name: 'Save changes' }).click();
const response = await responsePromise;
expect(response.status()).toBe(200);
await expect(page.getByRole('status'))
.toHaveText('Workspace updated');
await expect(page.getByRole('heading', { name: 'Quality Guild' }))
.toBeVisible();
});
The Response assertion identifies the service result. Playwright's locator assertions retry until the application reflects that result or the assertion times out. Do not replace the final assertion with an immediate textContent call because the React render may occur just after the network response.
Choose a user-observable endpoint:
- A status message appears.
- A button becomes enabled or disabled.
- A table row changes.
- Navigation reaches the confirmed URL.
- Reload preserves the saved state.
- An accessible error alert explains failure.
Only add a reload when persistence is part of the requirement. Reloading after every mutation increases test duration and can blur the scenario.
The Playwright assertions guide explains web-first matching, while the Playwright auto-waiting guide covers actionability and retry behavior.
7. Match GraphQL Operations Without Confusing Shared Endpoints
GraphQL often sends every operation to one /graphql endpoint, so URL and method are insufficient. Match operationName in the POST body.
type GraphQLPayload = {
operationName?: string;
variables?: Record<string, unknown>;
};
const responsePromise = page.waitForResponse(response => {
const request = response.request();
const url = new URL(response.url());
if (url.pathname !== '/graphql' || request.method() !== 'POST') {
return false;
}
const payload = request.postDataJSON() as GraphQLPayload | null;
return payload?.operationName === 'UpdateProfile';
});
await page.getByRole('button', { name: 'Save profile' }).click();
const response = await responsePromise;
expect(response.status()).toBe(200);
const payload = await response.json() as {
data?: { updateProfile?: { id: string; displayName: string } };
errors?: Array<{ message: string }>;
};
expect(payload.errors).toBeUndefined();
expect(payload.data?.updateProfile?.displayName).toBe('Asha Patel');
GraphQL can return HTTP 200 while the payload contains errors. That makes body validation essential. Match the operation first, then assert both errors and the scenario data.
If the client uses persisted queries, operationName may still be present. If not, match a stable hash or a distinctive variable, but avoid comparing a full serialized query because whitespace and client generation can change.
A page can send the same operation more than once, such as an optimistic update followed by a refetch. Add variable identity:
const payload = request.postDataJSON() as GraphQLPayload | null;
return payload?.operationName === 'UpdateTask' &&
payload.variables?.taskId === 'TASK-42';
Do not mutate parsed payloads in the predicate. Treat network objects as observations. When you need to simulate GraphQL errors or control payloads, route interception is the clearer tool.
8. Handle Multiple Responses, Polling, and Parallel Requests
One waitForResponse promise resolves once. If an action intentionally triggers two distinct calls, register both waits before the action.
const savePromise = page.waitForResponse(response =>
new URL(response.url()).pathname === '/api/profile' &&
response.request().method() === 'PUT',
);
const auditPromise = page.waitForResponse(response =>
new URL(response.url()).pathname === '/api/audit-events' &&
response.request().method() === 'POST',
);
await page.getByRole('button', { name: 'Save profile' }).click();
const [saveResponse, auditResponse] = await Promise.all([
savePromise,
auditPromise,
]);
expect(saveResponse.status()).toBe(200);
expect(auditResponse.status()).toBe(202);
When several calls hit the same endpoint, match an ID, request payload, query parameter, or response data. Be cautious about parsing every response body inside an async predicate. It is supported to use an asynchronous predicate, but body parsing across noisy traffic adds work and can make matching harder to reason about. Prefer request identity where possible.
Polling requires a different design. A single response wait may catch the first pending result rather than the final completed result. If the UI owns polling, assert the final UI state. If the requirement specifically concerns the completed response, use an asynchronous predicate that identifies completion:
const completedPromise = page.waitForResponse(async response => {
const url = new URL(response.url());
if (url.pathname !== '/api/exports/exp_42') return false;
if (response.status() !== 200) return false;
const body = await response.json() as { state?: string };
return body.state === 'completed';
});
await page.getByRole('button', { name: 'Start export' }).click();
const completedResponse = await completedPromise;
expect(completedResponse.ok()).toBe(true);
await expect(page.getByRole('link', { name: 'Download export' }))
.toBeVisible();
Set a realistic per-wait timeout for genuinely long jobs, but do not disable timeouts without a strong operational reason.
9. Test Error Responses and Network Failures Correctly
HTTP errors such as 400, 404, and 503 still produce Response objects. waitForResponse can observe them.
const responsePromise = page.waitForResponse(response =>
new URL(response.url()).pathname === '/api/coupons' &&
response.request().method() === 'POST',
);
await page.getByRole('button', { name: 'Apply coupon' }).click();
const response = await responsePromise;
expect(response.status()).toBe(422);
await expect(page.getByRole('alert'))
.toHaveText('This coupon has expired');
A transport failure is different. DNS failure, connection reset, or an explicitly aborted route may produce a requestfailed event and no Response. Waiting for a response in that case will time out.
const failedRequestPromise = page.waitForEvent('requestfailed', request =>
new URL(request.url()).pathname === '/api/recommendations',
);
await page.getByRole('button', { name: 'Load recommendations' }).click();
const failedRequest = await failedRequestPromise;
expect(failedRequest.failure()?.errorText).toBeTruthy();
await expect(page.getByRole('alert'))
.toHaveText('Recommendations are unavailable');
For deterministic negative UI testing, intercept the route before navigation or the trigger:
await page.route('**/api/recommendations', async route => {
await route.fulfill({
status: 503,
contentType: 'application/json',
body: JSON.stringify({ message: 'Service unavailable' }),
});
});
Then test the interface. Use waitForResponse when observing real traffic matters. Use route when the test must control an otherwise rare or destructive condition. Avoid mixing observation and heavy mocking until the test's purpose is explicit.
10. Diagnose a playwright waitForResponse Timeout
A timeout means no future response satisfied the matcher within the allowed time. It does not automatically mean the service was slow.
Check these causes in order:
- The listener was registered after the action.
- The action did not trigger a request.
- The URL path, host, method, or payload matcher is wrong.
- A status condition excluded the actual error response.
- The page used a service worker that hid traffic from page-level routing or events.
- The response belonged to a popup or another page.
- Authentication or test data changed the application path.
- The test timed out before the event wait could complete.
Temporarily record response identities:
page.on('response', response => {
const request = response.request();
console.log(
request.method(),
response.status(),
response.url(),
);
});
Run a focused test with trace evidence:
npx playwright test tests/profile.spec.ts --debug
npx playwright test tests/profile.spec.ts --trace=on
Remove noisy listeners after diagnosis or attach logs only when failures occur. Permanent console floods make CI evidence harder to scan.
If a service worker owns requests and the application can be tested without it, configure service workers to be blocked:
use: {
serviceWorkers: 'block',
}
Do not block them when offline or caching behavior is the feature under test. In that case, test service-worker behavior deliberately and choose signals the browser exposes.
Increasing the timeout only helps when evidence shows the correct request exists and legitimately takes longer. It does not fix a missed event or wrong matcher.
11. Design Reusable Helpers Without Hiding the Trigger
A helper can standardize URL and method matching, but it should not bury user actions or broad assumptions.
import type { Page, Response } from '@playwright/test';
type WaitForApiOptions = {
method: string;
pathname: string;
timeout?: number;
};
export function waitForApiResponse(
page: Page,
options: WaitForApiOptions,
): Promise<Response> {
return page.waitForResponse(response => {
const url = new URL(response.url());
return url.pathname === options.pathname &&
response.request().method() === options.method;
}, {
timeout: options.timeout,
});
}
Usage keeps the timing visible:
const responsePromise = waitForApiResponse(page, {
method: 'PATCH',
pathname: '/api/settings',
});
await page.getByRole('button', { name: 'Save settings' }).click();
const response = await responsePromise;
expect(response.status()).toBe(200);
Avoid a helper named clickAndWaitForAnyResponse. It couples two concerns, encourages generic matching, and becomes awkward when one action triggers multiple requests. Domain-specific helpers can be stronger:
function waitForOrderCreation(page: Page): Promise<Response> {
return waitForApiResponse(page, {
method: 'POST',
pathname: '/api/orders',
});
}
Page objects may expose an action that returns a response when the response is an essential postcondition. Document that behavior and keep assertions in the test unless every caller requires the same invariant.
A helper must preserve types, timeouts, and trace readability. Do not catch timeout errors and return undefined. A swallowed event failure turns a precise cause into a later null error or misleading UI timeout.
12. Choose Between waitForResponse, Routing, and APIRequestContext
These tools operate at different layers.
| Technique | Observes browser traffic | Controls response | Requires UI trigger | Best use |
|---|---|---|---|---|
| page.waitForResponse | Yes | No | Usually | Synchronize with one real page response |
| page.waitForRequest | Request only | No | Usually | Validate request method, URL, or payload |
| page.route | Yes | Yes | Usually | Stub errors, edge cases, or dependencies |
| browserContext.route | All context pages | Yes | Usually | Shared routing across pages and popups |
| APIRequestContext | No | Sends directly | No | Setup, cleanup, service-level tests |
Use the narrowest tool that proves the risk. If the goal is to confirm that clicking Save sends the right payload, waitForRequest may be enough. If the goal is to validate the server response and resulting UI, waitForResponse fits. If the goal is to display a deterministic 503 state, route.fulfill controls it. If the goal is to test fifty API input combinations, use APIRequestContext or service tests rather than driving a browser fifty times.
Network awareness should not replace user-focused assertions. A product can return 200 and render stale data. It can also render optimistic success before a later request fails. Assert at the point where the requirement becomes true.
This separation is also useful in code review. Ask whether the test observes, controls, or directly calls the network. Confused tests often attempt all three without a clear reason.
Interview Questions and Answers
Q: Why must waitForResponse be created before the click?
The response can arrive before the click promise returns. Registering first ensures Playwright is listening when the browser emits the event. Awaiting only after the action preserves the intended user sequence without missing fast traffic.
Q: Should status code be part of the predicate?
Include status only when the wait is specifically for that status. For most functional tests, match the request identity and assert status after resolution. That preserves an unexpected 500 as direct evidence instead of converting it into a vague timeout.
Q: How do you match GraphQL responses?
Match the /graphql path, POST method, and operationName from request.postDataJSON(). Add a stable variable such as taskId when the same operation can run more than once. Then check GraphQL errors because HTTP 200 can still contain operation failures.
Q: What is the difference between waitForRequest and waitForResponse?
waitForRequest resolves when the outgoing request is issued and is useful for payload validation. waitForResponse resolves when response status and headers arrive and gives access to status, headers, and body. Select the event that aligns with the requirement.
Q: Does waitForResponse handle a network connection failure?
Not as a completed response because no HTTP response exists. Observe requestfailed for transport failures. HTTP 404 or 503 responses are still valid Response objects and can be matched normally.
Q: How would you handle two API calls caused by one action?
Create two narrowly matched promises before the action, perform the action once, then await the promises together. Match each call by path, method, and operation identity. Assert only the responses relevant to the requirement.
Q: Is page.waitForTimeout a safe replacement?
No. A fixed sleep guesses about duration and does not identify the completed operation. Event waits and web-first assertions synchronize with observable behavior and fail with more useful evidence.
Q: When should you use page.route instead?
Use routing when the test needs to control the request or response, such as a 503, delayed service, or boundary payload. Use waitForResponse to observe real traffic without changing it.
Common Mistakes
- Registering the wait after the click and missing a fast response.
- Awaiting waitForResponse before performing the action, so the trigger never runs.
- Matching only a shared URL while ignoring method, query, body, or operation name.
- Requiring status 200 in the predicate, then receiving a timeout when the server returned 500.
- Using includes for an ID that also matches another endpoint or query value.
- Parsing every response body in a high-traffic predicate when request identity is enough.
- Asserting only the network response and never verifying the user-visible outcome.
- Treating HTTP error responses as requestfailed events.
- Using waitForTimeout or networkidle as a universal synchronization strategy.
- Setting timeout to zero and allowing a missing event to hang until the enclosing test fails.
- Swallowing TimeoutError in a helper and continuing with incomplete state.
- Blocking service workers when the feature under test depends on offline or cache behavior.
Conclusion
Use playwright waitForResponse as a precise event listener, not a general delay. Register it before the trigger, identify the operation with URL, method, and meaningful request data, then inspect the Response and assert what the user sees.
Start with one mutation test in your suite. Replace any fixed wait with a named response promise, tighten the predicate, and add a web-first outcome assertion. That small pattern produces faster failures, clearer traces, and more trustworthy network-aware coverage.
Interview Questions and Answers
Explain the race condition around page.waitForResponse.
A fast response can be emitted before a click promise completes. If the wait is registered afterward, it only observes future events and misses that response. I create the promise first, trigger the action second, then await the stored promise.
How do you make a response predicate reliable?
I match the parsed pathname, HTTP method, and a stable operation identity such as a query parameter, entity ID, or GraphQL operationName. I avoid broad substring matches. I usually assert status after matching so error responses remain visible.
What can you inspect on the returned Response?
I can inspect URL, status, ok state, headers, body, and the originating Request. The Request exposes method and submitted data, which helps distinguish calls that share an endpoint. I then connect those checks to a UI assertion.
When would you use waitForRequest instead of waitForResponse?
I use waitForRequest when the requirement concerns the outgoing method, URL, headers, or payload and the response is not needed. waitForResponse fits when status, headers, or response body matter. The choice follows the behavior being proven.
How do HTTP errors differ from transport failures in Playwright?
HTTP 400 or 503 still produces a Response because the server answered. A DNS error, reset connection, or aborted request may emit requestfailed with no Response. I select waitForResponse or requestfailed according to that distinction.
How do you validate a GraphQL mutation response?
I match the POST request by endpoint, operationName, and relevant variables. Then I assert the HTTP status and parse the body. Because GraphQL may return 200 with an errors array, I check errors and the scenario data explicitly.
How do you wait for multiple responses from one click?
I create every narrowly matched wait promise before clicking. After the action, I await them with Promise.all and validate only the calls required by the scenario. Matching by operation identity prevents polling or analytics from satisfying a wait.
Why not use networkidle for API synchronization?
Applications can poll, stream, or send analytics, so a globally idle network may never occur or may not mean the required operation finished. A specific response and a user-visible assertion express readiness more directly. They also produce clearer failures.
How do you debug a waitForResponse timeout?
I first confirm listener order and whether the action fired. Then I compare actual URL, method, status, payload, page ownership, and service-worker behavior using response logging and Trace Viewer. I increase the timeout only after evidence shows a legitimate slow response.
When is an asynchronous response predicate justified?
It is useful when a repeated endpoint can only be distinguished by response content, such as polling until state becomes completed. I prefer request identity when possible because body parsing across many responses adds complexity. The predicate stays side-effect free.
Should a browser test deeply validate the API schema?
Usually no. I validate fields that drive the user scenario and leave exhaustive schema combinations to API or contract tests. The browser test should also assert the rendered outcome, since a correct response can still be handled incorrectly.
How would you design a reusable response-wait helper?
I accept typed criteria such as method, pathname, and timeout, and return Promise<Response> without swallowing errors. I keep the triggering action outside the helper so registration order remains visible. Domain-specific helpers can add stable identifiers without becoming generic wrappers.
Frequently Asked Questions
How do I use Playwright waitForResponse correctly?
Create the waitForResponse promise before the action that triggers the network call. Perform the action, await the saved promise, then assert response status or body and the resulting UI state.
Can Playwright waitForResponse match a relative URL?
Yes. When baseURL is configured for the browser context, a path string is resolved against it with the URL constructor. Predicates that parse response.url() are often clearer when query parameters vary.
What is the default waitForResponse timeout?
The event wait defaults to 30 seconds unless the page or browser context default timeout changes it. A timeout option can be supplied for one wait, but increasing it will not fix a late listener or incorrect predicate.
How do I read a JSON response in Playwright?
After waitForResponse resolves, call await response.json() and cast or validate the returned data. Check status and content type first when the endpoint may return no content or non-JSON errors.
Why does waitForResponse time out even though the API appears in DevTools?
Common causes are registering after the trigger, matching the wrong method or query, excluding an error status, observing the wrong page, or service-worker behavior. Log response method, status, and URL temporarily, then inspect a trace.
Can waitForResponse handle GraphQL?
Yes. Match the GraphQL endpoint and inspect operationName and variables through response.request().postDataJSON(). After resolution, check both the HTTP status and any errors array in the GraphQL body.
Should I use waitForResponse or page.route?
Use waitForResponse to observe a real response without modifying it. Use page.route when the scenario must intercept, modify, abort, or fulfill traffic with a controlled result.
Does waitForResponse wait for the UI to update?
No. It waits for a matching network response, not for rendering or application state. Follow it with a locator assertion such as toHaveText or toBeVisible to prove the user outcome.