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.

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);
});
});
Related Resources
Related Articles
Ruby on Rails Cursor Rules: Rapid Backend Development
Cursor rules for Ruby on Rails covering MVC patterns, ActiveRecord, RESTful routing, Strong Parameters, background jobs, and RSpec testing.
Tailwind CSS Cursor Rules: Utility-First Styling Guide
Cursor rules for Tailwind CSS covering utility classes, responsive design, custom configurations, component patterns, and design system integration for rapid UI development.
Flutter Cursor Rules: Widgets, State Management, Performance
Flutter cursor rules for clean widgets, state management, performance, platform integration, testing, and deployment. Build maintainable, reliable mobile apps.