Skip to content

Cypress Reference: Selectors, Assertions, Custom Commands, Intercepting & Component Tests

Cypress runs tests in a real browser with direct DOM access — no separate driver, no WebDriver protocol. It’s highly debuggable because the test runner runs in the same browser loop as your app. The key mental model: commands are queued and executed asynchronously, but you write them synchronously — no await needed. The biggest gotcha: never assign Cypress command return values to variables; use .then() callbacks or aliases instead. Cypress also has a component testing mode that works alongside E2E tests.

1. Setup & cypress.config.ts

Install, config, base URL, env vars, and E2E vs component test structure
# npm install -D cypress
# npx cypress open   # first run, creates cypress/ folder

// cypress.config.ts:
import { defineConfig } from "cypress";

export default defineConfig({
  e2e: {
    baseUrl: "http://localhost:3000",
    specPattern: "cypress/e2e/**/*.cy.{ts,tsx,js}",
    supportFile: "cypress/support/e2e.ts",
    viewportWidth: 1280,
    viewportHeight: 720,
    video: false,          // disable video in CI to save space
    screenshotOnRunFailure: true,
    retries: {
      runMode: 2,          // CI: retry failed tests up to 2 times
      openMode: 0,         // local: no retry (debug mode)
    },
    env: {
      API_URL: "http://localhost:8080",
      TEST_USER_EMAIL: "test@example.com",
    },
  },
  component: {
    devServer: {
      framework: "react",
      bundler: "vite",     // or webpack
    },
    specPattern: "src/**/*.cy.{ts,tsx}",
  },
});

// cypress/e2e/login.cy.ts:
describe("Login", () => {
  beforeEach(() => {
    cy.visit("/login");
  });

  it("logs in successfully", () => {
    cy.get("[data-cy=email]").type("alice@example.com");
    cy.get("[data-cy=password]").type("secret123");
    cy.get("[data-cy=submit]").click();
    cy.url().should("include", "/dashboard");
    cy.get("h1").should("contain", "Dashboard");
  });
});

2. Querying & Assertions

cy.get() selectors, best practices, should() assertions, and async command queueing
// Preferred selectors (resilience order):
cy.get("[data-cy=submit-button]")     // data-cy attribute — best
cy.contains("button", "Sign in")      // text content — good
cy.get("button[type=submit]")         // attribute — ok
cy.get(".btn-primary")                // CSS class — avoid (brittle)
cy.get("#submit")                     // ID — ok but brittle

// Add data-cy attributes to HTML:
// <button data-cy="submit-button">Sign in</button>

// Assertions with .should():
cy.get("[data-cy=error]").should("be.visible");
cy.get("[data-cy=error]").should("contain.text", "Invalid email");
cy.get("[data-cy=submit]").should("be.disabled");
cy.get("[data-cy=checkbox]").should("be.checked");
cy.url().should("eq", "http://localhost:3000/dashboard");
cy.url().should("include", "/dashboard");

// Chain multiple assertions:
cy.get("[data-cy=user-name]")
  .should("be.visible")
  .and("contain.text", "Alice")
  .and("have.class", "font-bold");

// IMPORTANT: Never assign Cypress commands to variables!
// WRONG:
const button = cy.get("button");   // this doesn't work as expected
button.click();

// CORRECT: chain everything:
cy.get("button").click();

// CORRECT: use .then() for dynamic values:
cy.get("[data-cy=user-id]").invoke("text").then((id) => {
  cy.get(`[data-cy=delete-${id}]`).click();
});

// Wait for element:
cy.get("[data-cy=results]", { timeout: 10000 }).should("be.visible");

3. Custom Commands, Fixtures & Aliases

cy.login() custom command, fixture files, aliases to avoid redundant queries
// cypress/support/commands.ts — custom commands:
declare namespace Cypress {
  interface Chainable {
    login(email: string, password: string): Chainable<void>;
    loginByApi(email: string, password: string): Chainable<void>;
  }
}

// UI login command:
Cypress.Commands.add("login", (email, password) => {
  cy.visit("/login");
  cy.get("[data-cy=email]").type(email);
  cy.get("[data-cy=password]").type(password);
  cy.get("[data-cy=submit]").click();
  cy.url().should("include", "/dashboard");
});

// API login command (faster — bypasses UI):
Cypress.Commands.add("loginByApi", (email, password) => {
  cy.request("POST", "/api/auth/login", { email, password })
    .then(({ body }) => {
      window.localStorage.setItem("auth-token", body.token);
    });
});

// Use in tests:
beforeEach(() => { cy.loginByApi("alice@example.com", "secret123"); });

// Fixtures — static test data (cypress/fixtures/user.json):
cy.fixture("user").then((user) => {
  cy.get("[data-cy=name]").type(user.name);
});

// Aliases — cache queries, avoid re-selecting:
cy.get("[data-cy=submit-button]").as("submitBtn");
// Use throughout test:
cy.get("@submitBtn").click();
cy.get("@submitBtn").should("be.disabled");

// Intercept alias — wait for API call:
cy.intercept("POST", "/api/users").as("createUser");
cy.get("[data-cy=create-user]").click();
cy.wait("@createUser").its("response.statusCode").should("eq", 201);

4. Intercepting Network Requests

cy.intercept() to stub API calls, modify responses, and assert on requests
// Intercept and stub response:
cy.intercept("GET", "/api/users", {
  statusCode: 200,
  body: [{ id: 1, name: "Alice" }, { id: 2, name: "Bob" }],
}).as("getUsers");

cy.visit("/users");
cy.wait("@getUsers");
cy.get("[data-cy=user-list]").should("have.length", 2);

// Intercept with dynamic response:
cy.intercept("POST", "/api/users", (req) => {
  req.reply({ statusCode: 201, body: { id: 42, ...req.body } });
}).as("createUser");

// Stub error:
cy.intercept("GET", "/api/users", { statusCode: 500, body: { error: "Server error" } });
cy.visit("/users");
cy.get("[data-cy=error-message]").should("be.visible");

// Passthrough (don't stub, but assert request was made):
cy.intercept("PUT", "/api/users/*").as("updateUser");
cy.get("[data-cy=save]").click();
cy.wait("@updateUser").then(({ request, response }) => {
  expect(request.body.name).to.equal("Alice Smith");
  expect(response.statusCode).to.equal(200);
});

// Delay a request (test loading states):
cy.intercept("GET", "/api/data", (req) => {
  req.reply((res) => {
    res.delay(2000);
    res.send({ body: [] });
  });
});

5. Component Testing & CI

Mount React components, simulate events, and run Cypress in GitHub Actions
// Component testing — mount components in isolation (src/LoginForm.cy.tsx):
import { mount } from "cypress/react";
import { LoginForm } from "./LoginForm";

it("calls onSubmit with correct data", () => {
  const onSubmit = cy.stub().as("onSubmit");
  mount(<LoginForm onSubmit={onSubmit} />);

  cy.get("[data-cy=email]").type("alice@example.com");
  cy.get("[data-cy=password]").type("secret123");
  cy.get("[data-cy=submit]").click();

  cy.get("@onSubmit").should("have.been.calledWith", {
    email: "alice@example.com",
    password: "secret123",
  });
});

it("shows error for invalid email", () => {
  mount(<LoginForm onSubmit={cy.stub()} />);
  cy.get("[data-cy=email]").type("not-an-email");
  cy.get("[data-cy=submit]").click();
  cy.get("[data-cy=email-error]").should("contain", "Invalid email");
});

// GitHub Actions CI:
// - name: Cypress E2E
//   uses: cypress-io/github-action@v6
//   with:
//     start: npm run dev
//     wait-on: "http://localhost:3000"
//     browser: chrome
//     record: true   # requires CYPRESS_RECORD_KEY secret (Cypress Cloud)

// Run locally:
// npx cypress open            — interactive mode
// npx cypress run             — headless (CI mode)
// npx cypress run --spec "cypress/e2e/login.cy.ts"
// npx cypress run --component — component tests only

Track Node.js and testing toolchain at ReleaseRun. Related: Playwright Reference | Vitest Reference | TypeScript Reference

🔍 Free tool: npm Package Health Checker — check Cypress and cypress-related packages for known CVEs and EOL status before upgrading.

Founded

2023 in London, UK

Contact

hello@releaserun.com