ScreenshotNeo

BlogHow-to

How to Debug Page Navigation in Cypress Tests

Trace failed Cypress navigation with URL assertions, network intercepts, and command-level debugging. Find common causes and fixes, then capture the page with one ScreenshotNeo request.

By the ScreenshotNeo team4 October 202611 min read

To debug page navigation in Cypress, identify which stage failed: the initial document visit, a redirect, a client-side route change, or a request the application needs during startup. Assert on the destination with cy.location() or cy.url(), register any needed cy.intercept() before cy.visit(), and use a retrying assertion instead of a fixed sleep. Cypress waits for the page’s load event during cy.visit(); inspect its error and the Command Log before treating every failure as a router problem. Cypress documents the visit requirements and timeout behavior.

1. Start with the failing navigation stage

Run the failing test and inspect the Cypress Command Log. Find the first command that failed or timed out; later failures may only be consequences of that point.

  1. Did cy.visit() fail? Check its error, the response, redirects, content type, and whether the page fired load.
  2. Did the document load but land on the wrong address? Assert the relevant URL component and check redirect or authentication rules.
  3. Did a link or button fail to change routes? Check that the click happened, then assert the expected path or rendered destination state.
  4. Is a startup request involved? Register an intercept before visiting and wait for that request only if the route depends on it.
  5. Is the failure intermittent? Replace elapsed-time guesses with a request alias or retrying query that describes the expected state.

This sequence separates document navigation from application routing. A visit that times out waiting for load is a different problem from a page that loads successfully and then renders the wrong route.

2. Check the initial visit

cy.visit() follows redirects and requires an HTML response, a 2xx response after redirects, and an eventual load event. It can time out waiting for the load event or for assertions chained to the visit. A timeout therefore does not, by itself, prove that a client-side router is stuck. See the visit command reference.

Set baseUrl in Cypress configuration and use a relative path for the app under test. This keeps test navigation independent of repeated hard-coded origins; Cypress recommends this approach in its best practices.

// cypress.config.js
const { defineConfig } = require('cypress')

module.exports = defineConfig({
  e2e: {
    baseUrl: 'http://localhost:3000',
  },
})

// cypress/e2e/navigation.cy.js
describe('navigation', () => {
  it('loads the account page', () => {
    cy.visit('/accounts/123')
    cy.location('pathname').should('eq', '/accounts/123')
  })
})

When a visit fails, inspect the Command Log’s visit entry and the browser’s developer tools. Check whether the test server is reachable, whether the response is HTML, whether a redirect changes the destination, and whether a page resource is preventing the load event. If the visit itself succeeds, move on to asserting the final destination rather than increasing timeouts blindly.

3. Assert on the destination URL

Use the narrowest URL field that expresses the behavior. cy.location() yields properties such as pathname, search, hash, host, and href. cy.url() gives the full URL and is an alias for cy.location('href'). Location and URL are queries, so Cypress retries chained assertions while waiting for the application to reach the expected value. Refer to cy.location() and cy.url().

Assert a client-side route change

cy.get('[data-testid="open-account"]').click()
cy.location('pathname').should('eq', '/accounts/123')

Assert a redirect

cy.visit('/admin')
cy.location('pathname').should('eq', '/login')

Check query parameters or a hash route

cy.get('[data-testid="search-submit"]').click()
cy.location().should((location) => {
  expect(location.pathname).to.eq('/search')
  expect(location.search).to.include('q=cyprus')
})

// For an app whose router uses the URL fragment:
cy.location('hash').should('eq', '#/accounts/123')

Hash-based routing needs special care: a query string after the hash belongs to hash, not the browser’s search field. For example, /app/#/users/1?tab=profile has pathname /app/ and a hash containing the route and query. Cypress explains these URL parts in the location reference.

Choose the right assertion

What you need to verify Use Example
Route path only cy.location('pathname') .should('eq', '/login')
Query string cy.location('search') .should('include', 'page=2')
Hash route cy.location('hash') .should('include', '/settings')
Complete destination cy.url() or cy.location('href') .should('include', '/accounts/123')
Several URL parts together cy.location() Assert on pathname and search in one callback

Prefer an exact path assertion when the path itself is the contract. Use include when variable IDs or query ordering make a full-string equality assertion too brittle. If the visible page matters as well as the address, assert on a distinctive element too; a changed URL alone does not establish that the destination rendered correctly.

4. Capture startup requests before visiting

If the app sends a request as soon as it initializes, define the intercept before cy.visit(). The visit resolves on load, by which time startup code may already have issued the request. An intercept registered after the visit can miss it. Cypress describes this ordering in the visit documentation and shows alias-based waits in the intercept reference.

cy.intercept('GET', '/users/**').as('users')
cy.visit('/app')
cy.wait('@users')
cy.location('pathname').should('eq', '/app')
cy.get('[data-testid="user-list"]').should('be.visible')

The alias wait tells you whether the matching request and response cycle completed. You can inspect the response to distinguish a server error from a routing assertion failure:

cy.intercept('GET', '/api/session').as('session')
cy.visit('/app')

cy.wait('@session').then((interception) => {
  expect(interception.response, 'session response').to.exist
  expect(interception.response.statusCode).to.eq(200)
})

cy.location('pathname').should('eq', '/app')

If no response is expected because the request may fail at the network layer, account for that explicitly in the test rather than dereferencing a missing response. For an ordinary expected successful startup request, a missing alias match points you toward the matcher, request timing, caching, or whether the application made the request at all.

Make the matcher specific enough

Intercepts can match a URL, a method and URL, or a route matcher with fields such as hostname, path, pathname, and query. If you omit the method, the route can match any method. Use the method when the request type is part of what the test expects:

cy.intercept({
  method: 'GET',
  pathname: '/api/users',
  query: { page: '1' },
}).as('firstPageOfUsers')

cy.visit('/users')
cy.wait('@firstPageOfUsers')
  .its('response.statusCode')
  .should('eq', 200)

Route matcher string values use glob matching. For unusual URL patterns, Cypress also supports regular expressions. Review the supported matcher fields and examples in the intercept API reference.

5. Wait for conditions, not a number of milliseconds

A fixed delay such as cy.wait(2000) cannot tell whether the route has changed or the required request has finished. It can waste time on fast runs and still be too short on slow ones. Cypress identifies arbitrary time waits as an anti-pattern in its wait documentation.

Wait for the specific request when the navigation depends on network activity, then use a retrying query for the resulting page state:

cy.intercept('GET', '/api/accounts/123').as('account')
cy.get('[data-testid="open-account"]').click()
cy.wait('@account')
cy.location('pathname').should('eq', '/accounts/123')
cy.get('[data-testid="account-heading"]').should('be.visible')

The follow-up location or element assertion matters. An assertion chained directly to cy.wait('@alias') is an assertion about the yielded interception object; it does not keep retrying the application’s later rendered state. Cypress documents retry behavior in its retry-ability guide.

Not every route depends on a network request. If the app changes routes locally, assert on the URL or rendered state directly. Avoid waiting on an unrelated request just to add time to the test.

6. Pause after Cypress has run the command

Cypress queues commands and runs them after the test body enqueues them. A bare debugger written after cy.visit() in the test body can execute before the visit command. Put the breakpoint in a .then() callback reached after the command whose result you want to inspect. See Cypress debugging.

cy.visit('/app')
cy.location().then((location) => {
  debugger
  // Inspect location, window, and document in the browser devtools.
})

Or pause after a click and a route query:

cy.get('[data-testid="open-account"]').click()
cy.location('pathname').should('eq', '/accounts/123').then(() => {
  debugger
})

Open the browser developer tools before using the breakpoint. At the pause, inspect the current address, rendered DOM, console errors, and application state. The Cypress Command Log and its Routes panel also help show which intercepts were registered and which requests matched.

7. A complete navigation diagnostic test

This example checks a startup session request, follows a protected route to its destination, and verifies both the final URL and rendered page. Adapt selectors and endpoints to the application under test.

describe('protected account navigation', () => {
  it('loads the account after session initialization', () => {
    // Install the listener before startup can issue the request.
    cy.intercept('GET', '/api/session').as('session')

    cy.visit('/accounts/123')

    cy.wait('@session').then((interception) => {
      expect(interception.response, 'session response').to.exist
      expect(interception.response.statusCode).to.eq(200)
    })

    // This retries while the application settles on its route.
    cy.location('pathname').should('eq', '/accounts/123')
    cy.get('[data-testid="account-heading"]').should('be.visible')
  })

  it('redirects signed-out visitors to login', () => {
    cy.intercept('GET', '/api/session', {
      statusCode: 401,
      body: { authenticated: false },
    }).as('session')

    cy.visit('/admin')
    cy.wait('@session')
    cy.location('pathname').should('eq', '/login')
    cy.get('[data-testid="login-form"]').should('be.visible')
  })
})

The stub in the second test makes the unauthenticated response explicit. Use a stub when the test is meant to cover the app’s behavior for a controlled response; use a passive intercept when you want to observe the real service. An intercept that stubs the response does not verify the real backend.

8. Troubleshooting common navigation failures

Symptom Likely cause What to do
cy.visit() reports a non-2xx response The final response after redirects is an error, or the server is unavailable. Inspect the visit error and server logs; verify the test server and route. Do not hide an unexpected error with failOnStatusCode: false unless the test specifically needs to inspect an error page.
Visit times out waiting for load The page did not finish loading, a dependency is stalled, or the load event did not fire before the timeout. Inspect network activity and the visit failure detail. Determine whether the app is waiting on a resource or whether the server is slow. Adjust a timeout only when the expected load genuinely requires more time.
Visit rejects because the response is not HTML The URL returned an API payload, file, or another content type instead of a document. Visit the app’s HTML route. Use request-oriented commands for API checks rather than treating an API endpoint as a page.
Redirect assertion sees the wrong path The app redirected to a different destination, redirect conditions are not met, or the assertion checks the wrong URL component. Inspect cy.location() and the redirecting request or application logic. Check pathname, search, or hash according to the route design.
cy.wait('@alias') times out The intercept was registered too late, its matcher does not match, the request did not happen, or the browser served the response from cache. Register before the triggering action; verify method, host, path, and query; inspect the Command Log Routes panel and browser devtools. Cypress notes that cached responses do not reach the network layer and cannot be intercepted; see the intercept caching note.
URL assertion passes but the page looks wrong The browser address changed before the app rendered the expected view, or the route renders an error/empty state. Assert on a meaningful destination element as well as the URL. Wait for the state the user needs to see.
Test passes locally and flakes in CI A timing assumption, slower dependency, intermittent network response, or test data variation may affect navigation. Replace fixed sleeps with relevant aliases and retrying state assertions. Make data and response expectations explicit, then inspect the first failed command in CI.
debugger pauses before the page loads The breakpoint is in the test body, which enqueues Cypress commands before they execute. Move the breakpoint into a .then() callback after the command or query you need to inspect.
Cross-origin visit loads but interaction fails Cross-origin interaction requires Cypress’s origin handling. Use cy.origin() for interactions on the other origin and consult the visit documentation for its cross-origin constraints.

9. Performance and reliability practices

  • Keep waits tied to behavior. A request alias or retrying assertion gives the test a meaningful condition to wait for, rather than adding the same delay to every run.
  • Use the narrowest useful assertion. Checking the path is easier to diagnose than comparing an entire URL when only the route matters.
  • Separate network and UI failures. Assert on an important request response when it is part of the flow, then assert on the destination UI. This makes the failing layer clearer.
  • Keep intercepts scoped. Match the expected method and endpoint where possible, so unrelated traffic does not satisfy the wait.
  • Account for caching. A browser cache hit may bypass the network layer and therefore evade an intercept. Check the browser’s developer tools and Cypress’s intercept guidance before assuming the app never made the request.
  • Use timeout changes deliberately. A larger timeout can accommodate a legitimately slower operation, but it does not fix a wrong matcher, a missing request, or a route assertion that can never become true.
  • Prefer controlled responses for focused route tests. Stubbing a response can make a redirect scenario deterministic; retain separate coverage for behavior that depends on the real service.

10. Or skip the browser setup

For a visual check of the destination, ScreenshotNeo captures a page with one API request. It is a website screenshot API and MCP server for developers. This does not replace Cypress assertions about application behavior; it gives you a captured image or PDF of the page.

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

See the ScreenshotNeo API documentation for request options. Cookie banners, popups, and chat widgets are removed before the shot. Bot checks, blank pages, and failed loads are never billed. An MCP server lets AI agents take screenshots. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000.

Learn about ScreenshotNeo or sign up for 1,000 free screenshots a month with no card.

FAQ

Should I use cy.url() or cy.location()?

Use cy.url() for the complete current URL. Use cy.location('pathname'), search, or hash when only one component matters. Both support retrying assertions.

Does cy.visit() wait for my single-page app’s data to finish loading?

It waits for the document’s load event, not every later application request or render. Wait for a relevant request when appropriate, then assert on the route or rendered state.

Why did an intercept miss a request that appears in the browser?

The request may have started before the intercept was registered, failed to match its route pattern, or been served from cache without reaching the network layer. Check the intercept order, matcher, and browser network details.

Can I increase the timeout to fix a navigation failure?

Only when the operation is expected to take longer under the test conditions. A timeout increase cannot correct a wrong route, a request that never fires, or a matcher that excludes the request.

Can a screenshot prove that a redirect works?

A screenshot shows the rendered page at capture time. Use a Cypress URL assertion to verify the redirect behavior itself.