Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

If Cypress appears to remain on the login URL, first determine whether the browser really failed to navigate or whether the test is checking the wrong thing. Cypress follows HTTP redirects automatically; your application decides the destination. Submit the form, then use a retryable cy.location() or cy.url() assertion for the route your application should display. If that assertion fails, inspect the actual pathname, query string, hash, response status, and session setup before changing the test.

What “staying on the current URL” can mean

The phrase describes several different failures:

  • The server rejected the credentials and returned the login page.
  • The server issued an HTTP redirect, but it pointed to an unexpected path.
  • The login succeeded, but a client-side router has not navigated yet.
  • The test restored a cached session and is now on a blank page because the application was not revisited.
  • The flow moved to an external identity-provider origin that Cypress cannot interact with from the original origin.

Do not begin by adding a fixed cy.wait() or by forcing a URL. Capture what actually happened. A URL assertion retries until it passes or the command times out, so it also provides synchronization for an asynchronous route change.

Start with a retryable assertion for the intended route

Replace /dashboard with the route that represents a successful login in your application:

describe('login', () => {
  it('navigates to the dashboard after valid credentials', () => {
    cy.visit('/login')

    cy.get('[name=email]').type(Cypress.env('userEmail'))
    cy.get('[name=password]').type(Cypress.env('userPassword'), { log: false })
    cy.get('form').submit()

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

cy.url() yields the complete URL and is an alias for cy.location('href'). Use it when the query string or hash matters:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
cy.url().should('include', '/dashboard')
cy.location('search').should('eq', '?welcome=1')
cy.location('hash').should('eq', '#overview')

For precise diagnostics, assert each component separately. A failure showing /login?error=invalid is more useful than a generic “expected URL” message.

Identify which layer performs the navigation

Navigation layer What to inspect Cypress approach
HTTP redirect Status code and Location-derived destination cy.request() with redirects disabled, then inspect redirectedToUrl
Client-side router Browser pathname, search, and hash after JavaScript runs cy.location() or cy.url() with a chained assertion
Cached authentication Whether setup logged in and whether a page was loaded afterward Assert inside cy.session(), then visit the app route
External identity provider Whether the origin changed Use cy.origin() only for the different origin

Check an HTTP redirect with cy.request()

Use this branch when the server should redirect after a login request. Disabling redirect following exposes the first response instead of hiding it behind the final page:

cy.request({
  method: 'POST',
  url: '/login',
  form: true,
  followRedirect: false,
  body: {
    email: Cypress.env('userEmail'),
    password: Cypress.env('userPassword')
  }
}).then((response) => {
  expect(response.status).to.be.oneOf([301, 302, 303, 307, 308])
  expect(response.redirectedToUrl).to.eq('/dashboard')
})

A non-redirect status tells you to investigate the application response instead: validation errors, an expired account, a CSRF failure, or another server-side condition may be returning the login page. If the destination is correct in this request but the browser remains on the old route, the problem is in the browser flow or client-side routing rather than the server redirect.

Check a client-side route change

Single-page applications often return a successful document and navigate with router code. In that case there may be no HTTP 3xx response to inspect. Submit the form and assert the browser location:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
cy.get('form').submit()
cy.location('pathname').should('eq', '/dashboard')
cy.location('search').should('not.contain', 'error')

When this fails, record the actual URL from the Cypress error and inspect the application console and network requests. Confirm that the submit handler runs, that the login response is successful, and that the router receives the expected success state. Do not assume the requested path is the final path; the application may intentionally send different users to different destinations.

Handle a cross-origin provider only when the flow leaves your origin

If login redirects to an identity-provider domain and the test must fill or submit controls there, wrap those commands in cy.origin(). The command is conditional: do not add it to a same-origin login merely because a redirect exists.

cy.origin('https://idp.example.com', { args: {
  email: Cypress.env('userEmail'),
  password: Cypress.env('userPassword')
} }, ({ email, password }) => {
  cy.get('[name=email]').type(email)
  cy.get('[name=password]').type(password, { log: false })
  cy.get('form').submit()
})

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

Use the real provider origin in your test configuration. If the provider returns to your application, place the final application assertion after control returns to that origin.

Fix tests that use cy.session()

cy.session() caches cookies, local storage, and session storage so later tests do not repeat the login. The login flow and a successful-login assertion belong inside the session setup callback. A later URL check cannot prove that the cached setup originally authenticated correctly.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
function login() {
  cy.session('standard-user', () => {
    cy.visit('/login')
    cy.get('[name=email]').type(Cypress.env('userEmail'))
    cy.get('[name=password]').type(Cypress.env('userPassword'), { log: false })
    cy.get('form').submit()
    cy.location('pathname').should('eq', '/dashboard')
  })
}

describe('account page', () => {
  beforeEach(() => {
    login()
    cy.visit('/account')
  })

  it('shows the account', () => {
    cy.location('pathname').should('eq', '/account')
  })
})

After a session is restored, Cypress test isolation can leave the page blank. Visit the route needed by the next test after cy.session(); restoring authentication state is not the same as loading a document.

A reliable diagnostic workflow

  1. Run the login without an immediate URL assertion. Keep the form submission and let Cypress report the resulting page.
  2. Read the complete URL. Note pathname, search, and hash. A query such as ?error=1 can explain why the path looks unchanged.
  3. Confirm the submit event. Use the selector for the actual form or submit button, and ensure the button is not disabled or covered by another element.
  4. Inspect the response layer. Use cy.request() for a server redirect, or browser location assertions for a client-side router.
  5. Verify authentication state. Check that the response sets the cookie or storage value your application expects, without printing secrets to the Cypress log.
  6. Revisit the destination after session restoration. This is required when cy.session() leaves the document blank.
  7. Only then increase command timeouts. A longer timeout helps a slow, valid transition; it cannot repair rejected credentials or a wrong destination.

Common failures and targeted fixes

The test still shows /login

First check the response body or visible validation message. The application may have rejected the credentials, required an unhandled verification step, or returned a CSRF error. Use the same credentials and environment in a real browser, then compare the request headers, cookies, and payload with the Cypress request.

The URL changes briefly and then returns to login

This usually indicates that the protected route did not accept the session. Check cookie domain, path, secure and same-site attributes, clock skew, and whether the test is using the same host that issued the cookie. Also verify that a route guard is not redirecting an authenticated-but-unauthorized user.

The assertion times out even though the page looks correct

Print the exact location components and compare them with the assertion. The application may use a trailing slash, a locale prefix, a hash route, or a query parameter. Prefer an exact pathname assertion when only the path matters, and assert search parameters separately when they are part of the contract.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

The session test opens a blank page

Visit the application route after cy.session(). Keep the successful-login assertion in the setup callback so an invalid cached session cannot be mistaken for a navigation problem.

The login page is on another origin

Use cy.origin() for commands that interact with that external origin. If the test only needs to verify that your application eventually receives the callback, keep the provider interaction outside the same-origin command chain and assert the final application URL after the callback.

A fixed wait appears to solve it locally

Replace the wait with a condition Cypress can retry: a URL assertion, a page element that proves the authenticated view loaded, or a network-dependent assertion. Fixed delays make the test slower and still fail when the environment is slower than the chosen delay.

Timeouts, reliability, and maintainability

Use the smallest assertion that expresses the application contract. A route assertion waits for navigation without coupling the test to visual details; a page-element assertion can then prove that the destination finished rendering. Keep credentials in Cypress environment variables, mark password typing with { log: false }, and avoid logging tokens or cookies.

Free tools Windows power users keep installed

One-click scans. No signup required.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

For CI reliability, make the destination deterministic for the test account, seed or reset the account state, and use a dedicated test identity rather than a user whose permissions change during the run. If the application intentionally redirects administrators and standard users to different pages, encode that rule in separate tests instead of asserting one universal URL.

Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Or skip the browser setup

If you need a clean visual capture of a page involved in a login or redirect investigation, ScreenshotNeo can return a screenshot through one API call. Its capture flow accepts cookie and consent banners, removes more than 60 known consent platforms plus newsletter popups and chat widgets, and lets each cleanup step be disabled. Only clean shots are billed: bot checks or CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed, and the response identifies the result with X-Page-Verdict and X-Billed headers.

For API details and all capture parameters, see the ScreenshotNeo documentation.

cURL

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

Python

import requests

r = requests.get(
    "https://api.screenshotneo.com/v1/shot",
    params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"},
    timeout=90,
)
r.raise_for_status()
open("shot.webp", "wb").write(r.content)

Node.js

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 failed: ${res.status}`);
const fs = await import('node:fs/promises');
await fs.writeFile('shot.webp', Buffer.from(await res.arrayBuffer()));

Replace the example URL with the page you are authorized to capture. ScreenshotNeo also provides an MCP server with take_screenshot, get_page_info, and capture_pdf tools for Claude, Cursor, and other MCP clients. Its Free plan includes 1,000 screenshots per month without a card; paid plans start at $5 for 3,000 shots. Create a free ScreenshotNeo account to begin.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

FAQ

Should I assert the URL or a dashboard element?

Use both when they answer different questions: the URL proves routing, while a destination element proves that rendering and authorization completed. Keep the URL assertion focused on the route contract.

Can I test the redirect without logging into the browser?

Yes. A direct cy.request() with redirect following disabled can verify the server response independently. It does not replace a browser test of client-side routing or cookie behavior.

What if valid users have several legitimate destinations?

Model the rule explicitly. Use separate test identities or assert the allowed set of paths, then verify the role-specific page content so a broad URL match cannot hide an authorization error.

Frequently Asked Questions

Should I assert the URL or a dashboard element?

Use both when they answer different questions: the URL proves routing, while a destination element proves that rendering and authorization completed.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Can I test the redirect without logging into the browser?

Yes. A direct cy.request() with redirect following disabled verifies the server response independently of browser routing and cookie behavior.

What if valid users have several legitimate destinations?

Model the rule explicitly with separate identities or an allowed-path assertion, then verify role-specific content.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.