| name | Cypress Testing |
| description | This skill should be used when the user asks to "setup Cypress", "E2E testing", "Cypress tests", "component testing", "integration testing", "automated testing", or works with Cypress testing framework. |
| version | 0.1.0 |
Cypress Testing
Cypress is a next-generation front-end testing tool built for the modern web.
Core Concepts
Test Types
- E2E Tests - Full browser automation
- Component Tests - Isolate and test components
- API Tests - Test backend endpoints
Installation
npm install -D cypress
Open Cypress:
npx cypress open
Run headlessly:
npx cypress run
Project Structure
cypress/
โโโ e2e/ # E2E tests
โ โโโ *.cy.ts
โโโ fixtures/ # Test data
โ โโโ users.json
โโโ support/
โ โโโ commands.ts # Custom commands
โ โโโ component.ts # Component test config
โ โโโ e2e.ts # E2E test config
โโโ downloads/ # Downloaded files
Configuration
cypress.config.ts:
import { defineConfig } from 'cypress'
export default defineConfig({
e2e: {
baseUrl: 'http://localhost:3000',
supportFile: 'cypress/support/e2e.ts',
specPattern: 'cypress/e2e/**/*.cy.{js,jsx,ts,tsx}',
viewportWidth: 1280,
viewportHeight: 720,
defaultCommandTimeout: 4000,
requestTimeout: 10000,
responseTimeout: 10000,
pageLoadTimeout: 10000,
video: true,
screenshotOnRunFailure: true,
env: {
apiUrl: 'http://localhost:8080/api',
coverage: false,
},
setupNodeEvents(on, config) {
on('task', {
seedDatabase() {
return null
},
})
return config
},
},
component: {
devServer: {
framework: 'react',
bundler: 'vite',
},
specPattern: 'src/**/*.cy.{js,jsx,ts,tsx}',
},
})
E2E Tests
Basic test:
describe('Home Page', () => {
beforeEach(() => {
cy.visit('/')
})
it('displays welcome message', () => {
cy.contains('Welcome').should('be.visible')
})
it('has working navigation', () => {
cy.get('[data-testid="nav-about"]').click()
cy.url().should('include', '/about')
})
})
Form testing:
describe('Login Form', () => {
beforeEach(() => {
cy.visit('/login')
})
it('successfully logs in with valid credentials', () => {
cy.get('[data-testid="email-input"]')
.type('user@example.com')
cy.get('[data-testid="password-input"]')
.type('password123')
cy.get('[data-testid="login-button"]').click()
cy.url().should('include', '/dashboard')
cy.contains('Welcome back').should('be.visible')
})
it('shows error with invalid credentials', () => {
cy.get('[data-testid="email-input"]').type('invalid@example.com')
cy.get('[data-testid="password-input"]').type('wrongpassword')
cy.get('[data-testid="login-button"]').click()
cy.get('[data-testid="error-message"]')
.should('be.visible')
.and('contain', 'Invalid credentials')
})
it('validates required fields', () => {
cy.get('[data-testid="login-button"]').click()
cy.get('[data-testid="email-input"]:invalid').should('have.length', 1)
})
})
API waiting (NOT mocking):
Important: E2E tests should NOT mock API responses. Use cy.intercept only to wait for real API calls, not to fake responses. Mock testing belongs in unit tests (Vitest).
describe('User Dashboard', () => {
beforeEach(() => {
cy.intercept('GET', '/api/users').as('getUsers')
cy.visit('/dashboard')
})
it('displays users from real API', () => {
cy.wait('@getUsers')
cy.get('[data-testid="user-list"]').should('have.length.greaterThan', 0)
})
it('loads data successfully', () => {
cy.wait('@getUsers').its('response.statusCode').should('eq', 200)
})
})
Querying Elements
Best practices:
cy.get('[data-testid="submit-button"]')
cy.get('[aria-label="Submit form"]')
cy.findByRole('button', { name: /submit/i })
cy.contains('Click me')
cy.get('.btn-primary')
Querying methods:
cy.get('.item')
cy.get('.container').find('.item')
cy.contains('Submit')
cy.contains('button', 'Submit')
cy.get('form').within(() => {
cy.get('input[name="email"]')
})
cy.get('.loading').should('not.exist')
cy.get('.content').should('be.visible')
Actions
cy.get('button').click()
cy.get('button').click({ force: true })
cy.get('button').dblclick()
cy.get('button').rightclick()
cy.get('input').type('Hello World')
cy.get('input').type('{enter}')
cy.get('input').type('{ctrl}a{del}')
cy.get('input').clear()
cy.get('select').select('Option 1')
cy.get('select').select(['Option 1', 'Option 2'])
cy.get('[type="checkbox"]').check()
cy.get('[type="checkbox"]').uncheck()
cy.get('[type="radio"]').check('radio1')
cy.get('input').focus()
cy.get('input').blur()
cy.get('.item').scrollIntoView()
cy.scrollTo(0, 500)
cy.scrollTo('bottom')
Assertions
cy.get('.modal').should('be.visible')
cy.get('.modal').should('not.exist')
cy.get('.loading').should('not.be.visible')
cy.get('button').should('be.enabled')
cy.get('button').should('be.disabled')
cy.get('input').should('have.focus')
cy.get('.title').should('have.text', 'Hello')
cy.get('.title').should('contain', 'Hello')
cy.get('.count').should('have.value', '10')
cy.get('img').should('have.attr', 'src', 'photo.jpg')
cy.get('div').should('have.class', 'active')
cy.get('div').should('have.css', 'color', 'rgb(255, 0, 0)')
cy.get('.item').should('have.length', 3)
cy.get('.item').should('have.length.greaterThan', 2)
cy.get('.user')
.should('be.visible')
.and('have.class', 'active')
.and('contain', 'John')
cy.get('.price').should(($el) => {
const text = $el.text()
expect(text).to.match(/^\$\d+\.\d{2}$/)
})
Custom Commands
cypress/support/commands.ts:
declare global {
namespace Cypress {
interface Chainable {
login(email: string, password: string): Chainable<void>
logout(): Chainable<void>
createUser(user: UserInput): Chainable<void>
}
}
}
Cypress.Commands.add('login', (email, password) => {
cy.session([email, password], () => {
cy.request('POST', '/api/login', { email, password })
})
})
Cypress.Commands.add('logout', () => {
cy.request('POST', '/api/logout')
cy.visit('/login')
})
Cypress.Commands.add('createUser', (user) => {
cy.request('POST', '/api/users', user)
})
Usage:
describe('Protected Routes', () => {
beforeEach(() => {
cy.login('admin@example.com', 'password123')
})
it('accesses admin dashboard', () => {
cy.visit('/admin')
cy.get('[data-testid="admin-panel"]').should('be.visible')
})
afterEach(() => {
cy.logout()
})
})
Component Testing
Setup:
import { mount } from 'cypress/react18'
import './commands'
declare global {
namespace Cypress {
interface Chainable {
mount: typeof mount
}
}
}
Cypress.Commands.add('mount', mount)
Test component:
import { Button } from './Button'
describe('Button', () => {
it('renders correctly', () => {
cy.mount(<Button>Click me</Button>)
cy.get('button').should('contain', 'Click me')
})
it('handles click events', () => {
const onClick = cy.spy().as('onClick')
cy.mount(<Button onClick={onClick}>Click me</Button>)
cy.get('button').click()
cy.get('@onClick').should('have.been.calledOnce')
})
it('shows disabled state', () => {
cy.mount(<Button disabled>Disabled</Button>)
cy.get('button').should('be.disabled')
})
})
Running Tests
package.json scripts:
{
"scripts": {
"cypress:open": "cypress open",
"cypress:run": "cypress run",
"cypress:run:e2e": "cypress run --e2e",
"cypress:run:component": "cypress run --component",
"test:e2e": "start-server-and-test dev http://localhost:3000 cypress:run"
}
}
CI configuration:
name: E2E Tests
on: [push]
jobs:
e2e:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
- run: npm ci
- run: npm run build
- name: Run Cypress
uses: cypress-io/github-action@v6
with:
start: npm run preview
wait-on: 'http://localhost:4173'
Best Practices
- NO mocking in E2E tests - Test real integrations; use unit tests for mocks
- Use data-testid - Decouple tests from implementation
- Avoid cy.wait with fixed time - Use intercepts and aliases to wait for requests
- Use commands for common actions - DRY principle
- Clean state between tests - Use beforeEach
- Test user flows - Not implementation details
- Run in CI - Catch regressions early
Testing Strategy
| Test Type | Tool | Mock Usage |
|---|
| Unit tests | Vitest | โ
Yes |
| Component tests | Vitest + RTL | โ
Yes |
| E2E tests | Cypress | โ No |
Additional Resources
references/cypress-patterns.md - Testing patterns
examples/cypress-tests/ - Complete test examples