ScreenshotNeo

BlogEngineering

Testing Edge Cases With Cypress Network Stubbing and App Actions

Use Cypress app actions and cy.intercept() to reproduce rare API states, wait for the right request, and test what users see.

By the ScreenshotNeo team4 October 202610 min read

To test a rare API state in Cypress, register a narrowly matched cy.intercept() before the user action that sends the request, give the intercept an alias, perform the action, wait for that alias with cy.wait(), then assert on the response and the visible interface. A stub verifies how the client handles the response your test supplied; it does not prove that the production server returns that response contract.

Use stubs for deterministic edge cases such as an empty search result, a server error, an unusual payload, or a slow response. Keep meaningful tests against real server responses for critical flows so your suite also checks the client/server contract. Cypress’s own Real World App tests mostly use server responses and stub selectively for hard-to-create states. Cypress: Intercepting network requests

1. How app actions and network stubs fit together

An app action is a user-facing operation—typing in a search field, submitting a form, or selecting a filter—that causes the browser application to make an HTTP request. The test should follow the same sequence:

  1. Match the expected application request with cy.intercept().
  2. Optionally provide a controlled response, or let the request reach the server.
  3. Alias the matching route.
  4. Trigger the action through the UI.
  5. Wait for the alias, then check the request, response, and rendered state as needed.

Register the intercept before the action. Waiting for a specific request synchronizes the test on the event that causes the UI to change. It also helps diagnose whether a failure is due to a request that never happened or a response the app failed to render. Avoid replacing this synchronization with an arbitrary fixed sleep.

2. A runnable example: autocomplete with an empty result

This example assumes an app at /search with a search input, a request to /api/search?q=…, and a results region. Adapt selectors, route, and response shape to your application. The example stubs an empty list and checks the interface state users see.

describe('search edge cases', () => {
  it('shows an empty state when autocomplete returns no results', () => {
    cy.intercept('GET', '/api/search*', {
      statusCode: 200,
      headers: { 'content-type': 'application/json' },
      body: { results: [] },
    }).as('search');

    cy.visit('/search');
    cy.get('[data-cy=search]').type('no matching entry');

    cy.wait('@search').then(({ request, response }) => {
      expect(request.method).to.equal('GET');
      expect(request.url).to.include('q=no%20matching%20entry');
      expect(response.statusCode).to.equal(200);
      expect(response.body).to.deep.equal({ results: [] });
    });

    cy.get('[data-cy=empty-state]').should('be.visible');
    cy.get('[data-cy=result]').should('not.exist');
  });
});

The route matcher should identify the request that the app actually makes. If the app sends the query in a URL parameter, match the relevant path and inspect request.query when a precise query assertion matters. If typing causes several requests, make the matcher specific enough to wait for the intended one, or use a route handler that filters on the request details.

3. Stub status codes, payloads, and timing

A static response is appropriate when the route always needs the same controlled result. A route handler lets a test inspect the request and choose the response dynamically. Cypress intercepts can control response bodies, status codes, headers, and delays. See the cy.intercept() API reference for the current options and types.

Empty result, server error, and delayed response

// Empty result
cy.intercept('GET', '/api/items*', {
  statusCode: 200,
  body: { items: [] },
}).as('items');

// Server error
cy.intercept('POST', '/api/orders', {
  statusCode: 500,
  body: { error: 'temporarily_unavailable' },
}).as('createOrder');

// Slow response, useful for checking a loading indicator
cy.intercept('GET', '/api/report', {
  statusCode: 200,
  delay: 1200,
  body: { rows: [] },
}).as('report');

cy.get('[data-cy=load-report]').click();
cy.get('[data-cy=loading]').should('be.visible');
cy.wait('@report');
cy.get('[data-cy=loading]').should('not.exist');
cy.get('[data-cy=report-empty]').should('be.visible');

Choose a delay long enough to make the loading state observable but short enough to keep the suite practical. The purpose is to test the application’s response to latency, not to model a universal network speed.

Inspect a request before choosing the stub

cy.intercept('POST', '/api/checkout', (req) => {
  expect(req.body).to.have.property('quantity', 2);
  req.reply({
    statusCode: 422,
    body: { error: 'inventory_limit', available: 1 },
  });
}).as('checkout');

cy.get('[data-cy=submit-order]').click();
cy.wait('@checkout').its('response.statusCode').should('eq', 422);
cy.get('[data-cy=checkout-error]').should('contain', 'Only 1 available');

A route handler can also pass a request through with req.continue() when the test needs to observe real server traffic, or alter a response on its way back. Keep the test’s intent clear: a stubbed response tests client handling of that supplied response; a passed-through response exercises the real endpoint as well.

4. Use app actions to cover user-visible edge cases

For client behavior, drive the app as a user would and assert on the resulting interface. A test that only checks a stub’s response does not establish that the app displays it correctly.

Case Controlled response Useful UI assertion
No matching data Successful response with an empty collection Empty state is visible; result rows are absent
Validation or business rejection Relevant 4xx status and error payload Actionable error appears; invalid data is not presented as success
Service failure 5xx status and representative error body Failure message and retry path behave as intended
Slow response Successful response with a deliberate delay Loading state appears and then resolves
Partial or unusual data Payload missing optional fields or containing boundary values Fallbacks render; the view does not crash or mislead

Use payloads that represent cases your product actually needs to handle. The stub is under test control, so an invented malformed payload can test defensive UI behavior, but it says nothing about whether a server would produce that shape.

5. Keep real-server coverage for the contract

Stubs make hard-to-arrange conditions repeatable and can make tests faster. They also reduce confidence in the real client/server contract if every relevant response is stubbed. Preserve at least one meaningful critical-path test that uses real responses, with suitable test data or seeding. Real-response tests cover integration the stubs cannot, though they can take longer and depend on backend state.

Approach What it checks well Trade-off
Stubbed response Client behavior for a known response; rare states; repeatable UI assertions Does not prove the server returns that contract
Real server response Integrated client/server behavior on the exercised path May require data setup and can be slower or state-dependent

A balanced suite stubs exceptional conditions and keeps real traffic for important end-to-end flows. Cypress describes this selective approach in its Effective E2E testing guide.

6. cy.intercept() versus cy.request()

cy.intercept() observes or controls HTTP requests made by the browser application. cy.request() makes a direct API request from Cypress’s Node process. An intercept does not spy on or stub a cy.request() call, because that call is not browser app traffic.

// Drive the browser app and control the request it makes
cy.intercept('GET', '/api/profile', {
  statusCode: 200,
  body: { name: 'Ada' },
}).as('profile');
cy.visit('/profile');
cy.wait('@profile');
cy.get('[data-cy=profile-name]').should('contain', 'Ada');

// Call an endpoint directly from Cypress and inspect its response
cy.request('GET', '/api/health').then((response) => {
  expect(response.status).to.equal(200);
});

Use the first pattern to test how an app action and UI handle traffic. Use the second when the test needs to call an endpoint directly. A hybrid test can perform an action through the UI, then make a direct request to verify persisted state.

7. Route matching, aliases, and ordering

Match as narrowly as the test allows: specify the HTTP method and endpoint pattern instead of intercepting every request. Broad wildcard routes cause matching traffic to pass through intercept handling and can add overhead. See Cypress’s test performance guide.

  • Register the intercept before cy.visit() or the action if navigation itself can make the request.
  • Include the method so a GET and POST to the same path are not confused.
  • Use a specific path or route matcher; inspect query parameters when they matter.
  • Assign a descriptive alias and wait for that alias after the triggering action.
  • Remember that matching route order matters: routes are considered in reverse definition order except middleware routes, which run first. Check the API reference when combining handlers.
  • Intercepts are cleared before each test, so declare the routes each test needs.

8. Browser and network edge cases

Cached browser resources

A browser-cached resource may not produce a network request, so there may be nothing for cy.intercept() to observe on that load. Cypress 16’s native interception guide also documents that responses handled internally by Cypress are not stored in the browser HTTP cache, which can affect a later navigation. Confirm the behavior for your Cypress version and browser before relying on cache-related assertions. Native network interception

Cypress 16 native interception

Starting in Cypress 16, Chrome, Chromium, and Edge use native browser network interception for test traffic. Some observable transport details differ from older behavior; for example, browser-rejected responses are not observable in the same way, and some request or response properties may not be reported as before. Assert on the application’s error state where appropriate, and consult the version-specific guide before asserting transport metadata. This behavior is version- and browser-specific, so check the project’s installed version and browser matrix.

WebSockets

cy.intercept() does not natively stub individual WebSocket frames or messages. To test message-driven states, control the application’s callback or state boundary, coordinate messages through a server, or use a helper WebSocket client, as appropriate to the layer under test. The HTTP interception guide describes these limits.

9. Troubleshooting

Symptom Likely cause Fix
cy.wait('@alias') times out The route did not match, the request never fired, or the intercept was registered too late Register it before the action or visit; verify method, URL, query, and action behavior
The wait resolves, but the UI assertion fails The response shape does not match app expectations, or rendering is still in progress Inspect the intercepted response and assert on a stable UI condition after the wait
The intercept never sees a direct request The request came from cy.request(), not the browser app Use cy.request() assertions for direct calls, or trigger app traffic through the UI
The request is visible only sometimes Browser caching, conditional UI behavior, or a request made before route setup Set up the route earlier and account for cache behavior in the target version
An error response fails the test before app behavior is checked The test asserts transport details that differ by Cypress version/browser Check the native interception guide and assert the rendered error state
A wildcard intercept slows the suite Many unrelated requests match it Narrow the method and route to the request needed by the test
A WebSocket message is not stubbed HTTP intercepts do not control individual WebSocket frames Control the app callback, coordinate with a server, or use a helper client

10. Performance, reliability, and cost

Stubbed responses avoid dependence on a live backend for the cases they cover and are useful for repeatable, hard-to-arrange states. Keep route matchers narrow because every matching request is handled by the intercept machinery. Prefer request aliases over fixed sleeps: they wait for the event under test instead of making each run wait an arbitrary duration. Cypress notes that most stubbed responses return in less than 20 ms, but this is documentation guidance rather than a performance guarantee. Cypress network request guide

Reliability comes from matching the actual request, registering before it fires, using representative response shapes, and asserting on user-visible behavior. The main cost of over-stubbing is coverage confidence: a green test can validate a client against a response the real server never sends. Balance deterministic edge-case tests with critical real-server coverage.

11. Or skip the browser setup

If the task is to capture a page image while debugging a UI state or documenting a reproduced state, ScreenshotNeo offers a website screenshot API and MCP server. One GET request returns a screenshot or PDF; its screenshot API is separate from Cypress and does not replace network-stub tests. See the ScreenshotNeo website and API documentation.

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests

r = requests.get(
    "https://api.screenshotneo.com/v1/shot",
    params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"},
    timeout=90,
)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
await Bun.write('shot.webp', res);

Replace the target URL with the page you want to capture. For a page that requires a test session, configure the relevant cookies or headers using ScreenshotNeo’s documented options. Cookie banners, newsletter popups, and chat widgets are removed before the shot; bot checks, blank pages, and failed loads are never billed. Its MCP server lets AI agents take screenshots, and 1,000 screenshots a month are free with no card; paid plans start at $5 for 3,000.

Sign up free for 1,000 screenshots a month, with no card required.

12. FAQ

Can a stub prove the production API returns the same payload?

No. It proves the client handles the payload the test supplied. Use real-server coverage to exercise the actual contract.

Should I stub every request in an end-to-end test?

Usually not for critical flows. Keep important server-backed paths and use stubs selectively for deterministic edge cases.

Can I wait for a request without stubbing it?

Yes. Intercept the route, give it an alias, and allow it through; then wait for the alias and inspect the real response.

Why does an intercept not catch a cy.request() call?

cy.request() runs from Cypress’s Node process. Intercepts concern browser requests made by the application.

Does cy.intercept() cover every browser or Cypress version the same way?

No. Native interception behavior described for Cypress 16 applies to Chrome, Chromium, and Edge. Check the official guide for the project’s exact version and browser.

References