Resource library

QA How-To

Playwright waitForRequest: Examples and Best Practices

Use Playwright waitForRequest examples for GET filters, JSON, GraphQL, autosave, uploads, headers, redirects, concurrent calls, popups, and robust helpers.

16 min read | 2,041 words

TL;DR

The most reliable Playwright waitForRequest examples start the promise before the trigger, match a stable business identity, and parse each request according to its content type. Add response or UI assertions when the test must prove completion, and use context scope for a popup's initial document request.

Key Takeaways

  • Register every one-shot request wait before the user action that can emit traffic.
  • Decode GET parameters with URLSearchParams rather than comparing raw query strings.
  • Identify GraphQL traffic by operationName and variables, not only the shared endpoint.
  • Match debounced autosave requests by final payload instead of sleeping or counting events.
  • Inspect multipart uploads with safe buffer markers and synthetic files rather than JSON parsing.
  • Start separate narrowly matched promises for known concurrent requests and ignore arrival order.
  • Use BrowserContext request events for a popup's initial navigation and Page waits for later calls.

These Playwright waitForRequest examples turn outgoing browser traffic into focused assertions for search, JSON mutations, GraphQL, autosave, multipart upload, headers, redirects, debounced input, concurrent calls, and popups. Every example starts its wait before the trigger and matches enough request identity to avoid accepting unrelated traffic.

waitForRequest resolves when the browser issues the matching Request. It is ideal for request shape, but it does not prove the server succeeded or the UI consumed a response. The examples therefore pair request checks with a response or visible outcome when the scenario requires end-to-end evidence.

TL;DR

const savePromise = page.waitForRequest(request =>
  request.url().endsWith('/api/profile') && request.method() === 'PATCH',
);

await page.getByRole('button', { name: 'Save' }).click();
const saveRequest = await savePromise;

expect(saveRequest.postDataJSON()).toMatchObject({ theme: 'dark' });
Example Identity to match What to assert
Search GET Path plus method Decoded query parameters
JSON mutation Path plus POST, PUT, or PATCH Parsed JSON fields
GraphQL Endpoint plus operationName Operation and variables
Multipart upload Endpoint plus POST and content type Filename or safe body markers
Redirect Destination navigation request Redirect source and final UI
Concurrent calls Separate business paths Both captured requests and final state

Use a Page request wait for traffic from that Page. Use BrowserContext request events when the request happens before a new popup Page can be captured.

1. Build a deterministic request example harness

The most maintainable network examples control external replies but observe the real browser request. This helper serves an application document and a JSON endpoint through BrowserContext routing:

// tests/wait-for-request-examples.spec.ts
import {
  test,
  expect,
  type BrowserContext,
} from '@playwright/test';

async function servePage(
  context: BrowserContext,
  url: string,
  html: string,
): Promise<void> {
  await context.route(url, route => route.fulfill({
    contentType: 'text/html',
    body: html,
  }));
}

async function fulfillJson(
  context: BrowserContext,
  url: string,
  data: unknown,
  status = 200,
): Promise<void> {
  await context.route(url, route => route.fulfill({
    status,
    contentType: 'application/json',
    body: JSON.stringify(data),
  }));
}

Register routes before navigating or triggering fetch. A routed request still emits request events, so the wait can inspect what the application sent while the route provides a controlled response.

Keep route patterns narrow. A blanket route can accidentally fulfill documents, scripts, and assets with JSON. For a real application, route only dependencies outside the test's scope, and retain one smaller integration layer against representative services.

Do not use public websites as request fixtures. Their endpoints, consent layers, service workers, bot controls, and payloads are not your contract. A local or controlled test application gives deterministic examples and keeps data private.

2. Playwright waitForRequest examples for GET query parameters

Match a stable path and method, then use URLSearchParams for decoded values:

test('sends filters in a search request', async ({ page, context }) => {
  await servePage(context, 'https://search.example.test/', `
    <label>Query <input name="query"></label>
    <label><input type="checkbox" name="open"> Open only</label>
    <button>Search</button><p role="status"></p>
    <script>
      document.querySelector('button').addEventListener('click', async () => {
        const query = document.querySelector('[name="query"]').value;
        const open = document.querySelector('[name="open"]').checked;
        const response = await fetch(
          '/api/issues?q=' + encodeURIComponent(query) + '&status=' +
          (open ? 'open' : 'all') + '&page=1'
        );
        const data = await response.json();
        document.querySelector('[role="status"]').textContent =
          data.count + ' issues';
      });
    </script>
  `);
  await fulfillJson(
    context,
    'https://search.example.test/api/issues?q=timeout&status=open&page=1',
    { count: 3 },
  );

  await page.goto('https://search.example.test/');
  const searchPromise = page.waitForRequest(request => {
    const url = new URL(request.url());
    return url.pathname === '/api/issues' && request.method() === 'GET';
  });

  await page.getByLabel('Query').fill('timeout');
  await page.getByLabel('Open only').check();
  await page.getByRole('button', { name: 'Search' }).click();
  const search = await searchPromise;

  const url = new URL(search.url());
  expect(url.searchParams.get('q')).toBe('timeout');
  expect(url.searchParams.get('status')).toBe('open');
  expect(url.searchParams.get('page')).toBe('1');
  await expect(page.getByRole('status')).toHaveText('3 issues');
});

The predicate does not require one raw query order. Assertions explain which parameter failed. The visible count confirms the application consumed the mocked response.

Match origin too if multiple services share the path. Avoid includes('issues'), which can match analytics or assets.

3. Assert a POST JSON order payload

For JSON, identify endpoint and method before parsing the body:

test('submits the selected order values', async ({ page, context }) => {
  await servePage(context, 'https://shop.example.test/cart', `
    <button>Submit order</button><p role="status"></p>
    <script>
      document.querySelector('button').addEventListener('click', async () => {
        const response = await fetch('/api/orders', {
          method: 'POST',
          headers: { 'content-type': 'application/json' },
          body: JSON.stringify({
            sku: 'PW-42', quantity: 2, shipping: 'standard'
          })
        });
        const order = await response.json();
        document.querySelector('[role="status"]').textContent = order.id;
      });
    </script>
  `);
  await fulfillJson(
    context,
    'https://shop.example.test/api/orders',
    { id: 'ORDER-900' },
    201,
  );

  await page.goto('https://shop.example.test/cart');
  const orderPromise = page.waitForRequest(request =>
    request.url() === 'https://shop.example.test/api/orders' &&
    request.method() === 'POST',
  );

  await page.getByRole('button', { name: 'Submit order' }).click();
  const order = await orderPromise;

  expect(order.headers()['content-type']).toContain('application/json');
  expect(order.postDataJSON()).toEqual({
    sku: 'PW-42',
    quantity: 2,
    shipping: 'standard',
  });
  await expect(page.getByRole('status')).toHaveText('ORDER-900');
});

Use toEqual() when the full request shape is owned by the test. Use toMatchObject() when generated metadata or fields outside the scenario are legitimate. Do not normalize an unexpected field into a snapshot without reviewing the API contract.

Never log the entire payload when it may contain payment, address, or account data. Assertion diffs can also expose values, so use synthetic data.

4. Match a GraphQL operation by operationName

Many GraphQL operations share one /graphql URL and POST method. The JSON body provides business identity:

test('requests the BuildHistory GraphQL operation', async ({ page, context }) => {
  await servePage(context, 'https://ci.example.test/builds', `
    <button>Load history</button><p role="status"></p>
    <script>
      document.querySelector('button').addEventListener('click', async () => {
        const response = await fetch('/graphql', {
          method: 'POST',
          headers: { 'content-type': 'application/json' },
          body: JSON.stringify({
            operationName: 'BuildHistory',
            query: 'query BuildHistory($branch: String!) { builds(branch: $branch) { id } }',
            variables: { branch: 'main' }
          })
        });
        const result = await response.json();
        document.querySelector('[role="status"]').textContent =
          result.data.builds.length + ' builds';
      });
    </script>
  `);
  await fulfillJson(context, 'https://ci.example.test/graphql', {
    data: { builds: [{ id: 'B-1' }, { id: 'B-2' }] },
  });

  await page.goto('https://ci.example.test/builds');
  const operationPromise = page.waitForRequest(request => {
    if (!request.url().endsWith('/graphql') || request.method() !== 'POST') {
      return false;
    }
    const body = request.postDataJSON() as { operationName?: string };
    return body.operationName === 'BuildHistory';
  });

  await page.getByRole('button', { name: 'Load history' }).click();
  const operation = await operationPromise;
  const body = operation.postDataJSON() as {
    operationName: string;
    variables: { branch: string };
  };

  expect(body.operationName).toBe('BuildHistory');
  expect(body.variables).toEqual({ branch: 'main' });
  await expect(page.getByRole('status')).toHaveText('2 builds');
});

Matching only /graphql accepts the first unrelated query. Operation name is stable and readable. When persisted queries omit query text, operation name and variables remain better assertions than a generated hash unless hash behavior is the requirement.

5. Capture an autosave PUT without a fixed delay

Debounced or asynchronous autosave is an event problem, not a sleep problem. Start the request wait before editing and let it observe the real issue time:

test('autosaves a renamed test plan', async ({ page }) => {
  const savePromise = page.waitForRequest(request =>
    request.url().endsWith('/api/plans/17') && request.method() === 'PUT',
  );

  await page.getByLabel('Plan name').fill('Checkout regression');
  const save = await savePromise;

  expect(save.postDataJSON()).toMatchObject({
    name: 'Checkout regression',
  });
  await expect(page.getByRole('status')).toHaveText('Saved');
});

This assumes the real application test route provides the autosave response. The code waits for the actual request, whether the debounce is 200 milliseconds or 800 milliseconds. page.waitForTimeout() would copy an implementation delay and become fragile under load.

If typing generates intermediate requests rather than one debounced save, match the final payload in the predicate:

const finalSavePromise = page.waitForRequest(request => {
  if (!request.url().endsWith('/api/plans/17')) return false;
  if (request.method() !== 'PUT') return false;
  const body = request.postDataJSON() as { name?: string };
  return body.name === 'Checkout regression';
});

The business value is stronger than selecting the last request by timing.

6. Inspect a multipart file upload safely

Multipart bodies are not JSON. Match endpoint, method, and content type, then inspect only safe markers or use a lower-level contract test for detailed multipart parsing:

test('uploads a synthetic text report', async ({ page, context }) => {
  await servePage(context, 'https://files.example.test/upload', `
    <input type="file" aria-label="Report file">
    <button>Upload report</button><p role="status"></p>
    <script>
      document.querySelector('button').addEventListener('click', async () => {
        const data = new FormData();
        data.append('report', document.querySelector('input').files[0]);
        data.append('kind', 'qa-report');
        const response = await fetch('/api/uploads', { method: 'POST', body: data });
        document.querySelector('[role="status"]').textContent =
          response.ok ? 'Upload complete' : 'Upload failed';
      });
    </script>
  `);
  await fulfillJson(
    context,
    'https://files.example.test/api/uploads',
    { id: 'FILE-7' },
    201,
  );

  await page.goto('https://files.example.test/upload');
  await page.getByLabel('Report file').setInputFiles({
    name: 'quality-report.txt',
    mimeType: 'text/plain',
    buffer: Buffer.from('Synthetic quality report'),
  });

  const uploadPromise = page.waitForRequest(request =>
    request.url().endsWith('/api/uploads') &&
    request.method() === 'POST' &&
    (request.headers()['content-type'] ?? '').includes('multipart/form-data'),
  );
  await page.getByRole('button', { name: 'Upload report' }).click();
  const upload = await uploadPromise;

  const body = upload.postDataBuffer();
  expect(body).not.toBeNull();
  expect(body!.includes(Buffer.from('quality-report.txt'))).toBe(true);
  expect(body!.includes(Buffer.from('qa-report'))).toBe(true);
  await expect(page.getByRole('status')).toHaveText('Upload complete');
});

Do not print the multipart buffer. Real uploads can contain confidential documents. Use synthetic files and assert filename, media type, size, and response at the appropriate test layer.

7. Verify a safe custom header

Headers can prove versioning, correlation, locale, or feature context. Assert only values the client owns:

const exportPromise = page.waitForRequest(request =>
  request.url().endsWith('/api/exports') && request.method() === 'POST',
);

await page.getByRole('button', { name: 'Create export' }).click();
const exportRequest = await exportPromise;

expect(await exportRequest.headerValue('x-client-version')).toMatch(/^web-/);
expect(await exportRequest.headerValue('x-export-format')).toBe('csv');

Header lookup is case-insensitive. headers() uses lower-cased names and omits certain security-related headers. allHeaders() includes sensitive values such as cookie information, so use it only when the security contract requires it and prevent unrestricted failure logging.

Do not assert browser-generated headers such as every sec-ch-ua value in a cross-browser feature test. They can vary by browser and version. Test application-controlled headers here, and validate gateway or CDN behavior at the service boundary.

If you need to override a header, use routing rather than a request wait. The route continue with override guide explains modification. waitForRequest remains read-only.

8. Follow a server redirect request chain

Each redirect hop is a separate Request. This controlled example proves the final document request came from the expected source:

test('redirects a legacy report URL', async ({ page, context }) => {
  await context.route('https://app.example.test/legacy/report', route => {
    return route.fulfill({
      status: 302,
      headers: { location: 'https://app.example.test/reports/current' },
    });
  });
  await servePage(
    context,
    'https://app.example.test/reports/current',
    '<h1>Current report</h1><p>Report version: 6</p>',
  );

  const destinationPromise = page.waitForRequest(request =>
    request.url() === 'https://app.example.test/reports/current' &&
    request.isNavigationRequest(),
  );

  await page.goto('https://app.example.test/legacy/report');
  const destination = await destinationPromise;

  expect(destination.redirectedFrom()?.url())
    .toBe('https://app.example.test/legacy/report');
  await expect(page).toHaveURL('https://app.example.test/reports/current');
  await expect(page.getByRole('heading', { name: 'Current report' }))
    .toBeVisible();
});

Do not expect one Request to change URLs. Use redirectedFrom() and redirectedTo() to traverse the chain. Assert the final Page too, because the right destination request can still render an error.

9. Match the final debounced search request

Search-as-you-type interfaces may issue several requests. Match the final query value, not event order:

const finalSearchPromise = page.waitForRequest(request => {
  const url = new URL(request.url());
  return url.pathname === '/api/suggest' &&
    url.searchParams.get('q') === 'playwright' &&
    request.method() === 'GET';
});

await page.getByRole('searchbox').fill('playwright');
const finalSearch = await finalSearchPromise;

expect(new URL(finalSearch.url()).searchParams.get('q')).toBe('playwright');
await expect(page.getByRole('listbox')).toContainText('Playwright');

Starting the wait before fill() covers both immediate and debounced request issuance. It requires no knowledge of the application's timer length.

If the product cancels intermediate requests, do not treat those cancellations as failures unless cancellation behavior is the test subject. Validate the final query and results. To test cancellation explicitly, collect request and failure events with scoped listeners and assert the intended sequence.

Avoid a predicate that increments a counter and accepts the third call. Prefetching, rendering changes, or a different debounce strategy can alter count while leaving behavior correct.

10. Wait for two concurrent business requests

One action can intentionally issue independent requests. Start both narrowly matched waits before the action:

const summaryPromise = page.waitForRequest(request =>
  request.url().endsWith('/api/release/42/summary') &&
  request.method() === 'GET',
);
const risksPromise = page.waitForRequest(request =>
  request.url().endsWith('/api/release/42/risks') &&
  request.method() === 'GET',
);

await page.getByRole('button', { name: 'Load release' }).click();
const [summary, risks] = await Promise.all([
  summaryPromise,
  risksPromise,
]);

expect(summary.resourceType()).toBe('fetch');
expect(risks.resourceType()).toBe('fetch');
await expect(page.getByRole('heading', { name: 'Release 42' })).toBeVisible();
await expect(page.getByTestId('risk-count')).toHaveText(/\d+ risks/);

The waits do not assume which service responds first. Separate path identity represents the two requirements. If status codes matter, create matching response promises before the trigger or assert the final UI state.

For a variable number of calls, use a managed page.on('request') listener, remove it after a stable outcome, and assert the collection. A fixed group of one-shot waits is clearest when the expected set is known.

Do not leave a third speculative promise unawaited. It can time out later and produce an unhandled rejection.

11. Capture a popup's initial request at context scope

The popup Page event becomes available only after its initial response starts loading. Capture that first document request from the BrowserContext before the opening action:

test('observes the initial request for a report popup', async ({
  page,
  context,
}) => {
  await servePage(
    context,
    'https://reports.example.test/summary/42',
    '<h1>Release summary 42</h1>',
  );
  await page.setContent(`
    <a target="_blank" href="https://reports.example.test/summary/42">
      Open release summary
    </a>
  `);

  const initialRequestPromise = context.waitForEvent('request', {
    predicate: request =>
      request.url() === 'https://reports.example.test/summary/42' &&
      request.isNavigationRequest(),
  });
  const popupPromise = page.waitForEvent('popup');

  await page.getByRole('link', { name: 'Open release summary' }).click();
  const [initialRequest, popup] = await Promise.all([
    initialRequestPromise,
    popupPromise,
  ]);

  expect(initialRequest.resourceType()).toBe('document');
  await expect(popup.getByRole('heading', { name: 'Release summary 42' }))
    .toBeVisible();
});

For API calls made later by the popup, use popup.waitForRequest(). Do not wait on the opener Page for another Page's traffic. The popup and new tab examples cover page capture and cleanup in detail.

12. Review Playwright waitForRequest examples and helpers

A small helper can standardize JSON mutation identity while leaving the trigger and payload assertion visible:

import type { Page, Request } from '@playwright/test';

function waitForMutation(
  page: Page,
  pathname: string,
  method: 'POST' | 'PUT' | 'PATCH' | 'DELETE',
): Promise<Request> {
  return page.waitForRequest(request => {
    const url = new URL(request.url());
    return url.pathname === pathname &&
      request.method() === method &&
      (request.headers()['content-type'] ?? '').includes('application/json');
  });
}
const archivePromise = waitForMutation(page, '/api/projects/9', 'DELETE');
await page.getByRole('button', { name: 'Archive project' }).click();
const archive = await archivePromise;
expect(archive.postDataJSON()).toMatchObject({ reason: 'duplicate' });

Review each example for five properties: the wait begins before its trigger, matcher identity is business-specific, parsing matches content type, sensitive data stays out of diagnostics, and completion is asserted at the right layer.

Also review negative behavior. If invalid form input should prevent a request, attach a short-lived request listener before submitting, assert the validation message, remove the listener, and verify that the matching collection stayed empty. Do not express a no-request requirement by awaiting waitForRequest() until it times out. A timeout makes the test slow and reports an incidental waiting failure instead of the expected validation behavior. A scoped listener plus an observable UI endpoint gives the operation a natural finish.

Keep test ownership clear when routing is involved. A request-shape test may fulfill the backend response because serialization is its focus. A broader integration test may use a real test service and assert status plus UI state. Do not mock a dependency and then claim that the example verified its production authorization, persistence, or schema validation. Record the boundary in the test name so reviewers know what evidence the request assertion provides.

Finally, run network-sensitive examples across the browser projects the product supports. Browser-generated headers and resource classification can differ, but application-controlled URL, method, query, and JSON contracts should remain stable. If a matcher depends on engine-specific behavior, isolate it in the relevant project and explain why rather than weakening every assertion.

Do not combine clicking, request waiting, response mocking, payload assertions, and retries into one universal helper. That makes failures hard to attribute and prevents tests from expressing different outcomes. Helpers should reduce repeated mechanics without hiding causality.

For direct service coverage without UI, move the case to Playwright APIRequestContext examples. Keep waitForRequest where browser behavior, form serialization, client headers, routing, or UI-to-API wiring is the subject.

Interview Questions and Answers

Q: Show the reliable event sequence for waitForRequest.

Create the request promise first, perform exactly one action that can issue the request, then await the promise. After capture, assert the Request fields and the final response or UI state required by the scenario.

Q: How do you test query parameters without depending on order?

I construct a URL from request.url() and assert values through searchParams. I match path and method in the predicate, then keep complete parameter assertions outside it for clearer failure messages.

Q: How do you identify GraphQL requests?

I match the GraphQL endpoint and POST method, parse the body, and select the expected operationName. Then I assert relevant variables. URL alone is too broad when many operations share one endpoint.

Q: How do you test a debounced autosave?

I start a narrowly matched request wait before editing and await the request with the final expected payload. I do not copy the debounce duration into a sleep. The actual event is the synchronization signal.

Q: Can you call postDataJSON() for multipart data?

No. I inspect content type and use postDataBuffer() or safe textual markers for a controlled synthetic upload. Detailed multipart validation often belongs in an API-level test, and real file content should not be logged.

Q: How do you validate two concurrent requests?

I create two promises with distinct business matchers before one trigger and await them with Promise.all(). I do not depend on emission or response order. Final UI assertions prove the combined result.

Q: How do you observe the first request of a popup?

I register a BrowserContext request event wait before the opening action, alongside the popup event wait. The popup Page is surfaced only after its initial response starts, so Page-level observation is too late for that first navigation.

Q: What should a reusable request helper return?

It should return the typed Playwright Request. The caller should own the trigger and business assertions. This preserves event ordering, readable intent, and useful failures.

Common Mistakes

  • Starting the request wait after the click, fill, or navigation.
  • Matching a path substring without method, origin, or business identity.
  • Comparing raw query strings and failing on harmless parameter order.
  • Matching GraphQL only by /graphql.
  • Using sleeps for debounce or autosave timing.
  • Parsing multipart or arbitrary text with postDataJSON().
  • Printing complete upload buffers, cookies, tokens, or personal payloads.
  • Treating request issuance as proof of server success.
  • Selecting duplicate requests by count instead of content.
  • Assuming concurrent requests arrive in a fixed order.
  • Waiting on the opener Page for popup traffic.
  • Leaving request promises or listeners unconsumed.
  • Building a helper that hides the trigger and catches timeouts.

Conclusion

The strongest Playwright waitForRequest examples match business identity and preserve event order. Start listening before the trigger, parse URL or body according to its format, keep sensitive values out of logs, and assert response or UI completion when the scenario owns it.

Choose one flaky request test and remove its sleep or broad substring matcher. Replace it with an event-first predicate for the exact path, method, and payload value, then verify the visible outcome that matters to the user.

Interview Questions and Answers

Write the event-first waitForRequest pattern.

I assign `page.waitForRequest()` to a promise before interacting. I trigger exactly one user action, await the Request, and assert URL, method, or payload. I then verify response or UI completion if the scenario owns it.

How would you test a filtered search request?

I match the search path and GET method, then construct a URL from the returned Request. I assert decoded query, filter, and pagination values through `searchParams`. I also verify the result or empty state when end-to-end wiring is in scope.

How would you distinguish GraphQL operations?

I parse the POST body and match `operationName` in addition to the shared `/graphql` endpoint. I assert the variables that represent the user action. I avoid coupling the test to generated query formatting or persisted hashes unless those are the contract.

How do you handle an autosave that sends intermediate requests?

I match the final expected payload rather than selecting an event by count or delay. The promise begins before editing, so it covers immediate and debounced behavior. The saved indicator provides completion evidence.

What should a file upload request test assert?

I use a synthetic file and match endpoint, POST method, and multipart content type. I inspect only safe metadata or controlled body markers and assert the application result. I never dump real upload buffers into logs.

How do you test two parallel API calls from one click?

I start two distinct request waits before clicking and await both with `Promise.all()`. Matchers use their business paths or payloads, so arrival order is irrelevant. I assert the combined rendered outcome afterward.

How would you test that invalid input sends no request?

I collect matching requests with a scoped listener, submit invalid data, and wait for the expected validation message. I remove the listener and assert the collection is empty. This is faster and more expressive than waiting for `waitForRequest()` to time out.

What makes a good reusable request helper?

It matches stable request identity and returns a typed Request. It leaves the user trigger and business assertion visible at the call site. It does not hide clicks, response mocking, retries, swallowed timeouts, or secret logging.

Frequently Asked Questions

How do I test GET query parameters with waitForRequest?

Match the endpoint path and GET method, then parse `request.url()` with the standard `URL` class. Assert decoded values through `searchParams` so harmless query ordering does not break the test.

How do I test a JSON POST body in Playwright?

Start a request wait before the submit action and match URL plus POST. After capture, verify content type and use `postDataJSON()` with `toEqual()` for the whole contract or `toMatchObject()` for owned fields.

How do I wait for a GraphQL request in Playwright?

Match the GraphQL endpoint and POST method, parse the request body, and select the expected `operationName`. Assert the relevant variables after capture because many operations share the same URL.

How do I test a debounced autosave without waitForTimeout?

Create the request promise before editing and match the expected endpoint, method, and final payload. Await the actual request event and then assert the saved UI state.

Can postDataJSON parse a multipart upload?

No. Verify multipart content type and use `postDataBuffer()` or safe body markers with a synthetic file. Detailed multipart schema validation is often clearer in a direct API test.

How do I wait for multiple requests after one action?

Create one narrowly matched promise for each known business request before the action, then await them with `Promise.all()`. Do not depend on emission or response order.

How do I assert that no request was sent?

Use a short-lived request listener, perform the invalid action, wait for a natural UI endpoint such as a validation message, remove the listener, and assert that the matching collection is empty. Do not wait for a timeout as the primary no-request assertion.

How do I capture the first request from a popup?

Register a BrowserContext request event wait before the opening action, alongside the popup event promise. Use `popup.waitForRequest()` only for requests the new Page issues after it has been captured.

Related Guides