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