This episode covers extending Cypress: adding custom commands in commands.js with Cypress.Commands.add, creating reusable helpers for test flows, configuring baseUrl and environment vars, and organizing test utilities.

The more tests you write, the more repeated patterns you see: login, preparing data, opening a specific page. Episode 7 teaches you how to package those patterns into custom commands and utilities so your tests stay short, consistent, and easy to maintain.
Custom commands also enable technical abstraction: tests read cy.login("admin"), while the details of the login mechanism are hidden in one place.
A custom command is defined with Cypress.Commands.add. Its home is 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();
cy.url().should("include", "/dashboard");
});Cypress.Commands.add("login", (email, password) => { ... }) registers a new command named login that accepts two arguments. Every test in the project can now call cy.login(...).
For the command to load, make sure commands.js is imported by the support file. By default cypress/support/e2e.js loads it:
import "./commands";import "./commands" ensures all custom commands are available before a spec runs.
it("menambahkan produk ke keranjang", () => {
cy.login("user@example.com", "rahasia123");
cy.get("[data-cy=produk-pertama]").click();
cy.contains("Ditambahkan ke keranjang").should("be.visible");
});cy.login("user@example.com", "rahasia123") replaces five lines of login interaction with a single line. Every test that needs login is now consistent — and if the login flow changes, you only have to change one definition.
Besides commands, you can use plain JavaScript functions as helpers. Store them in cypress/support/utils.js:
export const formatHarga = (angka) =>
`Rp${angka.toLocaleString("id-ID")}`;
export const buatEmailAcak = () =>
`user-${Date.now()}@example.com`;Then import them in your spec file:
import { formatHarga, buatEmailAcak } from "../support/utils";
it("menampilkan harga terformat", () => {
cy.contains(formatHarga(45000)).should("be.visible");
});
it("mendaftar dengan email acak", () => {
cy.get("[data-cy=email]").type(buatEmailAcak());
});formatHarga(45000) and buatEmailAcak() are pure functions — easy to test on their own and free of browser state. This is the right place for pure logic that does not need access to Cypress.
We set baseUrl in episode 3. Its value keeps cy.visit("/") concise and makes moving between environments easy:
module.exports = defineConfig({
e2e: {
baseUrl: "http://localhost:3000",
},
});For values that differ between environments, use environment variables. Set them in the config or via the CLI:
npx cypress run --env apiBaseUrl=http://localhost:8080Access them from tests with Cypress.env:
const api = Cypress.env("apiBaseUrl");
cy.intercept("GET", `${api}/produk`).as("produk");Cypress.env("apiBaseUrl") reads the value passed via --env. For secret values, do not write them directly in the config — we will discuss secure credential management in episode 13.
As the project grows, split responsibilities:
cypress/support/
commands.js
commands/
auth.js
checkout.js
utils.js
e2e.jsimport "./commands/auth";
import "./commands/checkout";import "./commands/auth" loads the authentication commands. With this pattern, teams can add commands per domain without creating one giant file.
A few guidelines keep commands healthy:
Episode 7 expanded the Cypress vocabulary: custom commands with Cypress.Commands.add in commands.js, pure helpers for reusable logic, managing baseUrl and environment vars via --env and Cypress.env, and a support structure split by domain.
Key takeaways:
Cypress.Commands.add("nama", fn) registers a command you can call as cy.nama().cypress/support/e2e.js loads commands.js so commands are available globally.utils.js for non-browser logic like price formatting.--env and read with Cypress.env.In the next episode, episode 8, we will cover advanced interactions — handling iframes, popups, and multiple windows, drag and drop, uploads, keyboard events, testing single-page apps, and network stubbing with cy.intercept(). Your test interactions are about to touch real, complex scenarios.