Cypress Reference: Selectors, Assertions, Custom Commands, Intercepting & Component Tests
Cypress runs tests in a real browser with direct DOM access — no separate driver, no WebDriver protocol. It’s highly debuggable because the test runner runs in the same browser loop as your app. The key mental model: commands are queued and executed asynchronously, but you write them synchronously — no await needed. The biggest gotcha: never assign Cypress command return values to variables; use .then() callbacks or aliases instead. Cypress also has a component testing mode that works alongside E2E tests.
1. Setup & cypress.config.ts
Install, config, base URL, env vars, and E2E vs component test structure
# npm install -D cypress
# npx cypress open # first run, creates cypress/ folder
// cypress.config.ts:
import { defineConfig } from "cypress";
export default defineConfig({
e2e: {
baseUrl: "http://localhost:3000",
specPattern: "cypress/e2e/**/*.cy.{ts,tsx,js}",
supportFile: "cypress/support/e2e.ts",
viewportWidth: 1280,
viewportHeight: 720,
video: false, // disable video in CI to save space
screenshotOnRunFailure: true,
retries: {
runMode: 2, // CI: retry failed tests up to 2 times
openMode: 0, // local: no retry (debug mode)
},
env: {
API_URL: "http://localhost:8080",
TEST_USER_EMAIL: "test@example.com",
},
},
component: {
devServer: {
framework: "react",
bundler: "vite", // or webpack
},
specPattern: "src/**/*.cy.{ts,tsx}",
},
});
// cypress/e2e/login.cy.ts:
describe("Login", () => {
beforeEach(() => {
cy.visit("/login");
});
it("logs in successfully", () => {
cy.get("[data-cy=email]").type("alice@example.com");
cy.get("[data-cy=password]").type("secret123");
cy.get("[data-cy=submit]").click();
cy.url().should("include", "/dashboard");
cy.get("h1").should("contain", "Dashboard");
});
});
2. Querying & Assertions
cy.get() selectors, best practices, should() assertions, and async command queueing
// Preferred selectors (resilience order):
cy.get("[data-cy=submit-button]") // data-cy attribute — best
cy.contains("button", "Sign in") // text content — good
cy.get("button[type=submit]") // attribute — ok
cy.get(".btn-primary") // CSS class — avoid (brittle)
cy.get("#submit") // ID — ok but brittle
// Add data-cy attributes to HTML:
// <button data-cy="submit-button">Sign in</button>
// Assertions with .should():
cy.get("[data-cy=error]").should("be.visible");
cy.get("[data-cy=error]").should("contain.text", "Invalid email");
cy.get("[data-cy=submit]").should("be.disabled");
cy.get("[data-cy=checkbox]").should("be.checked");
cy.url().should("eq", "http://localhost:3000/dashboard");
cy.url().should("include", "/dashboard");
// Chain multiple assertions:
cy.get("[data-cy=user-name]")
.should("be.visible")
.and("contain.text", "Alice")
.and("have.class", "font-bold");
// IMPORTANT: Never assign Cypress commands to variables!
// WRONG:
const button = cy.get("button"); // this doesn't work as expected
button.click();
// CORRECT: chain everything:
cy.get("button").click();
// CORRECT: use .then() for dynamic values:
cy.get("[data-cy=user-id]").invoke("text").then((id) => {
cy.get(`[data-cy=delete-${id}]`).click();
});
// Wait for element:
cy.get("[data-cy=results]", { timeout: 10000 }).should("be.visible");
3. Custom Commands, Fixtures & Aliases
cy.login() custom command, fixture files, aliases to avoid redundant queries
// cypress/support/commands.ts — custom commands:
declare namespace Cypress {
interface Chainable {
login(email: string, password: string): Chainable<void>;
loginByApi(email: string, password: string): Chainable<void>;
}
}
// UI login command:
Cypress.Commands.add("login", (email, password) => {
cy.visit("/login");
cy.get("[data-cy=email]").type(email);
cy.get("[data-cy=password]").type(password);
cy.get("[data-cy=submit]").click();
cy.url().should("include", "/dashboard");
});
// API login command (faster — bypasses UI):
Cypress.Commands.add("loginByApi", (email, password) => {
cy.request("POST", "/api/auth/login", { email, password })
.then(({ body }) => {
window.localStorage.setItem("auth-token", body.token);
});
});
// Use in tests:
beforeEach(() => { cy.loginByApi("alice@example.com", "secret123"); });
// Fixtures — static test data (cypress/fixtures/user.json):
cy.fixture("user").then((user) => {
cy.get("[data-cy=name]").type(user.name);
});
// Aliases — cache queries, avoid re-selecting:
cy.get("[data-cy=submit-button]").as("submitBtn");
// Use throughout test:
cy.get("@submitBtn").click();
cy.get("@submitBtn").should("be.disabled");
// Intercept alias — wait for API call:
cy.intercept("POST", "/api/users").as("createUser");
cy.get("[data-cy=create-user]").click();
cy.wait("@createUser").its("response.statusCode").should("eq", 201);
4. Intercepting Network Requests
cy.intercept() to stub API calls, modify responses, and assert on requests
// Intercept and stub response:
cy.intercept("GET", "/api/users", {
statusCode: 200,
body: [{ id: 1, name: "Alice" }, { id: 2, name: "Bob" }],
}).as("getUsers");
cy.visit("/users");
cy.wait("@getUsers");
cy.get("[data-cy=user-list]").should("have.length", 2);
// Intercept with dynamic response:
cy.intercept("POST", "/api/users", (req) => {
req.reply({ statusCode: 201, body: { id: 42, ...req.body } });
}).as("createUser");
// Stub error:
cy.intercept("GET", "/api/users", { statusCode: 500, body: { error: "Server error" } });
cy.visit("/users");
cy.get("[data-cy=error-message]").should("be.visible");
// Passthrough (don't stub, but assert request was made):
cy.intercept("PUT", "/api/users/*").as("updateUser");
cy.get("[data-cy=save]").click();
cy.wait("@updateUser").then(({ request, response }) => {
expect(request.body.name).to.equal("Alice Smith");
expect(response.statusCode).to.equal(200);
});
// Delay a request (test loading states):
cy.intercept("GET", "/api/data", (req) => {
req.reply((res) => {
res.delay(2000);
res.send({ body: [] });
});
});
5. Component Testing & CI
Mount React components, simulate events, and run Cypress in GitHub Actions
// Component testing — mount components in isolation (src/LoginForm.cy.tsx):
import { mount } from "cypress/react";
import { LoginForm } from "./LoginForm";
it("calls onSubmit with correct data", () => {
const onSubmit = cy.stub().as("onSubmit");
mount(<LoginForm onSubmit={onSubmit} />);
cy.get("[data-cy=email]").type("alice@example.com");
cy.get("[data-cy=password]").type("secret123");
cy.get("[data-cy=submit]").click();
cy.get("@onSubmit").should("have.been.calledWith", {
email: "alice@example.com",
password: "secret123",
});
});
it("shows error for invalid email", () => {
mount(<LoginForm onSubmit={cy.stub()} />);
cy.get("[data-cy=email]").type("not-an-email");
cy.get("[data-cy=submit]").click();
cy.get("[data-cy=email-error]").should("contain", "Invalid email");
});
// GitHub Actions CI:
// - name: Cypress E2E
// uses: cypress-io/github-action@v6
// with:
// start: npm run dev
// wait-on: "http://localhost:3000"
// browser: chrome
// record: true # requires CYPRESS_RECORD_KEY secret (Cypress Cloud)
// Run locally:
// npx cypress open — interactive mode
// npx cypress run — headless (CI mode)
// npx cypress run --spec "cypress/e2e/login.cy.ts"
// npx cypress run --component — component tests only
Track Node.js and testing toolchain at ReleaseRun. Related: Playwright Reference | Vitest Reference | TypeScript Reference
🔍 Free tool: npm Package Health Checker — check Cypress and cypress-related packages for known CVEs and EOL status before upgrading.
Founded
2023 in London, UK
Contact
hello@releaserun.com