UI Automation
Cypress
Complete Guide

Cypress Testing Complete Guide

Parul Dhingra - Senior Quality Analyst
Parul Dhingra13+ Years ExperienceHire Me

Senior Quality Analyst

Updated: 1/23/2026

Cypress changed how developers think about testing. Running directly in the browser with automatic waiting, time-travel debugging, and real-time reloading, Cypress eliminates the pain points that made end-to-end testing frustrating. If you've struggled with flaky Selenium tests, Cypress will feel like a breath of fresh air.

This guide covers everything from initial setup to advanced patterns used in production applications.

Why Cypress?

Cypress is a JavaScript-based end-to-end testing framework built for modern web applications. Unlike Selenium, which operates outside the browser, Cypress runs directly inside the browser alongside your application. For a side-by-side comparison of the leading frameworks, see Selenium vs Playwright vs Cypress.

Key Advantages

FeatureBenefit
Automatic WaitingNo manual waits - Cypress waits for elements, animations, and API calls
Time TravelHover over commands to see exactly what happened at each step
Real-time ReloadingTests re-run automatically when you save changes
In-Browser DebuggingUse Chrome DevTools while tests run
Network ControlStub and spy on network requests easily
Screenshots & VideosAutomatic capture on failure
Consistent ResultsRuns in the same browser loop, eliminating flakiness

Cypress vs Selenium

AspectCypressSelenium
ArchitectureRuns in browserExternal WebDriver
LanguageJavaScript/TypeScriptMulti-language
Setupnpm installDrivers + bindings
WaitingAutomaticManual waits needed
DebuggingTime travel, snapshotsLimited
SpeedFastSlower
Browser SupportChrome, Firefox, Edge, ElectronAll browsers
MobileNo native supportVia Appium

When to Choose Cypress: Use Cypress for modern JavaScript applications where you need fast feedback, excellent debugging, and don't require cross-browser testing beyond Chrome-based browsers.

Installation and Setup

Prerequisites

  • Node.js 18.x or higher
  • npm or yarn
  • Modern browser (Chrome, Firefox, Edge)

Install Cypress

# Create new project (if needed)
mkdir cypress-tests && cd cypress-tests
npm init -y
 
# Install Cypress
npm install cypress --save-dev
 
# Or with yarn
yarn add cypress --dev

Open Cypress

# First run opens Cypress Launchpad
npx cypress open

The Launchpad guides you through:

  1. Choosing testing type (E2E or Component)
  2. Selecting a browser
  3. Creating initial configuration

Quick Setup with TypeScript

npm install typescript --save-dev

Cypress automatically detects TypeScript and creates tsconfig.json in the cypress folder.

Project Structure

After initialization, Cypress creates this structure:

project/
├── cypress/
│   ├── e2e/                  # Test files
│   │   └── spec.cy.js
│   ├── fixtures/             # Test data (JSON)
│   │   └── example.json
│   ├── support/              # Custom commands and setup
│   │   ├── commands.js       # Custom commands
│   │   └── e2e.js            # Support file (loads before tests)
│   └── downloads/            # Downloaded files (gitignored)
├── cypress.config.js         # Cypress configuration
└── package.json

Key Files

FilePurpose
cypress.config.jsMain configuration (base URL, timeouts, etc.)
cypress/e2e/*.cy.jsTest spec files
cypress/support/commands.jsCustom commands
cypress/support/e2e.jsRuns before every test file
cypress/fixtures/*.jsonStatic test data

Writing Your First Test

Basic Test Structure

// cypress/e2e/first-test.cy.js
 
describe('My First Test Suite', () => {
  beforeEach(() => {
    // Runs before each test
    cy.visit('https://example.com')
  })
 
  it('should display the homepage', () => {
    // Verify page loaded
    cy.contains('Example Domain')
  })
 
  it('should have working navigation', () => {
    cy.contains('More information').click()
    cy.url().should('include', 'iana.org')
  })
})

Test File Naming

Cypress looks for tests matching these patterns:

  • cypress/e2e/**/*.cy.{js,jsx,ts,tsx}

Common conventions:

  • login.cy.js - Feature-based naming
  • login.spec.js - Spec-based naming (configure in cypress.config.js)

Running Tests

# Open interactive mode
npx cypress open
 
# Run all tests headlessly
npx cypress run
 
# Run specific spec file
npx cypress run --spec "cypress/e2e/login.cy.js"
 
# Run in specific browser
npx cypress run --browser chrome
 
# Run with headed browser (visible)
npx cypress run --headed

Selecting Elements

Best Practice: Use data-* Attributes

<!-- In your application -->
<button data-cy="submit-btn" class="btn btn-primary">Submit</button>
// In your test
cy.get('[data-cy="submit-btn"]').click()

Using data-cy or data-testid attributes:

  • Decouples tests from CSS/styling changes
  • Makes test intent clear
  • Survives refactoring

Selector Methods

// By data attribute (recommended)
cy.get('[data-cy="search-input"]')
cy.get('[data-testid="submit-button"]')
 
// By CSS selector
cy.get('#username') // ID
cy.get('.error-message') // Class
cy.get('input[type="email"]') // Attribute
cy.get('form > button') // Child combinator
 
// By text content
cy.contains('Submit') // Find by text
cy.contains('button', 'Submit') // Element with text
 
// Within a container
cy.get('[data-cy="login-form"]').within(() => {
  cy.get('input[name="email"]').type('user@test.com')
  cy.get('input[name="password"]').type('password')
})
 
// Find within element
cy.get('.nav').find('a').first()

Selector Priority (Recommended)

  1. data-cy or data-testid (most stable)
  2. id (if stable and meaningful)
  3. name attribute (for form fields)
  4. CSS class (if semantic, not styling)
  5. Element + attribute combination
  6. cy.contains() for text (use carefully)

Interacting with Elements

Common Actions

// Click
cy.get('[data-cy="submit"]').click()
cy.get('[data-cy="menu"]').click({ force: true }) // Click even if covered
 
// Type text
cy.get('[data-cy="email"]').type('user@example.com')
cy.get('[data-cy="search"]').type('query{enter}') // Type and press Enter
 
// Clear and type
cy.get('[data-cy="email"]').clear().type('new@example.com')
 
// Select from dropdown
cy.get('[data-cy="country"]').select('United States') // By visible text
cy.get('[data-cy="country"]').select('US') // By value
 
// Check/Uncheck
cy.get('[data-cy="agree"]').check()
cy.get('[data-cy="newsletter"]').uncheck()
 
// Focus and blur
cy.get('[data-cy="input"]').focus()
cy.get('[data-cy="input"]').blur()
 
// Scroll
cy.get('[data-cy="footer"]').scrollIntoView()
cy.scrollTo('bottom')
 
// Hover (trigger mouseover)
cy.get('[data-cy="menu"]').trigger('mouseover')
 
// Right-click
cy.get('[data-cy="item"]').rightclick()
 
// Double-click
cy.get('[data-cy="item"]').dblclick()

Special Keys

cy.get('[data-cy="input"]').type('{enter}') // Enter key
cy.get('[data-cy="input"]').type('{esc}') // Escape key
cy.get('[data-cy="input"]').type('{backspace}') // Backspace
cy.get('[data-cy="input"]').type('{del}') // Delete
cy.get('[data-cy="input"]').type('{selectall}') // Select all
cy.get('[data-cy="input"]').type('{ctrl+a}') // Ctrl+A
cy.get('[data-cy="input"]').type('{shift+tab}') // Shift+Tab

File Upload

// With cypress-file-upload plugin or native
cy.get('[data-cy="file-input"]').selectFile('cypress/fixtures/test-file.pdf')
 
// Multiple files
cy.get('[data-cy="file-input"]').selectFile([
  'cypress/fixtures/file1.pdf',
  'cypress/fixtures/file2.pdf',
])
 
// Drag and drop
cy.get('[data-cy="dropzone"]').selectFile('cypress/fixtures/image.png', {
  action: 'drag-drop',
})

Assertions

Cypress uses Chai assertions with automatic retry until they pass or timeout.

Implicit Assertions (Chainable)

// Should assertions
cy.get('[data-cy="title"]').should('be.visible')
cy.get('[data-cy="title"]').should('have.text', 'Welcome')
cy.get('[data-cy="input"]').should('have.value', 'test@example.com')
cy.get('[data-cy="button"]').should('be.disabled')
 
// Chained assertions with .and()
cy.get('[data-cy="link"]')
  .should('have.attr', 'href', '/dashboard')
  .and('have.class', 'active')
  .and('be.visible')
 
// Negative assertions
cy.get('[data-cy="error"]').should('not.exist')
cy.get('[data-cy="button"]').should('not.be.disabled')

Common Should Assertions

AssertionDescription
be.visibleElement is visible
not.existElement doesn't exist in DOM
have.textExact text match
containText contains
have.valueInput value
have.attrHas attribute
have.classHas CSS class
be.checkedCheckbox/radio checked
be.disabledElement disabled
have.lengthNumber of elements

Explicit Assertions

// Using expect
cy.get('[data-cy="items"]').then(($items) => {
  expect($items).to.have.length(5)
  expect($items.first()).to.contain('Item 1')
})
 
// Using assert
cy.get('[data-cy="count"]')
  .invoke('text')
  .then((text) => {
    const count = parseInt(text)
    assert.isAbove(count, 0, 'Count should be positive')
  })

URL and Page Assertions

// URL assertions
cy.url().should('include', '/dashboard')
cy.url().should('eq', 'https://example.com/dashboard')
 
// Title
cy.title().should('eq', 'Dashboard | MyApp')
 
// Location
cy.location('pathname').should('eq', '/users/profile')
cy.location('search').should('include', 'page=2')

Handling Waits

Cypress automatically waits for:

  • Elements to exist
  • Elements to become visible
  • Elements to stop animating
  • Network requests (when using aliases)

Automatic Waiting

// Cypress waits up to 4 seconds (default) for element to exist
cy.get('[data-cy="button"]').click()
 
// Increase timeout for slow elements
cy.get('[data-cy="slow-content"]', { timeout: 10000 }).should('be.visible')

Wait for Network Requests

// Set up intercept and alias
cy.intercept('GET', '/api/users').as('getUsers')
 
// Trigger the request
cy.get('[data-cy="load-users"]').click()
 
// Wait for request to complete
cy.wait('@getUsers')
 
// Wait and assert on response
cy.wait('@getUsers').then((interception) => {
  expect(interception.response.statusCode).to.eq(200)
  expect(interception.response.body).to.have.length.greaterThan(0)
})

Avoid Arbitrary Waits

// Bad - arbitrary wait
cy.wait(5000)
 
// Good - wait for specific condition
cy.get('[data-cy="loading"]').should('not.exist')
cy.get('[data-cy="content"]').should('be.visible')
 
// Good - wait for network request
cy.intercept('POST', '/api/submit').as('submit')
cy.get('[data-cy="submit"]').click()
cy.wait('@submit')

Custom Commands

Create reusable commands for common operations.

Define Custom Commands

// cypress/support/commands.js
 
// 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 (faster)
Cypress.Commands.add('loginByApi', (email, password) => {
  cy.request('POST', '/api/auth/login', { email, password }).then(
    (response) => {
      window.localStorage.setItem('token', response.body.token)
    },
  )
})
 
// Get by data-cy shorthand
Cypress.Commands.add('getByDataCy', (selector) => {
  return cy.get(`[data-cy="${selector}"]`)
})

Use Custom Commands

// In your tests
describe('Dashboard', () => {
  beforeEach(() => {
    cy.loginByApi('user@test.com', 'password123')
    cy.visit('/dashboard')
  })
 
  it('should display user data', () => {
    cy.getByDataCy('user-name').should('contain', 'Test User')
  })
})

TypeScript Support for Commands

// cypress/support/index.d.ts
declare namespace Cypress {
  interface Chainable {
    login(email: string, password: string): Chainable<void>
    loginByApi(email: string, password: string): Chainable<void>
    getByDataCy(selector: string): Chainable<JQuery<HTMLElement>>
  }
}

Configuration

cypress.config.js

const { defineConfig } = require('cypress')
 
module.exports = defineConfig({
  e2e: {
    baseUrl: 'http://localhost:3000',
    specPattern: 'cypress/e2e/**/*.cy.{js,jsx,ts,tsx}',
    supportFile: 'cypress/support/e2e.js',
 
    // Timeouts
    defaultCommandTimeout: 4000,
    requestTimeout: 5000,
    responseTimeout: 30000,
    pageLoadTimeout: 60000,
 
    // Viewport
    viewportWidth: 1280,
    viewportHeight: 720,
 
    // Screenshots and videos
    screenshotOnRunFailure: true,
    video: true,
    videosFolder: 'cypress/videos',
    screenshotsFolder: 'cypress/screenshots',
 
    // Retries
    retries: {
      runMode: 2, // Retry failed tests in CI
      openMode: 0, // No retries in interactive mode
    },
 
    // Environment variables
    env: {
      apiUrl: 'http://localhost:3001/api',
    },
 
    setupNodeEvents(on, config) {
      // Plugin configuration
    },
  },
})

Environment Variables

// Access in tests
const apiUrl = Cypress.env('apiUrl')
cy.request(`${apiUrl}/users`)
 
// Set via command line
// npx cypress run --env apiUrl=https://api.example.com
 
// Set in cypress.env.json (gitignored)
{
  "apiUrl": "http://localhost:3001/api",
  "adminUser": "admin@test.com"
}

Best Practices

1. Use data-* Attributes

<!-- Don't rely on CSS classes -->
<button class="btn btn-blue-lg">Submit</button>
 
<!-- Use test-specific attributes -->
<button class="btn btn-blue-lg" data-cy="submit-form">Submit</button>

2. Don't Use Arbitrary Waits

// Bad
cy.wait(5000)
 
// Good
cy.intercept('POST', '/api/submit').as('submit')
cy.get('[data-cy="submit"]').click()
cy.wait('@submit')

3. Reset State Before Tests

// Clean state before each test, not after
beforeEach(() => {
  cy.request('POST', '/api/reset-db')
  cy.loginByApi('user@test.com', 'password')
})

4. Keep Tests Independent

// Bad - test depends on previous test
it('should create user', () => {
  /* ... */
})
it('should see created user', () => {
  /* depends on above */
})
 
// Good - each test is self-contained
it('should create user', () => {
  cy.createUser('new@test.com')
  cy.visit('/users')
  cy.contains('new@test.com')
})

5. Use Fixtures for Test Data

// cypress/fixtures/users.json
{
  "validUser": {
    "email": "test@example.com",
    "password": "SecurePass123"
  },
  "adminUser": {
    "email": "admin@example.com",
    "password": "AdminPass123"
  }
}
// In test
cy.fixture('users').then((users) => {
  cy.login(users.validUser.email, users.validUser.password)
})

6. Bypass UI for Setup

// Slow - using UI for login in every test
beforeEach(() => {
  cy.visit('/login')
  cy.get('#email').type('user@test.com')
  cy.get('#password').type('password')
  cy.get('#submit').click()
})
 
// Fast - use API for login
beforeEach(() => {
  cy.loginByApi('user@test.com', 'password')
})

Test Your Knowledge

Quiz on Cypress Testing

Your Score: 0/10

Question: What is the key architectural difference between Cypress and Selenium?


Continue Your Cypress Journey


Frequently Asked Questions

Frequently Asked Questions (FAQs) / People Also Ask (PAA)

Can Cypress test mobile applications?

What browsers does Cypress support?

How do I handle iframes in Cypress?

Why are my Cypress tests flaky?

How do I run Cypress tests in CI/CD?

Can Cypress test applications that require authentication?

What is the difference between cy.request() and cy.intercept()?

How do I debug failing Cypress tests?