Basic custom commands cover the most common needs, but Cypress offers advanced patterns that take your command library to production quality: overwriting built-in commands (add logging to every cy.visit()), creating dual commands that work as both parent and child chains, adding TypeScript type definitions for IntelliSense, and following naming conventions that keep your command library discoverable and collision-free.
Advanced Custom Command Patterns
These patterns are what separate a handful of convenience commands from a professional, well-typed command library.
// ── PATTERN 1: Overwriting built-in commands ──
// Add logging to every cy.visit() call
Cypress.Commands.overwrite('visit', (originalFn, url, options) => {
Cypress.log({ name: 'visit', message: `Navigating to: ${url}` });
return originalFn(url, options);
});
// ── PATTERN 2: Dual commands (parent + child chain) ──
// A command that works both as cy.getByTestId('x') and el.getByTestId('x')
Cypress.Commands.add('getByTestId', { prevSubject: 'optional' },
(subject, testId: string) => {
const selector = `[data-cy="${testId}"]`;
if (subject) {
// Chained from a parent: el.getByTestId('x') — scoped search
return cy.wrap(subject).find(selector);
}
// Called directly: cy.getByTestId('x') — document-wide search
return cy.get(selector);
}
);
// Usage:
// cy.getByTestId('submit-btn').click(); // From document root
// cy.get('.form').getByTestId('email-input').type('x'); // Scoped within .form
// ── PATTERN 3: Commands that return values via .then() ──
Cypress.Commands.add('getCartTotal', () => {
return cy.get('[data-cy="cart-total"]')
.invoke('text')
.then((text) => {
return parseFloat(text.replace(/[^0-9.]/g, ''));
});
});
// Usage:
// cy.getCartTotal().should('eq', 59.97);
// cy.getCartTotal().then((total) => { expect(total).to.be.greaterThan(0); });
// ── PATTERN 4: TypeScript type definitions ──
// File: cypress/support/index.d.ts
/*
declare namespace Cypress {
interface Chainable {
loginUI(username: string, password: string): Chainable;
loginAPI(username: string, password: string): Chainable;
addToCart(productIndex?: number): Chainable;
cartShouldHave(count: number): Chainable;
fillAddress(address: {
firstName: string;
lastName: string;
postcode: string;
}): Chainable;
getByTestId(testId: string): Chainable>;
getCartTotal(): Chainable;
}
}
*/
// With these type definitions:
// - VS Code shows autocomplete for cy.loginUI(), cy.addToCart(), etc.
// - TypeScript catches typos: cy.lognUI() → compile error
// - Parameter types are enforced: cy.addToCart('wrong') → type error
// ── PATTERN 5: Command naming conventions ──
const NAMING_CONVENTIONS = {
actions: "verb-based: cy.loginUI, cy.addToCart, cy.fillAddress, cy.submitForm",
assertions: "should-prefix: cy.cartShouldHave, cy.pageShouldShow, cy.urlShouldBe",
queries: "get-prefix: cy.getByTestId, cy.getCartTotal, cy.getProductNames",
prefixed: "domain-prefix for large suites: cy.authLogin, cy.cartAdd, cy.checkoutFill",
};
// ── PATTERN 6: Conditional commands (check before act) ──
Cypress.Commands.add('dismissCookieBanner', () => {
cy.get('body').then(($body) => {
if ($body.find('[data-cy="cookie-accept"]').length > 0) {
cy.get('[data-cy="cookie-accept"]').click();
cy.get('[data-cy="cookie-banner"]').should('not.exist');
}
// If no banner found, do nothing — no error
});
});
cy.loginUI() shows a TypeScript error (“Property ‘loginUI’ does not exist on type ‘cy'”), and developers get no autocomplete or parameter validation. The type definition file (cypress/support/index.d.ts) extends the Cypress.Chainable interface with your custom command signatures. Once added, VS Code provides full IntelliSense: autocomplete, parameter hints, and compile-time type checking.prevSubject: 'optional' option creates dual commands that work both as standalone (cy.getByTestId('x')) and chained (cy.get('.form').getByTestId('x')). This pattern is ideal for query commands like getByTestId that should search the entire document when called directly but scope within a parent when chained. It replaces the need for separate cy.getByTestId() and .findByTestId() commands.Cypress.Commands.overwrite) is powerful but dangerous. If your overwrite has a bug — for example, forgetting to call originalFn — every cy.visit() in your entire suite breaks. Test overwrites thoroughly before deploying them. Use overwrites sparingly: logging, performance timing, and retry enhancement are good use cases. Changing fundamental behaviour (modifying the URL, adding default options) is risky.Common Mistakes
Mistake 1 — Not adding TypeScript type definitions for custom commands
❌ Wrong: Custom commands work at runtime but show red squiggly lines in VS Code, no autocomplete, and TypeScript compilation warnings.
✅ Correct: Adding a cypress/support/index.d.ts file that declares all custom commands with their parameter types and return types.
Mistake 2 — Naming custom commands that collide with built-in ones
❌ Wrong: Cypress.Commands.add('check', ...) — shadows the built-in .check() command for checkboxes.
✅ Correct: Cypress.Commands.add('verifyCartCount', ...) or Cypress.Commands.add('cartShouldHave', ...) — unique, descriptive names.