adnenre
#Cypress#TypeScript

Cypress in Depth: Build Scalable E2E Tests with TypeScript

A comprehensive, hands-on guide to mastering end-to-end testing with Cypress and TypeScript. Covers everything from installation to testing a complete real-world projects.

Cypress is a modern, open-source end-to-end testing framework built specifically for JavaScript-based web applications. Unlike traditional testing tools that run outside the browser, Cypress operates directly within the browser, executing tests in the same run loop as the application.

Key Features:

  • In-Browser Execution: Runs tests directly in the browser for accurate user simulation.
  • Automatic Waiting: Commands and assertions wait automatically, eliminating manual sleeps or waits.
  • Time Travel Debugging: Inspect application state at any test step by hovering over commands.
  • Real-Time Reloads: View tests as they run with instant visual updates.
  • Network Traffic Control: Stub and intercept network requests to simulate edge cases.
  • Screenshots and Videos: Automatic capture on test failure.
  • Developer-Friendly: Intuitive API with readable error messages.

What Cypress Can Test:

  • End-to-end user journeys
  • Component interactions
  • API responses (via stubbing)
  • Page navigation and routing
  • Form inputs and submissions
  • Authentication flows

What Cypress Cannot Do:

  • Test multiple browser tabs natively (requires plugin)
  • Test iframes comprehensively
  • Run on Safari or Internet Explorer (only Chrome-based browsers and Firefox)
  • Automate native mobile applications
  • Drive two browsers simultaneously

#2. Philosophy: Testing Like a User

Cypress is built on the philosophy that tests should resemble how real users interact with your application.

Core Principles:

  1. Test Behavior, Not Implementation: Focus on what users see and do, not internal component state.
  2. Automatic Waiting: Built-in retry-ability ensures tests wait for elements to be ready, just like a user would.
  3. Real Browser Environment: Tests run in a real browser, not a simulated DOM.
  4. Developer Experience: Fast feedback loops, clear error messages, and time travel debugging.

The Given-When-Then Pattern: Cypress tests naturally follow the Arrange-Act-Assert pattern:

  • Given a user visits a page (cy.visit())
  • When they click a link (cy.contains().click())
  • Then the URL should change (cy.url().should('include', '/new-page'))

This philosophy results in tests that give you confidence your application actually works for real users.


#3. Installation and Setup

#Basic Installation

Install Cypress as a development dependency in your project:

npm install cypress --save-dev

#Project Structure

After installation, running Cypress for the first time creates the following folder structure:

my-project/
├── cypress/
   ├── e2e/           # Test files (specs)
   ├── fixtures/      # Test data (JSON files)
   ├── support/       # Reusable commands and configuration
   ├── commands.ts
   └── e2e.ts
   └── downloads/     # Files downloaded during tests
├── cypress.config.ts  # Cypress configuration (TypeScript)
├── tsconfig.json      # TypeScript config for Cypress
└── package.json

#Configuration with TypeScript

Create cypress.config.ts (ESM format):

import { defineConfig } from 'cypress';

export default defineConfig({
  e2e: {
    baseUrl: 'http://localhost:3000',
    viewportWidth: 1280,
    viewportHeight: 720,
    defaultCommandTimeout: 10000,
    screenshotOnRunFailure: true,
    video: true,
    setupNodeEvents(on, config) {
      // Implement node event listeners here
    },
  },
});

Create tsconfig.json in the cypress folder:

{
  "compilerOptions": {
    "target": "ES2020",
    "module": "ESNext",
    "moduleResolution": "node",
    "types": ["cypress"],
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true
  },
  "include": ["**/*.ts"]
}

#ES Modules Support

Cypress supports ES modules natively. Use import/export in your test files and support files. The configuration file also uses ESM syntax (as shown above).


#4. Cypress Architecture

#How Cypress Works

Cypress operates using a unique architecture that differs fundamentally from Selenium:

┌─────────────────────────────────────────────────────┐
                   Browser
  ┌─────────────────────────────────────────────┐
           Cypress Test Runner
  ┌──────────────┐    ┌──────────────┐
  App Iframe Test Iframe
 (Your App)   │    │ (Test Code)  │      │    │
  └──────────────┘    └──────────────┘
  └─────────────────────────────────────────────┘

 WebSocket
└──────────────────────────┼──────────────────────────┘

                    ┌──────▼──────┐
 Node.js
 Server
 (File access│
  Network
  Proxy)     │
                    └─────────────┘

Key Components:

  1. Browser Environment: Cypress injects itself into the browser, running tests inside the same event loop as your application.
  2. Dual iFrames: Your application runs in one iframe, the test code in another, providing isolation.
  3. Node.js Server: A local Node.js server handles file operations, network proxying, and cross-browser communication.
  4. Proxy Server: Intercepts all HTTP/HTTPS traffic, enabling request stubbing and mocking.
  5. WebSocket Connection: Real-time communication between the browser and Node.js server for instant feedback.

#The Event Loop and Automatic Waiting

Cypress commands don’t execute immediately; they’re queued and run asynchronously. This enables automatic waiting:

  • Commands wait for elements to exist in the DOM
  • Assertions retry until they pass or timeout
  • No manual sleep() or wait() calls needed

#Cypress vs Selenium: Key Differences

AspectCypressSelenium
ArchitectureRuns inside browserRuns outside browser via WebDriver
Language SupportJavaScript/TypeScript onlyJava, Python, C#, Ruby, JS, etc.
Browser SupportChrome, Firefox, EdgeAll major browsers
Setup ComplexitySimple (npm install)Complex (drivers, setup)
Test SpeedFast (in-browser)Slower (network communication)
Automatic WaitingBuilt-inRequires explicit waits
DebuggingTime travel, DevToolsExternal tools
Multi-tab TestingLimited (plugin needed)Full support
Mobile TestingNo native supportVia Appium

#5. Writing Your First Tests

#Test Structure: describe and it

Cypress uses Mocha’s BDD syntax with TypeScript:

describe('My Test Suite', () => {
  it('should do something', () => {
    // test code here
  });
});

#Visiting Pages

Use cy.visit() to navigate to a page:

describe('The Home Page', () => {
  it('successfully loads', () => {
    cy.visit('/'); // Uses baseUrl from config
  });
});

#Querying Elements

Find elements using various commands:

cy.get('.selector'); // CSS selector
cy.contains('text'); // Element containing text
cy.get('[data-cy=submit]'); // Data attribute (recommended)

#Interacting with Elements

Perform actions on elements:

cy.get('button').click();
cy.get('input').type('Hello World');
cy.get('select').select('option-value');
cy.get('form').submit();

#Making Assertions

Use .should() for assertions:

cy.get('h1').should('be.visible');
cy.get('h1').should('contain', 'Welcome');
cy.url().should('include', '/dashboard');

#Complete Example

Here’s a complete test that visits a page, clicks a link, and makes assertions:

describe('My First Test', () => {
  it('clicks the link "type"', () => {
    cy.visit('https://example.cypress.io');

    cy.contains('type').click();

    cy.url().should('include', '/commands/actions');
    cy.get('.action-email').type('test@example.com');
    cy.get('.action-email').should('have.value', 'test@example.com');
  });
});

#6. Cypress Commands Deep Dive

#Query Commands

CommandPurposeExample
cy.get()Select element by CSS selectorcy.get('.btn-primary')
cy.contains()Find element by text contentcy.contains('Submit')
cy.find()Find within previous subjectcy.get('form').find('input')
cy.first() / cy.last()First/last elementcy.get('li').first()
cy.eq()Element at indexcy.get('li').eq(2)
cy.parent()Parent elementcy.get('span').parent()
cy.children()Child elementscy.get('ul').children()

#Action Commands

CommandPurposeExample
cy.click()Click an elementcy.get('button').click()
cy.dblclick()Double clickcy.get('button').dblclick()
cy.rightclick()Right clickcy.get('button').rightclick()
cy.type()Type into inputcy.get('input').type('text')
cy.clear()Clear inputcy.get('input').clear()
cy.select()Select dropdown optioncy.get('select').select('option')
cy.check()Check checkbox/radiocy.get('[type=checkbox]').check()
cy.uncheck()Uncheck checkboxcy.get('[type=checkbox]').uncheck()
cy.submit()Submit formcy.get('form').submit()
cy.focus()Focus elementcy.get('input').focus()
cy.blur()Blur elementcy.get('input').blur()
cy.scrollTo()Scroll to positioncy.scrollTo('bottom')

#Window Commands

CommandPurposeExample
cy.visit()Navigate to URLcy.visit('/dashboard')
cy.go()Browser back/forwardcy.go('back')
cy.reload()Reload pagecy.reload()
cy.url()Get current URLcy.url().should('include', 'user')
cy.title()Get page titlecy.title().should('eq', 'Home')
cy.location()Get location objectcy.location('pathname')
cy.viewport()Set viewport sizecy.viewport(1280, 720)

#Assertion Commands

CommandPurposeExample
cy.should()Make assertioncy.get('h1').should('be.visible')
cy.and()Chain multiple assertionscy.get('h1').should('be.visible').and('contain', 'Title')
cy.expect()Use Chai expectexpect(true).to.be.true

#Utility Commands

CommandPurposeExample
cy.wrap()Wrap object as subjectcy.wrap({name: 'John'}).its('name')
cy.invoke()Invoke methodcy.get('button').invoke('text')
cy.its()Access propertycy.window().its('localStorage')
cy.then()Work with promisescy.get('div').then($el => {...})
cy.each()Iterate over elementscy.get('li').each($li => {...})
cy.spread()Spread array argumentscy.get('div').spread((div1, div2) => {...})
cy.wait()Wait for time or requestcy.wait(1000) or cy.wait('@getUser')
cy.pause()Pause executioncy.pause()
cy.debug()Debug testcy.debug()

#Custom Commands with TypeScript

Create reusable commands in cypress/support/commands.ts with proper typing:

// cypress/support/commands.ts
declare global {
  namespace Cypress {
    interface Chainable {
      login(email: string, password: string): Chainable<void>;
      createTodo(text: string): Chainable<void>;
    }
  }
}

Cypress.Commands.add('login', (email: string, password: string) => {
  cy.visit('/login');
  cy.get('[data-cy=email]').type(email);
  cy.get('[data-cy=password]').type(password);
  cy.get('[data-cy=submit]').click();
});

Cypress.Commands.add('createTodo', (text: string) => {
  cy.get('[data-cy=new-todo]').type(text);
  cy.get('[data-cy=add-todo]').click();
});

Then in your tests, the commands will be type-aware:

it('uses custom commands', () => {
  cy.login('user@example.com', 'password123');
  cy.createTodo('Learn Cypress');
});

#7. Selectors and Best Practices

#Selector Priority

Cypress uses a priority strategy for selector generation:

  1. data-cy – Highest priority, dedicated for testing
  2. data-test – Common test attribute
  3. data-testid – Alternative test ID
  4. data-qa – QA-specific attribute
  5. id – Unique identifier
  6. name – Form field name
  7. class – CSS class (fragile)
  8. tag – Element type
  9. attributes – Other attributes
  10. nth-child – Position-based (last resort)

#Data Attributes

The recommended approach is using dedicated test attributes:

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

Benefits:

  • Independent of CSS or JS changes
  • Clearly indicates testing hooks
  • Consistent across environments

#Text Content

Use cy.contains() for text-based selection:

cy.contains('Submit').click();
cy.contains('button', 'Submit'); // More specific

#8. Assertions and Should

Cypress bundles Chai, Sinon, and jQuery assertions.

#Built-in Assertions

Common assertions with .should():

// Visibility
cy.get('button').should('be.visible');
cy.get('button').should('not.be.visible');
cy.get('button').should('exist');
cy.get('button').should('not.exist');

// State
cy.get('button').should('be.disabled');
cy.get('button').should('be.enabled');
cy.get('input').should('be.focused');
cy.get('[type=checkbox]').should('be.checked');

// Text content
cy.get('h1').should('contain', 'Welcome');
cy.get('h1').should('have.text', 'Welcome Home');
cy.get('h1').should('include.text', 'Welcome');

// Attributes
cy.get('img').should('have.attr', 'alt', 'Logo');
cy.get('input').should('have.value', 'user@example.com');
cy.get('a').should('have.attr', 'href', '/about');

// CSS
cy.get('button').should('have.class', 'primary');
cy.get('button').should('have.css', 'color', 'rgb(0,0,0)');

// Length
cy.get('li').should('have.length', 5);
cy.get('li').should('have.length.gt', 0);

// URL and navigation
cy.url().should('include', '/dashboard');
cy.url().should('eq', 'http://localhost:3000/dashboard');
cy.title().should('eq', 'My App');

#Chai Assertions

Use expect() for BDD-style assertions:

expect(true).to.be.true;
expect(false).to.be.false;
expect(5).to.equal(5);
expect({ name: 'John' }).to.deep.equal({ name: 'John' });
expect([1, 2, 3]).to.include(2);

#Sinon Assertions

For spies and stubs:

const obj = { method: () => {} };
cy.spy(obj, 'method').as('methodSpy');
obj.method('arg');
cy.get('@methodSpy').should('have.been.called');
cy.get('@methodSpy').should('have.been.calledWith', 'arg');

#Custom Assertions

Create custom assertions using should(callback):

cy.get('div').should(($div) => {
  expect($div).to.have.length(1);
  expect($div[0].offsetWidth).to.be.gt(100);
});

#9. Network Testing

#cy.intercept()

Use cy.intercept() to control network requests:

// Spy on a request
cy.intercept('GET', '/api/users').as('getUsers');
cy.visit('/users');
cy.wait('@getUsers');

// Verify request/response
cy.wait('@getUsers').its('response.statusCode').should('eq', 200);

#Stubbing Responses

Mock API responses for reliable testing:

interface User {
  id: number;
  name: string;
}

cy.intercept<User[]>('GET', '/api/users', {
  statusCode: 200,
  body: [
    { id: 1, name: 'John' },
    { id: 2, name: 'Jane' },
  ],
}).as('getUsers');

cy.intercept('POST', '/api/users', {
  statusCode: 201,
  body: { id: 3, name: 'New User' },
}).as('createUser');

#Waiting for Requests

Wait for specific requests to complete:

cy.intercept('GET', '/api/posts').as('getPosts');
cy.visit('/posts');
cy.wait('@getPosts').then((interception) => {
  expect(interception.response.body).to.have.length(10);
});

#Testing Error States

Simulate server errors:

cy.intercept('GET', '/api/user', {
  statusCode: 500,
  body: { error: 'Server error' },
}).as('getUserError');

cy.visit('/profile');
cy.wait('@getUserError');
cy.contains('Error loading user').should('be.visible');

#GraphQL Testing

Test GraphQL endpoints using intercept:

cy.intercept('POST', '/graphql', (req) => {
  if (req.body.operationName === 'GetUser') {
    req.reply({
      data: {
        user: { id: 1, name: 'John' },
      },
    });
  }
}).as('gqlQuery');

#10. Fixtures and Test Data

#Loading Fixtures with Types

Store test data in cypress/fixtures/:

// cypress/fixtures/user.json
{
  "name": "John Doe",
  "email": "john@example.com",
  "role": "admin"
}
// Load fixture in test
cy.fixture<{ name: string; email: string; role: string }>('user').then((user) => {
  cy.get('[data-cy=name]').type(user.name);
  cy.get('[data-cy=email]').type(user.email);
});

// Or alias for reuse
cy.fixture('user').as('userData');
cy.get<User>('@userData').then((user) => {
  // use user data
});

#Dynamic Test Data

Generate data programmatically:

const timestamp = Date.now();
const user = {
  name: `User ${timestamp}`,
  email: `user${timestamp}@example.com`,
};

cy.intercept('POST', '/api/users', user).as('createUser');

#Environment Variables

Access environment variables from cypress.config.ts:

// In config
import { defineConfig } from 'cypress';

export default defineConfig({
  env: {
    apiUrl: 'https://api.example.com',
  },
});
// In tests
cy.visit(`${Cypress.env('apiUrl')}/users`);

#11. Hooks and Test Organization

#before, beforeEach, afterEach, after

Control test execution flow:

describe('User Dashboard', () => {
  before(() => {
    // Runs once before all tests
    cy.exec('npm run db:reset');
  });

  beforeEach(() => {
    // Runs before each test
    cy.login('test@example.com', 'password');
    cy.visit('/dashboard');
  });

  afterEach(() => {
    // Runs after each test
    cy.logout();
  });

  after(() => {
    // Runs once after all tests
    cy.exec('npm run db:cleanup');
  });

  it('displays user info', () => {
    cy.contains('Welcome, User').should('be.visible');
  });

  it('shows recent activity', () => {
    cy.get('[data-cy=activity]').should('exist');
  });
});

#Skipping and Focusing Tests

Control which tests run:

describe.only('Only this suite runs', () => {
  // tests here
});

describe.skip('This suite is skipped', () => {
  // tests here
});

it.only('Only this test runs', () => {
  // test here
});

it.skip('This test is skipped', () => {
  // test here
});

#Test Isolation

Best practice: Keep tests independent:

  • Use beforeEach to reset state
  • Don’t rely on previous test execution
  • Clean up after tests
  • Avoid test ordering dependencies

#12. Advanced Testing Patterns

#Page Object Model with TypeScript

Encapsulate page interactions:

// pages/LoginPage.ts
class LoginPage {
  visit(): this {
    cy.visit('/login');
    return this;
  }

  fillEmail(email: string): this {
    cy.get('[data-cy=email]').type(email);
    return this;
  }

  fillPassword(password: string): this {
    cy.get('[data-cy=password]').type(password);
    return this;
  }

  submit(): this {
    cy.get('[data-cy=submit]').click();
    return this;
  }

  verifySuccess(): this {
    cy.url().should('include', '/dashboard');
    return this;
  }
}

export default new LoginPage();
// Usage in test
import LoginPage from '../pages/LoginPage';

it('logs in successfully', () => {
  LoginPage.visit()
    .fillEmail('user@example.com')
    .fillPassword('password123')
    .submit()
    .verifySuccess();
});

#Custom Commands for Reusability

(Already covered in Custom Commands section.)

#Testing Authentication Flows

Test login, registration, and protected routes:

describe('Authentication', () => {
  it('logs in with valid credentials', () => {
    cy.visit('/login');
    cy.get('[data-cy=email]').type('user@example.com');
    cy.get('[data-cy=password]').type('password123');
    cy.get('[data-cy=login]').click();
    cy.url().should('include', '/dashboard');
    cy.get('[data-cy=user-name]').should('contain', 'User');
  });

  it('shows error with invalid credentials', () => {
    cy.visit('/login');
    cy.get('[data-cy=email]').type('invalid@example.com');
    cy.get('[data-cy=password]').type('wrong');
    cy.get('[data-cy=login]').click();
    cy.get('[data-cy=error]').should('be.visible');
  });

  it('redirects to login when accessing protected route', () => {
    cy.visit('/dashboard');
    cy.url().should('include', '/login');
  });
});

#Handling iframes

Use plugins for iframe testing:

npm install -D cypress-iframe
import 'cypress-iframe';

it('works with iframe', () => {
  cy.visit('/page-with-iframe');
  cy.frameLoaded('[data-cy=iframe]');
  cy.iframe('[data-cy=iframe]').find('button').click();
});

#Multiple Tabs

Cypress has limited multi-tab support, but plugins exist:

npm install -D cypress-puppeteer
// Alternative approach: Test in single tab
it('handles link that opens new tab', () => {
  cy.get('a[target="_blank"]').invoke('removeAttr', 'target').click();
  // Now test in same tab
});

#File Uploads

Use .selectFile() for file uploads:

cy.get('[data-cy=file-input]').selectFile('cypress/fixtures/image.jpg');
cy.get('[data-cy=upload]').click();
cy.contains('Upload successful').should('be.visible');

// Multiple files
cy.get('[data-cy=file-input]').selectFile([
  'cypress/fixtures/file1.jpg',
  'cypress/fixtures/file2.jpg',
]);

#Drag and Drop

Simulate drag and drop:

it('drags and drops', () => {
  cy.get('[data-cy=draggable]').trigger('mousedown', { which: 1 });
  cy.get('[data-cy=droppable]').trigger('mousemove').trigger('mouseup');
  cy.contains('Dropped!').should('be.visible');
});

#13. Cypress Studio: Visual Test Creation

Cypress Studio is a visual tool that generates tests by recording your interactions with the application. Instead of manually typing .get(), .click(), and .type() commands, you can record interactions in real time.

How to use:

  1. Open Cypress and run a spec file
  2. Click Edit in Studio on a test in the Command Log
  3. Interact with your application – clicks, typing, selections are recorded
  4. Click Save to generate test code

Adding Assertions Visually: Right-click any element to add assertions like “be visible”, “have text”, “be checked”, etc.

Limitations:

  • Works only in E2E tests (not Component Testing)
  • Cucumber tests not supported
  • Cannot record across multiple origins
  • Recording in iframes not supported

#14. Debugging and Troubleshooting

#Time Travel Debugging

Hover over commands in the Cypress Test Runner to see the application state at that moment. This makes debugging incredibly intuitive.

#Using debugger

Insert debugger statements in your tests:

it('debugs a test', () => {
  cy.visit('/');
  cy.get('button').click();
  debugger; // Execution pauses here
  cy.contains('Success').should('be.visible');
});

#cy.pause()

Pause test execution to inspect:

cy.get('button').click();
cy.pause(); // Test pauses here
cy.contains('Success').should('be.visible');

#Screenshots and Videos

Cypress automatically captures screenshots on failure. Videos are recorded for all tests when run headlessly.

View them in:

  • cypress/screenshots/ – Failure screenshots
  • cypress/videos/ – Test recordings

#Common Errors and Solutions

ErrorLikely CauseSolution
cy.get() timed outElement not in DOMIncrease timeout, check selector, ensure element exists
cy.contains() timed outText not foundCheck text content, wait for async load
Cross-origin errorNavigating to different originUse cy.origin() or test within same origin
Element detached from DOMDOM re-renderedRe-query element after changes
Request never completedNetwork issueCheck server, mock if needed

#15. Best Practices

#Test Independence

Each test should be able to run alone:

  • Use beforeEach to set up state
  • Don’t rely on previous test results
  • Clean up after tests

#Selector Strategy

Follow this priority:

  1. data-cy – Dedicated test attributes
  2. Text content – For user-facing elements
  3. Accessible roles – For accessibility testing
  4. CSS selectors – Last resort

#Avoiding Test Flakiness

  • Don’t use fixed waits: Avoid cy.wait(5000)
  • Rely on automatic waiting: Cypress waits automatically
  • Stub network requests: Control external dependencies
  • Isolate tests: Reset state before each test
  • Use retries: Enable test retries in CI

#Organizing Tests

cypress/
├── e2e/
   ├── auth/
   ├── login.cy.ts
   ├── registration.cy.ts
   └── password-reset.cy.ts
   ├── dashboard/
   ├── overview.cy.ts
   └── settings.cy.ts
   └── smoke/
       └── critical-paths.cy.ts
├── fixtures/
   ├── users.json
   └── products.json
├── support/
   ├── commands.ts
   └── e2e.ts
└── downloads/

#Performance Optimization

  • Run in headless mode for CI
  • Use parallelization with Cypress Cloud
  • Group related tests in same spec
  • Stub heavy network requests

#16. Continuous Integration

#Running Headless

Run tests without opening the browser:

npx cypress run
# Or for specific browser
npx cypress run --browser chrome

#GitHub Actions Integration

# .github/workflows/cypress.yml
name: Cypress Tests

on: [push]

jobs:
  cypress-run:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v3

      - name: Install dependencies
        run: npm ci

      - name: Start app
        run: npm start & npx wait-on http://localhost:3000

      - name: Run Cypress
        uses: cypress-io/github-action@v5
        with:
          browser: chrome
          record: true
        env:
          CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}

#Cypress Dashboard

Cypress Cloud (formerly Dashboard) provides:

  • Test recordings and video playback
  • Flake detection
  • Parallelization
  • Test analytics
  • Integration with GitHub, Slack, Jira

#Parallelization

Run tests in parallel for faster feedback:

npx cypress run --record --parallel --group "all-tests"

Requires:

  • Cypress Cloud account
  • Tests designed to run in parallel (independent)

#17. Migration from Selenium

#Top Reasons to Migrate

  1. Reduction of Flakiness: Automatic waiting and retries
  2. Ease of Setup: Two commands: npm install and npx cypress open
  3. Strong Community Support: 10,000+ Discord community
  4. Time Travel Debugging: Inspect any test step
  5. Better Developer Experience: Intuitive API

#Migration Strategy

Follow this approach:

  1. Evaluate test suite – Identify priority test cases
  2. Start small – Begin with low-hanging fruit
  3. Rebuild selectors – Replace XPath with data attributes
  4. Convert page objects – Adapt to Cypress patterns
  5. Replace waits – Remove explicit waits, trust automatic waiting

#Converting Page Objects

Selenium Page Object (Java):

public class LoginPage {
    WebDriver driver;

    public LoginPage(WebDriver driver) {
        this.driver = driver;
    }

    public void enterEmail(String email) {
        driver.findElement(By.id("email")).sendKeys(email);
    }

    public void clickLogin() {
        driver.findElement(By.id("login")).click();
    }
}

Cypress Equivalent (TypeScript):

class LoginPage {
  enterEmail(email: string) {
    cy.get('#email').type(email);
  }

  clickLogin() {
    cy.get('#login').click();
  }
}

#Handling Waits and Assertions

Selenium:

WebDriverWait wait = new WebDriverWait(driver, 10);
wait.until(ExpectedConditions.elementToBeClickable(By.id("button"))).click();
assert driver.findElement(By.id("message")).isDisplayed();

Cypress:

cy.get('#button').click(); // Auto-waits
cy.get('#message').should('be.visible'); // Auto-retries

#Cucumber Integration

For teams using Cucumber, use the Cypress Cucumber plugin:

npm install -D @badeball/cypress-cucumber-preprocessor
// Feature file
Feature: Login
  Scenario: Successful login
    Given I visit the login page
    When I enter valid credentials
    Then I should see the dashboard
// Step definitions (TypeScript)
import { Given, When, Then } from '@badeball/cypress-cucumber-preprocessor';

Given('I visit the login page', () => {
  cy.visit('/login');
});

When('I enter valid credentials', () => {
  cy.get('[data-cy=email]').type('user@example.com');
  cy.get('[data-cy=password]').type('password123');
  cy.get('[data-cy=login]').click();
});

Then('I should see the dashboard', () => {
  cy.url().should('include', '/dashboard');
});

#18. Real-World Project: E-Commerce Testing Suite

#Project Overview

Test a complete e-commerce application including:

  • User authentication
  • Product browsing and search
  • Shopping cart functionality
  • Checkout process
  • Error handling

#Setup and Configuration

mkdir ecommerce-tests
cd ecommerce-tests
npm init -y
npm install cypress --save-dev
npx cypress open

Update cypress.config.ts:

import { defineConfig } from 'cypress';

export default defineConfig({
  e2e: {
    baseUrl: 'http://localhost:3000',
    viewportWidth: 1280,
    viewportHeight: 720,
    defaultCommandTimeout: 10000,
    screenshotOnRunFailure: true,
    video: true,
    env: {
      apiUrl: 'http://localhost:3001/api',
    },
  },
});

#Testing User Authentication

// cypress/e2e/auth/login.cy.ts
describe('Login Flow', () => {
  beforeEach(() => {
    cy.visit('/login');
  });

  it('logs in with valid credentials', () => {
    cy.get('[data-cy=email]').type('customer@example.com');
    cy.get('[data-cy=password]').type('password123');
    cy.get('[data-cy=login-button]').click();

    cy.url().should('include', '/account');
    cy.get('[data-cy=welcome]').should('contain', 'Welcome back');
  });

  it('shows error with invalid credentials', () => {
    cy.get('[data-cy=email]').type('invalid@example.com');
    cy.get('[data-cy=password]').type('wrong');
    cy.get('[data-cy=login-button]').click();

    cy.get('[data-cy=error]').should('be.visible').and('contain', 'Invalid email or password');
  });

  it('validates required fields', () => {
    cy.get('[data-cy=login-button]').click();

    cy.get('[data-cy=email-error]').should('be.visible');
    cy.get('[data-cy=password-error]').should('be.visible');
  });

  it('allows password reset navigation', () => {
    cy.get('[data-cy=forgot-password]').click();
    cy.url().should('include', '/reset-password');
  });
});
// cypress/e2e/products/search.cy.ts
describe('Product Search', () => {
  beforeEach(() => {
    cy.visit('/products');
  });

  it('searches for products', () => {
    cy.get('[data-cy=search-input]').type('laptop{enter}');

    cy.url().should('include', 'search=laptop');
    cy.get('[data-cy=product-card]').should('have.length.at.least', 1);
    cy.get('[data-cy=product-title]').first().should('contain', 'laptop', { matchCase: false });
  });

  it('shows no results message when nothing found', () => {
    cy.intercept('GET', '/api/products?search=xyz123', {
      body: [],
    }).as('searchEmpty');

    cy.get('[data-cy=search-input]').type('xyz123{enter}');
    cy.wait('@searchEmpty');

    cy.get('[data-cy=no-results]').should('be.visible');
  });

  it('filters by category', () => {
    cy.get('[data-cy=category-electronics]').click();

    cy.get('[data-cy=product-card]').each(($card) => {
      cy.wrap($card).find('[data-cy=category]').should('contain', 'Electronics');
    });
  });
});

#Testing Add to Cart

// cypress/e2e/cart/add-to-cart.cy.ts
describe('Add to Cart', () => {
  beforeEach(() => {
    cy.visit('/products');
  });

  it('adds product to cart', () => {
    cy.get('[data-cy=product-card]')
      .first()
      .within(() => {
        cy.get('[data-cy=add-to-cart]').click();
      });

    cy.get('[data-cy=cart-count]').should('contain', '1');
    cy.get('[data-cy=cart-notification]')
      .should('be.visible')
      .and('contain', 'Product added to cart');
  });

  it('updates cart total', () => {
    // Add first product
    cy.get('[data-cy=product-card]')
      .first()
      .within(() => {
        cy.get('[data-cy=price]').invoke('text').as('price1');
        cy.get('[data-cy=add-to-cart]').click();
      });

    // Add second product
    cy.get('[data-cy=product-card]')
      .eq(1)
      .within(() => {
        cy.get('[data-cy=price]').invoke('text').as('price2');
        cy.get('[data-cy=add-to-cart]').click();
      });

    // Verify cart total
    cy.get('[data-cy=cart-total]').should(($total) => {
      const total = parseFloat($total.text().replace('$', ''));
      expect(total).to.be.greaterThan(0);
    });
  });

  it('handles out of stock product', () => {
    cy.get('[data-cy=product-card]')
      .contains('Out of Stock')
      .parents('[data-cy=product-card]')
      .within(() => {
        cy.get('[data-cy=add-to-cart]').should('be.disabled');
      });
  });
});

#Testing Checkout Flow

// cypress/e2e/checkout/checkout.cy.ts
describe('Checkout Flow', () => {
  beforeEach(() => {
    // Login and add item to cart
    cy.login('customer@example.com', 'password123');
    cy.visit('/products');
    cy.get('[data-cy=product-card]').first().find('[data-cy=add-to-cart]').click();
    cy.visit('/cart');
  });

  it('completes checkout successfully', () => {
    cy.get('[data-cy=checkout-button]').click();

    // Shipping information
    cy.url().should('include', '/checkout/shipping');
    cy.get('[data-cy=first-name]').type('John');
    cy.get('[data-cy=last-name]').type('Doe');
    cy.get('[data-cy=address]').type('123 Main St');
    cy.get('[data-cy=city]').type('New York');
    cy.get('[data-cy=zip]').type('10001');
    cy.get('[data-cy=continue]').click();

    // Payment information
    cy.url().should('include', '/checkout/payment');
    cy.get('[data-cy=card-number]').type('4242424242424242');
    cy.get('[data-cy=expiry]').type('12/25');
    cy.get('[data-cy=cvc]').type('123');
    cy.get('[data-cy=place-order]').click();

    // Order confirmation
    cy.url().should('include', '/order-confirmation');
    cy.get('[data-cy=order-number]').should('be.visible');
    cy.get('[data-cy=success-message]').should('contain', 'Thank you for your order');
  });

  it('validates shipping form', () => {
    cy.get('[data-cy=checkout-button]').click();
    cy.get('[data-cy=continue]').click();

    cy.get('[data-cy=first-name-error]').should('be.visible');
    cy.get('[data-cy=last-name-error]').should('be.visible');
    cy.get('[data-cy=address-error]').should('be.visible');
  });
});

#Testing Error Scenarios

// cypress/e2e/errors/api-errors.cy.ts
describe('API Error Handling', () => {
  it('handles server error during product load', () => {
    cy.intercept('GET', '/api/products', {
      statusCode: 500,
      body: { error: 'Server error' },
    }).as('productError');

    cy.visit('/products');
    cy.wait('@productError');

    cy.get('[data-cy=error-message]')
      .should('be.visible')
      .and('contain', 'Failed to load products');
    cy.get('[data-cy=retry-button]').should('be.visible');
  });

  it('handles network timeout', () => {
    cy.intercept('GET', '/api/products', (req) => {
      req.on('response', (res) => {
        res.setDelay(10000);
      });
    }).as('slowProducts');

    cy.visit('/products', { timeout: 5000 });
    cy.get('[data-cy=loading-spinner]').should('be.visible');
  });

  it('shows offline message when disconnected', () => {
    cy.intercept('GET', '/api/products', {
      forceNetworkError: true,
    }).as('networkError');

    cy.visit('/products');
    cy.wait('@networkError');

    cy.get('[data-cy=offline-message]').should('be.visible');
  });
});

#19. Real-World Project: Todo Application Testing

#Project Overview

Test a Todo application with:

  • Add, complete, delete todos
  • Filter todos (All/Active/Completed)
  • Local storage persistence
  • API integration

#Testing Todo Creation

// cypress/e2e/todos/create.cy.ts
describe('Todo Creation', () => {
  beforeEach(() => {
    cy.visit('/todos');
  });

  it('adds a new todo', () => {
    const todoText = 'Learn Cypress';

    cy.get('[data-cy=new-todo]').type(todoText);
    cy.get('[data-cy=add-todo]').click();

    cy.get('[data-cy=todo-list]').should('contain', todoText);
    cy.get('[data-cy=new-todo]').should('have.value', '');
  });

  it('prevents adding empty todo', () => {
    cy.get('[data-cy=add-todo]').click();

    cy.get('[data-cy=error]').should('be.visible');
    cy.get('[data-cy=todo-item]').should('have.length', 0);
  });

  it('adds multiple todos', () => {
    const todos = ['Todo 1', 'Todo 2', 'Todo 3'];

    todos.forEach((todo) => {
      cy.get('[data-cy=new-todo]').type(todo);
      cy.get('[data-cy=add-todo]').click();
    });

    cy.get('[data-cy=todo-item]').should('have.length', todos.length);
    todos.forEach((todo) => {
      cy.contains(todo).should('be.visible');
    });
  });
});

#Testing Todo Completion

// cypress/e2e/todos/toggle.cy.ts
describe('Todo Completion', () => {
  beforeEach(() => {
    cy.visit('/todos');
    cy.createTodo('Test Todo');
  });

  it('marks todo as completed', () => {
    cy.get('[data-cy=todo-checkbox]').check();

    cy.get('[data-cy=todo-text]').should('have.class', 'completed');
    cy.get('[data-cy=completed-count]').should('contain', '1');
  });

  it('unmarks completed todo', () => {
    cy.get('[data-cy=todo-checkbox]').check();
    cy.get('[data-cy=todo-checkbox]').uncheck();

    cy.get('[data-cy=todo-text]').should('not.have.class', 'completed');
    cy.get('[data-cy=completed-count]').should('contain', '0');
  });

  it('deletes a todo', () => {
    cy.get('[data-cy=delete-todo]').click();

    cy.get('[data-cy=todo-item]').should('not.exist');
    cy.get('[data-cy=empty-message]').should('be.visible');
  });
});

#Testing Filtering

// cypress/e2e/todos/filter.cy.ts
describe('Todo Filtering', () => {
  beforeEach(() => {
    cy.visit('/todos');

    // Create todos
    cy.createTodo('Active Todo 1');
    cy.createTodo('Active Todo 2');
    cy.createTodo('Completed Todo');

    // Complete one todo
    cy.get('[data-cy=todo-checkbox]').eq(2).check();
  });

  it('shows all todos', () => {
    cy.get('[data-cy=filter-all]').click();

    cy.get('[data-cy=todo-item]').should('have.length', 3);
    cy.contains('Active Todo 1').should('be.visible');
    cy.contains('Completed Todo').should('be.visible');
  });

  it('shows active todos only', () => {
    cy.get('[data-cy=filter-active]').click();

    cy.get('[data-cy=todo-item]').should('have.length', 2);
    cy.contains('Active Todo 1').should('be.visible');
    cy.contains('Completed Todo').should('not.exist');
  });

  it('shows completed todos only', () => {
    cy.get('[data-cy=filter-completed]').click();

    cy.get('[data-cy=todo-item]').should('have.length', 1);
    cy.contains('Completed Todo').should('be.visible');
    cy.contains('Active Todo 1').should('not.exist');
  });
});

#Testing Local Storage

// cypress/e2e/todos/persistence.cy.ts
describe('Todo Persistence', () => {
  it('saves todos to localStorage', () => {
    cy.visit('/todos');
    cy.createTodo('Persisted Todo');

    cy.window().then((win) => {
      const saved = JSON.parse(win.localStorage.getItem('todos') || '[]');
      expect(saved).to.have.length(1);
      expect(saved[0].text).to.equal('Persisted Todo');
    });
  });

  it('loads todos from localStorage', () => {
    // Seed localStorage with data
    const todos = [
      { id: '1', text: 'Saved Todo 1', completed: false },
      { id: '2', text: 'Saved Todo 2', completed: true },
    ];

    cy.window().then((win) => {
      win.localStorage.setItem('todos', JSON.stringify(todos));
    });

    cy.visit('/todos');

    cy.contains('Saved Todo 1').should('be.visible');
    cy.contains('Saved Todo 2').should('be.visible');
  });

  it('persists todo completion state', () => {
    cy.visit('/todos');
    cy.createTodo('Test Todo');
    cy.get('[data-cy=todo-checkbox]').check();

    // Reload page
    cy.reload();

    cy.get('[data-cy=todo-checkbox]').should('be.checked');
    cy.get('[data-cy=todo-text]').should('have.class', 'completed');
  });
});

#Testing API Integration

// cypress/e2e/todos/api.cy.ts
describe('Todo API Integration', () => {
  it('fetches todos from API', () => {
    const mockTodos = [
      { id: 1, text: 'API Todo 1', completed: false },
      { id: 2, text: 'API Todo 2', completed: true },
    ];

    cy.intercept('GET', '/api/todos', {
      statusCode: 200,
      body: mockTodos,
    }).as('getTodos');

    cy.visit('/todos');
    cy.wait('@getTodos');

    cy.contains('API Todo 1').should('be.visible');
    cy.contains('API Todo 2').should('be.visible');
  });

  it('creates todo via API', () => {
    const newTodo = { id: 3, text: 'New Todo', completed: false };

    cy.intercept('POST', '/api/todos', {
      statusCode: 201,
      body: newTodo,
    }).as('createTodo');

    cy.visit('/todos');
    cy.get('[data-cy=new-todo]').type('New Todo');
    cy.get('[data-cy=add-todo]').click();

    cy.wait('@createTodo').its('request.body').should('deep.equal', {
      text: 'New Todo',
    });

    cy.contains('New Todo').should('be.visible');
  });

  it('handles API error when creating todo', () => {
    cy.intercept('POST', '/api/todos', {
      statusCode: 500,
      body: { error: 'Server error' },
    }).as('createTodoError');

    cy.visit('/todos');
    cy.get('[data-cy=new-todo]').type('Failing Todo');
    cy.get('[data-cy=add-todo]').click();

    cy.wait('@createTodoError');
    cy.get('[data-cy=error]').should('be.visible');
  });
});

This guide covers the essential aspects of Cypress from beginner to advanced, with TypeScript examples throughout. Practice by building the real-world projects and refer to the official documentation for deeper dives. Happy testing!

Share this post