Let’s ditch the magic strings and make your Cypress tests as robust as your application code.
// cypress/support/commands.ts
declare global {
namespace Cypress {
interface Chainable<Subject = any> {
/**
* Custom command to log in a user with specific credentials.
* @example cy.login('testuser', 'password123');
*/
login(username: string, password: string): Chainable<any>;
}
}
}
// cypress/support/commands.ts
Cypress.Commands.add('login', (username, password) => {
cy.request('POST', '/api/login', { username, password }).then((response) => {
expect(response.status).to.eq(200);
// Assuming the API returns a token or session data
const sessionData = response.body;
// Store session data in localStorage or cookies for subsequent requests
window.localStorage.setItem('session', JSON.stringify(sessionData));
});
});
This setup immediately gives you type checking for your custom command. If you try to call cy.login('user') (missing the password), TypeScript will flag it as an error before you even run Cypress.
The real power comes when you integrate this with your application’s elements. Imagine you have a login form:
<!-- index.html -->
<form id="login-form">
<input id="username-input" type="text" placeholder="Username">
<input id="password-input" type="password" placeholder="Password">
<button id="submit-button" type="submit">Login</button>
</form>
Here’s how you’d write a type-safe test for it:
// cypress/e2e/login.cy.ts
describe('Login Flow', () => {
beforeEach(() => {
// Use the custom command to log in, ensuring type safety
cy.login('testuser', 'password123');
// Visit the page that should be protected by login
cy.visit('/dashboard');
});
it('should display the dashboard after successful login', () => {
// Type assertion for better intellisense and type safety
cy.get<HTMLHeadingElement>('#dashboard-title').should('contain', 'Welcome, testuser!');
cy.get<HTMLButtonElement>('#logout-button').should('be.visible');
});
it('should prevent access with invalid credentials', () => {
// You'd typically have a separate test for failed login, often bypassing the custom command
cy.visit('/login');
cy.get<HTMLInputElement>('#username-input').type('wronguser');
cy.get<HTMLInputElement>('#password-input').type('wrongpassword');
cy.get<HTMLButtonElement>('#submit-button').click();
cy.url().should('include', '/login'); // Or wherever it redirects on failure
cy.get('.error-message').should('be.visible');
});
});
Notice the <HTMLHeadingElement> and <HTMLInputElement> type assertions after cy.get. This isn’t strictly necessary for basic should assertions, but it unlocks a world of type safety when you need to interact with the DOM elements directly, like calling .focus() or .val(). If you try to call a method that doesn’t exist on that specific element type (e.g., calling .value on a HTMLButtonElement), TypeScript will catch it.
The core idea is to leverage TypeScript’s type system throughout the entire Cypress workflow. This includes:
- Custom Commands: As shown, defining types for your custom commands (
cy.login,cy.getByDataCy, etc.) makes them callable with correct arguments and return types. - DOM Element Interactions: Using type assertions with
cy.get()(e.g.,cy.get<HTMLInputElement>('#my-input')) ensures that subsequent.then()callbacks or direct DOM manipulations are type-checked against the actual HTML element type. - Page Object Models (POMs): While not strictly a TypeScript feature, POMs become significantly more powerful with TypeScript. You can define interfaces for your page objects, ensuring that methods and properties are accessed correctly and predictably.
// cypress/pages/LoginPage.ts
interface LoginPageElements {
usernameInput: () => Cypress.Chainable<JQuery<HTMLInputElement>>;
passwordInput: () => Cypress.Chainable<JQuery<HTMLInputElement>>;
submitButton: () => Cypress.Chainable<JQuery<HTMLButtonElement>>;
errorMessage: () => Cypress.Chainable<JQuery<HTMLDivElement>>;
}
export class LoginPage implements LoginPageElements {
visit() {
cy.visit('/login');
}
usernameInput() {
return cy.get<HTMLInputElement>('#username-input');
}
passwordInput() {
return cy.get<HTMLInputElement>('#password-input');
}
submitButton() {
return cy.get<HTMLButtonElement>('#submit-button');
}
errorMessage() {
return cy.get<HTMLDivElement>('.error-message');
}
login(username: string, password: string) {
this.usernameInput().type(username);
this.passwordInput().type(password);
this.submitButton().click();
}
}
// cypress/e2e/login_pom.cy.ts
import { LoginPage } from '../pages/LoginPage';
describe('Login Flow with POM', () => {
it('should log in successfully', () => {
const loginPage = new LoginPage();
loginPage.visit();
loginPage.login('testuser', 'password123');
// After login, you'd assert on the next page, e.g., dashboardPage.verifyWelcomeMessage();
cy.url().should('include', '/dashboard');
});
});
This POM example defines the types of the elements the page exposes. When you call loginPage.usernameInput(), TypeScript knows it’s getting a Cypress.Chainable<JQuery<HTMLInputElement>>, and you can safely use .type() on it.
The most subtle but powerful aspect is how TypeScript helps you reason about asynchronous operations. When you use cy.get(...).then(...), TypeScript understands that the then callback receives the JQuery<Element> as its argument, and you can perform type-safe operations within that scope. It prevents you from, for instance, trying to JSON.stringify an HTML element directly without first extracting its value.
The next hurdle you’ll likely encounter is managing complex application states and mocking API responses effectively in a type-safe manner.