Cipress Cheatsheet¶
¶
¶
< > > > > > > > > > "Clase de inscripción" Cypress es una herramienta de prueba frontal de próxima generación construida para la web moderna. Se dirige a los desarrolladores de puntos de dolor clave y los ingenieros de QA se enfrentan al probar aplicaciones modernas. Cypress le permite escribir todo tipo de pruebas: Pruebas de extremo a extremo, Pruebas de integración, Pruebas de unidad. ▪/p] ■/div titulada
¶
########################################################################################################################################################################################################################################################## Copiar todos los comandos¶
########################################################################################################################################################################################################################################################## Generar PDF seleccionado/button¶
■/div titulada ■/div titulada
Cuadro de contenidos¶
- Instalación
- Empezar
- Basic Commands
- Selectors
- Asserciones
- Redacción de red
- File Operations
- Comandos personales
- Configuración
- Page Objects
- API Testing
- Ensayo visual
- CI/CD Integration
- Las mejores prácticas
- Debugging
- Performance
- Solucionando
Instalación¶
Instalación básica¶
# Install Cypress via npm
npm install --save-dev cypress
# Install Cypress via yarn
yarn add --dev cypress
# Open Cypress for the first time
npx cypress open
# Run Cypress in headless mode
npx cypress run
Configuración de proyectos¶
# Initialize new project with Cypress
mkdir my-cypress-project
cd my-cypress-project
npm init -y
npm install --save-dev cypress
# Open Cypress (creates folder structure)
npx cypress open
Estructura de la carpeta¶
cypress/
├── e2e/ # End-to-end test files
│ └── spec.cy.js
├── fixtures/ # Test data
│ └── example.json
├── support/ # Support files
│ ├── commands.js # Custom commands
│ └── e2e.js # Global configuration
└── downloads/ # Downloaded files during tests
Paquete.json Scripts¶
{
"scripts": {
"cypress:open": "cypress open",
"cypress:run": "cypress run",
"cypress:run:chrome": "cypress run --browser chrome",
"cypress:run:firefox": "cypress run --browser firefox",
"cypress:run:edge": "cypress run --browser edge",
"cypress:run:headed": "cypress run --headed",
"cypress:run:record": "cypress run --record --key=your-record-key"
}
}
Comienzo¶
Primera prueba¶
// cypress/e2e/first-test.cy.js
describe('My First Test', () => {
it('Visits the Kitchen Sink', () => {
cy.visit('https://example.cypress.io')
cy.contains('type').click()
cy.url().should('include', '/commands/actions')
cy.get('.action-email')
.type('fake@email.com')
.should('have.value', 'fake@email.com')
})
})
Estructura de prueba básica¶
describe('Test Suite Name', () => {
beforeEach(() => {
// Runs before each test
cy.visit('/login')
})
it('should do something', () => {
// Test implementation
cy.get('[data-cy=username]').type('user@example.com')
cy.get('[data-cy=password]').type('password123')
cy.get('[data-cy=submit]').click()
cy.url().should('include', '/dashboard')
})
it('should do something else', () => {
// Another test
})
})
Test Hooks¶
describe('Hooks Example', () => {
before(() => {
// Runs once before all tests in the block
cy.task('seedDatabase')
})
after(() => {
// Runs once after all tests in the block
cy.task('clearDatabase')
})
beforeEach(() => {
// Runs before each test in the block
cy.login('user@example.com', 'password123')
})
afterEach(() => {
// Runs after each test in the block
cy.clearCookies()
cy.clearLocalStorage()
})
it('should test something', () => {
// Test implementation
})
})
Comandos básicos¶
Navegación¶
// Visit a URL
cy.visit('https://example.com')
cy.visit('/relative-path')
cy.visit('/', { timeout: 30000 })
// Navigate browser history
cy.go('back')
cy.go('forward')
cy.go(-1) // Go back one page
cy.go(1) // Go forward one page
// Reload page
cy.reload()
cy.reload(true) // Force reload
Element Interaction¶
// Click elements
cy.get('button').click()
cy.get('button').click({ force: true }) // Force click
cy.get('button').dblclick() // Double click
cy.get('button').rightclick() // Right click
// Type text
cy.get('input').type('Hello World')
cy.get('input').type('user@example.com{enter}')
cy.get('input').type('{selectall}New Text')
cy.get('input').clear().type('New Text')
// Select options
cy.get('select').select('Option 1')
cy.get('select').select(['Option 1', 'Option 2']) // Multiple
cy.get('select').select('value', { force: true })
// Check/uncheck
cy.get('input[type="checkbox"]').check()
cy.get('input[type="checkbox"]').uncheck()
cy.get('input[type="radio"]').check()
// File upload
cy.get('input[type="file"]').selectFile('path/to/file.pdf')
cy.get('input[type="file"]').selectFile(['file1.jpg', 'file2.jpg'])
Element Queries¶
// Get elements
cy.get('.class-name')
cy.get('#id')
cy.get('[data-cy="element"]')
cy.get('button').first()
cy.get('button').last()
cy.get('button').eq(2) // Third button (0-indexed)
// Find within elements
cy.get('.parent').find('.child')
cy.get('.parent').within(() => {
cy.get('.child').click()
})
// Filter elements
cy.get('button').filter('.active')
cy.get('button').not('.disabled')
// Contains text
cy.contains('Submit')
cy.contains('button', 'Submit')
cy.get('button').contains('Submit')
Traversal¶
// Parent/child relationships
cy.get('.child').parent()
cy.get('.element').parents('.ancestor')
cy.get('.parent').children()
cy.get('.parent').children('.specific-child')
// Siblings
cy.get('.element').siblings()
cy.get('.element').next()
cy.get('.element').prev()
cy.get('.element').nextAll()
cy.get('.element').prevAll()
// Closest
cy.get('.element').closest('.container')
Esperando¶
// Wait for element
cy.get('.loading').should('not.exist')
cy.get('.content').should('be.visible')
// Wait for specific time (not recommended)
cy.wait(1000)
// Wait for network requests
cy.intercept('GET', '/api/users').as('getUsers')
cy.wait('@getUsers')
// Wait for multiple requests
cy.wait(['@getUsers', '@getPosts'])
Selectores¶
CSS Selectors¶
// Basic selectors
cy.get('button') // Tag
cy.get('.btn') // Class
cy.get('#submit') // ID
cy.get('[type="submit"]') // Attribute
// Combinators
cy.get('form button') // Descendant
cy.get('form > button') // Direct child
cy.get('label + input') // Adjacent sibling
cy.get('h2 ~ p') // General sibling
// Pseudo-selectors
cy.get('button:first')
cy.get('button:last')
cy.get('button:nth-child(2)')
cy.get('input:checked')
cy.get('input:disabled')
cy.get('a:contains("Click me")')
Atributos de datos (recomendados)¶
// HTML
// <button data-cy="submit-btn">Submit</button>
// <input data-testid="username" />
// <div data-test="user-profile">...</div>
// Cypress tests
cy.get('[data-cy="submit-btn"]')
cy.get('[data-testid="username"]')
cy.get('[data-test="user-profile"]')
// Custom command for data-cy
Cypress.Commands.add('dataCy', (value) => {
return cy.get(`[data-cy=${value}]`)
})
// Usage
cy.dataCy('submit-btn').click()
XPath (con plugin)¶
// cypress/support/e2e.js
require('cypress-xpath')
// Usage in tests
cy.xpath('//button[contains(text(), "Submit")]')
cy.xpath('//input[@placeholder="Username"]')
cy.xpath('//div[@class="container"]//button[1]')
Selectores complejos¶
// Multiple attributes
cy.get('input[type="email"][required]')
// Partial attribute matching
cy.get('[class*="btn"]') // Contains
cy.get('[class^="btn"]') // Starts with
cy.get('[class$="primary"]') // Ends with
// Multiple selectors
cy.get('button, input[type="submit"]')
// Chaining selectors
cy.get('.form-group')
.find('input')
.filter('[required]')
.first()
Asserciones¶
En caso de sumas¶
// Existence
cy.get('button').should('exist')
cy.get('.loading').should('not.exist')
// Visibility
cy.get('button').should('be.visible')
cy.get('.modal').should('not.be.visible')
// Text content
cy.get('h1').should('contain', 'Welcome')
cy.get('h1').should('have.text', 'Welcome to our site')
cy.get('p').should('include.text', 'partial text')
// Attributes
cy.get('input').should('have.attr', 'placeholder', 'Enter email')
cy.get('button').should('have.class', 'btn-primary')
cy.get('input').should('have.value', 'test@example.com')
// CSS properties
cy.get('button').should('have.css', 'background-color', 'rgb(0, 123, 255)')
cy.get('div').should('have.css', 'display', 'none')
// Length
cy.get('li').should('have.length', 5)
cy.get('li').should('have.length.greaterThan', 3)
cy.get('li').should('have.length.lessThan', 10)
Supresiones esperadas¶
// Using expect (Chai)
cy.get('input').then(($input) => {
expect($input).to.have.value('expected value')
expect($input[0].validity.valid).to.be.true
})
// Multiple assertions
cy.get('button').then(($btn) => {
expect($btn).to.have.class('btn')
expect($btn).to.contain('Submit')
expect($btn).to.be.visible
})
// Custom assertions
cy.get('.user-list').then(($list) => {
const users = $list.find('.user-item')
expect(users).to.have.length.above(0)
expect(users.first()).to.contain('John Doe')
})
Aserciones URL¶
// URL should include
cy.url().should('include', '/dashboard')
cy.url().should('eq', 'https://example.com/dashboard')
// URL should match pattern
cy.url().should('match', /\/users\/\d+/)
// Location assertions
cy.location('pathname').should('eq', '/dashboard')
cy.location('search').should('eq', '?tab=profile')
cy.location('hash').should('eq', '#section1')
Aserciones aduaneras¶
// Add custom assertion
chai.Assertion.addMethod('beValidEmail', function() {
const obj = this._obj
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
this.assert(
emailRegex.test(obj),
'expected #{this} to be a valid email',
'expected #{this} not to be a valid email',
true
)
})
// Usage
cy.get('input[type="email"]')
.invoke('val')
.should('beValidEmail')
Pruebas de red¶
Solicitudes de interceptación¶
// Basic intercept
cy.intercept('GET', '/api/users').as('getUsers')
cy.visit('/users')
cy.wait('@getUsers')
// Intercept with response modification
cy.intercept('GET', '/api/users', { fixture: 'users.json' }).as('getUsers')
// Intercept with dynamic response
cy.intercept('GET', '/api/users', (req) => {
req.reply({
statusCode: 200,
body: {
users: [
{ id: 1, name: 'John Doe' },
{ id: 2, name: 'Jane Smith' }
]
}
})
}).as('getUsers')
// Intercept POST requests
cy.intercept('POST', '/api/users', {
statusCode: 201,
body: { id: 3, name: 'New User' }
}).as('createUser')
Solicitud de admisión¶
// Wait and assert request
cy.intercept('GET', '/api/users').as('getUsers')
cy.visit('/users')
cy.wait('@getUsers').then((interception) => {
expect(interception.response.statusCode).to.eq(200)
expect(interception.response.body).to.have.property('users')
})
// Assert request was made
cy.intercept('POST', '/api/users').as('createUser')
cy.get('[data-cy="create-user"]').click()
cy.wait('@createUser').its('request.body').should('deep.include', {
name: 'John Doe',
email: 'john@example.com'
})
// Assert request headers
cy.wait('@getUsers').its('request.headers').should('have.property', 'authorization')
Condiciones de red¶
// Simulate slow network
cy.intercept('GET', '/api/users', (req) => {
req.reply((res) => {
res.delay(2000) // 2 second delay
res.send({ fixture: 'users.json' })
})
}).as('getUsers')
// Simulate network failure
cy.intercept('GET', '/api/users', { forceNetworkError: true }).as('getUsersError')
// Simulate server error
cy.intercept('GET', '/api/users', {
statusCode: 500,
body: { error: 'Internal Server Error' }
}).as('getUsersError')
Múltiples interceptaciones¶
// Multiple endpoints
cy.intercept('GET', '/api/users').as('getUsers')
cy.intercept('GET', '/api/posts').as('getPosts')
cy.intercept('GET', '/api/comments').as('getComments')
cy.visit('/dashboard')
// Wait for all requests
cy.wait(['@getUsers', '@getPosts', '@getComments'])
// Or wait individually
cy.wait('@getUsers')
cy.wait('@getPosts')
cy.wait('@getComments')
Operaciones de archivo¶
Cargo de archivos¶
// Upload single file
cy.get('input[type="file"]').selectFile('cypress/fixtures/example.pdf')
// Upload multiple files
cy.get('input[type="file"]').selectFile([
'cypress/fixtures/file1.jpg',
'cypress/fixtures/file2.jpg'
])
// Upload with drag and drop
cy.get('.dropzone').selectFile('cypress/fixtures/example.pdf', {
action: 'drag-drop'
})
// Upload from fixtures
cy.fixture('example.pdf', 'base64').then(fileContent => {
cy.get('input[type="file"]').selectFile({
contents: Cypress.Buffer.from(fileContent, 'base64'),
fileName: 'example.pdf',
mimeType: 'application/pdf'
})
})
Archivo Descargar¶
// Download file and verify
cy.get('[data-cy="download-btn"]').click()
cy.readFile('cypress/downloads/report.pdf').should('exist')
// Verify downloaded file content
cy.get('[data-cy="download-csv"]').click()
cy.readFile('cypress/downloads/data.csv')
.should('contain', 'Name,Email,Age')
.and('contain', 'John Doe,john@example.com,30')
// Download with custom filename
cy.get('[data-cy="download-btn"]').click()
cy.task('downloadFile', {
url: 'https://example.com/file.pdf',
directory: 'cypress/downloads',
filename: 'custom-name.pdf'
})
Trabajando con Arreglos¶
// Load fixture data
cy.fixture('users.json').then((users) => {
cy.intercept('GET', '/api/users', { body: users }).as('getUsers')
})
// Use fixture in test
cy.fixture('user-data.json').as('userData')
cy.get('@userData').then((data) => {
cy.get('[data-cy="name"]').type(data.name)
cy.get('[data-cy="email"]').type(data.email)
})
// Fixture with alias
cy.fixture('config.json').as('config')
cy.get('@config').its('apiUrl').should('eq', 'https://api.example.com')
Archivos de lectura / escritura¶
// Read file
cy.readFile('cypress/fixtures/data.json').then((data) => {
expect(data).to.have.property('users')
})
// Write file
cy.writeFile('cypress/fixtures/output.json', {
timestamp: Date.now(),
testResults: 'passed'
})
// Append to file
cy.writeFile('cypress/fixtures/log.txt', 'New log entry\n', { flag: 'a+' })
Comandos Personalizados¶
Comandos Personales Básicos¶
// cypress/support/commands.js
// Simple command
Cypress.Commands.add('login', (email, password) => {
cy.visit('/login')
cy.get('[data-cy="email"]').type(email)
cy.get('[data-cy="password"]').type(password)
cy.get('[data-cy="submit"]').click()
})
// Command with options
Cypress.Commands.add('login', (email, password, options = {}) => {
const { rememberMe = false } = options
cy.visit('/login')
cy.get('[data-cy="email"]').type(email)
cy.get('[data-cy="password"]').type(password)
if (rememberMe) {
cy.get('[data-cy="remember-me"]').check()
}
cy.get('[data-cy="submit"]').click()
})
// Usage
cy.login('user@example.com', 'password123')
cy.login('user@example.com', 'password123', { rememberMe: true })
Comandos Aduaneros avanzados¶
// Command that returns value
Cypress.Commands.add('getLocalStorage', (key) => {
return cy.window().then((win) => {
return win.localStorage.getItem(key)
})
})
// Command with chaining
Cypress.Commands.add('dataCy', (value) => {
return cy.get(`[data-cy=${value}]`)
})
// Usage with chaining
cy.dataCy('submit-btn').click()
cy.dataCy('username').type('john@example.com')
// Command with retry logic
Cypress.Commands.add('waitForElement', (selector, timeout = 10000) => {
cy.get(selector, { timeout }).should('be.visible')
})
// API command
Cypress.Commands.add('apiLogin', (email, password) => {
cy.request({
method: 'POST',
url: '/api/auth/login',
body: { email, password }
}).then((response) => {
window.localStorage.setItem('authToken', response.body.token)
})
})
Sobreescribir Comandos¶
// Overwrite existing command
Cypress.Commands.overwrite('visit', (originalFn, url, options) => {
const domain = Cypress.config('baseUrl')
if (domain) {
const combinedUrl = `${domain}${url}`
return originalFn(combinedUrl, options)
}
return originalFn(url, options)
})
// Overwrite type command to clear first
Cypress.Commands.overwrite('type', (originalFn, element, text, options) => {
if (options && options.clearFirst !== false) {
element.clear()
}
return originalFn(element, text, options)
})
Soporte TipoScript¶
// cypress/support/commands.ts
declare global {
namespace Cypress {
interface Chainable {
login(email: string, password: string, options?: { rememberMe?: boolean }): Chainable<void>
dataCy(value: string): Chainable<JQuery<HTMLElement>>
apiLogin(email: string, password: string): Chainable<void>
}
}
}
Cypress.Commands.add('login', (email: string, password: string, options = {}) => {
// Implementation
})
Configuración¶
Configuración Cypress¶
// cypress.config.js
const { defineConfig } = require('cypress')
module.exports = defineConfig({
e2e: {
// Base URL for cy.visit() and cy.request()
baseUrl: 'http://localhost:3000',
// Viewport settings
viewportWidth: 1280,
viewportHeight: 720,
// Timeouts
defaultCommandTimeout: 10000,
requestTimeout: 10000,
responseTimeout: 30000,
pageLoadTimeout: 30000,
// Test files
specPattern: 'cypress/e2e/**/*.cy.{js,jsx,ts,tsx}',
excludeSpecPattern: ['**/examples/*', '**/temp/*'],
// Support file
supportFile: 'cypress/support/e2e.js',
// Fixtures
fixturesFolder: 'cypress/fixtures',
// Screenshots and videos
screenshotsFolder: 'cypress/screenshots',
videosFolder: 'cypress/videos',
video: true,
screenshotOnRunFailure: true,
// Browser settings
chromeWebSecurity: false,
// Setup function
setupNodeEvents(on, config) {
// implement node event listeners here
},
},
// Component testing
component: {
devServer: {
framework: 'react',
bundler: 'webpack',
},
},
})
Medio ambiente¶
// cypress.config.js
module.exports = defineConfig({
e2e: {
env: {
apiUrl: 'https://api.example.com',
username: 'testuser',
password: 'testpass'
}
}
})
// cypress.env.json
{
"apiUrl": "https://api.example.com",
"username": "testuser",
"password": "testpass"
}
// Usage in tests
cy.visit(Cypress.env('apiUrl'))
const username = Cypress.env('username')
Múltiples entornos¶
// cypress.config.js
module.exports = defineConfig({
e2e: {
setupNodeEvents(on, config) {
| | | | const environment = config.env.environment | | 'development' | | | |
const environments = {
development: {
baseUrl: 'http://localhost:3000',
apiUrl: 'http://localhost:3001'
},
staging: {
baseUrl: 'https://staging.example.com',
apiUrl: 'https://api-staging.example.com'
},
production: {
baseUrl: 'https://example.com',
apiUrl: 'https://api.example.com'
}
}
config.baseUrl = environments[environment].baseUrl
config.env.apiUrl = environments[environment].apiUrl
return config
}
}
})
// Run with specific environment
// npx cypress run --env environment=staging
Configuración del navegador¶
// cypress.config.js
module.exports = defineConfig({
e2e: {
// Browser launch options
setupNodeEvents(on, config) {
on('before:browser:launch', (browser = {}, launchOptions) => {
if (browser.name === 'chrome') {
launchOptions.args.push('--disable-dev-shm-usage')
launchOptions.args.push('--no-sandbox')
launchOptions.args.push('--disable-gpu')
}
if (browser.name === 'firefox') {
launchOptions.preferences['media.navigator.permission.disabled'] = true
}
return launchOptions
})
}
}
})
Page Objects¶
Objeto de página básica¶
// cypress/support/pages/LoginPage.js
class LoginPage {
visit() {
cy.visit('/login')
return this
}
fillEmail(email) {
cy.get('[data-cy="email"]').type(email)
return this
}
fillPassword(password) {
cy.get('[data-cy="password"]').type(password)
return this
}
submit() {
cy.get('[data-cy="submit"]').click()
return this
}
login(email, password) {
this.fillEmail(email)
this.fillPassword(password)
this.submit()
return this
}
// Getters for elements
get emailInput() {
return cy.get('[data-cy="email"]')
}
get passwordInput() {
return cy.get('[data-cy="password"]')
}
get submitButton() {
return cy.get('[data-cy="submit"]')
}
get errorMessage() {
return cy.get('[data-cy="error-message"]')
}
}
export default LoginPage
Usando objetos de página¶
// cypress/e2e/login.cy.js
import LoginPage from '../support/pages/LoginPage'
describe('Login Tests', () => {
const loginPage = new LoginPage()
beforeEach(() => {
loginPage.visit()
})
it('should login successfully', () => {
loginPage
.login('user@example.com', 'password123')
cy.url().should('include', '/dashboard')
})
it('should show error for invalid credentials', () => {
loginPage
.login('invalid@email.com', 'wrongpassword')
loginPage.errorMessage
.should('be.visible')
.and('contain', 'Invalid credentials')
})
})
Objeto de página avanzada¶
// cypress/support/pages/DashboardPage.js
class DashboardPage {
constructor() {
this.selectors = {
userMenu: '[data-cy="user-menu"]',
userProfile: '[data-cy="user-profile"]',
notifications: '[data-cy="notifications"]',
searchInput: '[data-cy="search"]',
sidebar: '[data-cy="sidebar"]',
mainContent: '[data-cy="main-content"]'
}
}
visit() {
cy.visit('/dashboard')
this.waitForLoad()
return this
}
waitForLoad() {
cy.get(this.selectors.mainContent).should('be.visible')
cy.get('.loading-spinner').should('not.exist')
return this
}
openUserMenu() {
cy.get(this.selectors.userMenu).click()
return this
}
search(query) {
cy.get(this.selectors.searchInput)
.clear()
.type(`${query}{enter}`)
return this
}
// Navigation methods
navigateToProfile() {
this.openUserMenu()
cy.get(this.selectors.userProfile).click()
return this
}
// Assertion methods
shouldShowWelcomeMessage(username) {
cy.contains(`Welcome, ${username}`).should('be.visible')
return this
}
shouldHaveNotificationCount(count) {
cy.get(this.selectors.notifications)
.find('.badge')
.should('contain', count)
return this
}
}
export default DashboardPage
Page Object with API Integration¶
// cypress/support/pages/UsersPage.js
class UsersPage {
visit() {
cy.intercept('GET', '/api/users').as('getUsers')
cy.visit('/users')
cy.wait('@getUsers')
return this
}
createUser(userData) {
cy.intercept('POST', '/api/users').as('createUser')
cy.get('[data-cy="create-user-btn"]').click()
cy.get('[data-cy="name"]').type(userData.name)
cy.get('[data-cy="email"]').type(userData.email)
cy.get('[data-cy="save"]').click()
cy.wait('@createUser')
return this
}
deleteUser(userId) {
cy.intercept('DELETE', `/api/users/${userId}`).as('deleteUser')
cy.get(`[data-cy="user-${userId}"]`)
.find('[data-cy="delete-btn"]')
.click()
cy.get('[data-cy="confirm-delete"]').click()
cy.wait('@deleteUser')
return this
}
shouldShowUser(userData) {
cy.get('[data-cy="users-table"]')
.should('contain', userData.name)
.and('contain', userData.email)
return this
}
}
export default UsersPage
API Testing¶
Pruebas básicas de API¶
describe('API Tests', () => {
it('should get users', () => {
cy.request('GET', '/api/users')
.then((response) => {
expect(response.status).to.eq(200)
expect(response.body).to.have.property('users')
expect(response.body.users).to.be.an('array')
})
})
it('should create user', () => {
const newUser = {
name: 'John Doe',
email: 'john@example.com'
}
cy.request('POST', '/api/users', newUser)
.then((response) => {
expect(response.status).to.eq(201)
expect(response.body).to.have.property('id')
expect(response.body.name).to.eq(newUser.name)
expect(response.body.email).to.eq(newUser.email)
})
})
})
Pruebas de API con autenticación¶
describe('Authenticated API Tests', () => {
let authToken
before(() => {
// Login and get token
cy.request({
method: 'POST',
url: '/api/auth/login',
body: {
email: 'admin@example.com',
password: 'password123'
}
}).then((response) => {
authToken = response.body.token
})
})
it('should get protected resource', () => {
cy.request({
method: 'GET',
url: '/api/admin/users',
headers: {
'Authorization': `Bearer ${authToken}`
}
}).then((response) => {
expect(response.status).to.eq(200)
expect(response.body).to.have.property('users')
})
})
it('should fail without token', () => {
cy.request({
method: 'GET',
url: '/api/admin/users',
failOnStatusCode: false
}).then((response) => {
expect(response.status).to.eq(401)
})
})
})
```_
### Ayudadores de prueba de API
```javascript
// cypress/support/api-helpers.js
Cypress.Commands.add('apiLogin', (email, password) => {
return cy.request({
method: 'POST',
url: '/api/auth/login',
body: { email, password }
}).then((response) => {
window.localStorage.setItem('authToken', response.body.token)
return response.body.token
})
})
Cypress.Commands.add('apiRequest', (method, url, body = null) => {
const token = window.localStorage.getItem('authToken')
return cy.request({
method,
url,
body,
headers: token ? { 'Authorization': `Bearer ${token}` } : {}
})
})
// Usage
cy.apiLogin('admin@example.com', 'password123')
cy.apiRequest('GET', '/api/users')
cy.apiRequest('POST', '/api/users', { name: 'John', email: 'john@example.com' })
Validación de Schema¶
// Install ajv for JSON schema validation
// npm install --save-dev ajv
// cypress/support/schema-validation.js
import Ajv from 'ajv'
const ajv = new Ajv()
Cypress.Commands.add('validateSchema', (data, schema) => {
const validate = ajv.compile(schema)
const valid = validate(data)
if (!valid) {
throw new Error(`Schema validation failed: ${JSON.stringify(validate.errors)}`)
}
})
// Usage
const userSchema = {
type: 'object',
properties: {
id: { type: 'number' },
name: { type: 'string' },
email: { type: 'string', format: 'email' }
},
required: ['id', 'name', 'email']
}
cy.request('GET', '/api/users/1')
.then((response) => {
cy.validateSchema(response.body, userSchema)
})
Pruebas visuales¶
Pruebas de pantalla¶
describe('Visual Tests', () => {
it('should match homepage screenshot', () => {
cy.visit('/')
cy.screenshot('homepage')
})
it('should match element screenshot', () => {
cy.visit('/dashboard')
cy.get('[data-cy="user-profile"]').screenshot('user-profile')
})
it('should match full page screenshot', () => {
cy.visit('/products')
cy.screenshot('products-page', { capture: 'fullPage' })
})
})
Regreso Visual con Percy¶
// cypress/support/e2e.js
import '@percy/cypress'
// Usage in tests
describe('Visual Regression Tests', () => {
it('should match visual snapshot', () => {
cy.visit('/')
cy.percySnapshot('Homepage')
})
it('should match responsive snapshots', () => {
cy.visit('/dashboard')
cy.percySnapshot('Dashboard - Desktop', {
widths: [1280]
})
cy.percySnapshot('Dashboard - Mobile', {
widths: [375]
})
})
})
```_
### Comandos Visuales Personalizados
```javascript
// cypress/support/commands.js
Cypress.Commands.add('compareSnapshot', (name, options = {}) => {
const { threshold = 0.1, clip } = options
cy.screenshot(name, { clip })
// Custom comparison logic would go here
// This is a simplified example
cy.task('compareImages', {
actualImage: `cypress/screenshots/${name}.png`,
expectedImage: `cypress/snapshots/${name}.png`,
threshold
})
})
// Usage
cy.visit('/login')
cy.compareSnapshot('login-page', { threshold: 0.05 })
Pruebas responsivas¶
describe('Responsive Tests', () => {
const viewports = [
{ width: 320, height: 568 }, // iPhone SE
{ width: 375, height: 667 }, // iPhone 8
{ width: 768, height: 1024 }, // iPad
{ width: 1024, height: 768 }, // iPad Landscape
{ width: 1280, height: 720 }, // Desktop
]
viewports.forEach((viewport) => {
it(`should display correctly at ${viewport.width}x${viewport.height}`, () => {
cy.viewport(viewport.width, viewport.height)
cy.visit('/')
cy.screenshot(`homepage-${viewport.width}x${viewport.height}`)
// Test responsive behavior
if (viewport.width < 768) {
cy.get('[data-cy="mobile-menu"]').should('be.visible')
cy.get('[data-cy="desktop-menu"]').should('not.be.visible')
} else {
cy.get('[data-cy="mobile-menu"]').should('not.be.visible')
cy.get('[data-cy="desktop-menu"]').should('be.visible')
}
})
})
})
CI/CD Integration¶
GitHub Actions¶
# .github/workflows/cypress.yml
name: Cypress Tests
on: [push, pull_request]
jobs:
cypress-run:
runs-on: ubuntu-latest
strategy:
matrix:
browser: [chrome, firefox, edge]
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Start application
run: npm start &
- name: Wait for application
run: npx wait-on http://localhost:3000
- name: Run Cypress tests
uses: cypress-io/github-action@v5
with:
browser: ${{ matrix.browser }}
record: true
parallel: true
env:
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Upload screenshots
uses: actions/upload-artifact@v3
if: failure()
with:
name: cypress-screenshots-${{ matrix.browser }}
path: cypress/screenshots
- name: Upload videos
uses: actions/upload-artifact@v3
if: always()
with:
name: cypress-videos-${{ matrix.browser }}
path: cypress/videos
Docker Integration¶
# Dockerfile.cypress
FROM cypress/included:12.17.0
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
# Run tests
CMD ["npx", "cypress", "run"]
# docker-compose.yml
version: '3.8'
services:
app:
build: .
ports:
- "3000:3000"
environment:
- NODE_ENV=test
cypress:
build:
context: .
dockerfile: Dockerfile.cypress
depends_on:
- app
environment:
- CYPRESS_baseUrl=http://app:3000
volumes:
- ./cypress/videos:/app/cypress/videos
- ./cypress/screenshots:/app/cypress/screenshots
Pruebas paralelas¶
// cypress.config.js
module.exports = defineConfig({
e2e: {
// Enable parallel testing
setupNodeEvents(on, config) {
// Split tests across multiple machines
if (config.env.SPLIT_TESTS) {
| | | | const totalMachines = config.env.TOTAL_MACHINES | | 1 | | | |
| | | | const currentMachine = config.env.CURRENT_MACHINE | | 0 | | | |
// Custom logic to split test files
config.specPattern = getSpecsForMachine(currentMachine, totalMachines)
}
return config
}
}
})
function getSpecsForMachine(current, total) {
// Implementation to split specs across machines
const allSpecs = [
'cypress/e2e/auth/*.cy.js',
'cypress/e2e/dashboard/*.cy.js',
'cypress/e2e/users/*.cy.js'
]
return allSpecs.filter((_, index) => index % total === current)
}
Test Reporting¶
# Install mochawesome reporter
npm install --save-dev mochawesome mochawesome-merge mochawesome-report-generator
// cypress.config.js
module.exports = defineConfig({
e2e: {
reporter: 'mochawesome',
reporterOptions: {
reportDir: 'cypress/reports',
overwrite: false,
html: false,
json: true
}
}
})
{
"scripts": {
"test:report": "cypress run && npm run merge:reports && npm run generate:report",
"merge:reports": "mochawesome-merge cypress/reports/*.json > cypress/reports/merged-report.json",
"generate:report": "marge cypress/reports/merged-report.json --reportDir cypress/reports --inline"
}
}
Buenas prácticas¶
Organización de los ensayos¶
// Good: Descriptive test names
describe('User Authentication', () => {
it('should allow user to login with valid credentials', () => {
// Test implementation
})
it('should display error message for invalid credentials', () => {
// Test implementation
})
it('should redirect to dashboard after successful login', () => {
// Test implementation
})
})
// Good: Group related tests
describe('Shopping Cart', () => {
describe('Adding Items', () => {
it('should add item to cart')
it('should update cart count')
it('should show item in cart sidebar')
})
describe('Removing Items', () => {
it('should remove item from cart')
it('should update cart total')
it('should show empty cart message when no items')
})
})
Selector Mejores Prácticas¶
// ❌ Bad: Fragile selectors
cy.get('.btn.btn-primary.submit-button')
cy.get('div > form > div:nth-child(2) > input')
cy.get('button:contains("Submit")')
// ✅ Good: Stable selectors
cy.get('[data-cy="submit-button"]')
cy.get('[data-testid="email-input"]')
cy.get('[aria-label="Submit form"]')
// ✅ Good: Custom commands for common patterns
Cypress.Commands.add('getByTestId', (testId) => {
return cy.get(`[data-testid="${testId}"]`)
})
cy.getByTestId('email-input').type('user@example.com')
Esperando mejores prácticas¶
// ❌ Bad: Arbitrary waits
cy.wait(3000)
cy.get('button').click()
// ✅ Good: Wait for specific conditions
cy.get('[data-cy="loading"]').should('not.exist')
cy.get('[data-cy="submit-button"]').should('be.enabled').click()
// ✅ Good: Wait for network requests
cy.intercept('POST', '/api/users').as('createUser')
cy.get('[data-cy="submit"]').click()
cy.wait('@createUser')
Gestión de datos de prueba¶
// ✅ Good: Use fixtures for test data
cy.fixture('users.json').then((users) => {
const testUser = users.validUser
cy.login(testUser.email, testUser.password)
})
// ✅ Good: Generate dynamic test data
const generateUser = () => ({
name: `Test User ${Date.now()}`,
email: `test${Date.now()}@example.com`,
password: 'password123'
})
// ✅ Good: Clean up test data
afterEach(() => {
cy.task('cleanupTestData')
})
Manejo de errores¶
// ✅ Good: Handle expected errors
cy.get('[data-cy="submit"]').click()
cy.get('[data-cy="error-message"]')
.should('be.visible')
.and('contain', 'Email is required')
// ✅ Good: Retry flaky operations
Cypress.Commands.add('retryableClick', (selector, maxRetries = 3) => {
let attempts = 0
const clickWithRetry = () => {
attempts++
return cy.get(selector).click().then(() => {
// Verify click was successful
return cy.get(selector).should('have.class', 'clicked')
}).catch((error) => {
if (attempts < maxRetries) {
cy.wait(1000)
return clickWithRetry()
}
throw error
})
}
return clickWithRetry()
})
Debugging¶
Debug Commands¶
// Pause test execution
cy.pause()
// Debug specific element
cy.get('[data-cy="button"]').debug()
// Log values
cy.get('[data-cy="input"]').then(($el) => {
console.log('Element value:', $el.val())
})
// Take screenshot at specific point
cy.screenshot('debug-point-1')
// Log network requests
cy.intercept('**', (req) => {
console.log('Request:', req.method, req.url)
})
Browser DevTools¶
// Open DevTools programmatically
cy.window().then((win) => {
win.debugger
})
// Inspect element in DevTools
cy.get('[data-cy="element"]').then(($el) => {
debugger // Will pause in DevTools
console.log($el[0]) // Inspect element
})
// Access application state
cy.window().its('store').then((store) => {
console.log('Redux state:', store.getState())
})
Comandos de depuración personalizados¶
// cypress/support/commands.js
Cypress.Commands.add('logElement', (selector) => {
cy.get(selector).then(($el) => {
console.log('Element:', $el[0])
console.log('Text:', $el.text())
console.log('Value:', $el.val())
console.log('Classes:', $el.attr('class'))
})
})
Cypress.Commands.add('debugState', () => {
cy.window().then((win) => {
if (win.store) {
console.log('Redux State:', win.store.getState())
}
if (win.localStorage) {
console.log('LocalStorage:', win.localStorage)
}
if (win.sessionStorage) {
console.log('SessionStorage:', win.sessionStorage)
}
})
})
// Usage
cy.logElement('[data-cy="user-profile"]')
cy.debugState()
Test Debugging Strategies¶
// Strategy 1: Isolate failing test
it.only('should debug this specific test', () => {
// Test implementation
})
// Strategy 2: Add detailed logging
it('should complete user flow', () => {
cy.log('Step 1: Visit login page')
cy.visit('/login')
cy.log('Step 2: Fill credentials')
cy.get('[data-cy="email"]').type('user@example.com')
cy.get('[data-cy="password"]').type('password123')
cy.log('Step 3: Submit form')
cy.get('[data-cy="submit"]').click()
cy.log('Step 4: Verify redirect')
cy.url().should('include', '/dashboard')
})
// Strategy 3: Break down complex tests
it('should login user', () => {
cy.login('user@example.com', 'password123')
})
it('should navigate to profile', () => {
cy.login('user@example.com', 'password123')
cy.get('[data-cy="profile-link"]').click()
cy.url().should('include', '/profile')
})
```_
## Ejecución
### Prueba de rendimiento
```javascript
// Optimize test setup
describe('Performance Optimized Tests', () => {
// Use beforeEach for common setup
beforeEach(() => {
cy.login('user@example.com', 'password123')
})
// Avoid unnecessary visits
it('should test multiple features on same page', () => {
cy.visit('/dashboard')
// Test multiple things on the same page
cy.get('[data-cy="user-name"]').should('contain', 'John Doe')
cy.get('[data-cy="notification-count"]').should('contain', '3')
cy.get('[data-cy="recent-activity"]').should('be.visible')
})
})
// Use API for setup when possible
Cypress.Commands.add('seedData', () => {
return cy.request('POST', '/api/test/seed', {
users: 10,
posts: 50,
comments: 100
})
})
// Usage
beforeEach(() => {
cy.seedData()
})
Ejecución paralela¶
# Run tests in parallel (requires Cypress Dashboard)
npx cypress run --record --parallel
# Split tests manually
npx cypress run --spec "cypress/e2e/auth/**/*"
npx cypress run --spec "cypress/e2e/dashboard/**/*"
npx cypress run --spec "cypress/e2e/users/**/*"
Optimización de memoria¶
// cypress.config.js
module.exports = defineConfig({
e2e: {
// Optimize memory usage
numTestsKeptInMemory: 5,
// Disable video for faster execution
video: false,
// Reduce screenshot quality
screenshotOnRunFailure: true,
setupNodeEvents(on, config) {
// Clear browser cache between tests
on('before:browser:launch', (browser, launchOptions) => {
if (browser.name === 'chrome') {
launchOptions.args.push('--disable-dev-shm-usage')
launchOptions.args.push('--memory-pressure-off')
}
return launchOptions
})
}
}
})
Solución de problemas¶
Cuestiones comunes¶
// Issue: Element not found
// Solution: Wait for element or check selector
cy.get('[data-cy="button"]', { timeout: 10000 }).should('exist')
// Issue: Element not clickable
// Solution: Ensure element is visible and enabled
cy.get('[data-cy="button"]')
.should('be.visible')
.and('not.be.disabled')
.click()
// Issue: Flaky tests
// Solution: Add proper waits and assertions
cy.intercept('GET', '/api/data').as('getData')
cy.visit('/page')
cy.wait('@getData')
cy.get('[data-cy="content"]').should('be.visible')
// Issue: Cross-origin errors
// Solution: Disable web security (not recommended for production)
// cypress.config.js
{
chromeWebSecurity: false
}
Configuración de depuración¶
// cypress.config.js
module.exports = defineConfig({
e2e: {
// Enable debug mode
setupNodeEvents(on, config) {
// Log all events
on('task', {
log(message) {
console.log(message)
return null
}
})
// Debug failed tests
on('after:spec', (spec, results) => {
if (results && results.stats.failures) {
console.log('Failed spec:', spec.relative)
console.log('Failures:', results.stats.failures)
}
})
}
}
})
// Usage in tests
cy.task('log', 'Debug message here')
Recuperación de errores¶
// Retry failed commands
Cypress.Commands.add('retryCommand', (command, maxRetries = 3) => {
let attempts = 0
const executeCommand = () => {
attempts++
return command().catch((error) => {
if (attempts < maxRetries) {
cy.wait(1000)
return executeCommand()
}
throw error
})
}
return executeCommand()
})
// Usage
cy.retryCommand(() => cy.get('[data-cy="flaky-element"]').click())
-...
Resumen¶
Cypress es un poderoso marco de pruebas de extremo a extremo que proporciona:
- Experiencia de desarrolladores: API intuitiva con vista previa del navegador en tiempo real
- Esperanza Automática: La espera inteligente de elementos y solicitudes de red
- Time Travel: Pruebas de depuración con instantáneas en cada paso
- Control de red: Interceptar y modificar las solicitudes de red
- Real Browser Testing: Las pruebas se ejecutan en navegadores reales para obtener resultados precisos
- Rich Ecosystem: Extensivo ecosistema de plugins e integraciones
- CI/CD List: Apoyo integrado para la integración continua
- ** Pruebas visuales**: Capacidades de prueba de regresión visual y de pantalla
Cypress destaca en la prueba de aplicaciones web modernas con su enfoque en la experiencia del desarrollador, herramientas de depuración integral y ejecución de pruebas confiable. Su arquitectura única y conjunto de características lo convierten en una excelente opción para los equipos que buscan implementar sólidos flujos de trabajo de pruebas de extremo a extremo.
" copia de la funciónToClipboard() {} comandos const = document.querySelectorAll('code'); que todos losCommands = '; comandos. paraCada(cmd = confianza allCommands += cmd.textContent + '\n'); navigator.clipboard.writeText(allCommands); alerta ('Todos los comandos copiados a portapapeles!'); }
función generaPDF() { ventana.print(); } ■/script título