Rule Writing

CLAUDE.md for Cypress Testing

AI generates Cypress tests with fragile selectors, implicit waits, and no page object pattern. Rules for data-testid, custom commands, intercept, and reliable tests.

7 min read·November 14, 2024

AI-generated Cypress tests break on every CSS change and styling refactor

data-testid selectors, cy.intercept mocking, retry-ability, and custom commands

Why Cypress Tests Need AI Rules

Cypress is the most popular E2E testing framework — and AI-generated Cypress tests are among the most fragile code AI produces. The AI uses CSS class selectors that break on styling changes, waits with cy.wait(5000) instead of proper assertions, doesn't mock API calls (making tests dependent on backend state), and writes monolithic test files instead of using the page object pattern.

Fragile E2E tests are worse than no E2E tests — they fail randomly in CI, slow down the pipeline, and erode team confidence in the test suite. Eventually someone disables them. Well-written Cypress tests are reliable, fast, and maintainable — but only with the right patterns.

These rules target Cypress 13+ with TypeScript. They cover selector strategy, API mocking, test organization, custom commands, and CI configuration.

Rule 1: data-testid Selectors, Not CSS Classes

The rule: 'Use data-testid attributes for all test selectors: cy.get("[data-testid=submit-button]"). Never use CSS classes (.btn-primary), IDs (#submit), or tag names (button) as selectors — they break on styling changes, refactors, and component library updates. Add data-testid to components: <button data-testid="submit-button">. Create a cy.getByTestId custom command for convenience.'

For the custom command: 'Cypress.Commands.add("getByTestId", (testId) => cy.get(`[data-testid=${testId}]`)). Use in tests: cy.getByTestId("submit-button").click(). This is shorter than the full attribute selector and makes refactoring easier — if the attribute name changes, update one command.'

AI generates cy.get(".btn-primary").click() — breaks the moment someone renames the class or switches component libraries. data-testid is decoupled from styling, structure, and content — it exists solely for testing and never changes unless the test needs to change.

  • data-testid on all testable elements — never CSS classes, IDs, or tag names
  • cy.getByTestId custom command — one place to maintain the selector pattern
  • data-testid is decoupled from styling — survives refactors and redesigns
  • For text content: cy.contains('Submit') is okay for labels that never change
  • Remove data-testid in production builds if desired — babel-plugin-react-remove-properties
💡 Decoupled from Styling

data-testid exists solely for testing — it never changes unless the test changes. CSS classes change with redesigns, IDs change with refactors, tag names change with component swaps. data-testid survives all of them.

Rule 2: cy.intercept for API Mocking

The rule: 'Mock all API calls with cy.intercept: cy.intercept("GET", "/api/users", { fixture: "users.json" }).as("getUsers"). Wait for the aliased request: cy.wait("@getUsers"). This makes tests deterministic — they don't depend on backend state, network speed, or database content. Use fixtures (cypress/fixtures/) for response data. Never let E2E tests hit a real API unless it's a dedicated test environment.'

For dynamic responses: 'Use cy.intercept with a function for conditional responses: cy.intercept("POST", "/api/login", (req) => { if (req.body.email === "admin@test.com") req.reply({ user: adminUser }); else req.reply(401, { error: "Unauthorized" }); }). This tests multiple scenarios without backend setup.'

AI generates tests that hit real APIs — they pass locally (where the backend is running) and fail in CI (where it's not). cy.intercept makes every test self-contained: the response is defined in the test, not in a database somewhere.

ℹ️ Tests Must Be Self-Contained

cy.intercept makes every test deterministic — responses are defined in the test, not a database. Tests pass in CI without a backend, without network, without seed data. Fixtures in cypress/fixtures/ are your test's backend.

Rule 3: Assertions, Not Waits

The rule: 'Never use cy.wait(milliseconds) for timing. Cypress automatically retries assertions until they pass or timeout. Use assertions that wait: cy.getByTestId("status").should("contain", "Success") retries until the element contains "Success" or the timeout expires. Use cy.wait("@alias") only for network request aliases — never for arbitrary delays.'

For retry-ability: 'Cypress commands that query the DOM (cy.get, cy.contains, cy.find) automatically retry. Assertions chained to them (.should, .and) retry with the command. This means cy.getByTestId("list").should("have.length", 5) waits for 5 items to appear — no timing guesswork. Set defaultCommandTimeout in cypress.config.ts for the retry window (default 4s).'

AI generates cy.wait(3000) before assertions — a brittle pattern that's either too slow (wastes 3s when the element appears in 100ms) or too fast (fails when the element takes 4s). Cypress's built-in retry-ability eliminates timing issues entirely.

  • Never cy.wait(ms) — Cypress retries assertions automatically
  • .should() retries until passing or timeout — no timing guesswork
  • cy.wait('@alias') only for network requests — never for DOM timing
  • defaultCommandTimeout: 4000 (adjustable) — retry window for assertions
  • If a test needs cy.wait(ms), the test has a design problem — fix the assertion
⚠️ Never cy.wait(ms)

cy.wait(3000) is either too slow (wastes 3s when the element appears in 100ms) or too fast (fails at 4s). Cypress retries .should() assertions automatically. If your test needs cy.wait(ms), the assertion is wrong — fix it.

Rule 4: Custom Commands for Reuse

The rule: 'Extract repeated test actions into custom commands: Cypress.Commands.add("login", (email, password) => { cy.visit("/login"); cy.getByTestId("email").type(email); cy.getByTestId("password").type(password); cy.getByTestId("submit").click(); cy.url().should("include", "/dashboard"); }). Use in tests: cy.login("admin@test.com", "password"). Define commands in cypress/support/commands.ts. Type commands in cypress/support/index.d.ts.'

For common commands: 'cy.login(email, password) for authentication. cy.getByTestId(testId) for selectors. cy.seedDatabase(fixture) for test data setup. cy.assertToast(message) for notification verification. cy.resetState() for cleanup between tests. Keep commands focused — one action per command.'

AI duplicates login flows, selector patterns, and API setup in every test. Custom commands extract the duplication into typed, reusable functions. One change to the login command updates every test that uses it.

Rule 5: Test Organization and CI

The rule: 'Organize tests by feature: cypress/e2e/auth/, cypress/e2e/dashboard/, cypress/e2e/checkout/. One spec file per user flow, not per page. Name specs as user stories: login.cy.ts, create-order.cy.ts, search-products.cy.ts. Use describe for the feature, it for specific scenarios. Use beforeEach for common setup (login, seed data). Use afterEach for cleanup if needed.'

For CI: 'Run Cypress in headless mode: npx cypress run. Use cypress-io/github-action for GitHub Actions. Record to Cypress Cloud for parallelization and failure debugging. Set video: false in CI (save storage) unless debugging failures. Use retries: { runMode: 2 } for CI retry on flaky tests — but fix the flakiness, don't just retry.'

For test isolation: 'Each test should be independent — never depend on the order of other tests. Use cy.session for efficient login caching across tests. Use API calls (cy.request) for test data setup instead of UI flows — it's faster and more reliable. Clean up test data in afterEach or use a test database that resets between runs.'

  • Feature-based: cypress/e2e/auth/, dashboard/, checkout/ — not per page
  • Spec per user flow: login.cy.ts, create-order.cy.ts — describe/it structure
  • beforeEach for setup — cy.session for login caching — cy.request for data seeding
  • CI: headless, Cypress Cloud for parallel, video: false, retries: { runMode: 2 }
  • Independent tests — no order dependency — test database resets between runs

Complete Cypress Rules Template

Consolidated rules for Cypress E2E testing.

  • data-testid selectors only — cy.getByTestId custom command — never CSS classes
  • cy.intercept for all APIs — fixtures for responses — tests are self-contained
  • Assertions retry automatically — never cy.wait(ms) — cy.wait('@alias') for network only
  • Custom commands: login, getByTestId, seedDatabase — typed in index.d.ts
  • Feature-based test organization — one spec per user flow — independent tests
  • cy.session for login caching — cy.request for fast data setup — no UI for setup
  • CI: headless, Cypress Cloud, retries: 2 — fix flakiness, don't just retry
  • TypeScript — cypress-io/github-action — eslint-plugin-cypress for lint rules