Cypress
"Cypress: end-to-end/component testing, commands, intercept (network mocking), fixtures, custom commands, CI, retries"
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 linesCypress
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.interceptto 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 globalaugmentation so IDE autocompletion works everywhere. - Use
data-testidattributes 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 inopenMode(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) orcy.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
Related Skills
K6
k6: load testing, performance testing, stress testing, soak testing, thresholds, checks, scenarios, browser testing, CI integration
MSW
"MSW (Mock Service Worker): API mocking, request handlers, rest/graphql, browser/node, setupServer, network-level interception"
Pact
Pact: consumer-driven contract testing, provider verification, pact broker, API compatibility, microservice integration testing
Playwright
"Playwright: end-to-end testing, browser automation, selectors, assertions, fixtures, page objects, visual regression, CI, codegen"
Storybook Test
Storybook Test: component testing via play functions, interaction testing, accessibility checks, visual regression, test-runner, portable stories
Testing Library
"Testing Library: React/DOM testing, queries (getBy/findBy/queryBy), user events, screen, waitFor, render, accessibility-first"