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:
- Test Behavior, Not Implementation: Focus on what users see and do, not internal component state.
- Automatic Waiting: Built-in retry-ability ensures tests wait for elements to be ready, just like a user would.
- Real Browser Environment: Tests run in a real browser, not a simulated DOM.
- 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:
- Browser Environment: Cypress injects itself into the browser, running tests inside the same event loop as your application.
- Dual iFrames: Your application runs in one iframe, the test code in another, providing isolation.
- Node.js Server: A local Node.js server handles file operations, network proxying, and cross-browser communication.
- Proxy Server: Intercepts all HTTP/HTTPS traffic, enabling request stubbing and mocking.
- 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()orwait()calls needed
#Cypress vs Selenium: Key Differences
| Aspect | Cypress | Selenium |
|---|---|---|
| Architecture | Runs inside browser | Runs outside browser via WebDriver |
| Language Support | JavaScript/TypeScript only | Java, Python, C#, Ruby, JS, etc. |
| Browser Support | Chrome, Firefox, Edge | All major browsers |
| Setup Complexity | Simple (npm install) | Complex (drivers, setup) |
| Test Speed | Fast (in-browser) | Slower (network communication) |
| Automatic Waiting | Built-in | Requires explicit waits |
| Debugging | Time travel, DevTools | External tools |
| Multi-tab Testing | Limited (plugin needed) | Full support |
| Mobile Testing | No native support | Via 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
| Command | Purpose | Example |
|---|---|---|
cy.get() | Select element by CSS selector | cy.get('.btn-primary') |
cy.contains() | Find element by text content | cy.contains('Submit') |
cy.find() | Find within previous subject | cy.get('form').find('input') |
cy.first() / cy.last() | First/last element | cy.get('li').first() |
cy.eq() | Element at index | cy.get('li').eq(2) |
cy.parent() | Parent element | cy.get('span').parent() |
cy.children() | Child elements | cy.get('ul').children() |
#Action Commands
| Command | Purpose | Example |
|---|---|---|
cy.click() | Click an element | cy.get('button').click() |
cy.dblclick() | Double click | cy.get('button').dblclick() |
cy.rightclick() | Right click | cy.get('button').rightclick() |
cy.type() | Type into input | cy.get('input').type('text') |
cy.clear() | Clear input | cy.get('input').clear() |
cy.select() | Select dropdown option | cy.get('select').select('option') |
cy.check() | Check checkbox/radio | cy.get('[type=checkbox]').check() |
cy.uncheck() | Uncheck checkbox | cy.get('[type=checkbox]').uncheck() |
cy.submit() | Submit form | cy.get('form').submit() |
cy.focus() | Focus element | cy.get('input').focus() |
cy.blur() | Blur element | cy.get('input').blur() |
cy.scrollTo() | Scroll to position | cy.scrollTo('bottom') |
#Window Commands
| Command | Purpose | Example |
|---|---|---|
cy.visit() | Navigate to URL | cy.visit('/dashboard') |
cy.go() | Browser back/forward | cy.go('back') |
cy.reload() | Reload page | cy.reload() |
cy.url() | Get current URL | cy.url().should('include', 'user') |
cy.title() | Get page title | cy.title().should('eq', 'Home') |
cy.location() | Get location object | cy.location('pathname') |
cy.viewport() | Set viewport size | cy.viewport(1280, 720) |
#Assertion Commands
| Command | Purpose | Example |
|---|---|---|
cy.should() | Make assertion | cy.get('h1').should('be.visible') |
cy.and() | Chain multiple assertions | cy.get('h1').should('be.visible').and('contain', 'Title') |
cy.expect() | Use Chai expect | expect(true).to.be.true |
#Utility Commands
| Command | Purpose | Example |
|---|---|---|
cy.wrap() | Wrap object as subject | cy.wrap({name: 'John'}).its('name') |
cy.invoke() | Invoke method | cy.get('button').invoke('text') |
cy.its() | Access property | cy.window().its('localStorage') |
cy.then() | Work with promises | cy.get('div').then($el => {...}) |
cy.each() | Iterate over elements | cy.get('li').each($li => {...}) |
cy.spread() | Spread array arguments | cy.get('div').spread((div1, div2) => {...}) |
cy.wait() | Wait for time or request | cy.wait(1000) or cy.wait('@getUser') |
cy.pause() | Pause execution | cy.pause() |
cy.debug() | Debug test | cy.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:
data-cy– Highest priority, dedicated for testingdata-test– Common test attributedata-testid– Alternative test IDdata-qa– QA-specific attributeid– Unique identifiername– Form field nameclass– CSS class (fragile)tag– Element typeattributes– Other attributesnth-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
beforeEachto 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:
- Open Cypress and run a spec file
- Click Edit in Studio on a test in the Command Log
- Interact with your application – clicks, typing, selections are recorded
- 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 screenshotscypress/videos/– Test recordings
#Common Errors and Solutions
| Error | Likely Cause | Solution |
|---|---|---|
cy.get() timed out | Element not in DOM | Increase timeout, check selector, ensure element exists |
cy.contains() timed out | Text not found | Check text content, wait for async load |
| Cross-origin error | Navigating to different origin | Use cy.origin() or test within same origin |
| Element detached from DOM | DOM re-rendered | Re-query element after changes |
| Request never completed | Network issue | Check server, mock if needed |
#15. Best Practices
#Test Independence
Each test should be able to run alone:
- Use
beforeEachto set up state - Don’t rely on previous test results
- Clean up after tests
#Selector Strategy
Follow this priority:
data-cy– Dedicated test attributes- Text content – For user-facing elements
- Accessible roles – For accessibility testing
- 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
- Reduction of Flakiness: Automatic waiting and retries
- Ease of Setup: Two commands:
npm installandnpx cypress open - Strong Community Support: 10,000+ Discord community
- Time Travel Debugging: Inspect any test step
- Better Developer Experience: Intuitive API
#Migration Strategy
Follow this approach:
- Evaluate test suite – Identify priority test cases
- Start small – Begin with low-hanging fruit
- Rebuild selectors – Replace XPath with data attributes
- Convert page objects – Adapt to Cypress patterns
- 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');
});
});
#Testing Product Search
// 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!