Cypress 自定义命令 #
为什么需要自定义命令? #
text
┌─────────────────────────────────────────────────────────────┐
│ 自定义命令的优势 │
├─────────────────────────────────────────────────────────────┤
│ │
│ ✅ 减少代码重复 │
│ ✅ 提高测试可读性 │
│ ✅ 统一测试逻辑 │
│ ✅ 便于维护和修改 │
│ ✅ 支持链式调用 │
│ │
└─────────────────────────────────────────────────────────────┘
创建自定义命令 #
基本语法 #
javascript
// cypress/support/commands.js
Cypress.Commands.add('commandName', (param1, param2) => {
// 命令逻辑
});
简单示例 #
javascript
// cypress/support/commands.js
// 登录命令
Cypress.Commands.add('login', (username, password) => {
cy.visit('/login');
cy.get('#username').type(username);
cy.get('#password').type(password);
cy.get('button[type="submit"]').click();
cy.url().should('include', '/dashboard');
});
// 使用
cy.login('testuser', 'password123');
命令类型 #
父命令(Parent Command) #
父命令开启新的命令链:
javascript
// 父命令 - 总是开始新链
Cypress.Commands.add('login', (username, password) => {
cy.visit('/login');
cy.get('#username').type(username);
cy.get('#password').type(password);
cy.get('button[type="submit"]').click();
});
// 使用
cy.login('user', 'pass'); // 开始新链
双重命令(Dual Command) #
双重命令可以链式调用:
javascript
// 双重命令 - 可以链式调用
Cypress.Commands.add('hasText', { prevSubject: true }, (subject, text) => {
cy.wrap(subject).should('contain', text);
});
// 使用
cy.get('.title').hasText('Welcome'); // 链式调用
子命令(Child Command) #
子命令必须跟在父命令后面:
javascript
// 子命令 - 必须跟在父命令后
Cypress.Commands.add('clickAndVerify', { prevSubject: 'element' }, (subject, expectedText) => {
cy.wrap(subject).click();
cy.get('.result').should('contain', expectedText);
});
// 使用
cy.get('.button').clickAndVerify('Success');
命令选项 #
prevSubject 选项 #
javascript
// element - 必须跟在元素后面
Cypress.Commands.add('customClick', { prevSubject: 'element' }, (subject) => {
cy.wrap(subject).click();
});
// document - 接收 document
Cypress.Commands.add('getTitle', { prevSubject: 'document' }, (doc) => {
return doc.title;
});
// window - 接收 window
Cypress.Commands.add('getLocation', { prevSubject: 'window' }, (win) => {
return win.location.href;
});
// optional - 可选的前置主体
Cypress.Commands.add('optionalCommand', { prevSubject: 'optional' }, (subject) => {
if (subject) {
cy.wrap(subject).click();
} else {
cy.get('.default').click();
}
});
多种 subject 类型 #
javascript
Cypress.Commands.add('logSubject', { prevSubject: ['element', 'document', 'window'] }, (subject) => {
console.log('Subject type:', typeof subject);
return subject;
});
返回值 #
返回值传递 #
javascript
// 返回值可以链式传递
Cypress.Commands.add('getUser', (userId) => {
return cy.request(`/api/users/${userId}`).its('body');
});
// 使用
cy.getUser(1).then((user) => {
cy.log(user.name);
});
// 链式调用
cy.getUser(1)
.its('name')
.should('eq', 'John');
返回 Cypress 链 #
javascript
Cypress.Commands.add('waitForLoading', () => {
// 返回 Cypress 链,支持继续链式调用
return cy.get('.loading')
.should('not.exist')
.get('.content')
.should('be.visible');
});
// 使用
cy.waitForLoading()
.find('.item')
.should('have.length', 5);
实用自定义命令示例 #
认证命令 #
javascript
// cypress/support/commands/auth.js
// 登录命令
Cypress.Commands.add('login', (username = 'testuser', password = 'password123') => {
cy.session([username, password], () => {
cy.request({
method: 'POST',
url: '/api/auth/login',
body: { username, password }
}).then((response) => {
window.localStorage.setItem('token', response.body.token);
});
}, {
validate() {
return cy.window().its('localStorage.token').should('exist');
}
});
});
// 登出命令
Cypress.Commands.add('logout', () => {
window.localStorage.removeItem('token');
cy.visit('/login');
});
// 检查登录状态
Cypress.Commands.add('shouldBeLoggedIn', () => {
cy.get('.user-menu').should('be.visible');
cy.get('.login-button').should('not.exist');
});
表单命令 #
javascript
// cypress/support/commands/form.js
// 填写表单
Cypress.Commands.add('fillForm', (formData) => {
Object.entries(formData).forEach(([field, value]) => {
const selector = `[name="${field}"], [data-cy="${field}"]`;
cy.get('body').then(($body) => {
if ($body.find(selector).is('select')) {
cy.get(selector).select(value);
} else if ($body.find(selector).is('[type="checkbox"], [type="radio"]')) {
cy.get(selector).check(value);
} else {
cy.get(selector).clear().type(value);
}
});
});
});
// 清空表单
Cypress.Commands.add('clearForm', () => {
cy.get('input, textarea').clear();
cy.get('select').each(($select) => {
cy.wrap($select).select(0);
});
cy.get('input[type="checkbox"], input[type="radio"]').uncheck();
});
// 提交表单
Cypress.Commands.add('submitForm', () => {
cy.get('form').submit();
});
等待命令 #
javascript
// cypress/support/commands/wait.js
// 等待页面加载完成
Cypress.Commands.add('waitForPageLoad', () => {
cy.get('.loading-spinner', { timeout: 10000 }).should('not.exist');
cy.get('.page-content').should('be.visible');
});
// 等待 API 请求完成
Cypress.Commands.add('waitForApi', (alias) => {
cy.wait(`@${alias}`).its('response.statusCode').should('eq', 200);
});
// 等待元素出现并消失
Cypress.Commands.add('waitForElement', (selector, options = {}) => {
const { timeout = 5000, shouldDisappear = false } = options;
if (shouldDisappear) {
cy.get(selector, { timeout }).should('not.exist');
} else {
cy.get(selector, { timeout }).should('be.visible');
}
});
数据操作命令 #
javascript
// cypress/support/commands/data.js
// 创建测试数据
Cypress.Commands.add('createUser', (userData = {}) => {
const defaultUser = {
name: 'Test User',
email: `test${Date.now()}@example.com`,
password: 'Password123!'
};
return cy.request({
method: 'POST',
url: '/api/users',
body: { ...defaultUser, ...userData }
}).its('body');
});
// 清理测试数据
Cypress.Commands.add('cleanupTestData', () => {
cy.request('POST', '/api/test/cleanup');
});
// 重置数据库
Cypress.Commands.add('resetDatabase', () => {
cy.request('POST', '/api/test/reset-database');
});
导航命令 #
javascript
// cypress/support/commands/navigation.js
// 导航到页面
Cypress.Commands.add('navigateTo', (page) => {
const routes = {
home: '/',
login: '/login',
dashboard: '/dashboard',
profile: '/profile',
settings: '/settings'
};
const path = routes[page] || page;
cy.visit(path);
});
// 返回上一页
Cypress.Commands.add('goBack', () => {
cy.go('back');
});
// 刷新页面
Cypress.Commands.add('refreshPage', () => {
cy.reload();
});
TypeScript 类型定义 #
添加类型声明 #
typescript
// cypress/support/index.d.ts
declare global {
namespace Cypress {
interface Chainable {
login(username?: string, password?: string): Chainable<void>;
logout(): Chainable<void>;
shouldBeLoggedIn(): Chainable<void>;
fillForm(formData: Record<string, string>): Chainable<void>;
createUser(userData?: Partial<User>): Chainable<User>;
navigateTo(page: string): Chainable<void>;
}
}
}
interface User {
id: number;
name: string;
email: string;
}
export {};
使用类型 #
typescript
// 现在有完整的类型提示
cy.login('user', 'pass');
cy.fillForm({ name: 'John', email: 'john@example.com' });
cy.createUser({ name: 'Test' }).then((user) => {
cy.log(user.id); // 类型安全
});
组织自定义命令 #
文件结构 #
text
cypress/support/
├── commands/
│ ├── auth.js # 认证相关命令
│ ├── form.js # 表单相关命令
│ ├── navigation.js # 导航相关命令
│ ├── data.js # 数据操作命令
│ └── utils.js # 工具命令
├── e2e.js # 入口文件
└── index.d.ts # TypeScript 类型定义
导入命令 #
javascript
// cypress/support/e2e.js
// 导入所有自定义命令
import './commands/auth';
import './commands/form';
import './commands/navigation';
import './commands/data';
import './commands/utils';
// 全局配置
beforeEach(() => {
cy.clearCookies();
cy.clearLocalStorage();
});
命令最佳实践 #
1. 命名清晰 #
javascript
// ✅ 好的命名
Cypress.Commands.add('loginAsAdmin', () => {});
Cypress.Commands.add('waitForPageLoad', () => {});
Cypress.Commands.add('createTestUser', () => {});
// ❌ 不好的命名
Cypress.Commands.add('doLogin', () => {});
Cypress.Commands.add('wait', () => {});
Cypress.Commands.add('create', () => {});
2. 参数默认值 #
javascript
// ✅ 提供合理的默认值
Cypress.Commands.add('login', (username = 'testuser', password = 'password123') => {
// ...
});
// 使用默认值
cy.login(); // 使用默认凭据
// 使用自定义值
cy.login('admin', 'adminpass');
3. 返回值支持链式调用 #
javascript
// ✅ 返回 Cypress 链
Cypress.Commands.add('getUser', (id) => {
return cy.request(`/api/users/${id}`).its('body');
});
// 支持链式调用
cy.getUser(1)
.its('name')
.should('eq', 'John');
4. 添加日志 #
javascript
Cypress.Commands.add('login', (username, password) => {
Cypress.log({
name: 'login',
message: `Logging in as ${username}`,
consoleProps: () => ({
username,
password: '***'
})
});
cy.visit('/login');
cy.get('#username').type(username);
cy.get('#password').type(password);
cy.get('button[type="submit"]').click();
});
5. 错误处理 #
javascript
Cypress.Commands.add('waitForElement', (selector, options = {}) => {
const { timeout = 5000 } = options;
return cy.get(selector, { timeout }).then(($el) => {
if (!$el.length) {
throw new Error(`Element "${selector}" not found within ${timeout}ms`);
}
return $el;
});
});
完整示例 #
javascript
// cypress/support/commands/auth.js
Cypress.Commands.add('login', (username = 'testuser', password = 'password123') => {
Cypress.log({
name: 'login',
message: `Logging in as ${username}`,
consoleProps: () => ({
username,
timestamp: new Date().toISOString()
})
});
cy.session([username, password], () => {
cy.request({
method: 'POST',
url: '/api/auth/login',
body: { username, password }
}).then((response) => {
window.localStorage.setItem('authToken', response.body.token);
cy.setCookie('refreshToken', response.body.refreshToken);
});
}, {
validate() {
return cy.window().its('localStorage.authToken').should('exist');
},
cacheAcrossSpecs: true
});
});
Cypress.Commands.add('loginAsAdmin', () => {
cy.login('admin', 'adminpassword');
});
Cypress.Commands.add('logout', () => {
Cypress.log({ name: 'logout', message: 'Logging out' });
window.localStorage.removeItem('authToken');
cy.clearCookie('refreshToken');
cy.visit('/login');
});
javascript
// cypress/e2e/dashboard.cy.js
describe('Dashboard', () => {
beforeEach(() => {
cy.login();
cy.visit('/dashboard');
});
it('显示用户信息', () => {
cy.get('.user-name').should('be.visible');
cy.get('.user-email').should('be.visible');
});
it('管理员可以访问设置', () => {
cy.loginAsAdmin();
cy.visit('/dashboard');
cy.get('.settings-link').should('be.visible');
});
it('用户可以登出', () => {
cy.get('.logout-button').click();
cy.url().should('include', '/login');
});
});
下一步 #
现在你已经掌握了 Cypress 自定义命令的创建和使用,接下来学习 页面对象模式 了解如何组织测试代码结构!
最后更新:2026-03-28