Learn Cypress in 10 DaysDay 7: Custom Commands and Utilities
Chapter 7Learn Cypress in 10 Days

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

  1. Extract repetitive operations into custom commands for better reusability
  2. Use cy.session() for login flows to improve test speed
  3. Manage sensitive data with cypress.env.json or environment variables, and never commit them to Git
  4. 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 UI
  • cy.loginByAPI(email, password) - Login via API
  • cy.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.