Every Cypress test starts somewhere — a URL the browser loads before you type, click, or assert anything. Chapter 1 introduced cy.visit("/"). This lesson covers the full navigation toolkit: visiting URLs with options, walking the browser history, asserting on the current location, refreshing the page, and handling redirects. By the end you'll have a typed, multi-test spec that drives a real auth-protected route the way a real user would.
cy.visit() — the entry point
cy.visit() navigates the browser to a URL. It's almost always the first command in a test:
describe("Product browsing", () => {
it("loads the products page", () => {
cy.visit("/products");
cy.get("h1").should("contain", "Products");
});
});When baseUrl is configured in cypress.config.ts, cy.visit("/products") resolves to http://localhost:3000/products. Without baseUrl, you must pass a fully-qualified URL:
cy.visit("https://example.cypress.io/products");cy.visit does more than just navigate. It waits for the page to fire the load event before continuing, then runs through DOMContentLoaded and all blocking script tags. So by the time the next command runs, the page is fully ready — no manual sleep, no waiting for "page is interactive."
For pages that genuinely take longer (heavy SSR pages, slow staging), bump the timeout:
cy.visit("/admin/reports", { timeout: 30_000 });The options second argument also accepts method (POST visits), body (form data for the visit), headers, qs (query-string params), failOnStatusCode (set false to allow a 4xx/5xx without erroring), and auth (basic-auth credentials). The Cypress API reference and the Cypress commands cheat sheet list all of them.
cy.go() — back and forward through history
Once you've navigated to two or more pages within a test, cy.go() walks the browser history exactly like the back and forward buttons:
it("returns to the search after viewing a product", () => {
cy.visit("/products");
cy.get("[data-testid='product-card']").first().click();
cy.url().should("match", /\/products\/\d+/);
cy.go("back");
cy.url().should("eq", `${Cypress.config("baseUrl")}/products`);
});Two argument styles are interchangeable:
cy.go("back"); // same as cy.go(-1)
cy.go("forward"); // same as cy.go(1)
cy.go(-2); // back two pagesUse cy.go to test browser-history-sensitive flows: "back from a confirmation page should land me on cart, not on /payment." Pure forward-only journeys never need it.
cy.url() and cy.location() — asserting where you are
cy.url() yields the full URL string. Pair it with should to assert what page the test landed on:
cy.url().should("include", "/dashboard");
cy.url().should("eq", "https://app.example.com/dashboard");
cy.url().should("match", /\/orders\/[a-f0-9]{8}/);cy.location() is the structured version — it lets you target a specific piece of the URL without parsing it yourself:
cy.location("pathname").should("eq", "/products");
cy.location("search").should("include", "category=electronics");
cy.location("hash").should("eq", "#section-2");
cy.location("port").should("eq", "3000");
cy.location("origin").should("eq", "http://localhost:3000");Prefer cy.location("pathname") over cy.url() when you only care about the path — it's resilient to changes in port, host, and query parameters that aren't relevant to the assertion. Use cy.url() when you need the full string for a regex match.
cy.reload() — refreshing the page
Tests sometimes need to verify that state survives a refresh — a localStorage cart, a logged-in session, a saved draft:
it("keeps the cart contents after refresh", () => {
cy.visit("/products");
cy.contains("Add to cart").first().click();
cy.get("[data-testid='cart-count']").should("have.text", "1");
cy.reload();
cy.get("[data-testid='cart-count']").should("have.text", "1");
});By default cy.reload() is a normal reload that may use the browser cache. Pass true to force a hard reload that bypasses the cache:
cy.reload(true);Use the hard form when you've changed a build artefact mid-test (rare) or when you suspect cache pollution is masking a bug. The default is what production users hit on F5.
Handling redirects
Cypress follows HTTP redirects automatically. cy.visit("/admin") to a route that returns a 302 to /login ends up on /login — and Cypress raises no error. Assert the final URL with cy.url:
it("redirects unauthenticated users to login", () => {
cy.visit("/admin");
cy.url().should("include", "/login");
});If you specifically want the test to fail when a redirect happens, set failOnStatusCode: false on the visit and inspect the response yourself, or use cy.request (which gives you the redirect chain) for the lower-level assertion.
A complete auth-redirect flow
Tying it all together — a single typed spec that exercises every navigation command:
describe("Auth redirect flow", () => {
it("redirects to login, then back to admin after sign-in", () => {
// 1. Hitting /admin while unauthenticated bounces to /login
cy.visit("/admin");
cy.location("pathname").should("eq", "/login");
// 2. Login form submission
cy.get("[data-testid='email']").type("admin@test.com");
cy.get("[data-testid='password']").type("Sup3rS3cret!");
cy.get("[data-testid='submit']").click();
// 3. App should redirect us back to the originally requested /admin
cy.location("pathname").should("eq", "/admin");
cy.get("h1").should("contain", "Admin Dashboard");
// 4. Refresh to confirm the session sticks
cy.reload();
cy.location("pathname").should("eq", "/admin");
// 5. Walk back through the history
cy.go("back");
cy.location("pathname").should("eq", "/login");
});
});This one test covers a guarded route, a login redirect, a session persistence check, and a back-button assertion — five different things you might otherwise write five separate specs for.
Picking the right command
⚠️ Common mistakes
- Hardcoding
http://localhost:3000in everycy.visit. SetbaseUrlonce incypress.config.tsand pass relative paths everywhere. Hardcoded URLs are exactly what breaks when staging is onhttps://staging.example.comand prod is onhttps://app.example.com— your tests should be portable across environments. - Asserting the full URL with
cy.url().should("eq", ...)in a flow that includes query strings. Tracking parameters, A/B test flags, and analytics hashes can sneak into the URL.cy.location("pathname")is the right tool when the path is what you actually care about. - Reaching for
cy.wait(2000)aftercy.visit"to let the page settle."cy.visitalready waits for the load event. If the next assertion is still flaky, the page is genuinely doing async work after load — wait on a real signal (cy.intercept+cy.wait('@alias')from chapter 4) or assert withshould, which retries automatically.
🎯 Practice task
Wire up a real navigation test against Sauce Demo. 20-25 minutes.
- In your scaffolded project (lesson 2 of chapter 1), set
baseUrl: "https://www.saucedemo.com"incypress.config.ts. - Create
cypress/e2e/navigation.cy.tswith adescribeblock called "Navigation" and three tests:- "loads the login page" —
cy.visit("/")and assertcy.location("pathname")equals/. - "redirects to inventory after login" — type
standard_user/secret_sauce, click login, assertcy.location("pathname")equals/inventory.html. - "back button returns to inventory after viewing a product" — log in, click any product, use
cy.go("back"), assert the path is back at/inventory.html.
- "loads the login page" —
- Add a fourth test that logs in, then runs
cy.reload()and asserts the session sticks (the path is still/inventory.htmland the inventory list is visible). - Trigger a deliberate redirect failure. Visit
/secret/page(a non-existent path). Sauce Demo will land you somewhere unexpected. Read the actual URL withcy.url().then(url => cy.log(url))to see exactly where the app sent you. This is the workflow for diagnosing redirect-related test breakages. - Stretch: add a test that uses
cy.location("search")to assert that adding an item to cart sets a query parameter (or doesn't — Sauce Demo uses local state, so the assertion will be thatsearchis empty""). This kind of assertion catches subtle regressions where developers accidentally start syncing state through the URL.
The next lesson goes deeper into selectors — get, find, contains, within, and how to compose them — the commands you'll use in every line of every Cypress test from here on.