ScreenshotNeo

BlogHow-to

How to Test Navigation in Cypress

Test clicks, redirects, URL changes, browser history, and cross-origin navigation in Cypress with reliable assertions and runnable examples.

By the ScreenshotNeo team4 October 20268 min read

A reliable Cypress navigation test starts at a known page, performs the action a user would take, and checks both where the browser landed and what the destination displays. For example:

it('navigates from the home page to actions', () => {
  cy.visit('/')
  cy.contains('type').click()
  cy.url().should('include', '/commands/actions')
  cy.get('h1').should('be.visible')
})

The URL assertion verifies the route; the page assertion helps catch a broken or incorrect destination. Cypress’s first end-to-end test guide uses this setup, action, and assertion pattern.

1. Configure a stable starting URL

Set baseUrl in Cypress configuration so tests can visit relative paths without repeating a host or port that changes by environment. For example, in cypress.config.js:

const { defineConfig } = require('cypress')

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

Then start a test from a route with cy.visit('/') or cy.visit('/products'). Cypress waits for the page’s load event. Its visit requirements include an HTML response and a successful 2xx response after redirects. See cy.visit() and Cypress best practices.

Use a selector tied to the application’s test contract where possible, such as a data-cy attribute. Assert a URL component that expresses the behavior, then verify meaningful destination content.

it('opens the edit page for a user', () => {
  cy.visit('/users/1')
  cy.get('[data-cy=edit-user]').click()

  cy.location('pathname').should('eq', '/users/1/edit')
  cy.get('h1').should('contain', 'Edit user')
})

cy.url() yields the full current URL and is an alias for the href location value. cy.location() lets you inspect individual parts such as pathname, search, and hash. Cypress retries URL assertions until they pass or time out. See cy.url() and cy.location().

Behavior under test Useful assertion
Route or redirect destination cy.location('pathname')
Query state cy.location('search')
Hash-based route or anchor cy.location('hash')
Full URL, including origin cy.url()

For a full-host comparison, derive the expected URL from the configured baseUrl when practical. For hash routing, route and query values after # belong to the hash, not the regular search string.

3. Test redirects in the browser

cy.visit() follows redirects. To test what a person sees after visiting a protected route, assert the final browser location and the destination UI:

it('redirects an unauthenticated visitor to login', () => {
  cy.visit('/admin')
  cy.location('pathname').should('eq', '/login')
  cy.get('form[data-cy=login-form]').should('be.visible')
})

This tests the browser landing page, rather than the redirect response itself. Use cy.location() or cy.url() to assert the final location.

4. Test query strings and hash routes

Assert only the URL component that matters. This keeps a route test from failing because an unrelated query parameter or host changed.

it('preserves the selected filter in the URL', () => {
  cy.visit('/products')
  cy.get('[data-cy=filter-in-stock]').click()
  cy.location('search').should('include', 'availability=in-stock')
})

it('navigates to a hash route', () => {
  cy.visit('/app')
  cy.get('[data-cy=open-settings]').click()
  cy.location('hash').should('include', '/settings')
})

If query encoding is part of the behavior, assert the encoded value or parse the search string according to the application’s contract. Hash-based routers put their route and any route query after the hash, so check hash for those values.

5. Test browser back and forward

Use cy.go('back') and cy.go('forward') to exercise browser history. The numeric forms -1 and 1 mean the same directions.

it('returns to the product list with browser back', () => {
  cy.visit('/products')
  cy.get('[data-cy=product-link]').click()
  cy.location('pathname').should('include', '/products/')

  cy.go('back')
  cy.location('pathname').should('eq', '/products')
  cy.get('[data-cy=product-list]').should('be.visible')

  cy.go('forward')
  cy.location('pathname').should('include', '/products/')
})

When history navigation triggers a full page refresh, Cypress waits for the new page load. History changes such as hash navigation that do not load a new page resolve immediately. Assert the expected route and relevant page state after the history command. See cy.go().

6. Check the HTTP redirect response separately

A browser visit answers “where does the user land?” If you need to inspect the HTTP redirect itself, use cy.request() with followRedirect: false and assert the redirect destination information.

it('returns a redirect response for the legacy route', () => {
  cy.request({
    url: '/legacy',
    followRedirect: false,
    failOnStatusCode: false,
  }).then((response) => {
    expect(response.status).to.be.oneOf([301, 302, 303, 307, 308])
    expect(response.redirectedToUrl).to.include('/new-page')
  })
})

Use the browser for user-visible landing behavior and a request for HTTP response behavior. Cypress documents redirect handling in cy.request() and its API testing guide.

7. Coordinate route changes with network requests

If navigation triggers an application request, register cy.intercept() before cy.visit() whenever app startup may issue the request. Registering the intercept after the visit can miss a request the app already started.

it('shows the order page after its data loads', () => {
  cy.intercept('GET', '/api/orders/42').as('getOrder')
  cy.visit('/orders/42')

  cy.wait('@getOrder')
  cy.location('pathname').should('eq', '/orders/42')
  cy.get('[data-cy=order-summary]').should('be.visible')
})

Observe the real response when the test needs to cover integration with the backend. Stub a response when the test is focused on how the UI handles a known response. See Cypress’s network requests guide.

8. Test transitions to another origin

For interactions with a different origin in the same test, use cy.origin() for that origin’s commands. Current Cypress guidance applies this even to origins within the same superdomain. Cypress 14.0.0 changed cross-origin behavior by stopping automatic document.domain injection by default, so check your installed version before applying version-specific setup.

it('continues through an external sign-in flow', () => {
  cy.visit('/sign-in')
  cy.get('[data-cy=external-login]').click()

  cy.origin('https://identity.example.test', () => {
    cy.get('input[name=email]').type('developer@example.test')
    cy.get('button[type=submit]').click()
  })

  cy.location('pathname').should('eq', '/account')
})

Replace the example origin and selectors with the actual identity provider and application routes. See cross-origin testing and cy.origin().

A navigation test should verify the destination that matters to the user. For links that open a new tab, consult Cypress’s recipes entry for opening links in a new tab and choose an approach that matches the behavior under test. If the goal is the destination page, a focused test can visit that destination directly and check its content; if the goal is the link contract, assert the link’s destination attribute and target behavior. Avoid treating a URL attribute check as proof that the destination page works.

10. Troubleshooting navigation tests

Symptom Likely cause Fix
cy.visit() fails on a non-2xx response The route returned an error or an unexpected response after redirects. Check the application/server response and route setup. If testing an HTTP error is intentional, use a request-level assertion that handles the expected status.
The URL assertion passes but the page is wrong The test checks location alone. Assert a meaningful element or behavior on the destination page as well.
An intercept never sees the request The app started the request before the intercept was registered. Register cy.intercept() before cy.visit() when startup can trigger it.
A query assertion fails unexpectedly The query is encoded, reordered, or represented in a hash route. Assert the relevant component and account for encoding; check hash for hash-router state.
Cross-origin commands fail Commands target another origin without the required origin context, or the setup assumes older Cypress behavior. Use cy.origin() for the other origin and confirm the Cypress version.
Back navigation does not show the expected page The test did not first create the expected history entry, or the transition does not cause a full load. Perform the navigation first, then assert the route and visible state after cy.go('back').
A link destination assertion is brittle The assertion hard-codes a host or port that varies by environment. Prefer baseUrl, relative visits, and the URL component relevant to the behavior.

11. Make navigation tests reliable and fast

  • Assert behavior, not timing. Cypress retries supported assertions such as URL checks. Prefer a state assertion over an arbitrary sleep.
  • Install network observers early. Intercepts placed before a visit can catch requests fired during app startup.
  • Keep each test’s route contract clear. Check the path, query, hash, or full URL that represents success, then check a destination element.
  • Choose real or stubbed network traffic intentionally. Real traffic covers integration but depends on the service; stubs isolate UI behavior for a known response.
  • Separate browser and HTTP questions. A browser visit checks the landing experience; a request with redirects disabled checks the redirect response.

Navigation reliability depends on stable application state, selectors, and network behavior. Avoid fixed waits as a substitute for observing the event or state the test needs. Cypress’s retrying assertions and network interception are documented in the URL and network request guides.

12. Or skip the browser setup

If you also need screenshots of the destination pages, ScreenshotNeo is a website screenshot API and MCP server for developers. A single GET request returns a PNG, JPEG, WebP, or PDF. See the 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}`);

Cookie banners, popups, and chat widgets are removed before the shot. Bot checks, blank pages, failed loads, timeouts, and cache hits are never billed; response headers report the page verdict and billing status. Its MCP server lets AI agents use take_screenshot, get_page_info, and capture_pdf. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000.

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

FAQ

Should I assert the full URL or just the path?

Assert the component that represents the behavior. Use the path for route changes, search for query state, hash for hash routing, and the full URL when the origin is part of the contract.

Does cy.visit() stop at a redirect?

No. It follows redirects; assert the location where the browser lands.

When should I use cy.request() instead?

Use it when the HTTP redirect response itself is under test. Use a browser visit when the user-visible destination is the question.

How do I verify browser back?

Create a history entry through navigation, call cy.go('back'), and assert the resulting location and page state.