Introduction to Cypress
Cypress is a modern JavaScript-based end-to-end testing framework designed to address the pain points developers experience with traditional testing tools. Similar to how React revolutionized UI development, Cypress has transformed E2E testing with its developer-friendly approach.
Key Differentiators
- Architecture: Runs directly in the browser, not over WebDriver
- Visibility: Tests run in the same loop as your application
- Debugging: Time-travel capabilities with screenshots at each step
- Waiting: Automatic waiting without explicit sleep statements
- Network Control: Ability to stub and spy on network requests
- Testing Experience: Developer-friendly with modern JavaScript syntax
Cypress vs. Selenium: A Developer's Perspective
Traditional WebDriver-based tools like Selenium operate outside the browser, sending commands over a network connection. This creates timing and synchronization issues that plague many E2E test suites.
Cypress, by comparison, executes directly in the browser alongside your application code. This gives it real-time access to DOM elements, network requests, and application state without the flakiness caused by timing issues.
Think of it like the difference between remotely controlling a car versus sitting in the driver's seat.
Setting Up Cypress
Installation
To get started with Cypress, you'll need to install it as a development dependency in your project:
npm install cypress --save-dev
Once installed, you can open Cypress with:
npx cypress open
The first time you run this command, Cypress will create its initial folder structure including example tests.
Project Structure
After initialization, you'll have a cypress folder with the following structure:
cypress/
├── e2e/ # Contains your test files
├── fixtures/ # Test data files (JSON, etc.)
├── support/ # Support files (commands, utilities)
│ ├── commands.js # Custom commands
│ └── e2e.js # Global configuration
└── downloads/ # Files downloaded during tests
Configuration
Cypress can be configured through a cypress.config.js file in your project root:
// cypress.config.js
const { defineConfig } = require('cypress')
module.exports = defineConfig({
e2e: {
baseUrl: 'http://localhost:3000',
viewportWidth: 1280,
viewportHeight: 720,
defaultCommandTimeout: 5000,
experimentalStudio: true,
setupNodeEvents(on, config) {
// implement node event listeners here
},
},
})
Adding Cypress Scripts to package.json
Add convenient npm scripts to run Cypress:
"scripts": {
"cypress:open": "cypress open",
"cypress:run": "cypress run",
"test:e2e": "start-server-and-test start http://localhost:3000 cypress:run"
}
Testing in Different Environments
You can create different configurations for various environments:
// cypress.config.js
const { defineConfig } = require('cypress')
module.exports = defineConfig({
e2e: {
// Base configuration for all environments
defaultCommandTimeout: 5000,
viewportWidth: 1280,
viewportHeight: 720,
// Environment-specific settings
env: {
// Default (development)
baseUrl: 'http://localhost:3000',
apiUrl: 'http://localhost:3001/api',
// Staging
staging: {
baseUrl: 'https://staging.example.com',
apiUrl: 'https://api-staging.example.com'
},
// Production
production: {
baseUrl: 'https://example.com',
apiUrl: 'https://api.example.com'
}
},
setupNodeEvents(on, config) {
// Use environment variable to switch environments
const environment = process.env.CYPRESS_ENVIRONMENT || 'development'
if (environment !== 'development') {
// Override settings with environment-specific ones
config.baseUrl = config.env[environment].baseUrl
config.env.apiUrl = config.env[environment].apiUrl
}
return config
}
}
})
Then run with: CYPRESS_ENVIRONMENT=staging npx cypress run
Writing Your First Cypress Test
Let's start with a simple test to understand the basic structure and syntax of Cypress tests.
Basic Test Structure
// cypress/e2e/home.cy.js
describe('Home Page', () => {
beforeEach(() => {
// Visit the homepage before each test
cy.visit('/')
})
it('displays the welcome message', () => {
// Check if the welcome message is displayed
cy.get('h1').should('contain', 'Welcome to Our App')
})
it('navigates to login page when login button is clicked', () => {
// Find and click the login button
cy.get('a[href="/login"]').click()
// Verify we've navigated to the login page
cy.url().should('include', '/login')
cy.get('h1').should('contain', 'Login')
})
})
Key Cypress Commands
cy.visit()- Navigate to a URLcy.get()- Select elements using CSS selectorscy.click()- Click on an elementcy.type()- Type text into an input fieldcy.should()- Make assertions about elementscy.wait()- Wait for a specific amount of time or for an aliascy.intercept()- Intercept and manage network requests
Cypress Chaining Syntax
Cypress uses a unique chainable API that's both powerful and readable:
cy.get('form')
.find('input[name="email"]')
.type('test@example.com')
.should('have.value', 'test@example.com')
cy.get('form')
.find('input[name="password"]')
.type('password123')
.should('have.value', 'password123')
cy.get('form')
.find('button[type="submit"]')
.click()
cy.url().should('include', '/dashboard')
Understanding Cypress's Automatic Waiting
One of Cypress's most powerful features is its automatic waiting. Unlike other testing tools, Cypress automatically waits for elements to exist and be actionable before interacting with them.
// No need for explicit waits like this:
// await driver.wait(until.elementLocated(By.css('.dynamic-element')), 5000);
// Cypress automatically waits up to the defaultCommandTimeout:
cy.get('.dynamic-element').click() // Waits for element before clicking
This eliminates the need for arbitrary timeouts and sleep commands that make tests flaky.
Selectors and Element Interaction
Best Practices for Selectors
Choosing the right selectors is crucial for test stability. Here's a ranking from most to least stable:
- Data Attributes:
cy.get('[data-testid="submit-button"]') - CSS IDs:
cy.get('#username') - CSS Classes:
cy.get('.btn-primary') - Element Types with Attributes:
cy.get('input[name="email"]') - Text Content:
cy.contains('Sign In') - DOM Structure:
cy.get('form > button')
Adding Data Attributes for Testing
The most maintainable approach is to add dedicated data attributes to your HTML:
<button
data-testid="login-button"
class="btn btn-primary"
>
Sign In
</button>
// In your test:
cy.get('[data-testid="login-button"]').click()
Common Element Interactions
// Clicking elements
cy.get('[data-testid="submit-button"]').click()
cy.get('.dropdown-toggle').click()
// Typing and clearing input
cy.get('#email').type('test@example.com')
cy.get('#password').type('password123')
cy.get('#search').clear().type('new search term')
// Checkboxes and radio buttons
cy.get('#terms-checkbox').check()
cy.get('#terms-checkbox').uncheck()
cy.get('[type="radio"]').check('option1')
// Select dropdowns
cy.get('select').select('Option 1')
// Multiple elements
cy.get('ul > li').should('have.length', 5)
cy.get('ul > li').eq(2).should('contain', 'Item 3')
// Dragable elements
cy.get('.dragable').drag('.drop-target') // Requires plugin
Working with iframes
Working with iframes requires the cypress-iframe plugin:
// Install the plugin
npm install -D cypress-iframe
// Import in support file
// cypress/support/e2e.js
import 'cypress-iframe'
// Use in tests
cy.frameLoaded('#my-iframe') // Load the iframe
cy.iframe().find('#element-inside-iframe').should('be.visible')
cy.iframe().find('#button-inside-iframe').click()
Working with Shadow DOM
Modern web components often use Shadow DOM, which requires special handling:
// Basic Shadow DOM traversal
cy.get('my-component')
.shadow()
.find('.inside-shadow-dom')
.click()
// Deeper Shadow DOM traversal
cy.get('parent-component')
.shadow()
.find('child-component')
.shadow()
.find('.deeply-nested')
.should('be.visible')
Assertions and Expectations
Cypress builds on Chai, Sinon, and other libraries to provide powerful assertion capabilities.
Basic Assertions
// Visibility
cy.get('.element').should('be.visible')
cy.get('.hidden-element').should('not.be.visible')
// Existence
cy.get('.existing-element').should('exist')
cy.get('.non-existent-element').should('not.exist')
// Text content
cy.get('h1').should('contain', 'Welcome')
cy.get('p').should('have.text', 'Exact text match')
// Attribute values
cy.get('input').should('have.value', 'Input text')
cy.get('[data-testid="status"]').should('have.attr', 'data-status', 'active')
// CSS classes
cy.get('button').should('have.class', 'active')
cy.get('button').should('not.have.class', 'disabled')
// CSS properties
cy.get('.element').should('have.css', 'color', 'rgb(255, 0, 0)')
// Element state
cy.get('input').should('be.disabled')
cy.get('input').should('be.enabled')
cy.get('input[type="checkbox"]').should('be.checked')
// Count elements
cy.get('ul > li').should('have.length', 5)
cy.get('tr').should('have.length.at.least', 1)
cy.get('.results').should('have.length.lessThan', 10)
Chaining Assertions
cy.get('.element')
.should('be.visible')
.and('contain', 'Expected text')
.and('have.class', 'active')
Common Assertion Patterns
// Verify a successful form submission
cy.get('form').submit()
cy.get('.success-message').should('be.visible')
cy.get('.error-message').should('not.exist')
// Verify correct navigation
cy.get('.nav-link').click()
cy.url().should('include', '/expected-path')
cy.get('h1').should('contain', 'Expected Page Title')
// Verify list contents
cy.get('.user-list li')
.should('have.length', 3)
.first().should('contain', 'User One')
.next().should('contain', 'User Two')
.next().should('contain', 'User Three')
// Verify element state after interaction
cy.get('.accordion-header').click()
cy.get('.accordion-content')
.should('be.visible')
.and('contain', 'Accordion content')
// More complex DOM verification
cy.get('table tbody tr')
.should('have.length.greaterThan', 0)
.each(($row) => {
cy.wrap($row)
.find('td')
.first()
.should('not.be.empty')
})
Advanced: Custom Assertions
You can create custom assertions for specific application needs:
// cypress/support/commands.js
Chai.Assertion.addMethod('containIgnoreCase', function(text) {
const actualText = this._obj.text().toLowerCase()
const expectedText = text.toLowerCase()
this.assert(
actualText.includes(expectedText),
`expected '${actualText}' to contain '${expectedText}' (case insensitive)`,
`expected '${actualText}' not to contain '${expectedText}' (case insensitive)`
)
})
// Register custom Cypress command that uses the assertion
Cypress.Commands.add('shouldContainIgnoreCase', { prevSubject: true }, (subject, text) => {
cy.wrap(subject).should(($el) => {
new Chai.Assertion($el).containIgnoreCase(text)
})
return cy.wrap(subject)
})
// Use in tests
cy.get('h1').shouldContainIgnoreCase('welcome')
Testing User Authentication
Authentication is a critical part of most applications, and testing it efficiently is important.
Testing the Login Flow via UI
describe('Authentication', () => {
it('should login successfully with valid credentials', () => {
cy.visit('/login')
cy.get('[data-testid="email-input"]').type('valid@example.com')
cy.get('[data-testid="password-input"]').type('correctpassword')
cy.get('[data-testid="login-button"]').click()
// Verify successful login
cy.url().should('include', '/dashboard')
cy.get('[data-testid="user-greeting"]').should('contain', 'Welcome')
cy.getCookie('auth_token').should('exist')
})
it('should show error with invalid credentials', () => {
cy.visit('/login')
cy.get('[data-testid="email-input"]').type('valid@example.com')
cy.get('[data-testid="password-input"]').type('wrongpassword')
cy.get('[data-testid="login-button"]').click()
// Verify error message
cy.get('[data-testid="login-error"]')
.should('be.visible')
.and('contain', 'Invalid email or password')
// Verify we're still on the login page
cy.url().should('include', '/login')
})
it('should validate form inputs', () => {
cy.visit('/login')
// Submit without entering anything
cy.get('[data-testid="login-button"]').click()
// Verify validation errors
cy.get('[data-testid="email-error"]').should('be.visible')
cy.get('[data-testid="password-error"]').should('be.visible')
// Enter invalid email
cy.get('[data-testid="email-input"]').type('not-an-email')
cy.get('[data-testid="login-button"]').click()
// Verify email validation error
cy.get('[data-testid="email-error"]')
.should('be.visible')
.and('contain', 'valid email')
})
})
Programmatic Login for Efficiency
For tests that require an authenticated user but aren't testing the login flow itself, use a programmatic login approach to save time:
// cypress/support/commands.js
Cypress.Commands.add('login', (email = 'test@example.com', password = 'password123') => {
// Option 1: UI Login (slow but tests full flow)
// cy.visit('/login')
// cy.get('[data-testid="email-input"]').type(email)
// cy.get('[data-testid="password-input"]').type(password)
// cy.get('[data-testid="login-button"]').click()
// Option 2: API Login (fast, skips UI)
cy.request({
method: 'POST',
url: '/api/login',
body: { email, password }
}).then((response) => {
expect(response.status).to.eq(200)
expect(response.body.token).to.exist
// Set the token in localStorage or as a cookie
window.localStorage.setItem('authToken', response.body.token)
cy.setCookie('auth_token', response.body.token)
})
// Visit the app to apply the authentication
cy.visit('/')
})
// Use in tests
describe('Authenticated Features', () => {
beforeEach(() => {
cy.login()
})
it('should display user profile', () => {
cy.visit('/profile')
cy.get('[data-testid="profile-name"]').should('contain', 'Test User')
})
})
Testing Authorization and Protected Routes
describe('Authorization', () => {
it('should redirect unauthenticated users to login', () => {
// Clear any existing auth
cy.clearCookies()
cy.clearLocalStorage()
// Try to access protected route
cy.visit('/admin/settings')
// Verify redirect to login
cy.url().should('include', '/login')
})
it('should deny access to unauthorized resources', () => {
// Login as regular user
cy.login('regular@example.com', 'password123')
// Try to access admin-only route
cy.visit('/admin/settings')
// Verify access denied
cy.get('[data-testid="access-denied"]').should('be.visible')
})
it('should allow admins to access admin features', () => {
// Login as admin
cy.login('admin@example.com', 'adminpass')
// Access admin route
cy.visit('/admin/settings')
// Verify access granted
cy.get('[data-testid="admin-settings"]').should('be.visible')
})
})
Testing OAuth Flows
Testing OAuth can be challenging due to third-party redirects. Here are some approaches:
- Mock the OAuth Provider: Use
cy.intercept()to simulate the OAuth flow - Use a Test User: Create a real user on the OAuth provider for testing
- Programmatic Authentication: Bypass the UI flow and set tokens directly
// Example of mocking OAuth response
cy.intercept('GET', '/api/auth/callback/google*', {
statusCode: 200,
body: {
token: 'fake-oauth-token',
user: {
id: '123',
name: 'Test User',
email: 'test@example.com'
}
}
}).as('oauthCallback')
cy.get('[data-testid="google-login"]').click()
cy.wait('@oauthCallback')
cy.url().should('include', '/dashboard')
Testing Forms and User Input
Basic Form Interaction
describe('Contact Form', () => {
beforeEach(() => {
cy.visit('/contact')
})
it('should submit the form with valid data', () => {
// Fill out the form
cy.get('#name').type('John Doe')
cy.get('#email').type('john@example.com')
cy.get('#subject').select('Support')
cy.get('#message').type('This is a test message')
cy.get('#terms').check()
// Submit the form
cy.get('form').submit()
// Verify success message
cy.get('.success-message')
.should('be.visible')
.and('contain', 'Thank you for your message')
})
it('should show validation errors for empty fields', () => {
// Submit empty form
cy.get('form').submit()
// Verify validation errors
cy.get('#name-error').should('be.visible')
cy.get('#email-error').should('be.visible')
cy.get('#message-error').should('be.visible')
// Fill one field and verify error is removed
cy.get('#name').type('John Doe')
cy.get('#name-error').should('not.exist')
// Other errors should still be visible
cy.get('#email-error').should('be.visible')
})
it('should validate email format', () => {
cy.get('#email').type('invalid-email')
cy.get('form').submit()
cy.get('#email-error')
.should('be.visible')
.and('contain', 'valid email')
})
})
Working with Different Input Types
describe('Form Input Types', () => {
beforeEach(() => {
cy.visit('/form-demo')
})
it('interacts with various input types', () => {
// Text input
cy.get('#text-input').type('Hello, World!')
// Number input
cy.get('#number-input').type('42')
// Date input
cy.get('#date-input').type('2025-01-01')
// Textarea
cy.get('#textarea').type('Multiple\nlines\nof text')
// Radio buttons
cy.get('#radio-option-2').check()
// Checkboxes
cy.get('#checkbox-1').check()
cy.get('#checkbox-3').check()
// Select dropdown
cy.get('#select-input').select('option2')
// Multi-select
cy.get('#multi-select').select(['option1', 'option3'])
// Range slider (requires using invoke to set value)
cy.get('#range-input').invoke('val', 75).trigger('change')
// Color picker
cy.get('#color-input').invoke('val', '#00ff00').trigger('change')
// File upload (requires a plugin or workaround)
cy.get('#file-input').attachFile('test-file.jpg')
})
})
Testing Form Submissions
describe('Form Submission', () => {
beforeEach(() => {
cy.visit('/signup')
})
it('submits form with network request', () => {
// Intercept the API request
cy.intercept('POST', '/api/users').as('createUser')
// Fill and submit form
cy.get('#username').type('newuser')
cy.get('#email').type('new@example.com')
cy.get('#password').type('Password123')
cy.get('#confirm-password').type('Password123')
cy.get('#signup-button').click()
// Wait for the request and verify payload
cy.wait('@createUser').then((interception) => {
expect(interception.request.body).to.deep.include({
username: 'newuser',
email: 'new@example.com'
})
// Password should be included but not verified here
expect(interception.request.body).to.have.property('password')
})
// Verify UI reflects successful submission
cy.url().should('include', '/welcome')
cy.get('.welcome-message').should('contain', 'newuser')
})
it('handles submission errors', () => {
// Intercept and mock a server error
cy.intercept('POST', '/api/users', {
statusCode: 400,
body: {
error: 'Email already exists'
}
}).as('failedCreate')
// Fill and submit form
cy.get('#username').type('newuser')
cy.get('#email').type('existing@example.com')
cy.get('#password').type('Password123')
cy.get('#confirm-password').type('Password123')
cy.get('#signup-button').click()
// Verify error handling
cy.wait('@failedCreate')
cy.get('.error-message')
.should('be.visible')
.and('contain', 'Email already exists')
// Form should not be cleared on error
cy.get('#username').should('have.value', 'newuser')
cy.get('#email').should('have.value', 'existing@example.com')
})
})
Testing File Uploads
File uploads require special handling in Cypress. The recommended approach is to use the cypress-file-upload plugin:
// Install the plugin
npm install --save-dev cypress-file-upload
// Import in your support file
// cypress/support/e2e.js
import 'cypress-file-upload';
// Use in your tests
describe('File Upload', () => {
it('uploads a file', () => {
cy.visit('/upload')
// Add test file to fixtures folder first
cy.get('input[type="file"]').attachFile('example.pdf')
cy.get('button[type="submit"]').click()
cy.get('.upload-success').should('be.visible')
cy.get('.file-name').should('contain', 'example.pdf')
})
it('uploads multiple files', () => {
cy.visit('/upload')
cy.get('input[type="file"][multiple]').attachFile([
'example1.pdf',
'example2.jpg'
])
cy.get('button[type="submit"]').click()
cy.get('.upload-success').should('be.visible')
cy.get('.file-count').should('contain', '2')
})
})
Network Requests and Mocking
Cypress provides powerful tools for testing API interactions and network requests.
Intercepting Network Requests
describe('Network Requests', () => {
beforeEach(() => {
cy.visit('/products')
})
it('loads products from API', () => {
// Intercept GET request to products API
cy.intercept('GET', '/api/products*').as('getProducts')
// Wait for the request to complete
cy.wait('@getProducts')
// Verify products are displayed
cy.get('.product-card').should('have.length.greaterThan', 0)
})
it('shows loading state while fetching data', () => {
// Intercept but delay the response
cy.intercept('GET', '/api/products*', (req) => {
req.reply((res) => {
// Delay the response by 1 second
res.delay = 1000
return res
})
}).as('delayedProducts')
// Reload the page
cy.reload()
// Verify loading state is shown
cy.get('.loading-spinner').should('be.visible')
// Wait for request to finish
cy.wait('@delayedProducts')
// Verify loading state is hidden
cy.get('.loading-spinner').should('not.exist')
// Verify products are displayed
cy.get('.product-card').should('have.length.greaterThan', 0)
})
})
Mocking API Responses
describe('API Mocking', () => {
it('displays product data from mock API', () => {
// Create mock product data
const mockProducts = [
{ id: 1, name: 'Mock Product 1', price: 19.99, image: 'mock1.jpg' },
{ id: 2, name: 'Mock Product 2', price: 29.99, image: 'mock2.jpg' },
{ id: 3, name: 'Mock Product 3', price: 39.99, image: 'mock3.jpg' }
]
// Intercept the API request and return mock data
cy.intercept('GET', '/api/products*', {
statusCode: 200,
body: mockProducts
}).as('getMockProducts')
// Visit the products page
cy.visit('/products')
// Wait for the mocked request
cy.wait('@getMockProducts')
// Verify mock data is displayed
cy.get('.product-card').should('have.length', 3)
cy.get('.product-card').first().should('contain', 'Mock Product 1')
cy.get('.product-card').first().should('contain', '$19.99')
})
it('handles empty product list', () => {
// Mock empty product list
cy.intercept('GET', '/api/products*', {
statusCode: 200,
body: []
}).as('getEmptyProducts')
// Visit the products page
cy.visit('/products')
// Wait for the mocked request
cy.wait('@getEmptyProducts')
// Verify empty state is displayed
cy.get('.product-card').should('not.exist')
cy.get('.empty-state').should('be.visible')
cy.get('.empty-state').should('contain', 'No products found')
})
it('handles API error states', () => {
// Mock server error
cy.intercept('GET', '/api/products*', {
statusCode: 500,
body: {
error: 'Internal server error'
}
}).as('getProductsError')
// Visit the products page
cy.visit('/products')
// Wait for the error response
cy.wait('@getProductsError')
// Verify error state is displayed
cy.get('.error-message').should('be.visible')
cy.get('.error-message').should('contain', 'Failed to load products')
// Retry button should be visible
cy.get('.retry-button').should('be.visible')
// Test retry functionality
cy.intercept('GET', '/api/products*', {
statusCode: 200,
body: [{ id: 1, name: 'Product after retry', price: 19.99 }]
}).as('retryProducts')
cy.get('.retry-button').click()
cy.wait('@retryProducts')
// Verify products loaded after retry
cy.get('.product-card').should('have.length', 1)
cy.get('.error-message').should('not.exist')
})
})
Testing Form Submissions with Network Requests
describe('Form with API Interaction', () => {
beforeEach(() => {
cy.visit('/checkout')
})
it('submits order and shows confirmation', () => {
// Mock a successful order submission
cy.intercept('POST', '/api/orders', {
statusCode: 201,
body: {
id: 'order-123',
status: 'confirmed',
total: 99.99
}
}).as('createOrder')
// Fill out the checkout form
cy.get('#name').type('Test User')
cy.get('#address').type('123 Test St')
cy.get('#city').type('Test City')
cy.get('#zip').type('12345')
cy.get('#payment-card').type('4242424242424242')
cy.get('#payment-expiry').type('1225')
cy.get('#payment-cvc').type('123')
// Submit the form
cy.get('#checkout-button').click()
// Wait for the API request and verify the payload
cy.wait('@createOrder').then((interception) => {
// Verify request payload
expect(interception.request.body).to.have.property('name', 'Test User')
expect(interception.request.body).to.have.property('address', '123 Test St')
// Verify payment details are included but don't check actual values for security
expect(interception.request.body).to.have.property('payment')
})
// Verify confirmation page
cy.url().should('include', '/confirmation')
cy.get('.order-id').should('contain', 'order-123')
cy.get('.order-status').should('contain', 'confirmed')
cy.get('.order-total').should('contain', '99.99')
})
it('handles validation errors from server', () => {
// Mock a validation error response
cy.intercept('POST', '/api/orders', {
statusCode: 400,
body: {
errors: {
name: 'Name is required',
payment: 'Invalid card number'
}
}
}).as('orderValidationError')
// Fill out form with some invalid data
cy.get('#address').type('123 Test St')
cy.get('#city').type('Test City')
cy.get('#zip').type('12345')
// Submit the form
cy.get('#checkout-button').click()
// Wait for the error response
cy.wait('@orderValidationError')
// Verify error messages are displayed
cy.get('#name-error').should('contain', 'Name is required')
cy.get('#payment-error').should('contain', 'Invalid card number')
// Verify we're still on the checkout page
cy.url().should('include', '/checkout')
})
it('handles network failures gracefully', () => {
// Mock a network failure
cy.intercept('POST', '/api/orders', {
forceNetworkError: true
}).as('networkError')
// Fill out the checkout form
cy.get('#name').type('Test User')
cy.get('#address').type('123 Test St')
cy.get('#city').type('Test City')
cy.get('#zip').type('12345')
cy.get('#payment-card').type('4242424242424242')
cy.get('#payment-expiry').type('1225')
cy.get('#payment-cvc').type('123')
// Submit the form
cy.get('#checkout-button').click()
// Wait for the network error
cy.wait('@networkError')
// Verify error message is displayed
cy.get('.network-error')
.should('be.visible')
.and('contain', 'connection')
// Verify retry option is available
cy.get('.retry-button').should('be.visible')
})
})
Advanced: Stubbing Complex API Behaviors
You can simulate more complex API behaviors by using dynamic responses:
describe('Shopping Cart', () => {
beforeEach(() => {
// Start with an empty cart
cy.intercept('GET', '/api/cart', {
statusCode: 200,
body: { items: [], total: 0 }
}).as('getEmptyCart')
cy.visit('/products')
})
it('adds and removes items from cart', () => {
// Keep track of cart items for dynamic responses
let cartItems = []
// Mock add to cart endpoint
cy.intercept('POST', '/api/cart/items', (req) => {
const newItem = req.body
cartItems.push(newItem)
req.reply({
statusCode: 200,
body: {
items: cartItems,
total: cartItems.reduce((sum, item) => sum + item.price * item.quantity, 0)
}
})
}).as('addToCart')
// Mock remove from cart endpoint
cy.intercept('DELETE', '/api/cart/items/*', (req) => {
const itemId = req.url.split('/').pop()
cartItems = cartItems.filter(item => item.id !== itemId)
req.reply({
statusCode: 200,
body: {
items: cartItems,
total: cartItems.reduce((sum, item) => sum + item.price * item.quantity, 0)
}
})
}).as('removeFromCart')
// Add first product to cart
cy.get('.product-card').first().find('.product-name').invoke('text').as('firstProductName')
cy.get('.product-card').first().find('.add-to-cart').click()
// Wait for the request and verify cart updated
cy.wait('@addToCart')
cy.get('.cart-count').should('contain', '1')
// Go to cart page
cy.get('.cart-icon').click()
cy.url().should('include', '/cart')
// Verify product is in cart
cy.get('@firstProductName').then((productName) => {
cy.get('.cart-item-name').should('contain', productName)
})
// Remove item from cart
cy.get('.remove-item').click()
// Wait for the request and verify cart is empty
cy.wait('@removeFromCart')
cy.get('.cart-item').should('not.exist')
cy.get('.empty-cart-message').should('be.visible')
})
})
Managing Test Data and State
Proper test data management is essential for reliable and maintainable E2E tests.
Using Fixtures for Test Data
// cypress/fixtures/users.json
{
"admin": {
"id": "1",
"email": "admin@example.com",
"password": "adminpass",
"name": "Admin User",
"role": "admin"
},
"customer": {
"id": "2",
"email": "customer@example.com",
"password": "customerpass",
"name": "Test Customer",
"role": "customer"
}
}
// cypress/fixtures/products.json
[
{
"id": "prod-1",
"name": "Test Product 1",
"price": 19.99,
"description": "This is a test product",
"image": "test1.jpg",
"category": "electronics"
},
{
"id": "prod-2",
"name": "Test Product 2",
"price": 29.99,
"description": "Another test product",
"image": "test2.jpg",
"category": "clothing"
}
]
// Using fixtures in tests
describe('Product Management', () => {
let adminUser;
let testProducts;
before(() => {
// Load test data from fixtures
cy.fixture('users').then((users) => {
adminUser = users.admin;
});
cy.fixture('products').then((products) => {
testProducts = products;
});
});
beforeEach(() => {
// Login as admin
cy.login(adminUser.email, adminUser.password);
// Mock products API using fixture data
cy.intercept('GET', '/api/products', {
statusCode: 200,
body: testProducts
}).as('getProducts');
cy.visit('/admin/products');
cy.wait('@getProducts');
});
it('displays product list', () => {
cy.get('.product-row').should('have.length', testProducts.length);
// Check first product details
cy.get('.product-row').first().within(() => {
cy.get('.product-name').should('contain', testProducts[0].name);
cy.get('.product-price').should('contain', testProducts[0].price);
});
});
})
Data Setup Using Custom Commands
// cypress/support/commands.js
Cypress.Commands.add('createTestProduct', (overrides = {}) => {
// Default product data
const defaultProduct = {
name: `Test Product ${Date.now()}`,
price: 19.99,
description: 'This is an automatically created test product',
category: 'test',
inventory: 100
};
// Merge defaults with overrides
const product = { ...defaultProduct, ...overrides };
// Create product via API
return cy.request({
method: 'POST',
url: '/api/products',
headers: {
'Authorization': `Bearer ${Cypress.env('adminToken')}`
},
body: product
}).then((response) => {
expect(response.status).to.eq(201);
return response.body; // Return created product
});
});
Cypress.Commands.add('deleteTestProduct', (productId) => {
return cy.request({
method: 'DELETE',
url: `/api/products/${productId}`,
headers: {
'Authorization': `Bearer ${Cypress.env('adminToken')}`
}
}).then((response) => {
expect(response.status).to.eq(204);
});
});
// Using data setup commands in tests
describe('Product Detail', () => {
let testProduct;
before(() => {
// Set admin token (could come from a login command)
Cypress.env('adminToken', 'your-admin-token');
// Create a test product
cy.createTestProduct().then((product) => {
testProduct = product;
});
});
after(() => {
// Clean up test product
if (testProduct && testProduct.id) {
cy.deleteTestProduct(testProduct.id);
}
});
it('displays product details correctly', () => {
cy.visit(`/products/${testProduct.id}`);
cy.get('.product-name').should('contain', testProduct.name);
cy.get('.product-price').should('contain', testProduct.price);
cy.get('.product-description').should('contain', testProduct.description);
});
})
Advanced: Database Seeding via Tasks
For more complex data needs, you can use Node.js tasks to seed the database directly:
// cypress.config.js
const { defineConfig } = require('cypress');
const { Pool } = require('pg'); // PostgreSQL client
const { MongoClient } = require('mongodb'); // MongoDB client
module.exports = defineConfig({
e2e: {
setupNodeEvents(on, config) {
// Database tasks
on('task', {
// PostgreSQL database seeding
async seedPostgresDatabase({ sql, params }) {
const pool = new Pool({
connectionString: config.env.TEST_DATABASE_URL
});
try {
const result = await pool.query(sql, params);
await pool.end();
return result.rows;
} catch (error) {
console.error('Database task error:', error);
throw error;
}
},
// MongoDB database seeding
async seedMongoDatabase({ collection, documents }) {
const client = new MongoClient(config.env.MONGO_URI);
try {
await client.connect();
const db = client.db('test-db');
const result = await db.collection(collection).insertMany(documents);
await client.close();
return result.insertedIds;
} catch (error) {
console.error('MongoDB task error:', error);
throw error;
}
},
// Clean up test data
async cleanTestData({ collection }) {
const client = new MongoClient(config.env.MONGO_URI);
try {
await client.connect();
const db = client.db('test-db');
await db.collection(collection).deleteMany({
isTestData: true
});
await client.close();
return null;
} catch (error) {
console.error('Clean up error:', error);
throw error;
}
}
});
}
}
});
// Using database seeding in tests
describe('Order History', () => {
const testUser = {
_id: `user_${Date.now()}`,
email: `test_${Date.now()}@example.com`,
name: 'Test User',
isTestData: true
};
const testOrders = [
{
_id: `order_${Date.now()}_1`,
userId: testUser._id,
total: 99.99,
status: 'completed',
items: [{ name: 'Test Product', price: 99.99, quantity: 1 }],
createdAt: new Date(Date.now() - 86400000), // 1 day ago
isTestData: true
},
{
_id: `order_${Date.now()}_2`,
userId: testUser._id,
total: 149.98,
status: 'processing',
items: [{ name: 'Another Product', price: 74.99, quantity: 2 }],
createdAt: new Date(),
isTestData: true
}
];
before(() => {
// Seed database with test user
cy.task('seedMongoDatabase', {
collection: 'users',
documents: [testUser]
});
// Seed database with test orders
cy.task('seedMongoDatabase', {
collection: 'orders',
documents: testOrders
});
// Create auth token for the test user
cy.request('POST', '/api/test-auth', {
userId: testUser._id
}).then((response) => {
Cypress.env('testUserToken', response.body.token);
});
});
after(() => {
// Clean up test data
cy.task('cleanTestData', { collection: 'users' });
cy.task('cleanTestData', { collection: 'orders' });
});
beforeEach(() => {
// Set auth token before each test
cy.visit('/orders', {
onBeforeLoad: (win) => {
win.localStorage.setItem('authToken', Cypress.env('testUserToken'));
}
});
});
it('displays order history for user', () => {
cy.get('.order-item').should('have.length', 2);
// Verify order details
cy.get('.order-item').first().within(() => {
cy.get('.order-status').should('contain', 'processing');
cy.get('.order-total').should('contain', '149.98');
});
cy.get('.order-item').last().within(() => {
cy.get('.order-status').should('contain', 'completed');
cy.get('.order-total').should('contain', '99.99');
});
});
})
Test Isolation Best Practices
Properly isolating tests improves reliability and prevents interdependencies:
- Reset Application State: Clear localStorage, cookies, and other state between tests
- Use Unique Test Data: Include timestamps or random values to prevent collisions
- Clean Up After Tests: Remove test data created during tests
- Mock External Dependencies: Control the behavior of external services
- Avoid Shared State Between Tests: Don't rely on state from previous tests
// Example of test isolation setup
beforeEach(() => {
// Clear application state
cy.clearLocalStorage();
cy.clearCookies();
// Reset API state
cy.request({
method: 'POST',
url: '/api/testing/reset',
failOnStatusCode: false
});
// Create fresh test data with unique identifiers
const timestamp = Date.now();
const randomId = Math.random().toString(36).substring(2);
const testData = {
id: `test_${randomId}`,
name: `Test Item ${timestamp}`,
// ...other properties
};
// Save for use in test
cy.wrap(testData).as('testData');
});
Advanced Cypress Techniques
Custom Commands for Common Operations
// cypress/support/commands.js
// Navigation command
Cypress.Commands.add('navigateTo', (page) => {
const pages = {
home: '/',
products: '/products',
cart: '/cart',
checkout: '/checkout',
account: '/account',
admin: '/admin'
};
if (!pages[page]) {
throw new Error(`Unknown page: ${page}`);
}
return cy.visit(pages[page]);
});
// Table verification command
Cypress.Commands.add('verifyTableData', (selector, expectedData) => {
cy.get(selector).find('tbody tr').should('have.length', expectedData.length);
expectedData.forEach((rowData, rowIndex) => {
const row = cy.get(selector).find('tbody tr').eq(rowIndex);
Object.entries(rowData).forEach(([key, value]) => {
if (key.startsWith('_')) return; // Skip fields starting with underscore
row.find(`td.${key}, td[data-field="${key}"]`).should('contain', value);
});
});
});
// Verification command with retry logic
Cypress.Commands.add('verifyTextWithRetry', (selector, expectedText, options = {}) => {
const { timeout = 10000, interval = 1000 } = options;
const startTime = Date.now();
const checkText = () => {
return cy.get(selector).invoke('text').then((text) => {
if (text.includes(expectedText)) {
return true;
}
const elapsed = Date.now() - startTime;
if (elapsed > timeout) {
throw new Error(`Text "${expectedText}" not found in "${text}" after ${timeout}ms`);
}
cy.wait(interval);
return checkText();
});
};
return checkText().then(() => {
return cy.get(selector).should('contain', expectedText);
});
});
// Using custom commands
describe('Product Listing', () => {
beforeEach(() => {
cy.navigateTo('products');
});
it('displays product data in table', () => {
const expectedProducts = [
{ name: 'Product 1', price: '$19.99', category: 'Electronics' },
{ name: 'Product 2', price: '$29.99', category: 'Clothing' }
];
cy.verifyTableData('.products-table', expectedProducts);
});
it('updates inventory count after purchase', () => {
// Add product to cart and checkout
cy.get('.product-card').first().find('.add-to-cart').click();
cy.navigateTo('cart');
cy.get('.checkout-button').click();
// Complete checkout process...
// Verify inventory updated (using retry logic for eventual consistency)
cy.navigateTo('products');
cy.verifyTextWithRetry('.in-stock-count', 'In stock: 99', {
timeout: 15000,
interval: 1000
});
});
});
Working with iframes and Shadow DOM
// Testing payment form in iframe
describe('Payment Processing', () => {
beforeEach(() => {
cy.visit('/checkout');
cy.get('#payment-form').should('be.visible');
});
it('processes payment through iframe', () => {
// Load the payment iframe
cy.frameLoaded('#payment-iframe');
// Interact with elements inside the iframe
cy.iframe('#payment-iframe').within(() => {
cy.get('#card-number').type('4242424242424242');
cy.get('#card-expiry').type('1225');
cy.get('#card-cvc').type('123');
cy.get('#card-name').type('Test User');
cy.get('#submit-button').click();
});
// Verify payment success outside the iframe
cy.get('.payment-success').should('be.visible');
});
});
// Testing Shadow DOM components
describe('Custom Web Components', () => {
beforeEach(() => {
cy.visit('/components');
});
it('interacts with custom rating component', () => {
// Get the custom element with Shadow DOM
cy.get('custom-rating')
.shadow() // Access Shadow DOM
.find('.star') // Find elements inside Shadow DOM
.eq(3) // Select the fourth star (zero-based)
.click();
// Verify the rating was updated
cy.get('custom-rating')
.invoke('attr', 'value')
.should('eq', '4');
// Access nested Shadow DOM
cy.get('custom-form')
.shadow()
.find('custom-input')
.shadow()
.find('input')
.type('Test Value');
// Verify the value
cy.get('custom-form')
.shadow()
.find('custom-input')
.invoke('attr', 'value')
.should('eq', 'Test Value');
});
});
Visual Testing and Screenshots
// Basic visual testing with screenshots
describe('Visual Regression', () => {
beforeEach(() => {
cy.viewport(1280, 720); // Consistent viewport size
cy.visit('/products');
});
it('product listing matches visual baseline', () => {
// Wait for all images to load
cy.get('img').should(($imgs) => {
const promises = Array.from($imgs).map((img) => {
return new Promise((resolve) => {
if (img.complete) {
resolve();
} else {
img.addEventListener('load', resolve);
img.addEventListener('error', resolve); // Handle broken images
}
});
});
return Promise.all(promises);
});
// Take screenshot for comparison
cy.screenshot('product-listing', { capture: 'viewport' });
});
it('takes component-specific screenshots', () => {
// Capture specific component
cy.get('.featured-products')
.should('be.visible')
.screenshot('featured-products-section');
// Capture another component
cy.get('.product-filters')
.should('be.visible')
.screenshot('product-filters');
});
});
// Using Percy for visual testing (requires setup)
describe('Percy Visual Testing', () => {
it('captures homepage for visual review', () => {
cy.visit('/');
cy.percySnapshot('Home Page');
});
it('captures product listing with different viewport sizes', () => {
cy.visit('/products');
// Desktop view
cy.viewport(1280, 800);
cy.percySnapshot('Product Listing - Desktop');
// Tablet view
cy.viewport(768, 1024);
cy.percySnapshot('Product Listing - Tablet');
// Mobile view
cy.viewport(375, 667);
cy.percySnapshot('Product Listing - Mobile');
});
it('captures application states', () => {
cy.visit('/products');
// Default state
cy.percySnapshot('Products - Default');
// With filters applied
cy.get('[data-testid="category-filter"]').select('Electronics');
cy.get('[data-testid="price-filter"]').invoke('val', 50).trigger('change');
cy.get('[data-testid="apply-filters"]').click();
cy.percySnapshot('Products - Filtered');
// Empty state
cy.get('[data-testid="category-filter"]').select('Non-existent Category');
cy.get('[data-testid="apply-filters"]').click();
cy.get('[data-testid="empty-results"]').should('be.visible');
cy.percySnapshot('Products - Empty Results');
});
});
Testing Accessibility with Cypress
Integrating accessibility testing into your E2E tests helps ensure your application is usable by everyone.
// Install cypress-axe
npm install --save-dev cypress-axe axe-core
// Import in support file
// cypress/support/e2e.js
import 'cypress-axe'
// Use in tests
describe('Accessibility Testing', () => {
beforeEach(() => {
cy.visit('/')
cy.injectAxe() // Inject axe-core into the page
})
it('has no detectable accessibility violations on home page', () => {
cy.checkA11y() // Test the entire page
})
it('has no accessibility violations in specific component', () => {
cy.checkA11y('.navigation-menu') // Test specific component
})
it('can exclude specific elements from testing', () => {
cy.checkA11y({
exclude: ['.third-party-widget', '.legacy-component']
})
})
it('tests pages with dynamic content', () => {
// Test initial state
cy.checkA11y()
// Interact with page
cy.get('.accordion-header').first().click()
// Test expanded state
cy.checkA11y('.accordion-panel')
// Open modal
cy.get('.open-modal-button').click()
// Test modal for accessibility
cy.get('.modal').should('be.visible')
cy.checkA11y('.modal', {
rules: {
// Exclude specific rule for modals
'focus-trap': { enabled: false }
}
})
})
});
Organizing and Structuring Cypress Tests
Page Object Pattern
Using the Page Object Pattern helps organize test code and reduce duplication.
// cypress/support/pages/LoginPage.js
class LoginPage {
// Selectors
get emailInput() { return '[data-testid="email-input"]' }
get passwordInput() { return '[data-testid="password-input"]' }
get loginButton() { return '[data-testid="login-button"]' }
get errorMessage() { return '[data-testid="login-error"]' }
// Actions
visit() {
cy.visit('/login')
return this
}
typeEmail(email) {
cy.get(this.emailInput).clear().type(email)
return this
}
typePassword(password) {
cy.get(this.passwordInput).clear().type(password)
return this
}
clickLogin() {
cy.get(this.loginButton).click()
return this
}
// Combined actions
login(email, password) {
this.typeEmail(email)
this.typePassword(password)
this.clickLogin()
return this
}
// Assertions
shouldShowError(message) {
cy.get(this.errorMessage)
.should('be.visible')
.and('contain', message)
return this
}
shouldNavigateToDashboard() {
cy.url().should('include', '/dashboard')
cy.get('[data-testid="dashboard-header"]').should('be.visible')
return this
}
}
export default new LoginPage()
// cypress/support/pages/DashboardPage.js
class DashboardPage {
// Selectors
get header() { return '[data-testid="dashboard-header"]' }
get userGreeting() { return '[data-testid="user-greeting"]' }
get notificationBell() { return '[data-testid="notification-bell"]' }
get notificationCount() { return '[data-testid="notification-count"]' }
get userMenu() { return '[data-testid="user-menu"]' }
get logoutButton() { return '[data-testid="logout-button"]' }
// Actions
visit() {
cy.visit('/dashboard')
return this
}
clickNotificationBell() {
cy.get(this.notificationBell).click()
return this
}
openUserMenu() {
cy.get(this.userMenu).click()
return this
}
logout() {
this.openUserMenu()
cy.get(this.logoutButton).click()
return this
}
// Assertions
shouldGreetUser(name) {
cy.get(this.userGreeting).should('contain', name)
return this
}
shouldShowNotifications(count) {
if (count > 0) {
cy.get(this.notificationCount).should('contain', count)
} else {
cy.get(this.notificationCount).should('not.exist')
}
return this
}
}
export default new DashboardPage()
// Using Page Objects in tests
import LoginPage from '../support/pages/LoginPage'
import DashboardPage from '../support/pages/DashboardPage'
describe('Authentication Flow', () => {
it('logs in with valid credentials', () => {
LoginPage
.visit()
.login('test@example.com', 'password123')
.shouldNavigateToDashboard()
DashboardPage
.shouldGreetUser('Test User')
.shouldShowNotifications(2)
})
it('shows error with invalid credentials', () => {
LoginPage
.visit()
.login('test@example.com', 'wrongpassword')
.shouldShowError('Invalid email or password')
})
it('logs out successfully', () => {
// Login first
LoginPage
.visit()
.login('test@example.com', 'password123')
// Then log out
DashboardPage
.logout()
// Verify back at login page
cy.url().should('include', '/login')
})
})
Test Organization with Folders and Files
cypress/
├── e2e/
│ ├── auth/
│ │ ├── login.cy.js
│ │ ├── register.cy.js
│ │ ├── password-reset.cy.js
│ │ └── logout.cy.js
│ ├── products/
│ │ ├── browsing.cy.js
│ │ ├── filtering.cy.js
│ │ ├── details.cy.js
│ │ └── admin.cy.js
│ ├── cart/
│ │ ├── add-to-cart.cy.js
│ │ ├── update-quantities.cy.js
│ │ └── remove-items.cy.js
│ ├── checkout/
│ │ ├── shipping-info.cy.js
│ │ ├── payment.cy.js
│ │ └── order-confirmation.cy.js
│ └── account/
│ ├── profile.cy.js
│ ├── orders.cy.js
│ └── preferences.cy.js
├── fixtures/
│ ├── users.json
│ ├── products.json
│ └── orders.json
├── support/
│ ├── commands.js
│ ├── e2e.js
│ └── pages/
│ ├── LoginPage.js
│ ├── RegisterPage.js
│ ├── ProductsPage.js
│ ├── ProductDetailPage.js
│ ├── CartPage.js
│ ├── CheckoutPage.js
│ └── AccountPage.js
├── plugins/
│ └── index.js
└── screenshots/
└── (automatically generated during runs)
Running Specific Test Groups
// Run all tests
npx cypress run
// Run tests in a specific folder
npx cypress run --spec "cypress/e2e/auth/**/*.cy.js"
// Run a single test file
npx cypress run --spec "cypress/e2e/checkout/payment.cy.js"
// Run tests by tag (requires cypress-grep plugin)
npx cypress run --env grep="@smoke"
Tagging Tests for Different Purposes
// Install cypress-grep
npm install -D cypress-grep
// Import in support/e2e.js
import 'cypress-grep'
// Use tags in tests
describe('Checkout Process', { tags: ['@smoke', '@critical'] }, () => {
it('completes checkout with valid payment', { tags: '@payment' }, () => {
// Test code
})
it('validates required fields', { tags: '@validation' }, () => {
// Test code
})
})
// Run tests with specific tags
npx cypress run --env grep="@smoke"
npx cypress run --env grep="@critical+@payment" // AND condition
npx cypress run --env grep="@smoke+@payment,@critical" // OR condition
Test Strategy Organization
A comprehensive test strategy might include different types of tests:
- Smoke Tests (@smoke): Quick tests that verify core functionality
- Critical Path Tests (@critical): Tests of essential user flows
- Regression Tests (@regression): Tests for areas prone to breaking
- Feature Tests (@feature): Tests organized by feature area
- Visual Tests (@visual): Tests focused on UI appearance
- Performance Tests (@performance): Tests for response times and efficiency
Tags enable running different test suites for different purposes, like running smoke tests on every commit and full regression suites nightly.
Cypress in CI/CD Pipelines
GitHub Actions Integration
// .github/workflows/cypress.yml
name: E2E Tests
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main, develop ]
jobs:
cypress-run:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
# Run tests across multiple browsers
browser: [ chrome, firefox, edge ]
services:
# Start MongoDB for the test database
mongodb:
image: mongo:5.0
ports:
- 27017:27017
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: 16
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Build frontend
run: npm run build
- name: Start backend server
run: |
npm run seed:test-db
npm run start:server &
env:
NODE_ENV: test
DATABASE_URL: mongodb://localhost:27017/test-db
PORT: 3001
- name: Start frontend
run: |
npm run start &
env:
REACT_APP_API_URL: http://localhost:3001/api
PORT: 3000
- name: Wait for servers to start
run: |
npx wait-on http://localhost:3000
npx wait-on http://localhost:3001/api/health
- name: Cypress run
uses: cypress-io/github-action@v5
with:
browser: ${{ matrix.browser }}
record: true # Requires Cypress Dashboard
parallel: true # Parallelize tests
group: 'UI Tests - ${{ matrix.browser }}'
spec: cypress/e2e/**/*.cy.js
env:
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CYPRESS_BASE_URL: http://localhost:3000
CYPRESS_API_URL: http://localhost:3001/api
- name: Upload screenshots
uses: actions/upload-artifact@v3
if: failure()
with:
name: cypress-screenshots-${{ matrix.browser }}
path: cypress/screenshots
if-no-files-found: ignore
- name: Upload videos
uses: actions/upload-artifact@v3
if: always()
with:
name: cypress-videos-${{ matrix.browser }}
path: cypress/videos
if-no-files-found: ignore
CircleCI Integration
// .circleci/config.yml
version: 2.1
orbs:
cypress: cypress-io/cypress@2
workflows:
build-and-test:
jobs:
- cypress/install
- cypress/run:
requires:
- cypress/install
start: npm start
wait-on: 'http://localhost:3000'
store_artifacts: true
post-steps:
- store_test_results:
path: cypress/results
parallel: true
parallelism: 4
group: 'all tests'
browser: chrome
Optimizing Cypress in CI
// cypress.config.js optimized for CI
const { defineConfig } = require('cypress')
module.exports = defineConfig({
e2e: {
baseUrl: 'http://localhost:3000',
// CI-specific settings
videoCompression: 15,
video: process.env.CI ? true : false,
screenshotOnRunFailure: true,
// Retry failed tests in CI
retries: {
runMode: 2,
openMode: 0
},
// Reduced viewportWidth for CI to avoid issues with small VMs
viewportWidth: 1280,
viewportHeight: 720,
// Faster performance
numTestsKeptInMemory: 10,
experimentalMemoryManagement: true,
// Organize test results for CI
reporter: process.env.CI ? 'junit' : 'spec',
reporterOptions: {
junit: {
outputFile: 'cypress/results/results.xml'
}
}
}
})
Splitting Tests for Parallel Execution
// package.json scripts for test splitting
{
"scripts": {
"cy:smoke": "cypress run --spec cypress/e2e/smoke/**/*.cy.js",
"cy:auth": "cypress run --spec cypress/e2e/auth/**/*.cy.js",
"cy:products": "cypress run --spec cypress/e2e/products/**/*.cy.js",
"cy:cart": "cypress run --spec cypress/e2e/cart/**/*.cy.js",
"cy:checkout": "cypress run --spec cypress/e2e/checkout/**/*.cy.js",
"cy:account": "cypress run --spec cypress/e2e/account/**/*.cy.js",
"cy:all-parallel": "npm-run-all --parallel --continue-on-error cy:auth cy:products cy:cart cy:checkout cy:account"
}
}
CI/CD Strategy for Different Environments
A complete E2E testing strategy might look like this:
- Commit Stage: Run smoke tests (@smoke) on every commit
- PR Stage: Run critical path tests (@critical) on pull requests
- Development Deployment: Run regression tests (@regression) after deploying to dev
- Staging Deployment: Run full E2E suite in parallel across browsers before promoting to production
- Production Deployment: Run smoke tests against production to verify deployment
- Scheduled Tests: Run full E2E suite nightly against production to detect issues
This approach balances testing thoroughness with performance, ensuring critical issues are caught early while still providing comprehensive coverage.
Debugging Cypress Tests
Using Cypress's Time Travel Debugging
Cypress's native time travel debugging is one of its most powerful features.
- Run tests in interactive mode with
npx cypress open - Click on any command in the Command Log to see the state at that point
- Hover over commands to see elements highlighted in the application
- Use the browser's DevTools to inspect elements and state
- Examine before/after screenshots for each step
Debugging with console.log and cy.log
// Using console.log for debugging
it('debugs with console.log', () => {
cy.visit('/products')
cy.get('.product-card').then(($elements) => {
console.log('Number of products:', $elements.length)
console.log('First product:', $elements.first().text())
})
// Print values during test execution
cy.get('.product-price').invoke('text').then((text) => {
console.log('Product price:', text)
})
})
// Using cy.log for in-test messages
it('debugs with cy.log', () => {
cy.log('Starting test')
cy.visit('/products')
cy.log('Page loaded')
cy.get('.product-card').each(($el, index) => {
// This will appear in the Cypress command log
cy.log(`Product ${index}: ${$el.find('.product-name').text()}`)
})
})
Using .debug() Command
it('debugs with .debug()', () => {
cy.visit('/products')
// This will pause execution and open DevTools
cy.get('.product-card').first().debug()
// Continue execution by clicking "Resume" in Cypress
cy.get('.product-price').should('be.visible')
})
Handling and Debugging Async Issues
// Common async debugging pattern
it('handles async operations correctly', () => {
cy.visit('/data-loading-page')
// Bad approach: Wait for arbitrary time
// cy.wait(5000) // Not recommended
// Good approach: Wait for specific condition
cy.get('.loading-indicator').should('not.exist')
cy.get('.data-table').should('be.visible')
// Debugging async issues
cy.get('.data-table').then(($table) => {
// Log the state when element is found
console.log('Table found:', $table.html())
const rowCount = $table.find('tr').length
cy.log(`Found ${rowCount} rows in table`)
if (rowCount === 0) {
// Take a screenshot for debugging empty tables
cy.screenshot('empty-table-debug')
}
})
// Continue with test
cy.get('.data-table tr').should('have.length.greaterThan', 0)
})
Troubleshooting Common Issues
// Element not found troubleshooting
it('troubleshoots element not found', () => {
cy.visit('/products')
// Is the element in the DOM at all?
cy.document().then((doc) => {
const element = doc.querySelector('.product-details')
cy.log(`Element exists in DOM: ${!!element}`)
if (element) {
cy.log(`Element visibility: ${window.getComputedStyle(element).display}`)
cy.log(`Element position: ${JSON.stringify({
top: element.getBoundingClientRect().top,
left: element.getBoundingClientRect().left
})}`)
}
})
// Try a more relaxed selector
cy.get('body').find('.product-details').should('exist')
// Force interaction if element is being covered
cy.get('.product-details').click({ force: true })
})
// Debugging form interaction issues
it('troubleshoots form interactions', () => {
cy.visit('/contact')
// Check if element is disabled
cy.get('#email').then(($input) => {
cy.log(`Input disabled: ${$input.prop('disabled')}`)
cy.log(`Input readonly: ${$input.prop('readonly')}`)
})
// Try typing with specific options
cy.get('#email').type('test@example.com', { delay: 100, force: true })
// Check for overlays that might block interaction
cy.document().then((doc) => {
const overlays = doc.querySelectorAll('.modal, .overlay, .popup')
cy.log(`Found ${overlays.length} potential overlays`)
if (overlays.length > 0) {
cy.log('Overlay z-index values:')
Array.from(overlays).forEach((el) => {
cy.log(`${el.className}: ${window.getComputedStyle(el).zIndex}`)
})
}
})
})
Debugging Real-World Flaky Tests
Common causes of flaky tests and solutions:
- Race conditions: Wait for specific elements or conditions, not arbitrary times
- Network timing issues: Use
cy.intercept()withcy.wait()to wait for specific network requests - Animation interference: Disable animations in test environment or wait for animations to complete
- Selector problems: Use more stable selectors like data attributes
- Test isolation issues: Ensure tests don't depend on state from previous tests
// Handling a typical animation issue
cy.visit('/dashboard')
// Wait for page transition animation to complete
cy.get('.page-transition').should('not.have.class', 'animating')
// Wait for async data loading
cy.intercept('GET', '/api/dashboard-data').as('dashboardData')
cy.wait('@dashboardData')
// Now interact with the page
cy.get('.dashboard-card').first().click()
// Wait for modal animation to complete
cy.get('.modal')
.should('be.visible')
.and('not.have.class', 'modal-animating')
Practice Activities
Activity 1: Basic Cypress Test Suite
Create a basic Cypress test suite for a to-do application:
- Set up Cypress in a new or existing project
- Create tests for adding, completing, and deleting to-do items
- Test validation (empty to-dos, maximum length, etc.)
- Test persistence (to-dos remain after page refresh)
- Run your tests and debug any issues
Sample app URL: https://example.cypress.io/todo
Activity 2: E-commerce User Journey
Create an end-to-end test for a complete e-commerce user journey:
- User registration or login
- Browsing products and applying filters
- Adding items to cart
- Checkout process (shipping, payment)
- Order confirmation
Use mock data and API interception to simulate server responses. Apply Page Object Pattern to organize your tests.
Activity 3: Implement API Mocking
Create tests that use Cypress's cy.intercept() to mock API responses:
- Mock a successful API response
- Mock an error response to test error handling
- Mock a slow response to test loading states
- Mock an empty data response to test empty states
- Verify the UI updates correctly for each scenario
This will help you understand how to test different application states without relying on actual backend responses.