Cypress is usually chosen for a specific reason: fast feedback while testing a web app in a real browser with a test API that feels close to the browser itself. That same design also creates some constraints, especially around cross-browser coverage, multi-tab flows, and how you structure waits and assertions. This cheatsheet focuses on the parts teams use most often, the parts that fail in practice, and the decisions that matter when you are writing or maintaining Cypress tests.

If you want a broader comparison of code-based and platform-based approaches, the Cypress ecosystem also sits alongside tools such as Playwright, Selenium, and newer AI-assisted testing platforms. For teams that need AI-assisted test creation, cross-browser execution, and more end-to-end workflow management than Cypress code typically provides, Endtest, an agentic AI test automation platform, is one relevant alternative to evaluate. This article stays focused on Cypress itself, with only a brief note where platform alternatives fit.

Cypress at a glance

Cypress is a JavaScript end-to-end testing framework for browser applications. It runs in the browser context, gives you a command queue, automatic retrying for many assertions, and built-in tooling for debugging, screenshots, videos, and network interception. The official documentation is the best source for exact API behavior and release changes: Cypress docs.

When Cypress fits well

  • You are testing a web UI built with React, Vue, Angular, or similar frameworks.
  • Your team wants tests written in JavaScript or TypeScript.
  • Most of your value comes from rapid authoring, readable tests, and strong local debugging.
  • Your application is mainly single-tab and browser-based.

Where Cypress is less comfortable

  • Heavy multi-tab, multi-window, or cross-origin user journeys.
  • Very broad cross-browser execution requirements, especially if your plan depends on many engine combinations.
  • Teams that want a single platform for test creation, execution, and maintenance with minimal framework code.

A practical way to think about Cypress is this: it is strongest when your app and your testing style both stay inside the browser and inside a JavaScript-first workflow.

Installation and project setup

A minimal Cypress setup usually starts with a Node project and a test runner configured for your app.

npm install -D cypress
npx cypress open

If you prefer TypeScript, Cypress supports it through the project config and your test files. Most teams keep spec files in cypress/e2e and reusable helpers in cypress/support.

A typical cypress.config.ts looks like this:

import { defineConfig } from 'cypress'

export default defineConfig({ e2e: { baseUrl: ‘http://localhost:3000’, supportFile: ‘cypress/support/e2e.ts’, }, })

Common setup choices

  • baseUrl: reduces repetitive host names in cy.visit().
  • supportFile: central place for custom commands and shared setup.
  • environment variables: useful for credentials, URLs, feature flags, and API tokens.
  • test data strategy: decide early whether tests seed data through the UI, APIs, or fixtures.

A useful rule

If you need to click through the same setup steps in every test, move that work into beforeEach() only if the setup is stable and cheap. If setup is expensive or flaky, consider using API calls, fixtures, or backend reset hooks instead.

Core Cypress commands

These are the commands that show up in most suites.

typescript cy.visit(‘/login’) cy.get(‘[data-cy=email]’).type(‘alice@example.com’) cy.get(‘[data-cy=password]’).type(‘secret’) cy.contains(‘Sign in’).click() cy.url().should(‘include’, ‘/dashboard’)

Frequent commands

  • cy.visit(url) opens a page.
  • cy.get(selector) finds elements.
  • cy.contains(text) finds elements by visible text.
  • cy.click() clicks an element.
  • cy.type() types into inputs.
  • cy.clear() clears an input.
  • cy.should() adds assertions with retries.
  • cy.then() is useful for non-retry logic, transformations, or one-off callbacks.
  • cy.intercept() stubs or observes network traffic.
  • cy.wait() waits for aliases or explicit time, though explicit time is usually a smell.

Minimal command style

Prefer readable chains over deep nesting. Cypress already queues commands, so extra nesting often makes tests harder to reason about.

Selectors: the most important design choice

Selector quality often decides whether a Cypress suite is pleasant or fragile. The best long-term option is usually a stable test attribute, such as data-cy, data-testid, or similar. Avoid selectors that depend on styling classes or brittle DOM structure unless there is no alternative.

Good selector examples

typescript cy.get(‘[data-cy=save-button]’).click() cy.get(‘[data-cy=search-input]’).type(‘laptop’) cy.contains(‘button’, ‘Save’).click()

Less stable selectors

typescript cy.get(‘.btn.primary.large’) cy.get(‘div > header > nav > ul > li:nth-child(2) > button’)

Practical selector guidance

  • Use data-* attributes for stable application-owned hooks.
  • Use text selectors for user-visible actions when the label is unlikely to change.
  • Avoid index-based selectors unless the list is truly fixed.
  • If content is localized, text selectors can become fragile. In that case, a test hook is often better than visible text.

Selector strategy is not only about convenience, it is part of your test design. Stable hooks reduce maintenance more than almost any other change.

Assertions: what Cypress is good at

Cypress assertions are usually written with .should() and benefit from automatic retrying. That makes them useful for UI states that appear asynchronously.

typescript cy.get(‘[data-cy=toast]’).should(‘contain’, ‘Saved’) cy.get(‘[data-cy=submit]’).should(‘be.disabled’) cy.url().should(‘include’, ‘/checkout’) cy.get(‘[data-cy=items]’).should(‘have.length’, 3)

Common Cypress assertions

  • have.text
  • contain
  • be.visible
  • be.disabled
  • exist
  • not.exist
  • have.length
  • include
  • eq

Assertion examples

typescript cy.get(‘[data-cy=price]’).should(‘have.text’, ‘$49.00’) cy.get(‘[data-cy=error]’).should(‘not.exist’) cy.get(‘[data-cy=avatar]’).should(‘be.visible’)

Better assertion habits

  • Assert the user-visible outcome, not only implementation details.
  • Prefer one assertion per intent. A test with too many assertions becomes difficult to diagnose.
  • Use .should() where retrying helps, and .then() when you need synchronous inspection of a yielded subject.

A common failure mode

Tests sometimes pass locally but fail in CI because they assert too early. The fix is usually not a larger timeout first. It is more often to wait on the correct condition, such as a network alias, a visible element, or a final URL.

Waiting without making tests slow

Cypress retries many queries and assertions automatically, so the best waiting strategy is usually to assert on state rather than sleep. Explicit time-based waiting is usually the last resort.

Prefer these patterns

typescript cy.get(‘[data-cy=spinner]’).should(‘not.exist’) cy.get(‘[data-cy=success]’).should(‘be.visible’) cy.url().should(‘include’, ‘/done’)

Use explicit waits sparingly

typescript cy.wait(1000)

That pattern is simple, but it often creates slow and flaky tests because the real condition may happen sooner or later than the fixed delay.

Better than arbitrary waits

  • Wait for a network call alias.
  • Wait for a spinner or loading state to disappear.
  • Wait for a route change.
  • Wait for the specific element that represents completion.

Network interception and API-aware tests

cy.intercept() is one of Cypress’s most useful features. It lets you observe, stub, or synchronize on network traffic.

Intercept an API call

typescript cy.intercept(‘GET’, ‘/api/todos’).as(‘getTodos’) cy.visit(‘/todos’) cy.wait(‘@getTodos’) cy.get(‘[data-cy=todo-item]’).should(‘have.length.greaterThan’, 0)

Stub a response

cy.intercept('GET', '/api/profile', {
  statusCode: 200,
  body: { name: 'Alice' },
}).as('profile')

cy.visit(‘/profile’) cy.get(‘[data-cy=name]’).should(‘contain’, ‘Alice’)

Why intercepts matter

  • They reduce dependence on slow or unstable backend environments.
  • They let you test edge cases that are hard to force through the real system.
  • They help isolate frontend behavior from backend availability.

Tradeoff

Stubbing everything can make tests too optimistic. If you only test mocked responses, you can miss integration problems such as schema drift, authorization failures, and unexpected backend latency. A balanced suite usually mixes real requests, targeted stubs, and backend-assisted test setup.

Fixtures and test data

Fixtures are a practical way to keep test data readable and reusable.

Load a fixture

cy.fixture('user.json').then((user) => {
  cy.get('[data-cy=email]').type(user.email)
})

Example fixture file

{ “email”: “alice@example.com”, “password”: “secret” }

When fixtures help

  • Repeated test inputs.
  • Mocked API payloads.
  • Small, stable datasets.

When fixtures are not enough

Fixtures do not solve test isolation by themselves. If your test depends on account state, inventory, permissions, or seeded database records, you still need a reliable reset strategy. That may mean API seeding, database cleanup, or environment-specific setup hooks.

Screenshots and debugging artifacts

Cypress can capture screenshots during failures or on demand, which is useful when diagnosing CI-only issues.

Capture a screenshot

typescript cy.screenshot(‘checkout-summary’)

Capture the page state after a key step

typescript cy.get(‘[data-cy=confirm]’).click() cy.get(‘[data-cy=receipt]’).should(‘be.visible’) cy.screenshot(‘receipt-visible’)

Practical debugging tips

  • Add screenshots after the point where the test is expected to diverge.
  • Keep test names descriptive so artifacts are easier to map back to intent.
  • Combine screenshots with network aliases and console output when tracking intermittent failures.

The value of screenshots is not cosmetic. They shorten the path from “test failed” to “what changed?”

CI commands and common execution patterns

Most teams run Cypress headlessly in continuous integration. The exact command depends on whether you want the GUI runner, the headless runner, or a browser-specific run.

Run tests headlessly

bash npx cypress run

Open the interactive runner locally

bash npx cypress open

Run against a specific browser

bash npx cypress run –browser chrome

Common CI flow with GitHub Actions

name: cypress

on: push: pull_request:

jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 20 - run: npm ci - run: npm run start:test & - run: npx cypress run

CI considerations

  • Start the app before running tests, or use a job step that waits for readiness.
  • Make sure the environment is deterministic, including feature flags and test credentials.
  • Record artifacts on failure if your pipeline needs faster triage.

A few compact Cypress examples

Login flow

describe('login', () => {
  it('signs in', () => {
    cy.visit('/login')
    cy.get('[data-cy=email]').type('alice@example.com')
    cy.get('[data-cy=password]').type('secret')
    cy.contains('button', 'Sign in').click()
    cy.url().should('include', '/dashboard')
  })
})

Form submit with intercept

describe('contact form', () => {
  it('submits successfully', () => {
    cy.intercept('POST', '/api/contact').as('submitContact')
    cy.visit('/contact')
    cy.get('[data-cy=name]').type('Alice')
    cy.get('[data-cy=message]').type('Hello')
    cy.contains('Submit').click()
    cy.wait('@submitContact')
    cy.contains('Thanks for reaching out').should('be.visible')
  })
})

Empty state test

it('shows empty state', () => {
  cy.intercept('GET', '/api/orders', { body: [] })
  cy.visit('/orders')
  cy.contains('No orders yet').should('be.visible')
})

Maintenance rules that save time

Cypress suites tend to become expensive when they are treated like a pile of recorded browser actions. The cheapest suites are the ones designed around stable application contracts.

Keep these rules in mind

  • Use page objects sparingly, or use lightweight helper functions instead of deep abstraction.
  • Put selectors in one place when they are reused broadly.
  • Keep test data explicit.
  • Prefer application-state setup through APIs when possible.
  • Remove duplication only when it improves readability, not just because it is duplicated.

Typical failure modes

  • Selectors tied to CSS classes or layout structure.
  • Overuse of fixed waits.
  • Excessive mocking, which hides integration bugs.
  • Tests that depend on shared state between cases.
  • Hard-to-read command chains that are difficult to debug under failure.

Cypress versus broader platforms

Cypress is a solid choice when your team wants code-level control and a browser-first developer experience. It is less suited to teams that want low-code authoring, AI-assisted test creation, or a broader workflow that spans test generation, execution, and maintenance without leaning heavily on framework code.

For that kind of workflow, Endtest’s AI Assertions and Visual AI are worth a look, especially if you want natural-language checks, visual validation, and editable platform-native steps instead of a large custom Cypress layer. The tradeoff is straightforward, Cypress gives your engineers direct control in code, while a platform like Endtest can reduce maintenance burden for teams that prefer human-readable tests and AI-assisted authoring.

Quick reference

Most-used Cypress commands

  • cy.visit()
  • cy.get()
  • cy.contains()
  • cy.click()
  • cy.type()
  • cy.should()
  • cy.intercept()
  • cy.wait()
  • cy.fixture()
  • cy.screenshot()

Most-reliable patterns

  • Stable data-* selectors
  • Assertions on visible user outcomes
  • Network aliases instead of arbitrary sleeps
  • Small, readable tests with explicit setup
  • CI runs that mimic production-relevant conditions

Good default decisions

  • Use Cypress when your team wants code-first browser automation.
  • Use fixed waits only when there is no reliable state to observe.
  • Use stubs selectively, not universally.
  • Revisit your test strategy if maintenance starts to dominate the value of the suite.

Further reading

A Cypress cheatsheet is useful only if it helps you make better tradeoffs quickly. The main ones are stable selectors over fragile DOM queries, assertions over sleeps, targeted interception over blanket mocking, and a test structure that makes failures easy to understand. If your team wants more than code-based browser automation, especially AI-assisted creation and cross-browser workflows, evaluate a broader platform alongside Cypress rather than after the suite is already difficult to maintain.