Skip to content

Cypress Custom Commands

Custom commands let you extract repeated logic into a reusable, first-class Cypress command. This lesson covers how to define them, the different command types, and how to add TypeScript types for them.

Cypress.Commands.add()

Cypress.Commands.add() registers a new command on the global cy object, available in every spec file once defined in a support file. It takes a command name, an optional command type, and an implementation function.

// cypress/support/commands.js
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();
});

// in a spec file
cy.login('user@example.com', 'SecurePass123!');

Once defined, cy.login(...) behaves exactly like a built-in command, fully chainable and appearing in the command log.

Command Types

Cypress.Commands.add('name', fn)                      // parent command
Cypress.Commands.add('name', { prevSubject: true }, fn) // child command
Cypress.Commands.add('name', { prevSubject: 'optional' }, fn) // dual command
  • Parent commands start a new chain, like cy.login(), and don't need a previous subject.
  • Child commands must be chained onto an existing subject, like .shouldHaveError().
  • Dual commands work either standalone or chained, offering flexibility for utility-style commands.
  • Command implementations can use any other Cypress command internally, including .then().

Custom Commands Cheat Sheet

Patterns for defining and calling custom commands.

Type Definition Option Called As
Parent no prevSubject cy.login(...)
Child { prevSubject: true } cy.get(x).myChildCommand()
Dual { prevSubject: 'optional' } Either standalone or chained
Element-only child { prevSubject: 'element' } Requires a DOM element subject

A Practical Child Command

Child commands are useful for reusable assertions or transformations applied to an already-located element, keeping call sites concise and expressive.

Cypress.Commands.add(
  'shouldBeInvalidWithMessage',
  { prevSubject: true },
  (subject, message) => {
    cy.wrap(subject)
      .should('have.class', 'is-invalid')
      .siblings('[data-cy="error"]')
      .should('contain', message);
  }
);

// usage
cy.get('[data-cy="email"]').shouldBeInvalidWithMessage('Email is required');

Adding TypeScript Types for Custom Commands

In a TypeScript project, custom commands need a type declaration merged into Cypress's global Chainable interface, otherwise TypeScript will not recognize them and editor autocompletion will not work.

// cypress/support/index.d.ts
declare namespace Cypress {
  interface Chainable {
    login(email: string, password: string): Chainable<void>;
  }
}

This declaration file should be referenced from your TypeScript config so the editor and compiler pick it up.

Common Mistakes

  • Defining a custom command as a child command when it doesn't actually need a previous subject.
  • Forgetting to add TypeScript type declarations, causing confusing type errors for custom commands.
  • Putting UI-specific logic inside a command meant to be reused across unrelated features.
  • Not registering commands in a support file that actually gets loaded before spec files run.

Key Takeaways

  • Cypress.Commands.add() registers reusable custom commands on the global cy object.
  • Commands can be parent, child, or dual, depending on whether they need a previous subject.
  • Custom commands need explicit TypeScript type declarations in TypeScript projects.
  • Custom commands should live in a support file that loads before every spec.

Pro Tip

Name custom commands as verbs describing intent, like cy.login() or cy.seedCart(), rather than describing implementation, like cy.fillLoginFormAndClickSubmit(), this keeps call sites readable and gives you freedom to change the implementation later without renaming every usage.