QA How-To
How to Wait for an API response in Playwright (2026)
Learn playwright how to wait for an API response using waitForResponse, predicates, Promise.all, assertions, debugging, and stable test patterns in CI.
19 min read | 3,521 words
TL;DR
playwright how to wait for an api response works best when the test synchronizes with the exact application event, uses stable locators or request predicates, and asserts the resulting business state. Start with the simplest public Playwright API, then add a focused fallback only when the application requires it.
Key Takeaways
- Start with Playwright's public, action-oriented APIs and stable user-facing locators.
- Begin waiting before the action that triggers asynchronous work.
- Assert the final business outcome, not only that an automation call returned.
- Keep test data isolated and make cleanup safe for retries and parallel workers.
- Use traces, screenshots, and targeted diagnostics to explain failures.
- Treat fallback techniques as documented exceptions with focused coverage.
playwright how to wait for an api response is a practical testing workflow that verifies user-visible behavior, not merely that automation commands completed. This guide gives you runnable patterns, assertion strategies, debugging methods, and design choices suitable for a maintained 2026 test suite.
The examples emphasize deterministic synchronization, accessible locators, useful failure evidence, and tests that stay readable as the application evolves. Use the quick answer first, then work through the numbered sections for production details and interview reasoning.
These Playwright API response waiting show how to synchronize tests with GET, POST, GraphQL, polling, startup, and parallel browser requests. Each recipe registers the event wait before the action, identifies one operation, and validates the outcome without a fixed sleep.
Use the examples as patterns, not blind snippets. Replace paths, roles, payload types, and expected statuses with your application's real contract. The method is most valuable when a specific response is part of the behavior you need to prove.
TL;DR
const responsePromise = page.waitForResponse(response => {
const url = new URL(response.url());
return url.pathname === '/api/tasks/TASK-42' &&
response.request().method() === 'PATCH';
});
await page.getByRole('button', { name: 'Complete task' }).click();
const response = await responsePromise;
expect(response.status()).toBe(200);
await expect(page.getByRole('status')).toHaveText('Task completed');
| Matcher style | Use it when | Main risk |
|---|---|---|
| Exact string | Host and full URL are stable | Query or environment changes |
| Relative string | baseURL is configured and path is exact | Still exact after URL resolution |
| Regular expression | A simple, bounded URL pattern is enough | Overbroad expressions |
| Predicate | Method, query, request body, or status matters | Excess logic inside callback |
| Async predicate | Response body is the only stable identity | Parsing many noisy responses |
1. playwright how to wait for an api response: Core Concepts
Every reliable example has four phases:
- Describe the future response.
- Register that description as a promise.
- Trigger the request through the user workflow.
- Await evidence and assert the result.
const responsePromise = page.waitForResponse(predicate);
await triggerAction();
const response = await responsePromise;
expect(response.ok()).toBe(true);
await expect(resultLocator).toBeVisible();
The first line starts listening but does not block the test. If you await it immediately, the action never runs. If you create it after triggerAction, a fast API response may already be gone.
This structure also separates failures. A click timeout means the UI was not actionable. A response timeout means no future response matched. A status assertion exposes a service error. A locator assertion exposes a rendering or state-management problem.
Avoid making the predicate prove everything. Its primary job is identity. For example, match POST /api/tasks/TASK-42 in the predicate, then assert status 200 afterward. If status 500 is included as a rejection condition inside the predicate, the test may wait until timeout and conceal the response that explained the defect.
Use a short comment only when operation identity is not obvious. Clear names such as updateTaskResponsePromise usually communicate enough.
This article focuses on a recipe collection. For deeper timing and failure semantics, review how to use Playwright waitForResponse.
2. playwright how to wait for an api response: Setup and First Working Test
An exact absolute URL is concise:
const responsePromise = page.waitForResponse(
'https://app.example.test/api/health',
);
It is also tied to one host. When use.baseURL is set, a path is resolved relative to that base:
const responsePromise = page.waitForResponse('/api/health');
A regular expression handles a stable path plus a variable identifier:
const responsePromise = page.waitForResponse(
/\/api\/users\/[a-z0-9_-]+$/,
);
Anchor expressions when possible. A loose /users/ pattern might match avatars, permissions, or analytics URLs.
Predicates are the default for production suites because method and parsed URL make intent explicit:
const responsePromise = page.waitForResponse(response => {
const url = new URL(response.url());
return url.hostname === 'api.example.test' &&
url.pathname === '/v1/users/me' &&
response.request().method() === 'GET';
});
Environment-specific hosts can make hostname matching undesirable. If the same trusted test environment changes hosts, match pathname and method:
return new URL(response.url()).pathname === '/v1/users/me' &&
response.request().method() === 'GET';
Do not use response.url().includes('/api/user') when /api/users, /api/user-settings, and /api/user-photo all exist. URL parsing removes accidental substring matches and makes query handling readable.
The matching callback should return boolean or Promise
3. Example: Wait for a POST and Validate Its Request Body
Suppose a checkout page posts one order. Match POST /api/orders, then verify the submitted data through the originating Request and the created order through the Response.
import { test, expect } from '@playwright/test';
type OrderRequest = {
shippingMethod: string;
couponCode: string | null;
};
type OrderResponse = {
id: string;
status: 'confirmed' | 'pending';
};
test('places an order with express shipping', async ({ page }) => {
await page.goto('/checkout');
await page.getByLabel('Shipping method').selectOption('express');
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;
const submitted = response.request().postDataJSON() as OrderRequest;
expect(submitted).toMatchObject({
shippingMethod: 'express',
couponCode: null,
});
expect(response.status()).toBe(201);
const created = await response.json() as OrderResponse;
expect(created.id).toMatch(/^ord_/);
expect(created.status).toBe('confirmed');
await expect(page.getByRole('heading', { name: 'Order confirmed' }))
.toBeVisible();
});
This browser test checks only scenario-critical fields. Exhaustive validation of every order property belongs in API or contract coverage.
postDataJSON() can return parsed form data or JSON depending on content. If no post data exists, it can be null. Use a defensive type when the route can be triggered in multiple forms.
If several order requests are possible, make identity part of the predicate by examining a unique cart ID. Do not parse the response body merely to find the request when the request already contains stable identity.
4. Example: Wait for GET Results With Query Parameters
Search interfaces frequently call one endpoint many times. Match the exact query that belongs to the test.
test('loads page two of playwright results', async ({ page }) => {
await page.goto('/search?q=playwright');
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' &&
response.request().method() === 'GET';
});
await page.getByRole('button', { name: 'Next page' }).click();
const response = await responsePromise;
expect(response.status()).toBe(200);
const body = await response.json() as {
page: number;
items: Array<{ id: string; title: string }>;
};
expect(body.page).toBe(2);
expect(body.items.length).toBeGreaterThan(0);
await expect(page.getByRole('status')).toHaveText('Page 2 loaded');
});
URLSearchParams decodes encoded values and avoids dependence on parameter order. The following URLs represent the same logical query but are different strings:
/api/search?q=playwright&page=2
/api/search?page=2&q=playwright
For type-ahead search, several requests may be in flight. Match the final query value and assert the rendered results. If the product cancels earlier requests, those aborted calls do not produce successful Responses.
A debounce is not a reason to call waitForTimeout. Fill the input, register the wait before the final key or action that causes the request, and await the specific query. If filling the entire value itself triggers the request, register before fill:
const responsePromise = page.waitForResponse(response => {
const url = new URL(response.url());
return url.pathname === '/api/search' &&
url.searchParams.get('q') === 'playwright';
});
await page.getByRole('searchbox').fill('playwright');
await responsePromise;
5. Example: Assert Status, Headers, Text, JSON, and Binary Bodies
Choose the body API that matches the content. Do not assume every response is JSON.
| Response content | API | Typical assertion |
|---|---|---|
| JSON | response.json() | Object fields or schema boundary |
| Text, HTML, CSV | response.text() | Header line or meaningful token |
| Binary | response.body() | Length, signature, or parser result |
| Headers | response.headerValue(name) | Content type, cache, request ID |
| Any success | response.ok() | True for 200 through 299 |
| Exact semantics | response.status() | 201, 202, 204, or expected error |
A CSV export example:
const responsePromise = page.waitForResponse(response =>
new URL(response.url()).pathname === '/api/reports/orders.csv' &&
response.request().method() === 'GET',
);
await page.getByRole('button', { name: 'Export CSV' }).click();
const response = await responsePromise;
expect(response.status()).toBe(200);
expect(await response.headerValue('content-type')).toContain('text/csv');
const csv = await response.text();
expect(csv.split('\n')[0]).toBe('order_id,status,total');
A binary document example:
const responsePromise = page.waitForResponse(response =>
new URL(response.url()).pathname === '/api/invoices/inv_42.pdf',
);
await page.getByRole('link', { name: 'Download invoice' }).click();
const response = await responsePromise;
expect(await response.headerValue('content-type'))
.toContain('application/pdf');
const bytes = await response.body();
expect(bytes.subarray(0, 4).toString('ascii')).toBe('%PDF');
For an actual browser download, also use the download event if filename or saved content is the requirement. A response wait proves network content, while Download represents browser download handling.
Never call response.json() twice in a helper chain without a reason. Parse once, type the result, and pass the object to focused assertions.
6. Example: Wait for Page-Load and Lazy-Load Responses
When navigation initiates the call, register before page.goto:
test('loads dashboard summary', async ({ page }) => {
const summaryPromise = page.waitForResponse(response =>
new URL(response.url()).pathname === '/api/dashboard/summary' &&
response.request().method() === 'GET',
);
await page.goto('/dashboard');
const summaryResponse = await summaryPromise;
expect(summaryResponse.ok()).toBe(true);
await expect(page.getByRole('heading', { name: 'Dashboard' }))
.toBeVisible();
await expect(page.getByTestId('summary-cards')).toBeVisible();
});
Do not register after page.goto if the initial script starts the request during navigation.
Lazy loading may be triggered by scrolling an element into view:
const activityPromise = page.waitForResponse(response => {
const url = new URL(response.url());
return url.pathname === '/api/activity' &&
url.searchParams.get('cursor') === 'next';
});
await page.getByTestId('activity-sentinel').scrollIntoViewIfNeeded();
const activityResponse = await activityPromise;
expect(activityResponse.status()).toBe(200);
await expect(page.getByRole('listitem')).toHaveCount(40);
The count should reflect your controlled fixture, not an arbitrary production total. Prefer asserting specific newly loaded records when data varies.
Some pages prefetch data before the user opens a panel. If prefetch is an intentional product optimization, waiting after the panel click may be too late because the response already happened. Assert the resulting UI or register before the event that causes prefetch, such as navigation or hover. Event waits observe future traffic only.
7. Example: Wait for Multiple Parallel Responses
A dashboard refresh may start independent summary, alerts, and tasks calls. Register every required wait first.
const summaryPromise = page.waitForResponse(response =>
new URL(response.url()).pathname === '/api/dashboard/summary',
);
const alertsPromise = page.waitForResponse(response =>
new URL(response.url()).pathname === '/api/dashboard/alerts',
);
const tasksPromise = page.waitForResponse(response => {
const url = new URL(response.url());
return url.pathname === '/api/tasks' &&
url.searchParams.get('assigned') === 'me';
});
await page.getByRole('button', { name: 'Refresh dashboard' }).click();
const [summary, alerts, tasks] = await Promise.all([
summaryPromise,
alertsPromise,
tasksPromise,
]);
expect(summary.ok()).toBe(true);
expect(alerts.ok()).toBe(true);
expect(tasks.ok()).toBe(true);
await expect(page.getByRole('status'))
.toHaveText('Dashboard refreshed');
Do not loop over endpoints after clicking and create waits one by one. The earliest responses can finish before their listener exists.
Only wait for calls that define the user outcome. Waiting for analytics, feature-flag refresh, or image responses couples the test to incidental implementation.
When calls are sequential, the same pre-registration pattern still works if all waits can safely listen from the start. If the second endpoint can occur earlier for unrelated reasons, register it immediately before the user step that specifically starts it.
For repeated calls to one endpoint, a plain array of identical waits can all resolve to the same first response. Distinguish each call by query, body, or another stable property. If you need a stream of diagnostic responses, use page.on('response') and collect them with careful cleanup instead of assuming identical event waits consume distinct events.
8. Example: Match REST Errors and GraphQL Errors
A REST validation response remains a normal Response:
const validationPromise = page.waitForResponse(response =>
new URL(response.url()).pathname === '/api/users' &&
response.request().method() === 'POST',
);
await page.getByRole('button', { name: 'Create user' }).click();
const validationResponse = await validationPromise;
expect(validationResponse.status()).toBe(422);
const error = await validationResponse.json() as {
code: string;
field: string;
};
expect(error).toEqual({
code: 'EMAIL_ALREADY_EXISTS',
field: 'email',
});
await expect(page.getByText('An account already uses this email'))
.toBeVisible();
GraphQL usually needs operation matching and body-level error checks:
type GraphQLRequest = {
operationName?: string;
variables?: { email?: string };
};
const invitePromise = page.waitForResponse(response => {
const request = response.request();
if (new URL(response.url()).pathname !== '/graphql') return false;
if (request.method() !== 'POST') return false;
const body = request.postDataJSON() as GraphQLRequest | null;
return body?.operationName === 'InviteMember' &&
body.variables?.email === 'existing@example.test';
});
await page.getByRole('button', { name: 'Send invitation' }).click();
const inviteResponse = await invitePromise;
expect(inviteResponse.status()).toBe(200);
const result = await inviteResponse.json() as {
data: { inviteMember: null };
errors: Array<{ message: string; extensions?: { code?: string } }>;
};
expect(result.errors[0]?.extensions?.code).toBe('ALREADY_MEMBER');
await expect(page.getByRole('alert')).toHaveText('Already a member');
Do not require status 200 inside the GraphQL predicate unless the wait specifically targets success. An upstream 502 is useful evidence and should fail at the status assertion, not disappear into a timeout.
9. Example: Handle Polling, Popups, and Frames
Polling endpoints can return pending several times. When the final response content is the only differentiator, an async predicate is reasonable:
const completedPromise = page.waitForResponse(async response => {
const url = new URL(response.url());
if (url.pathname !== '/api/jobs/job_42') return false;
if (response.status() !== 200) return false;
const body = await response.json() as { state?: string };
return body.state === 'completed';
}, {
timeout: 60_000,
});
await page.getByRole('button', { name: 'Start processing' }).click();
await completedPromise;
await expect(page.getByRole('status')).toHaveText('Processing complete');
Use the UI completion signal instead if the response body is not itself part of the requirement.
For a popup, get the popup first, then register on the page that will perform the next operation:
const popupPromise = page.waitForEvent('popup');
await page.getByRole('link', { name: 'Open billing portal' }).click();
const popup = await popupPromise;
const plansPromise = popup.waitForResponse(response =>
new URL(response.url()).pathname === '/api/plans',
);
await popup.getByRole('button', { name: 'Compare plans' }).click();
const plansResponse = await plansPromise;
expect(plansResponse.ok()).toBe(true);
Requests made by frames attached to a page are associated with that page's network activity, so a page-level wait can observe them. Match the resource carefully because advertisements and embedded tools may be noisy. If the frame is third-party, ask whether waiting on its private endpoint makes the test too coupled. A visible frame outcome is usually a more durable contract.
10. Example: Combine waitForResponse With Controlled Routing
Observation and control can work together when the purpose is explicit. Register a route before the page can call it, fulfill a realistic response, and still wait for the browser response to prove the UI made the request.
test('renders an empty notification state', async ({ page }) => {
await page.route('**/api/notifications', async route => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ items: [], unreadCount: 0 }),
});
});
const responsePromise = page.waitForResponse(response =>
new URL(response.url()).pathname === '/api/notifications',
);
await page.goto('/notifications');
const response = await responsePromise;
expect(response.status()).toBe(200);
await expect(page.getByText('You have no notifications')).toBeVisible();
});
This is a deterministic UI integration test, not proof that the real notification service works. Preserve separate API, contract, or end-to-end coverage for that integration.
For a network failure, abort the route and wait for requestfailed, not waitForResponse:
await page.route('**/api/recommendations', route =>
route.abort('connectionfailed'),
);
const failedPromise = page.waitForEvent('requestfailed', request =>
new URL(request.url()).pathname === '/api/recommendations',
);
await page.getByRole('button', { name: 'Retry recommendations' }).click();
const failedRequest = await failedPromise;
expect(failedRequest.failure()).not.toBeNull();
Read Playwright network mocking examples for route, fulfill, continue, and abort strategy.
11. Turn the Recipes Into Typed Domain Helpers
A typed helper reduces repeated matching while keeping action order visible:
import type { Page, Response } from '@playwright/test';
type ApiMatch = {
pathname: string;
method?: string;
query?: Record<string, string>;
};
export function waitForApi(
page: Page,
match: ApiMatch,
): Promise<Response> {
return page.waitForResponse(response => {
const url = new URL(response.url());
if (url.pathname !== match.pathname) return false;
if (match.method &&
response.request().method() !== match.method) {
return false;
}
return Object.entries(match.query ?? {}).every(
([name, value]) => url.searchParams.get(name) === value,
);
});
}
A test uses it without hiding the trigger:
const responsePromise = waitForApi(page, {
pathname: '/api/tasks',
method: 'GET',
query: { status: 'open', owner: 'me' },
});
await page.getByRole('button', { name: 'Refresh tasks' }).click();
const response = await responsePromise;
expect(response.ok()).toBe(true);
Do not make the helper return parsed any data by default. Different endpoints can return JSON, text, no content, or binary. Returning Response preserves the native API and lets callers choose.
For domain-level clarity, wrap only stable operations:
function waitForInvoiceApproval(page: Page, invoiceId: string) {
return waitForApi(page, {
pathname: '/api/invoices/' + invoiceId + '/approval',
method: 'POST',
});
}
Avoid helpers that catch timeout errors, log "optional response", and return null. If the response is optional, the test likely needs a different user-visible condition. If it is required, the timeout should fail clearly.
12. Apply Playwright API response waiting as Best Practices
Use this review checklist:
- Listener registration appears before the request-producing action.
- Predicate matches parsed path and HTTP method.
- Query, ID, body, or GraphQL operation distinguishes repeated traffic.
- Unexpected status is asserted after the match.
- Body parsing is limited to scenario-critical data.
- Web-first UI assertion proves rendering and state.
- Per-wait timeout reflects a documented service expectation.
- Trace or focused logs can explain failures.
- Mocking is identified as controlled UI coverage.
- No fixed wait or global network-idle dependency remains.
Service workers can complicate network observation and routing. If the feature does not require them, a test project may set serviceWorkers: 'block'. If offline behavior or cache strategy is under test, do not block them. Design a separate project around that requirement.
When you need every response for diagnostics, attach page.on('response') before the action and remove the listener afterward. For one functional event, waitForResponse is clearer and self-cleaning.
For locator and assertion design around the final result, use Playwright locator best practices and Playwright web-first assertions.
Interview Questions and Answers
Q: What is the safest waitForResponse sequence?
Create the promise first, perform the action second, then await the promise and assert. This listener-before-trigger sequence prevents fast responses from being missed.
Q: Why parse the URL instead of using includes?
URL exposes pathname and URLSearchParams without depending on host, query order, or accidental substrings. It makes the predicate express endpoint identity precisely.
Q: Why assert status outside the predicate?
An unexpected error response still matches the intended request and fails with its real status. Putting success status in the predicate can transform that evidence into a timeout.
Q: Can two identical waitForResponse promises capture two calls?
They may both resolve on the same first matching response. Distinguish calls by request data or collect response events when the requirement truly concerns a sequence.
Q: When is response.json unsafe?
It is inappropriate for 204 responses, non-JSON content, and some error pages. Check expected status and content type, then parse once.
Q: How do you test a polling endpoint?
Prefer the final visible UI state. If the completed response itself matters, use a carefully bounded async predicate that parses only the target endpoint and returns true for the completed state.
Q: Can page waitForResponse observe iframe requests?
Requests from frames attached to the page are part of page network activity. Match carefully because embedded frames may generate noisy traffic, and prefer a visible frame outcome when private endpoints are unstable.
Q: How do routing and response waits differ?
Routing controls traffic by continuing, modifying, fulfilling, or aborting it. waitForResponse observes a completed response. A test can use both, but it must state that the real service is mocked.
Common Mistakes
- Copying an example but registering the promise after the click.
- Using a broad regular expression that matches analytics or prefetch traffic.
- Requiring a success status in the predicate and hiding a real error response.
- Comparing a raw query string whose parameter order can change.
- Waiting for two identical predicates and assuming they consume separate events.
- Parsing JSON from 204, HTML, CSV, or binary content.
- Inspecting every response body in an async predicate.
- Waiting for incidental calls that are unrelated to user acceptance criteria.
- Using a page listener for a later request initiated by a popup.
- Calling waitForResponse for a transport failure that produces requestfailed.
- Leaving route mocks active across scenarios without fixture isolation.
- Asserting the API only and skipping the final locator assertion.
Production Review Checklist: How to Wait for an API response in Playwright
Before merging a test, confirm that it proves an observable requirement and can fail for the right reason. Keep setup explicit, use a locator that communicates intent, and place synchronization before the action that triggers the state change. Assert the final business state, not only an intermediate implementation detail. Run the case repeatedly in a clean worker and once under CI-like resource constraints. Preserve a trace, screenshot, or response detail when a failure would otherwise be difficult to reproduce.
A reviewer should also check isolation. The test must create or identify its own data, avoid depending on execution order, and clean up server-side records when appropriate. Timeouts should express a known service-level expectation rather than hide uncertainty. If the feature has several meaningful variants, use a small data table or project matrix instead of copying the entire test. Finally, keep one clear responsibility per test so that the report names the behavior that actually broke.
Conclusion
The best Playwright API response waiting are small, explicit, and tied to one requirement. Register before the trigger, match the actual operation, inspect only meaningful response data, and let locator assertions prove that the interface reached the expected state.
Choose one flaky network-dependent test and rewrite it with the four-phase template. If the matcher still feels complicated, that is useful design feedback: move exhaustive contract checks to API tests, control edge cases with routing, and keep the browser scenario focused on user risk.
Interview Questions and Answers
Show the canonical waitForResponse pattern.
I create const responsePromise = page.waitForResponse(predicate), perform the user action, and then await responsePromise. I assert the returned Response and a web-first UI outcome. The promise is created first so fast traffic cannot be missed.
What makes a good response predicate?
It identifies one operation using parsed pathname, method, and a stable discriminator such as query, entity ID, request body, or GraphQL operationName. It is side-effect free and inexpensive. Success status is usually asserted after matching.
How do you validate POST payload and response together?
After matching the response, I access response.request().postDataJSON() for the submitted payload and response.json() for returned data. I validate only scenario-critical fields. Then I assert the rendered result.
How do you match a search request with unordered query parameters?
I construct new URL(response.url()), compare url.pathname, and read each required value with url.searchParams.get(). This avoids dependence on host, encoding, and raw parameter order.
How do you handle multiple calls from one button?
I register each distinct response promise before clicking and await all after the action. Each matcher includes enough identity to prevent one event from satisfying several logical waits. I ignore incidental traffic.
What problem occurs with two identical response waits?
Both listeners can observe and resolve from the same first matching event. Event waits are not a queue that automatically consumes one response each. I add request identity or use a scoped event collector for sequences.
When would you parse body inside the predicate?
Only when response content is the stable differentiator, such as polling until a job reaches completed. I filter by URL and status before parsing. For most calls, request method, query, or payload provides cheaper identity.
How do you test a REST validation error?
I match the request independent of success status, trigger it, and assert the expected 4xx Response. I parse the error fields that drive the UI, then verify the accessible validation or alert text.
How do you test GraphQL errors returned with HTTP 200?
I match operationName and variables from the originating request. After resolution, I parse the response and assert the errors array or extensions code in addition to status. I verify the UI presents the intended recovery message.
When should a test use the download event instead?
Use page.waitForEvent('download') when browser download handling, suggested filename, or saved artifact matters. waitForResponse can validate network headers and bytes. Some scenarios benefit from both signals, registered before the click.
How do you keep helpers from hiding timing bugs?
The helper returns a native Promise<Response> for explicit typed criteria, while the caller performs the action. That keeps listener registration visibly before the trigger. I do not swallow timeout errors or return untyped parsed bodies.
What final assertion belongs after a response check?
A web-first assertion on the user-observable outcome, such as a status message, changed row, enabled control, navigation, or persisted state. Network success alone cannot prove that the application consumed the response correctly.
Frequently Asked Questions
What is a basic Playwright waitForResponse example?
Save page.waitForResponse with a narrow predicate, click the control that triggers the call, then await the saved promise. Assert the response and follow with a locator assertion for the visible outcome.
How do I wait for a POST response in Playwright?
Match both the parsed response URL and response.request().method() === 'POST'. Create the promise before submitting the form or clicking, then inspect status, request post data, or response body after it resolves.
How do I wait for a response with query parameters?
Construct a URL from response.url() and compare pathname plus URLSearchParams values. This is safer than comparing a raw query string because parameter ordering and encoding may vary.
Can Playwright wait for multiple API responses?
Yes. Create all distinct waitForResponse promises before the action, then await them with Promise.all. Make each predicate unique so several waits do not resolve from the same response.
How do I wait for a GraphQL response?
Match the GraphQL path and POST method, then read operationName and relevant variables from request.postDataJSON(). Check the returned errors array as well as the HTTP status.
Can waitForResponse read a response body?
The returned Response supports json(), text(), and body(). Select the method from the content type and status, parse once, and validate only data relevant to the browser scenario.
Should I use a fixed timeout before waitForResponse?
No. Register the event promise before the trigger, which directly prevents the missed-event race. A fixed delay only makes the test slower and cannot prove that the intended response occurred.
How do I wait for a failed network request?
Use requestfailed for transport failures such as connection errors or aborted routes. HTTP 400 and 500 responses are completed Responses and can be observed with waitForResponse.
Related Guides
- How to Wait for an API response in Cypress (2026)
- How to Wait for an API response in Selenium (2026)
- Playwright waitForResponse: Examples and Best Practices
- How to Scroll to an element in Playwright (2026)
- How to Debug a failing test in VS Code in Playwright (2026)
- How to Run tests in headed mode in Playwright (2026)