Skip to main content
Technology & EngineeringTesting Services354 lines

Cypress

"Cypress: end-to-end/component testing, commands, intercept (network mocking), fixtures, custom commands, CI, retries"

Quick Summary18 lines
Cypress is a JavaScript end-to-end and component testing framework that runs directly inside the browser. Unlike Selenium-based tools, Cypress operates in the same run loop as the application, giving it native access to the DOM, network layer, and application state. This architecture eliminates the need for explicit waits in most cases because commands are automatically retried until assertions pass or a timeout is reached. Cypress embraces a "batteries included" approach: time-travel debugging, automatic screenshots on failure, video recording, and a built-in test runner with a real-time browser preview are all available out of the box.

## Key Points

- Use `cy.intercept` to control the network layer for deterministic tests; avoid hitting real backends in CI.
- Prefer API-based login (`cy.request`) over UI login for non-authentication tests to save execution time.
- Keep custom commands typed with `declare global` augmentation so IDE autocompletion works everywhere.
- Use `data-testid` attributes for test selectors to decouple tests from CSS and markup structure.
- Organize tests by user workflow or feature, not by page, to reflect real usage patterns.
- Enable retries in `runMode` (CI) but disable in `openMode` (local development) so flakes are caught without slowing feedback.
- Use `cy.session()` for caching login state across tests within a spec file to speed up suites.
- Never use `cy.wait(3000)` for arbitrary delays; use assertion-based waiting (`should`) or `cy.wait("@alias")` for network calls.
- Do not mix async/await with Cypress commands; Cypress commands are enqueued and not true Promises.
- Avoid testing implementation details like internal component state; test what the user sees and interacts with.
- Do not rely on execution order across spec files; each file should be independently runnable.
- Avoid `cy.get("button.sc-aXZVg")` or auto-generated class selectors; they break on every style change.
skilldb get testing-services-skills/CypressFull skill: 354 lines
Paste into your CLAUDE.md or agent config

Cypress

Core Philosophy

Cypress is a JavaScript end-to-end and component testing framework that runs directly inside the browser. Unlike Selenium-based tools, Cypress operates in the same run loop as the application, giving it native access to the DOM, network layer, and application state. This architecture eliminates the need for explicit waits in most cases because commands are automatically retried until assertions pass or a timeout is reached. Cypress embraces a "batteries included" approach: time-travel debugging, automatic screenshots on failure, video recording, and a built-in test runner with a real-time browser preview are all available out of the box.

Setup

Installation and Configuration

// npm install -D cypress @cypress/code-coverage

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

export default defineConfig({
  e2e: {
    baseUrl: "http://localhost:3000",
    specPattern: "cypress/e2e/**/*.cy.ts",
    supportFile: "cypress/support/e2e.ts",
    viewportWidth: 1280,
    viewportHeight: 720,
    retries: {
      runMode: 2,
      openMode: 0,
    },
    video: true,
    screenshotOnRunFailure: true,
    experimentalRunAllSpecs: true,
    setupNodeEvents(on, config) {
      require("@cypress/code-coverage/task")(on, config);
      return config;
    },
  },
  component: {
    devServer: {
      framework: "react",
      bundler: "vite",
    },
    specPattern: "src/**/*.cy.tsx",
  },
  env: {
    API_URL: "http://localhost:4000",
  },
});

Project Structure

cypress/
  e2e/
    auth/
      login.cy.ts
      signup.cy.ts
    dashboard.cy.ts
  fixtures/
    users.json
    products.json
  support/
    commands.ts
    e2e.ts
    component.ts
  plugins/
    index.ts
src/
  components/
    Button.cy.tsx       # component tests live beside source

Key Techniques

Core Commands and Querying

describe("Product Catalog", () => {
  beforeEach(() => {
    cy.visit("/products");
  });

  it("displays product list and allows filtering", () => {
    // Querying with retry-ability
    cy.get("[data-testid='product-card']").should("have.length.gte", 5);

    // Interacting with form elements
    cy.get("[data-testid='search-input']").type("laptop");
    cy.get("[data-testid='category-select']").select("Electronics");
    cy.get("[data-testid='apply-filters']").click();

    // Assertions on filtered results
    cy.get("[data-testid='product-card']")
      .should("have.length", 2)
      .first()
      .within(() => {
        cy.get("h3").should("contain.text", "Laptop");
        cy.get("[data-testid='price']").should("contain.text", "$");
      });
  });

  it("navigates to product detail", () => {
    cy.contains("[data-testid='product-card']", "Wireless Mouse").click();
    cy.url().should("include", "/products/");
    cy.get("h1").should("have.text", "Wireless Mouse");
    cy.get("[data-testid='add-to-cart']").should("be.visible");
  });
});

Network Interception with cy.intercept

describe("Dashboard Data Loading", () => {
  it("displays data from API", () => {
    // Intercept and stub API calls
    cy.intercept("GET", "/api/dashboard/stats", {
      statusCode: 200,
      body: {
        totalUsers: 1250,
        activeUsers: 843,
        revenue: 52400,
      },
    }).as("getStats");

    cy.intercept("GET", "/api/dashboard/activity", {
      fixture: "activity-feed.json",
    }).as("getActivity");

    cy.visit("/dashboard");

    // Wait for specific network calls to complete
    cy.wait("@getStats");
    cy.wait("@getActivity");

    cy.get("[data-testid='total-users']").should("have.text", "1,250");
    cy.get("[data-testid='revenue']").should("have.text", "$52,400");
  });

  it("handles API errors gracefully", () => {
    cy.intercept("GET", "/api/dashboard/stats", {
      statusCode: 500,
      body: { error: "Internal Server Error" },
    }).as("getStatsFail");

    cy.visit("/dashboard");
    cy.wait("@getStatsFail");

    cy.get("[data-testid='error-banner']")
      .should("be.visible")
      .and("contain.text", "Failed to load dashboard");
    cy.get("[data-testid='retry-button']").should("be.visible");
  });

  it("spies on POST requests without stubbing", () => {
    cy.intercept("POST", "/api/events/track").as("trackEvent");

    cy.visit("/dashboard");
    cy.get("[data-testid='feature-card']").first().click();

    cy.wait("@trackEvent").then((interception) => {
      expect(interception.request.body).to.deep.include({
        event: "feature_click",
      });
    });
  });
});

Custom Commands

// cypress/support/commands.ts
declare global {
  namespace Cypress {
    interface Chainable {
      login(email: string, password: string): Chainable<void>;
      loginByApi(email: string, password: string): Chainable<void>;
      getByTestId(testId: string): Chainable<JQuery<HTMLElement>>;
      dragTo(
        subject: JQuery<HTMLElement>,
        target: string
      ): Chainable<void>;
    }
  }
}

// Fast API-based login (skip the UI for non-auth tests)
Cypress.Commands.add("loginByApi", (email: string, password: string) => {
  cy.request({
    method: "POST",
    url: "/api/auth/login",
    body: { email, password },
  }).then((response) => {
    window.localStorage.setItem("auth_token", response.body.token);
  });
});

// UI-based login (use only in auth-specific tests)
Cypress.Commands.add("login", (email: string, password: string) => {
  cy.visit("/login");
  cy.get("[data-testid='email-input']").type(email);
  cy.get("[data-testid='password-input']").type(password);
  cy.get("[data-testid='login-button']").click();
  cy.url().should("not.include", "/login");
});

// Shorthand for data-testid queries
Cypress.Commands.add("getByTestId", (testId: string) => {
  return cy.get(`[data-testid='${testId}']`);
});

export {};

Fixtures and Test Data

// cypress/fixtures/users.json
// [
//   { "id": 1, "name": "Alice", "role": "admin" },
//   { "id": 2, "name": "Bob", "role": "viewer" }
// ]

describe("User Management", () => {
  beforeEach(() => {
    // Load fixture data and use it for intercepts
    cy.fixture("users.json").then((users) => {
      cy.intercept("GET", "/api/users", { body: users }).as("getUsers");
    });
    cy.loginByApi("admin@test.com", "password123");
    cy.visit("/admin/users");
    cy.wait("@getUsers");
  });

  it("renders user table from fixture data", () => {
    cy.get("table tbody tr").should("have.length", 2);
    cy.contains("td", "Alice").should("exist");
    cy.contains("td", "admin").should("exist");
  });
});

Component Testing

// src/components/Counter.cy.tsx
import { Counter } from "./Counter";

describe("<Counter />", () => {
  it("increments count on button click", () => {
    cy.mount(<Counter initialCount={0} />);

    cy.get("[data-testid='count-display']").should("have.text", "0");
    cy.get("[data-testid='increment-btn']").click();
    cy.get("[data-testid='count-display']").should("have.text", "1");
  });

  it("calls onChange callback with new value", () => {
    const onChange = cy.stub().as("onChange");
    cy.mount(<Counter initialCount={5} onChange={onChange} />);

    cy.get("[data-testid='increment-btn']").click();
    cy.get("@onChange").should("have.been.calledWith", 6);
  });

  it("disables decrement at zero", () => {
    cy.mount(<Counter initialCount={0} />);
    cy.get("[data-testid='decrement-btn']").should("be.disabled");
  });
});

Retry Configuration and Stability

describe("Flaky-proof patterns", () => {
  it("waits for dynamic content correctly", () => {
    cy.visit("/notifications");

    // Assertion-based waiting (correct)
    cy.get("[data-testid='notification-list']")
      .should("exist")
      .find("li")
      .should("have.length.gte", 1);

    // Retry on specific assertions
    cy.get("[data-testid='live-count']", { timeout: 10000 }).should(
      "not.have.text",
      "Loading..."
    );
  });
});

CI Configuration

# .github/workflows/cypress.yml
name: Cypress Tests
on: [push]
jobs:
  e2e:
    runs-on: ubuntu-latest
    strategy:
      fail-fast: false
      matrix:
        containers: [1, 2, 3]
    steps:
      - uses: actions/checkout@v4
      - uses: cypress-io/github-action@v6
        with:
          build: npm run build
          start: npm run start
          wait-on: "http://localhost:3000"
          record: true
          parallel: true
          group: "E2E"
        env:
          CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

  component:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: cypress-io/github-action@v6
        with:
          component: true

Best Practices

  • Use cy.intercept to control the network layer for deterministic tests; avoid hitting real backends in CI.
  • Prefer API-based login (cy.request) over UI login for non-authentication tests to save execution time.
  • Keep custom commands typed with declare global augmentation so IDE autocompletion works everywhere.
  • Use data-testid attributes for test selectors to decouple tests from CSS and markup structure.
  • Organize tests by user workflow or feature, not by page, to reflect real usage patterns.
  • Enable retries in runMode (CI) but disable in openMode (local development) so flakes are caught without slowing feedback.
  • Use cy.session() for caching login state across tests within a spec file to speed up suites.

Anti-Patterns

  • Never use cy.wait(3000) for arbitrary delays; use assertion-based waiting (should) or cy.wait("@alias") for network calls.
  • Do not mix async/await with Cypress commands; Cypress commands are enqueued and not true Promises.
  • Avoid testing implementation details like internal component state; test what the user sees and interacts with.
  • Do not rely on execution order across spec files; each file should be independently runnable.
  • Avoid cy.get("button.sc-aXZVg") or auto-generated class selectors; they break on every style change.
  • Do not disable test isolation (testIsolation: false) to share state between tests; it creates hidden dependencies and ordering bugs.

Install this skill directly: skilldb add testing-services-skills

Get CLI access →