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
Frontend Framework Cursor Rules for Modern Web Development
Master cursor rules for React, Vue, Angular, Next.js, Svelte, Qwik, HTMX, and Astro. Framework-specific best practices for efficient web development workflows.
React Native Cursor Rules: Mobile Best Practices Guide
Professional React Native cursor rules guiding component architecture, TypeScript, navigation, testing with Jest and Detox, and performance optimization for cross‑platform iOS and Android apps.
Remix Cursor Rules: React Web Framework Guide
Cursor rules for Remix development covering nested routes, loader/action patterns, form handling, error boundaries, and server-side rendering with enhanced UX.