How to Integrate Custom Commands in Cypress: A Complete Guide with Code Examples
Use Cypress.Commands.add() to extend the cy object with new commands, or Cypress.Commands.overwrite() to modify existing ones—all from your cypress/support/commands.ts file.
Integrating custom commands into your Cypress test suite lets you encapsulate repetitive workflows, extend the cy API, and keep tests readable. According to the cypress-io/cypress source code, the browser-side driver handles all registration through the Cypress.Commands object defined in packages/driver/src/cypress/commands.ts. This guide walks through the internals, command types, and practical implementations.
Where Custom Commands Live in Cypress Internals
Cypress runs across three runtimes: the Node CLI, the Electron main process, and the browser-side driver. The custom command API lives exclusively in the driver (@packages/driver), ensuring your functions execute in the same sandbox as the application under test.
When the test runner initializes, the Commands factory in packages/driver/src/cypress/commands.ts builds two internal registries:
commands— stores definitions for chainable custom commandsqueries— stores value-returning helpers (likecy.get,cy.find)
Calls from test files serialize to the driver, which executes your functions and handles retries, logging, and error bubbling exactly like native commands.
Adding Custom Commands with Cypress.Commands.add()
The Cypress.Commands.add(name, [options], fn) method is the primary entry point. The driver validates the name against built-ins and reserved properties, then derives the command type from the optional prevSubject flag.
Simple Parent Commands
Parent commands start a new command chain without requiring a previous subject.
// cypress/support/commands.ts
Cypress.Commands.add('login', (username: string, password: string) => {
cy.visit('/login')
cy.get('#user').type(username)
cy.get('#pass').type(password)
cy.get('form').submit()
})
Usage in tests:
cy.login('alice', 'password123')
cy.url().should('include', '/dashboard')
Child Commands with prevSubject
Child commands operate on a subject yielded by a previous command. Set prevSubject: 'element' (or 'document', 'window') in options.
Cypress.Commands.add(
'selectOption',
{ prevSubject: 'element' },
(subject: JQuery<HTMLElement>, option: string) => {
cy.wrap(subject).select(option)
}
)
Usage:
cy.get('select#colors').selectOption('red')
Dual-Type Commands with Optional Subjects
Use prevSubject: 'optional' to create commands that work both ways—independently or chained.
Cypress.Commands.add(
'logMessage',
{ prevSubject: 'optional' },
(subject: JQuery<HTMLElement> | void, msg: string) => {
if (subject) {
cy.wrap(subject).then(() => console.log(msg))
} else {
console.log(msg)
}
}
)
Both patterns work:
cy.logMessage('Standalone message')
cy.get('#some-element').logMessage('Message after element')
Overwriting Existing Cypress Commands
Modify built-in behavior with Cypress.Commands.overwrite(name, fn). The original implementation is passed as the first argument to your function.
Cypress.Commands.overwrite(
'click',
(originalFn, subject: JQuery<HTMLElement>, options?: Partial<Cypress.ClickOptions>) => {
cy.log('About to click an element')
return originalFn(subject, options).then(() => {
cy.log('Click completed')
})
}
)
The wrapper pattern preserves native retry logic and logging while letting you inject pre/post processing.
Adding Query Helpers with addQuery()
Queries return values rather than chainables. They're registered via addQuery in the same commands.ts file.
Cypress.Commands.addQuery('sessionStorageItem', (key: string) => {
return () => {
return window.sessionStorage.getItem(key)
}
})
Usage:
cy.sessionStorageItem('token').should('eq', 'abc123')
Unlike commands, queries don't return a Cypress chainable—they yield the value directly for assertions.
Command Type Resolution and Validation
The driver uses getTypeByPrevSubject (in packages/driver/src/cypress/commands.ts) to determine command type:
prevSubject value |
Resulting type | Behavior |
|---|---|---|
undefined (default) |
parent | Starts new chain |
'element' / 'document' / 'window' |
child | Requires matching subject |
'optional' |
dual | Works either way |
false |
parent | Explicitly no subject |
Name conflicts trigger internalError if you attempt to shadow built-ins like get, visit, or log. The PLACEHOLDER_COMMANDS set (containing .mount, .hover, etc.) allows special exceptions for commands added without triggering duplicate errors.
Complete Working Example
Here's a production-ready cypress/support/commands.ts combining all patterns:
// cypress/support/commands.ts
// Extend Cypress namespace for TypeScript
declare global {
namespace Cypress {
interface Chainable {
login(username: string, password: string): Chainable<void>
selectOption(value: string): Chainable<Element>
logMessage(message: string): Chainable<Element | void>
sessionStorageItem(key: string): Chainable<string | null>
}
}
}
// Parent command
Cypress.Commands.add('login', (username, password) => {
cy.visit('/login')
cy.get('[data-testid=username]').type(username)
cy.get('[data-testid=password]').type(password)
cy.get('[data-testid=submit]').click()
})
// Child command
Cypress.Commands.add(
'selectOption',
{ prevSubject: 'element' },
(subject, value) => {
cy.wrap(subject).select(value)
return cy.wrap(subject)
}
)
// Dual command
Cypress.Commands.add(
'logMessage',
{ prevSubject: 'optional' },
(subject, message) => {
const timestamp = new Date().toISOString()
if (subject) {
cy.wrap(subject).then($el => {
console.log(`[${timestamp}] ${message}:`, $el)
})
return cy.wrap(subject)
}
console.log(`[${timestamp}] ${message}`)
}
)
// Query helper
Cypress.Commands.addQuery('sessionStorageItem', (key) => {
const val = window.sessionStorage.getItem(key)
return () => val
})
Key Source Files for Reference
| File | Purpose |
|---|---|
packages/driver/src/cypress/commands.ts |
Core implementation of add, overwrite, addQuery, overwriteQuery; contains getTypeByPrevSubject, internalError, PLACEHOLDER_COMMANDS |
cli/types/cypress.d.ts |
TypeScript definitions including command API overloads |
packages/driver/src/cypress/command_queue.ts |
Underlying queue mechanics for command execution and retries |
Summary
Cypress.Commands.add()registers new commands in the driver-side registry, with type derived fromprevSubjectprevSubject: 'element'creates child commands;'optional'creates dual commands; omitting it creates parent commandsCypress.Commands.overwrite()wraps original implementations so you can augment or replace behavioraddQuery()registers value-returning helpers distinct from chainable commands- All registration flows through
packages/driver/src/cypress/commands.ts, which validates names against built-ins and reserved properties
Frequently Asked Questions
How do I add TypeScript types for custom commands?
Extend the Cypress.Chainable interface in a declaration merged namespace. Place this in cypress/support/commands.ts or a dedicated cypress/support/index.d.ts. The definitions in cli/types/cypress.d.ts show how Cypress structures these overloads for built-ins.
Can I overwrite built-in commands like cy.get or cy.visit?
Technically yes, but the driver throws via internalError for reserved names. Only commands in PLACEHOLDER_COMMANDS (like .mount, .hover) can be added without conflict. For others, use overwrite, which the driver permits for extending behavior while preserving the original reference.
What's the difference between a command and a query?
Commands return a chainable and participate in Cypress's retry- and log-system. Queries (added via addQuery) return a value directly and are stored in the separate queries registry. Use queries for simple value extraction; commands for operations that need chaining and automatic retries.
Where should I register custom commands?
Define them in cypress/support/commands.ts (or .js), which the scaffolded cypress.config.js imports by default. This file runs before specs, making additions available globally via cy.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →