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.

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?