Cypress Cursor Rules: End-to-End Component Testing

Cursor rules for Cypress covering cy.intercept network stubbing, data-cy selectors, custom commands, retry-ability patterns, component testing with cy.mount, fixtures, and CI/CD parallelization.

June 11, 2026by PromptGenius Team
cypresscursor-rulestestinge2ecomponent-testingjavascript
Cypress Cursor Rules: End-to-End Component Testing

Overview

Cypress is the most widely adopted end-to-end testing framework for web applications, with built-in retry-ability, real-time reloading, and a unique architecture that runs directly in the browser. These cursor rules enforce data-cy selector conventions, cy.intercept network stubbing, custom command patterns, retry-ability best practices, component testing with cy.mount, fixture management, and CI/CD configuration so AI assistants generate reliable, maintainable Cypress tests.

Note:

Enforces data-cy attributes as the primary selector strategy, cy.intercept() for all API calls with route aliases, custom commands via Cypress.Commands.add(), test isolation with beforeEach, retry-ability patterns (no cy.wait ms), fixture-based test data, and CI-friendly configuration for parallel execution.

Rules Configuration

---
description: Enforces Cypress best practices including data-cy selectors, cy.intercept network stubbing, custom commands, retry-ability patterns, component testing with cy.mount, fixture management, and CI/CD configuration.
globs: **/*.cy.ts,**/*.cy.js,**/*.cy.tsx,cypress.config.ts,cypress.config.js,cypress/support/**/*
---
# Cypress Best Practices

You are an expert in Cypress, end-to-end testing, and test automation for modern web applications.
You understand selectors, network stubbing, retry-ability, custom commands, component testing, and CI/CD testing pipelines.

### Selector Strategy
- Use `data-cy` attributes as the PRIMARY selector: `cy.get('[data-cy="submit-button"]')`
- Use `cy.contains()` for text-based selection: `cy.contains('button', 'Submit')`
- Use `cy.get('input[name="email"]')` for form fields with name attributes
- NEVER use CSS class selectors (`cy.get('.btn-primary')`) — classes change with styling
- NEVER use XPath or complex CSS paths — they're fragile and unreadable
- NEVER use ID selectors unless IDs are stable and guaranteed unique
- Chain commands for scoping: `cy.get('[data-cy="nav"]').find('[data-cy="link"]')`
- Use `.first()`, `.last()`, `.eq(n)` when multiple matching elements exist
- Set `defaultCommandTimeout` in config rather than per-test `.timeout()` options

### Network Stubbing (cy.intercept)
- Stub ALL external API calls: `cy.intercept('GET', '/api/users', { fixture: 'users.json' })`
- Use route aliases: `cy.intercept('POST', '/api/login').as('loginRequest')`
- Wait for API calls: `cy.wait('@loginRequest')` before asserting on results
- Assert on request body: `cy.wait('@loginRequest').its('request.body').should('deep.equal', {...})`
- Assert on response: `cy.wait('@loginRequest').its('response.statusCode').should('eq', 200)`
- Use `cy.intercept` in `beforeEach` for consistent test state — never inside individual tests
- Stub error states: `cy.intercept('GET', '/api/data', { statusCode: 500, body: { error: 'Server error' } })`
- Use fixture files for response bodies: `{ fixture: 'users.json' }` — keep fixtures in `cypress/fixtures/`
- Use `route.continue()` to pass through real requests when needed for hybrid testing

### Retry-ability & Assertions
- NEVER use `cy.wait(ms)` with a fixed timeout — Cypress retries assertions automatically
- Chain assertions after commands: `cy.get('[data-cy="modal"]').should('be.visible')`
- Use `should('exist')` to wait for DOM elements, `should('not.exist')` for removed elements
- Assert visibility state: `should('be.visible')`, `should('be.hidden')`
- Assert on text: `should('have.text', 'Expected')`, `should('contain', 'partial match')`
- Assert on attributes: `should('have.attr', 'disabled')`, `should('have.class', 'active')`
- Assert on CSS: `should('have.css', 'color', 'rgb(255, 0, 0)')`
- Use `should('have.length', 3)` for element count assertions
- Assert on URL: `cy.url().should('include', '/dashboard')`, `cy.location('pathname').should('eq', '/login')`

### Custom Commands
- Define reusable commands in `cypress/support/commands.ts`
- Wrap common workflows: `Cypress.Commands.add('login', (email, password) => { ... })`
- Add TypeScript declarations: `declare global { namespace Cypress { interface Chainable { login(email: string, password: string): void } } }`
- Chain commands for composition: `cy.login('[email protected]', 'password').visit('/dashboard')`
- NEVER use `this` inside custom commands — use closure scope instead
- Prefer App Actions over UI login sequences for speed: `cy.loginViaApi()` sets session directly
- Document custom commands with JSDoc comments for discoverability

### Test Structure & Organization
- Use `describe` blocks grouped by feature or page: `describe('Login Page', () => { ... })`
- Use `context` for state variations: `context('when logged in', () => { ... })`
- Use `beforeEach` for consistent starting state: visit page, set up mocks
- Use `before` for one-time setup: seed database, create test user
- NEVER depend on test execution order — each test must be independently runnable
- Use `afterEach` and `after` for cleanup only when absolutely necessary
- Name tests descriptively: `it('displays error for invalid credentials')`, not `it('test login')`
- Keep tests focused on single behaviors — one assertion concept per test

### Component Testing (Cypress 10+)
- Use `cy.mount()` for component tests: `cy.mount(<Button label="Submit" />)`
- Mount with props: `cy.mount(<UserProfile user={testUser} />)`
- Test component behavior, not implementation details
- Pass event handlers as spies: `const onClick = cy.spy().as('clickHandler')`
- Assert on emitted events: `cy.get('@clickHandler').should('have.been.calledOnce')`
- Test accessibility: `cy.get('button').should('have.attr', 'aria-label', 'Close')`
- Test keyboard navigation: `cy.get('input').type('{enter}')`

### Configuration (cypress.config.ts)
- Set `e2e.baseUrl` for application URL: `baseUrl: 'http://localhost:3000'`
- Set `defaultCommandTimeout` (default 4000ms) for command retry duration
- Set `viewportWidth` and `viewportHeight` for consistent test dimensions
- Set `video: false` in local dev, `video: true` in CI for debugging
- Configure `retries: { runMode: 2, openMode: 0 }` for CI retries
- Set `experimentalModifyObstructiveThirdPartyCode: true` to suppress iframe errors
- Use `env` object for environment-specific configuration

### CI/CD Best Practices
- Always run: `cypress run` (not `cypress open`) in CI
- Use `--record --parallel` for Cypress Cloud parallelization
- Store screenshots and videos as CI artifacts for failure debugging
- Set `CYPRESS_CACHE_FOLDER` in CI to cache Cypress binary between runs
- Run component tests and e2e tests in separate CI jobs
- Wait for the dev server before running tests: `npx wait-on http://localhost:3000 && cypress run`
- NEVER skip tests with `.skip` in CI — use tags or environment variables for conditional execution

Installation

Create cypress.mdc in your project's .cursor/rules/ directory and paste the configuration above. Cursor and Windsurf both read .cursor/rules/ — Copilot users place it in .github/copilot-instructions.md instead.

npm install -D cypress

# Add to package.json
{
  "scripts": {
    "cy:open": "cypress open",
    "cy:run": "cypress run",
    "cy:run:chrome": "cypress run --browser chrome"
  }
}

# First run generates cypress.config.ts and folder structure
npx cypress open

Examples

// cypress/e2e/login.cy.ts — Login flow with custom command and API stubs
describe("Login Page", () => {
  beforeEach(() => {
    cy.intercept("POST", "/api/auth/login").as("loginRequest");
    cy.visit("/login");
  });

  it("displays error for invalid credentials", () => {
    cy.intercept("POST", "/api/auth/login", {
      statusCode: 401,
      body: { error: "Invalid email or password" },
    }).as("failedLogin");

    cy.get('[data-cy="email-input"]').type("[email protected]");
    cy.get('[data-cy="password-input"]').type("wrongpass");
    cy.get('[data-cy="submit-button"]').click();

    cy.wait("@failedLogin");
    cy.get('[data-cy="error-message"]')
      .should("be.visible")
      .and("contain", "Invalid email or password");
  });

  it("redirects to dashboard on successful login", () => {
    cy.intercept("POST", "/api/auth/login", {
      statusCode: 200,
      body: { token: "jwt-token", user: { id: "1", name: "Test User" } },
    }).as("successfulLogin");

    cy.get('[data-cy="email-input"]').type("[email protected]");
    cy.get('[data-cy="password-input"]').type("correctpass");
    cy.get('[data-cy="submit-button"]').click();

    cy.wait("@successfulLogin");
    cy.url().should("include", "/dashboard");
    cy.get('[data-cy="welcome-message"]').should("contain", "Test User");
  });

  it("validates required fields", () => {
    cy.get('[data-cy="submit-button"]').click();

    cy.get('[data-cy="email-error"]').should("contain", "Email is required");
    cy.get('[data-cy="password-error"]').should("contain", "Password is required");
    cy.get("@loginRequest").should("not.exist"); // No API call was made
  });
});
// cypress/support/commands.ts — Custom command with TypeScript types
declare global {
  namespace Cypress {
    interface Chainable {
      loginViaApi(email: string, password: string): Chainable<void>;
    }
  }
}

Cypress.Commands.add("loginViaApi", (email: string, password: string) => {
  cy.request("POST", "/api/auth/login", { email, password }).then((response) => {
    window.localStorage.setItem("token", response.body.token);
  });
});