Day 7: Custom Commands and Utilities
What You'll Learn Today
- How to create custom commands with Cypress.Commands.add()
- The difference between parent, child, and dual commands
- Practical examples of building a login command
- Session management with cy.session()
- Organizing support/commands.js
- Adding TypeScript support with type definitions
- Managing environment variables with Cypress.env()
What Are Custom Commands?
As you write tests, you'll encounter repetitive operations like login flows and API requests. Cypress custom commands let you define these as reusable commands.
flowchart TB
subgraph Before["Before Custom Commands"]
T1["Test 1\nLogin logic written"]
T2["Test 2\nLogin logic written"]
T3["Test 3\nLogin logic written"]
end
subgraph After["After Custom Commands"]
CMD["cy.login()\nCustom Command"]
TA["Test 1"]
TB["Test 2"]
TC["Test 3"]
CMD --> TA
CMD --> TB
CMD --> TC
end
style Before fill:#ef4444,color:#fff
style After fill:#22c55e,color:#fff
| Approach | Pros | Cons |
|---|---|---|
| Copy-paste code | Simple | Hard to maintain |
| Extract as function | Reusable | Cannot chain with cy |
| Custom command | Chainable, natural syntax | Slightly higher learning curve |
Basics of Cypress.Commands.add()
Custom commands are defined in cypress/support/commands.js (or .ts).
Basic Syntax
Cypress.Commands.add(name, options, callbackFn)
- name: Command name (string)
- options: Options (optional)
- callbackFn: The command implementation
Your First Custom Command
// cypress/support/commands.js
// A simple custom command
Cypress.Commands.add('getBySel', (selector) => {
return cy.get(`[data-testid="${selector}"]`)
})
// Usage in tests
// cy.getBySel('submit-button').click()
// cypress/support/commands.js
// A custom command with multiple arguments
Cypress.Commands.add('fillForm', (name, email, message) => {
cy.get('#name').type(name)
cy.get('#email').type(email)
cy.get('#message').type(message)
})
// Usage in tests
// cy.fillForm('John', 'john@example.com', 'Hello!')
Three Types of Commands
Cypress custom commands come in three types.
1. Parent Command
Used at the beginning of a chain. Starts with cy..
// Parent command: cy.login()
Cypress.Commands.add('login', (username, password) => {
cy.visit('/login')
cy.get('#username').type(username)
cy.get('#password').type(password)
cy.get('button[type="submit"]').click()
cy.url().should('include', '/dashboard')
})
2. Child Command
Operates on the result of a previous command. Uses the prevSubject option.
// Child command: .shouldBeVisible()
Cypress.Commands.add('shouldBeVisible', { prevSubject: true }, (subject) => {
cy.wrap(subject).should('be.visible')
})
// Usage: cy.get('.header').shouldBeVisible()
// Child command: .typeAndValidate()
Cypress.Commands.add('typeAndValidate', { prevSubject: 'element' }, (subject, text) => {
cy.wrap(subject).clear().type(text)
cy.wrap(subject).should('have.value', text)
})
// Usage: cy.get('#email').typeAndValidate('test@example.com')
3. Dual Command
Can be used as either a parent or child command.
// Dual command: .highlight() / cy.highlight()
Cypress.Commands.add('highlight', { prevSubject: 'optional' }, (subject, color = 'yellow') => {
if (subject) {
cy.wrap(subject).then(($el) => {
$el.css('background-color', color)
})
} else {
cy.get('body').then(($body) => {
$body.css('background-color', color)
})
}
})
// Usage:
// cy.get('.important').highlight('red') // As a child command
// cy.highlight('blue') // As a parent command
flowchart LR
subgraph Parent["Parent Command"]
P["cy.login()\ncy.getBySel()"]
end
subgraph Child["Child Command"]
C[".shouldBeVisible()\n.typeAndValidate()"]
end
subgraph Dual["Dual Command"]
D[".highlight()\nprevSubject: optional"]
end
P -->|"Start of chain"| C
style Parent fill:#3b82f6,color:#fff
style Child fill:#8b5cf6,color:#fff
style Dual fill:#f59e0b,color:#fff
| Type | prevSubject | Usage |
|---|---|---|
| Parent command | None (default) | cy.commandName() |
| Child command | true or 'element' |
.commandName() |
| Dual command | 'optional' |
Either works |
Practical Example: Building a Login Command
The most commonly used custom command in real projects is a login flow.
Basic UI Login
// cypress/support/commands.js
Cypress.Commands.add('loginByUI', (username, password) => {
cy.visit('/login')
cy.get('[data-testid="username"]').type(username)
cy.get('[data-testid="password"]').type(password)
cy.get('[data-testid="login-button"]').click()
cy.get('[data-testid="dashboard"]').should('be.visible')
})
Fast Login via API
Logging in through the UI slows down tests, so using the API directly is recommended.
// cypress/support/commands.js
Cypress.Commands.add('loginByAPI', (username, password) => {
cy.request({
method: 'POST',
url: '/api/auth/login',
body: { username, password },
}).then((response) => {
window.localStorage.setItem('authToken', response.body.token)
})
})
Using It in Tests
describe('Dashboard', () => {
beforeEach(() => {
cy.loginByAPI('admin', 'password123')
cy.visit('/dashboard')
})
it('should display welcome message', () => {
cy.getBySel('welcome-message').should('contain', 'Welcome, admin')
})
})
Session Management with cy.session()
Starting with Cypress 12, cy.session() lets you cache login sessions and reuse them across tests.
Cypress.Commands.add('login', (username, password) => {
cy.session(
[username, password], // session ID (unique key)
() => {
// session setup
cy.visit('/login')
cy.get('#username').type(username)
cy.get('#password').type(password)
cy.get('button[type="submit"]').click()
cy.url().should('include', '/dashboard')
},
{
validate() {
// session validation
cy.request('/api/auth/me').its('status').should('eq', 200)
},
}
)
})
flowchart TB
START["cy.login() called"] --> CHECK{"Session\ncached?"}
CHECK -->|"No"| CREATE["Create session\n(perform login)"]
CHECK -->|"Yes"| VALIDATE["Validate session\n(validate function)"]
CREATE --> SAVE["Save session\nto cache"]
SAVE --> RESTORE["Restore session"]
VALIDATE -->|"Valid"| RESTORE
VALIDATE -->|"Invalid"| CREATE
RESTORE --> DONE["Run test"]
style START fill:#3b82f6,color:#fff
style CHECK fill:#f59e0b,color:#fff
style CREATE fill:#8b5cf6,color:#fff
style RESTORE fill:#22c55e,color:#fff
| Session Management | Description |
|---|---|
| Session ID | Uniquely identified by the username and password combination |
| Setup function | Executed when no session exists (performs login) |
| Validate function | Checks whether a cached session is still valid |
| Cache | Reuses the session within a test suite |
Organizing support/commands.js
As your project grows, organizing custom commands becomes important.
Example File Structure
cypress/
βββ support/
β βββ commands.js # Main commands file
β βββ commands/
β β βββ auth.js # Authentication commands
β β βββ navigation.js # Navigation commands
β β βββ api.js # API commands
β βββ e2e.js # E2E test support config
β βββ utils/
β βββ helpers.js # General helper functions
β βββ constants.js # Constants
// cypress/support/commands/auth.js
Cypress.Commands.add('login', (username, password) => {
cy.session([username, password], () => {
cy.request('POST', '/api/auth/login', { username, password })
})
})
Cypress.Commands.add('logout', () => {
cy.request('POST', '/api/auth/logout')
cy.clearCookies()
cy.clearLocalStorage()
})
// cypress/support/commands/navigation.js
Cypress.Commands.add('visitAndWait', (url) => {
cy.visit(url)
cy.get('[data-testid="page-loaded"]').should('exist')
})
// cypress/support/commands.js (main entry)
import './commands/auth'
import './commands/navigation'
import './commands/api'
TypeScript Support
In TypeScript projects, adding type definitions for custom commands enables IDE autocompletion.
Creating a Type Definition File
// cypress/support/index.d.ts
declare namespace Cypress {
interface Chainable {
/**
* Custom command to log in via API
* @param username - the user's username
* @param password - the user's password
*/
login(username: string, password: string): Chainable<void>
/**
* Custom command to select element by data-testid
* @param selector - the data-testid value
*/
getBySel(selector: string): Chainable<JQuery<HTMLElement>>
/**
* Custom command to type and validate input
* @param text - the text to type
*/
typeAndValidate(text: string): Chainable<JQuery<HTMLElement>>
}
}
tsconfig.json Configuration
// cypress/tsconfig.json
{
"compilerOptions": {
"target": "es5",
"lib": ["es5", "dom"],
"types": ["cypress", "node"]
},
"include": ["**/*.ts", "support/index.d.ts"]
}
Environment Variable Management with Cypress.env()
Configuration values used in tests (API URLs, user credentials, etc.) should be managed through environment variables as a best practice.
Defining in cypress.config.js
// cypress.config.js
const { defineConfig } = require('cypress')
module.exports = defineConfig({
e2e: {
baseUrl: 'http://localhost:3000',
env: {
apiUrl: 'http://localhost:3000/api',
adminUser: 'admin',
adminPassword: 'admin123',
},
},
})
Referencing Environment Variables
// Usage in tests
cy.request({
method: 'POST',
url: `${Cypress.env('apiUrl')}/auth/login`,
body: {
username: Cypress.env('adminUser'),
password: Cypress.env('adminPassword'),
},
})
Per-Environment Configuration Files
// cypress.env.json (recommended to gitignore)
{
"apiUrl": "http://localhost:3000/api",
"adminUser": "admin",
"adminPassword": "secret_password"
}
# Pass environment variables via CLI
npx cypress run --env apiUrl=http://staging.example.com/api,adminUser=test
# Pass via OS environment variables
CYPRESS_API_URL=http://staging.example.com/api npx cypress run
| Method | Priority | Use Case |
|---|---|---|
CLI --env |
Highest | Temporary overrides in CI/CD |
CYPRESS_* env vars |
High | CI/CD environment configuration |
cypress.env.json |
Medium | Local development secrets |
cypress.config.js env |
Low | Default values |
Organizing Utility Functions
Operations that don't need to be custom commands can be organized as regular utility functions.
// cypress/support/utils/helpers.js
// Generate a random email address
export function generateEmail() {
const timestamp = Date.now()
return `test-${timestamp}@example.com`
}
// Format a date
export function formatDate(date) {
return date.toISOString().split('T')[0]
}
// Generate test data
export function createUser(overrides = {}) {
return {
name: 'Test User',
email: generateEmail(),
role: 'user',
...overrides,
}
}
// Usage in tests
import { createUser, formatDate } from '../support/utils/helpers'
describe('User Registration', () => {
it('should register a new user', () => {
const user = createUser({ role: 'admin' })
cy.visit('/register')
cy.get('#name').type(user.name)
cy.get('#email').type(user.email)
cy.get('#role').select(user.role)
cy.get('button[type="submit"]').click()
cy.contains(`Welcome, ${user.name}`)
})
})
Summary
| Concept | Description |
|---|---|
| Cypress.Commands.add() | API for defining custom commands |
| Parent command | Starting point of a chain, begins with cy. |
| Child command | Operates on a previous subject (prevSubject: true) |
| Dual command | Works as either parent or child (prevSubject: 'optional') |
| cy.session() | Caches and reuses sessions |
| Cypress.env() | Configuration management via environment variables |
| Type definitions | Declaration files that enable TypeScript autocompletion |
Key Takeaways
- Extract repetitive operations into custom commands for better reusability
- Use
cy.session()for login flows to improve test speed - Manage sensitive data with
cypress.env.jsonor environment variables, and never commit them to Git - Distinguish between custom commands and utility functions to keep your code organized
Exercises
Exercise 1: Basics
Create a custom command cy.getByDataCy(value) that selects elements by their data-cy attribute.
Exercise 2: Intermediate
Create the following custom commands:
cy.login(email, password)- Login via UIcy.loginByAPI(email, password)- Login via APIcy.logout()- Logout
Challenge
Create a custom command that uses cy.session() to cache login sessions. Include a validate function that verifies session validity and re-authenticates if the session is invalid. Also add TypeScript type definitions.
References
Next up: In Day 8, we'll explore fixtures and data-driven testing. You'll learn how to leverage external data files to manage test cases efficiently.