E2E Testing with Cypress
Introduction
Cypress provides end-to-end testing in real browsers, validating complete user flows from UI interactions to API calls. Use Cypress to test critical paths that users actually follow.
Focus: User journeys, authentication flows, CRUD operations, navigation.
Cypress Setup
Configuration
// test/cypress.config.js
module.exports = defineConfig({
e2e: {
baseUrl: 'http://localhost:5173',
specPattern: 'test/cypress/e2e/**/*.cy.{js,ts}',
viewportWidth: 1280,
viewportHeight: 720,
video: true,
screenshotOnRunFailure: true,
env: {
TEST_USER_EMAIL: 'user@example.com',
TEST_USER_PASSWORD: 'Testing1234',
},
},
})
Running Cypress
# Interactive mode (development)
pnpm cypress:open
# Headless mode (CI)
pnpm cypress:run
# Specific test file
pnpm cypress:run --spec "test/cypress/e2e/auth/login.cy.ts"
# Specific browser
pnpm cypress:run --browser chrome
Test Structure
Basic Test Pattern
// test/cypress/e2e/tasks/create-task.cy.ts
describe('Create Task', () => {
beforeEach(() => {
// Setup: Login before each test
cy.visit('/login')
cy.get('[data-cy="email"]').type('user@example.com')
cy.get('[data-cy="password"]').type('password123')
cy.get('[data-cy="login-btn"]').click()
cy.url().should('include', '/dashboard')
})
it('should create a new task successfully', () => {
// Navigate to tasks
cy.visit('/dashboard/tasks')
// Open create modal
cy.get('[data-cy="create-task-btn"]').click()
// Fill form
cy.get('[data-cy="task-title"]').type('Test Task')
cy.get('[data-cy="task-description"]').type('Task description')
// Submit
cy.get('[data-cy="submit-btn"]').click()
// Verify success
cy.contains('Task created successfully')
cy.get('[data-cy="task-list"]').should('contain', 'Test Task')
})
it('should show validation error for empty title', () => {
cy.visit('/dashboard/tasks')
cy.get('[data-cy="create-task-btn"]').click()
cy.get('[data-cy="submit-btn"]').click()
cy.contains('Title is required')
})
})
Best Practices
Use data-cy Attributes
// ✅ CORRECT - Use data-cy for test selectors
<button data-cy="submit-btn">Submit</button>
<input data-cy="email-input" type="email" />
// ❌ WRONG - Don't use CSS classes or IDs
<button className="btn-primary">Submit</button>
<input id="email" type="email" />
Why: data-cy selectors are stable and don't break when styling changes.
Custom Commands
// test/cypress/support/commands.ts
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="login-btn"]').click()
cy.url().should('include', '/dashboard')
})
// Usage in tests
describe('Dashboard', () => {
beforeEach(() => {
cy.login('user@example.com', 'password123')
})
it('should display dashboard', () => {
cy.contains('Welcome to Dashboard')
})
})
Common Patterns
API Testing
describe('Tasks API', () => {
it('should fetch tasks via API', () => {
cy.request({
method: 'GET',
url: '/api/v1/tasks',
headers: {
'Authorization': `Bearer ${Cypress.env('API_KEY')}`
}
}).then((response) => {
expect(response.status).to.eq(200)
expect(response.body).to.have.property('data')
expect(response.body.data).to.be.an('array')
})
})
it('should create task via API', () => {
cy.request({
method: 'POST',
url: '/api/v1/tasks',
headers: {
'Authorization': `Bearer ${Cypress.env('API_KEY')}`
},
body: {
title: 'API Test Task',
description: 'Created via API'
}
}).then((response) => {
expect(response.status).to.eq(201)
expect(response.body.data.title).to.eq('API Test Task')
})
})
})
Waiting for Elements
// Wait for element to exist
cy.get('[data-cy="task-list"]').should('exist')
// Wait for element to be visible
cy.get('[data-cy="modal"]').should('be.visible')
// Wait for text content
cy.contains('Loading...').should('not.exist')
cy.contains('Data loaded')
// Wait for API call
cy.intercept('GET', '/api/v1/tasks').as('getTasks')
cy.visit('/dashboard/tasks')
cy.wait('@getTasks')
Testing Forms
it('should validate form inputs', () => {
cy.visit('/dashboard/tasks/new')
// Empty form submission
cy.get('[data-cy="submit"]').click()
cy.contains('Title is required')
// Fill form
cy.get('[data-cy="title"]').type('Valid Title')
cy.get('[data-cy="description"]').type('Valid description')
// Successful submission
cy.get('[data-cy="submit"]').click()
cy.url().should('include', '/dashboard/tasks')
})
Testing Authentication
Login Flow
describe('Authentication', () => {
it('should login successfully', () => {
cy.visit('/login')
cy.get('[data-cy="email"]').type('user@example.com')
cy.get('[data-cy="password"]').type('password123')
cy.get('[data-cy="login-btn"]').click()
cy.url().should('include', '/dashboard')
cy.contains('Welcome')
})
it('should show error for invalid credentials', () => {
cy.visit('/login')
cy.get('[data-cy="email"]').type('wrong@example.com')
cy.get('[data-cy="password"]').type('wrongpassword')
cy.get('[data-cy="login-btn"]').click()
cy.contains('Invalid credentials')
cy.url().should('include', '/login')
})
it('should logout successfully', () => {
cy.login('user@example.com', 'password123')
cy.get('[data-cy="user-menu"]').click()
cy.get('[data-cy="logout-btn"]').click()
cy.url().should('include', '/login')
})
})
Intercepting API Calls
Mock API Responses
describe('Tasks with mocked API', () => {
beforeEach(() => {
// Mock API response
cy.intercept('GET', '/api/v1/tasks', {
statusCode: 200,
body: {
data: [
{ id: '1', title: 'Task 1' },
{ id: '2', title: 'Task 2' },
]
}
}).as('getTasks')
})
it('should display mocked tasks', () => {
cy.visit('/dashboard/tasks')
cy.wait('@getTasks')
cy.contains('Task 1')
cy.contains('Task 2')
})
})
Videos and Screenshots
Automatic Capture
// Videos: Automatically recorded in test/cypress/videos/
// Screenshots: Captured on test failure in test/cypress/screenshots/
// Manual screenshot
cy.screenshot('custom-name')
// Screenshot specific element
cy.get('[data-cy="chart"]').screenshot('chart-view')
Best Practices Summary
✅ DO
// Use data-cy selectors
cy.get('[data-cy="submit-btn"]')
// Wait for async operations
cy.wait('@apiCall')
cy.get('[data-cy="result"]').should('exist')
// Clean up after tests
afterEach(() => {
// Delete created data
})
// Use descriptive test names
it('should create task with valid title and description', () => {})
❌ DON'T
// Use fragile selectors
cy.get('.btn-primary') // Breaks if CSS changes
cy.get('#submit') // Breaks if ID changes
// Use arbitrary waits
cy.wait(5000) // Use cy.wait('@alias') instead
// Test too much in one test
// Split into multiple focused tests
// Ignore test isolation
// Each test should be independent
Quick Reference
// Navigation
cy.visit('/dashboard')
cy.go('back')
cy.reload()
// Selectors
cy.get('[data-cy="element"]')
cy.contains('Text content')
// Actions
cy.click()
cy.type('text')
cy.select('option')
cy.check()
cy.uncheck()
// Assertions
.should('exist')
.should('be.visible')
.should('have.text', 'Expected')
.should('have.value', 'value')
// API
cy.request('GET', '/api/endpoint')
cy.intercept('POST', '/api/endpoint').as('apiCall')
cy.wait('@apiCall')
Next Steps
- Write E2E tests for critical flows
- Add data-cy attributes to UI components
- See Testing API Endpoints for API testing patterns
Last Updated: 2025-11-20
Version: 1.0.0
Status: In Development