Custom Commands
As a test suite grows, the same sequences — logging in, adding an item to a cart, navigating to a specific settings page — tend to repeat across dozens of test files. Cypress.Commands.add() lets you package such a sequence once and reuse it anywhere as a first-class command, like cy.login('user@example.com', 'password123'), keeping tests DRY and making the intent of a test easier to read at a glance.
Cricket analogy: A team develops a signature fielding drill that gets reused in every practice session instead of re-explaining footwork from scratch each time, similar to how Cypress.Commands.add('login', ...) packages a repeated login sequence into one reusable cy.login() call instead of retyping it in every test.
Defining a Custom Command
A custom command is defined by calling Cypress.Commands.add(name, callbackFn) inside cypress/support/commands.js, which runs before every spec file since commands.js is imported from cypress/support/e2e.js. The callback function contains ordinary Cypress commands — cy.visit(), cy.get(), cy.type(), cy.click() — exactly as you'd write them inline in a test, just wrapped up under a reusable name.
Cricket analogy: A fielding coach documents the exact drill steps — starting position, first step, dive technique — in a training manual so any coach can run it identically, similar to how Cypress.Commands.add('login', (email, password) => {...}) documents the exact sequence of steps a custom command performs.
// cypress/support/commands.js
Cypress.Commands.add('login', (email, password) => {
cy.visit('/login');
cy.get('#email').type(email);
cy.get('#password').type(password);
cy.get('button[type=submit]').click();
cy.url().should('include', '/dashboard');
});
// usage in a test
cy.login('user@example.com', 'password123');Parent, Child, and Dual Commands
By default, Cypress.Commands.add() creates a parent command, which always starts a fresh chain regardless of what came before it, like cy.login() does. Passing { prevSubject: true } as an options argument creates a child command that receives the previous command's yielded subject as its first argument, letting you write something like cy.get('.price-list').getTotal(), where getTotal operates on the elements cy.get() already found. Setting prevSubject: 'optional' creates a dual command that can be used either way.
Cricket analogy: A fresh new-ball bowler starts an over from scratch regardless of what happened before, but a substitute fielder coming on continues exactly from where the previous fielder was standing, mirroring how a parent command starts fresh like cy.login() while a child command with {prevSubject: true} continues operating on the subject the previous command yielded.
Register custom commands in cypress/support/commands.js. If the project uses TypeScript, also add a declaration in cypress/support/index.d.ts extending the global Cypress.Chainable interface, so cy.login() gets full autocomplete and type checking in your editor.
Best Practices for Custom Commands
Custom commands work best for genuinely repeated, cohesive sequences like authentication or seeding test data via an API call. Simple one-off interactions don't need a custom command wrapper — plain cy.get().click() is often clearer than a needlessly abstracted cy.clickTheThing(). Keep each command focused on one responsibility, and avoid burying conditional branching logic (if/else based on page state) inside a command, since that kind of logic makes tests harder to reason about and debug.
Cricket analogy: A captain who calls for a DRS review on every single delivery dilutes its value and slows the game down, similar to how wrapping every single assertion in its own custom command can obscure what a test is actually checking and make failures harder to diagnose.
Be cautious about wrapping assertions inside custom commands, especially ones with vague names. A command like cy.verifyPage() that silently checks five unrelated things hides exactly which condition failed when a test breaks, and future readers can't tell what's actually being verified without opening commands.js.
- Cypress.Commands.add(name, callbackFn) packages a repeated command sequence into a reusable named command.
- Custom commands are typically registered in cypress/support/commands.js, loaded before every spec.
- Parent commands (the default) always start a fresh chain; child commands, created with { prevSubject: true }, operate on the previous command's yielded subject.
- Dual commands (prevSubject: 'optional') can be used either as a parent or a child.
- TypeScript projects should extend Cypress.Chainable in index.d.ts for autocomplete and type safety.
- Reserve custom commands for genuinely repeated, cohesive sequences rather than trivial one-off interactions.
- Avoid burying vague, multi-purpose assertions or conditional logic inside a custom command, since it obscures failures.
Practice what you learned
1. What function is used to define a custom Cypress command?
2. Where should custom commands typically be registered?
3. What does passing { prevSubject: true } to Cypress.Commands.add() create?
4. What does a dual command (prevSubject: 'optional') allow?
5. What's a downside of wrapping too many unrelated assertions inside one vaguely-named custom command?
Was this page helpful?
You May Also Like
Chaining and Retry-ability
Understand how Cypress commands chain together via the command queue and how built-in retry-ability makes assertions resilient to asynchronous UI changes.
Interacting with Elements (type, click, check)
Cover Cypress's core interaction commands — type(), click(), check(), select() — and the actionability checks Cypress performs before firing each one.
Assertions with should() and expect()
Learn the difference between Cypress's retrying should()/and() assertions and Chai's one-time expect() assertions, and when to use each.