What Is Cypress?

Cypress is a modern end-to-end testing framework built specifically for the web. Unlike Selenium, which communicates with browsers through an external driver, Cypress runs directly inside the browser. This architectural difference is fundamental — it means Cypress has native access to everything happening in the application: DOM elements, network requests, timers, local storage, and even the application’s JavaScript objects.

When you run a Cypress test, the framework loads your application into an iframe and executes test commands alongside it in the same browser instance. There is no network hop between the test runner and the browser, no serialization of commands, and no waiting for responses over HTTP. Commands execute at the speed of the browser itself.

Cypress was created in 2014 and has grown into one of the most popular testing tools in the JavaScript ecosystem. It is designed to make testing as fast and reliable as possible, with built-in features that address the most common pain points of UI testing: flakiness, slow execution, and difficult debugging.

Cypress Architecture

Understanding the architecture helps explain why Cypress behaves differently from other tools.

In-Browser Execution

Traditional Selenium Architecture:
  Test Code → HTTP → WebDriver → Browser

Cypress Architecture:
  Test Code → [runs inside the same browser process] → Application

The test runner (Node.js process) starts the browser and injects the Cypress test code directly into it. The test code and application code run in the same event loop. This means Cypress can:

  • Directly access and manipulate the DOM
  • Intercept and modify network requests before they leave the browser
  • Control time (fast-forward timers, mock dates)
  • Access application state and even call application functions directly

The Command Queue

Cypress commands do not execute immediately. When you write:

cy.visit('/login')
cy.get('#email').type('user@example.com')
cy.get('#password').type('secret123')
cy.get('button[type="submit"]').click()

These commands are added to a queue and executed sequentially. Each command waits for the previous one to complete before starting. This eliminates the need for explicit waits or async/await syntax.

Automatic Waiting and Retries

One of the most important features of Cypress is its automatic retry mechanism. When you write:

cy.get('.success-message').should('contain', 'Welcome')

Cypress will:

  1. Try to find the .success-message element
  2. If not found, retry after a short interval
  3. Keep retrying until the element is found OR the timeout expires (default 4 seconds)
  4. Once found, check if it contains “Welcome”
  5. If the text does not match, retry the entire chain

This built-in retry logic eliminates the need for manual waitForElement calls that plague Selenium tests.

Getting Started

Installation

# Create a new project
mkdir my-cypress-project && cd my-cypress-project
npm init -y

# Install Cypress
npm install cypress --save-dev

# Open Cypress (generates default config and folder structure)
npx cypress open

Project Structure

After first launch, Cypress creates:

cypress/
  e2e/           # Test files (.cy.js)
  fixtures/      # Test data (JSON files)
  support/
    commands.js  # Custom commands
    e2e.js       # Global setup/teardown
cypress.config.js  # Configuration

Configuration

// cypress.config.js
const { defineConfig } = require('cypress')

module.exports = defineConfig({
  e2e: {
    baseUrl: 'http://localhost:3000',
    viewportWidth: 1280,
    viewportHeight: 720,
    defaultCommandTimeout: 10000,
    video: true,
    screenshotOnRunFailure: true,
    retries: {
      runMode: 2,    // Retries in CI
      openMode: 0    // No retries in interactive mode
    }
  }
})

Writing Your First Test

// cypress/e2e/login.cy.js
describe('Login Page', () => {
  beforeEach(() => {
    cy.visit('/login')
  })

  it('should login with valid credentials', () => {
    cy.get('[data-testid="email"]').type('admin@example.com')
    cy.get('[data-testid="password"]').type('correctPassword')
    cy.get('[data-testid="submit"]').click()

    cy.url().should('include', '/dashboard')
    cy.get('.welcome-header').should('contain', 'Welcome, Admin')
  })

  it('should show error with invalid credentials', () => {
    cy.get('[data-testid="email"]').type('admin@example.com')
    cy.get('[data-testid="password"]').type('wrongPassword')
    cy.get('[data-testid="submit"]').click()

    cy.get('.error-message')
      .should('be.visible')
      .and('contain', 'Invalid email or password')
  })
})

Core Commands

Querying Elements

cy.get('.class-name')              // CSS selector
cy.get('[data-testid="submit"]')   // Data attribute (recommended)
cy.contains('Submit Order')        // Find by text content
cy.get('form').find('input')       // Chain selectors
cy.get('li').first()               // First matching element
cy.get('li').eq(2)                 // Third element (zero-indexed)

Interacting with Elements

cy.get('input').type('Hello World')
cy.get('input').clear().type('New text')
cy.get('button').click()
cy.get('button').dblclick()
cy.get('button').rightclick()
cy.get('select').select('Option 2')
cy.get('input[type="checkbox"]').check()
cy.get('input[type="checkbox"]').uncheck()
cy.get('.item').trigger('mouseover')

Assertions

Cypress uses Chai assertions with a .should() syntax:

cy.get('.title').should('have.text', 'Dashboard')
cy.get('.list').should('have.length', 5)
cy.get('.button').should('be.visible')
cy.get('.button').should('be.disabled')
cy.get('.input').should('have.value', 'Hello')
cy.get('.error').should('not.exist')
cy.url().should('include', '/products')
cy.get('.price').should('contain', '$29.99')

Network Interception with cy.intercept()

One of Cypress’s most powerful features is the ability to intercept and control network requests.

Stubbing API Responses

it('should display products from API', () => {
  cy.intercept('GET', '/api/products', {
    statusCode: 200,
    body: [
      { id: 1, name: 'Product A', price: 29.99 },
      { id: 2, name: 'Product B', price: 49.99 }
    ]
  }).as('getProducts')

  cy.visit('/products')
  cy.wait('@getProducts')

  cy.get('.product-card').should('have.length', 2)
  cy.get('.product-card').first().should('contain', 'Product A')
})

Waiting for Real Requests

it('should submit form and wait for server response', () => {
  cy.intercept('POST', '/api/orders').as('createOrder')

  cy.get('[data-testid="place-order"]').click()

  cy.wait('@createOrder').then((interception) => {
    expect(interception.response.statusCode).to.equal(201)
    expect(interception.request.body).to.have.property('items')
  })
})

Simulating Errors

it('should handle server errors gracefully', () => {
  cy.intercept('GET', '/api/products', {
    statusCode: 500,
    body: { error: 'Internal Server Error' }
  }).as('serverError')

  cy.visit('/products')
  cy.wait('@serverError')

  cy.get('.error-state')
    .should('be.visible')
    .and('contain', 'Something went wrong')
  cy.get('.retry-button').should('be.visible')
})

Custom Commands

Custom commands extend Cypress’s API with reusable functions.

// cypress/support/commands.js
Cypress.Commands.add('login', (email, password) => {
  cy.session([email, password], () => {
    cy.visit('/login')
    cy.get('[data-testid="email"]').type(email)
    cy.get('[data-testid="password"]').type(password)
    cy.get('[data-testid="submit"]').click()
    cy.url().should('include', '/dashboard')
  })
})

Cypress.Commands.add('createProduct', (product) => {
  cy.request({
    method: 'POST',
    url: '/api/products',
    body: product,
    headers: { Authorization: `Bearer ${Cypress.env('API_TOKEN')}` }
  })
})

Usage in tests:

describe('Product Management', () => {
  beforeEach(() => {
    cy.login('admin@example.com', 'password123')
  })

  it('should display newly created product', () => {
    cy.createProduct({ name: 'New Widget', price: 19.99 })
    cy.visit('/products')
    cy.contains('New Widget').should('be.visible')
  })
})

Fixtures and Test Data

Fixtures store static test data in JSON files.

// cypress/fixtures/user.json
{
  "admin": {
    "email": "admin@example.com",
    "password": "admin123",
    "role": "administrator"
  },
  "regular": {
    "email": "user@example.com",
    "password": "user123",
    "role": "user"
  }
}
// Using fixtures in tests
it('should login as admin', () => {
  cy.fixture('user').then((users) => {
    cy.get('#email').type(users.admin.email)
    cy.get('#password').type(users.admin.password)
    cy.get('#submit').click()
    cy.contains('Administrator Dashboard').should('be.visible')
  })
})

Environment Variables and Multiple Environments

// cypress.config.js
module.exports = defineConfig({
  e2e: {
    env: {
      apiUrl: 'http://localhost:8080',
      coverage: false
    }
  }
})
# Override via command line
npx cypress run --env apiUrl=https://staging.example.com

# Or via cypress.env.json (not committed to git)
// Access in tests
cy.request(`${Cypress.env('apiUrl')}/api/health`)

Debugging Techniques

Time Travel

Cypress records snapshots at every step. In the interactive runner, hovering over a command shows the exact DOM state at that moment. This “time travel” feature makes debugging straightforward — you can see exactly what the page looked like when a command executed.

cy.pause() and cy.debug()

it('debugging example', () => {
  cy.visit('/checkout')
  cy.get('.cart-items').should('have.length', 3)
  cy.pause()  // Pauses test execution for manual inspection
  cy.get('.checkout-button').click()
  cy.debug()  // Opens DevTools debugger at this point
})

Exercises

Exercise 1: E-Commerce Test Suite

Write a Cypress test suite for a product search feature:

  1. Visit the products page
  2. Type a search query into the search input
  3. Intercept the search API call and verify the request parameters
  4. Assert the filtered product list displays the correct number of results
  5. Verify each visible product contains the search term

Exercise 2: Custom Command Library

Create a set of custom commands for common authentication flows:

  1. cy.login(email, password) — login via UI with session caching
  2. cy.apiLogin(email, password) — login via API for faster test setup
  3. cy.logout() — clear session and verify redirect to login page
  4. Write tests that use these commands and verify they work correctly

Exercise 3: Error Handling Tests

Using cy.intercept(), write tests that verify the application handles these scenarios:

  1. 500 Internal Server Error — shows error page with retry button
  2. 401 Unauthorized — redirects to login page
  3. Network timeout — shows timeout message
  4. Slow response (3+ seconds) — shows loading spinner before data appears