Custom Commands¶
What was run¶
A real custom Cypress command (Cypress.Commands.add("login", ...)), used by two real tests, then a
deliberate real id rename on the login button to measure — not assume — the actual cost of duplicating a
login flow versus centralizing it in one command.
Cypress.Commands.add("login", (username, password) => {
cy.visit("/login.html");
cy.get("#username").type(username);
cy.get("#password").type(password);
cy.get("#submit-btn").click();
});
it("logs in via the real custom cy.login() command", () => {
cy.login("realuser", "realpass123");
cy.get("#result").should("have.text", "Login Successful");
});
it("reuses the identical custom command for a failing case", () => {
cy.login("realuser", "wrongpass");
cy.get("#result").should("have.text", "Login Failed");
});
✓ logs in via the real custom cy.login() command (458ms)
✓ reuses the identical custom command for a failing case (358ms)
A real rename, and its real cost¶
# real change: id="submit-btn" -> id="log-in-btn" in login.html
npx cypress run --spec cypress/e2e/custom-commands.cy.js
1) logs in via the real custom cy.login() command:
AssertionError: Timed out retrying after 4000ms: Expected to find element: `#submit-btn`, but never found it.
2) reuses the identical custom command for a failing case:
AssertionError: Timed out retrying after 4000ms: Expected to find element: `#submit-btn`, but never found it.
0 passing (9s)
2 failing
Both real tests broke from one real markup change — expected, since both call the same cy.login()
command. The real question is what fixing it actually costs. Editing only the command definition
(#submit-btn → #log-in-btn, one line, one place) and re-running without touching either test file:
✓ logs in via the real custom cy.login() command (437ms)
✓ reuses the identical custom command for a failing case (375ms)
2 passing (863ms)
Both tests recovered from a single edit in a single location.
How It Actually Works¶
Cypress.Commands.add() registers a real new method on the cy object, available to every spec in the
suite after it's defined — it's not syntactic sugar for a shared function call, it genuinely becomes part
of Cypress's own command queue, which means a custom command auto-waits and chains exactly like a built-in
one (cy.login(...) queues real cy.visit, cy.get, and .type calls, each with its own real
retry-ability). This lesson's real rename experiment demonstrates the same underlying lesson this network's
Selenium Mastery Path
measured for the Page Object Model: centralizing a locator in exactly one place means exactly one place
needs to change when the real application's markup changes, regardless of how many tests depend on it.
Common mistakes¶
- Defining the same login steps inline in every test "because it's simple enough" — this lesson's own real 2-test suite already shows the cost: one markup change broke both, and would break a third, fourth, and fifth test the same way if they existed.
- Putting custom commands in a spec file instead of a shared support file in a real project (this course's
lab keeps
supportFile: falseand defines commands per-lesson for self-containment — a real project should centralize them incypress/support/commands.jsso every spec can use them without copy-pasting theCypress.Commands.add()call itself). - Assuming a custom command needs to return something to be chainable —
cy.login(...)here returns nothing explicitly, but the command queue still lets you chain further real commands after it, because chaining is about the queue, not a return value.
Exercise¶
Add a second custom command, cy.addTodo(text), wrapping this course's real todo-add flow
(cy.get("#new-todo").type(text) then cy.get("#add-btn").click()), and rewrite the
Level 1 todo test to use it. Then deliberately rename #add-btn in
todos.html and confirm, by actually running the suite, that only the command definition needs a fix —
directly reproducing this lesson's real result on a second, independent real workflow.