Cypress 页面对象模式 #

什么是页面对象模式? #

页面对象模式(Page Object Model,POM)是一种设计模式,将页面元素和操作封装到独立的类中,使测试代码更加清晰、可维护。

text
┌─────────────────────────────────────────────────────────────┐
│                    传统测试代码的问题                         │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  ❌ 选择器分散在测试代码中                                    │
│  ❌ 页面结构变化需要修改多处                                  │
│  ❌ 测试代码难以阅读和维护                                    │
│  ❌ 代码重复严重                                             │
│                                                             │
└─────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────┐
│                    页面对象模式的优势                         │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  ✅ 选择器集中管理                                           │
│  ✅ 页面变化只需修改一处                                      │
│  ✅ 测试代码清晰易读                                         │
│  ✅ 代码复用性高                                             │
│  ✅ 易于维护和扩展                                           │
│                                                             │
└─────────────────────────────────────────────────────────────┘

基本结构 #

目录组织 #

text
cypress/
├── e2e/
│   └── auth/
│       └── login.cy.js        # 测试文件
├── support/
│   ├── pages/                  # 页面对象
│   │   ├── BasePage.js
│   │   ├── LoginPage.js
│   │   ├── DashboardPage.js
│   │   └── ProfilePage.js
│   ├── components/             # 组件对象
│   │   ├── Navigation.js
│   │   ├── Modal.js
│   │   └── Form.js
│   └── e2e.js
└── fixtures/
    └── users.json

基础页面对象 #

BasePage 基类 #

javascript
// cypress/support/pages/BasePage.js
class BasePage {
  visit(path) {
    cy.visit(path);
    return this;
  }

  waitForPageLoad() {
    cy.get('.loading').should('not.exist');
    return this;
  }

  getTitle() {
    return cy.title();
  }

  getUrl() {
    return cy.url();
  }

  takeScreenshot(name) {
    cy.screenshot(name);
    return this;
  }

  log(message) {
    cy.log(message);
    return this;
  }
}

export default BasePage;

LoginPage 页面对象 #

javascript
// cypress/support/pages/LoginPage.js
import BasePage from './BasePage';

class LoginPage extends BasePage {
  // 选择器
  selectors = {
    usernameInput: '[data-cy="username-input"]',
    passwordInput: '[data-cy="password-input"]',
    loginButton: '[data-cy="login-button"]',
    errorMessage: '[data-cy="error-message"]',
    forgotPasswordLink: '[data-cy="forgot-password-link"]',
    rememberMeCheckbox: '[data-cy="remember-me-checkbox"]'
  };

  // 访问页面
  visit() {
    super.visit('/login');
    return this;
  }

  // 输入用户名
  typeUsername(username) {
    cy.get(this.selectors.usernameInput).clear().type(username);
    return this;
  }

  // 输入密码
  typePassword(password) {
    cy.get(this.selectors.passwordInput).clear().type(password);
    return this;
  }

  // 点击登录按钮
  clickLoginButton() {
    cy.get(this.selectors.loginButton).click();
    return this;
  }

  // 勾选记住我
  checkRememberMe() {
    cy.get(this.selectors.rememberMeCheckbox).check();
    return this;
  }

  // 点击忘记密码
  clickForgotPassword() {
    cy.get(this.selectors.forgotPasswordLink).click();
    return this;
  }

  // 获取错误消息
  getErrorMessage() {
    return cy.get(this.selectors.errorMessage);
  }

  // 验证错误消息
  verifyErrorMessage(message) {
    this.getErrorMessage().should('contain', message);
    return this;
  }

  // 登录操作(组合方法)
  login(username, password, rememberMe = false) {
    this.visit()
      .typeUsername(username)
      .typePassword(password);
    
    if (rememberMe) {
      this.checkRememberMe();
    }
    
    this.clickLoginButton();
    return this;
  }
}

export default LoginPage;

DashboardPage 页面对象 #

javascript
// cypress/support/pages/DashboardPage.js
import BasePage from './BasePage';

class DashboardPage extends BasePage {
  selectors = {
    welcomeMessage: '[data-cy="welcome-message"]',
    userMenu: '[data-cy="user-menu"]',
    logoutButton: '[data-cy="logout-button"]',
    sidebar: '[data-cy="sidebar"]',
    notificationBadge: '[data-cy="notification-badge"]'
  };

  visit() {
    super.visit('/dashboard');
    return this;
  }

  getWelcomeMessage() {
    return cy.get(this.selectors.welcomeMessage);
  }

  verifyWelcomeMessage(username) {
    this.getWelcomeMessage().should('contain', username);
    return this;
  }

  openUserMenu() {
    cy.get(this.selectors.userMenu).click();
    return this;
  }

  logout() {
    this.openUserMenu();
    cy.get(this.selectors.logoutButton).click();
    return this;
  }

  getNotificationCount() {
    return cy.get(this.selectors.notificationBadge).invoke('text');
  }

  verifyNotifications(count) {
    this.getNotificationCount().should('eq', count.toString());
    return this;
  }
}

export default DashboardPage;

使用页面对象 #

测试文件 #

javascript
// cypress/e2e/auth/login.cy.js
import LoginPage from '../../support/pages/LoginPage';
import DashboardPage from '../../support/pages/DashboardPage';

describe('登录功能', () => {
  const loginPage = new LoginPage();
  const dashboardPage = new DashboardPage();

  beforeEach(() => {
    loginPage.visit();
  });

  it('用户可以使用正确的凭据登录', () => {
    loginPage
      .login('testuser', 'password123')
      .waitForPageLoad();
    
    dashboardPage
      .verifyWelcomeMessage('testuser');
  });

  it('错误的密码应该显示错误消息', () => {
    loginPage
      .typeUsername('testuser')
      .typePassword('wrongpassword')
      .clickLoginButton()
      .verifyErrorMessage('用户名或密码错误');
  });

  it('记住登录状态', () => {
    loginPage
      .login('testuser', 'password123', true)
      .waitForPageLoad();
    
    dashboardPage.verifyWelcomeMessage('testuser');
    
    // 刷新页面验证登录状态
    cy.reload();
    dashboardPage.verifyWelcomeMessage('testuser');
  });

  it('点击忘记密码跳转到重置页面', () => {
    loginPage.clickForgotPassword();
    cy.url().should('include', '/forgot-password');
  });
});

组件对象 #

导航组件 #

javascript
// cypress/support/components/Navigation.js
class Navigation {
  selectors = {
    menu: '[data-cy="nav-menu"]',
    menuItem: '[data-cy="nav-item"]',
    mobileMenuToggle: '[data-cy="mobile-menu-toggle"]',
    searchInput: '[data-cy="search-input"]',
    searchButton: '[data-cy="search-button"]'
  };

  openMobileMenu() {
    cy.get(this.selectors.mobileMenuToggle).click();
    return this;
  }

  clickMenuItem(itemName) {
    cy.get(this.selectors.menuItem).contains(itemName).click();
    return this;
  }

  search(query) {
    cy.get(this.selectors.searchInput).type(query);
    cy.get(this.selectors.searchButton).click();
    return this;
  }

  verifyMenuItemExists(itemName) {
    cy.get(this.selectors.menuItem).contains(itemName).should('exist');
    return this;
  }
}

export default Navigation;

模态框组件 #

javascript
// cypress/support/components/Modal.js
class Modal {
  selectors = {
    modal: '[data-cy="modal"]',
    title: '[data-cy="modal-title"]',
    content: '[data-cy="modal-content"]',
    closeButton: '[data-cy="modal-close"]',
    confirmButton: '[data-cy="modal-confirm"]',
    cancelButton: '[data-cy="modal-cancel"]'
  };

  verifyVisible() {
    cy.get(this.selectors.modal).should('be.visible');
    return this;
  }

  verifyTitle(title) {
    cy.get(this.selectors.title).should('contain', title);
    return this;
  }

  verifyContent(content) {
    cy.get(this.selectors.content).should('contain', content);
    return this;
  }

  close() {
    cy.get(this.selectors.closeButton).click();
    return this;
  }

  confirm() {
    cy.get(this.selectors.confirmButton).click();
    return this;
  }

  cancel() {
    cy.get(this.selectors.cancelButton).click();
    return this;
  }

  verifyClosed() {
    cy.get(this.selectors.modal).should('not.exist');
    return this;
  }
}

export default Navigation;

表单组件 #

javascript
// cypress/support/components/Form.js
class Form {
  constructor(formSelector) {
    this.formSelector = formSelector;
  }

  getInput(name) {
    return cy.get(`${this.formSelector} [name="${name}"]`);
  }

  typeInField(name, value) {
    this.getInput(name).clear().type(value);
    return this;
  }

  selectOption(name, value) {
    cy.get(`${this.formSelector} select[name="${name}"]`).select(value);
    return this;
  }

  checkCheckbox(name) {
    cy.get(`${this.formSelector} input[name="${name}"]`).check();
    return this;
  }

  submit() {
    cy.get(`${this.formSelector} button[type="submit"]`).click();
    return this;
  }

  verifyFieldError(name, errorMessage) {
    cy.get(`${this.formSelector} [data-field="${name}"] .error`)
      .should('contain', errorMessage);
    return this;
  }

  fillForm(data) {
    Object.entries(data).forEach(([field, value]) => {
      this.typeInField(field, value);
    });
    return this;
  }
}

export default Form;

在页面对象中使用组件 #

javascript
// cypress/support/pages/ProductsPage.js
import BasePage from './BasePage';
import Navigation from '../components/Navigation';
import Modal from '../components/Modal';

class ProductsPage extends BasePage {
  selectors = {
    productList: '[data-cy="product-list"]',
    productItem: '[data-cy="product-item"]',
    addToCartButton: '[data-cy="add-to-cart"]',
    filterDropdown: '[data-cy="filter-dropdown"]'
  };

  navigation = new Navigation();
  modal = new Modal();

  visit() {
    super.visit('/products');
    return this;
  }

  getProductCount() {
    return cy.get(this.selectors.productItem).its('length');
  }

  clickProduct(index) {
    cy.get(this.selectors.productItem).eq(index).click();
    return this;
  }

  addToCart(productId) {
    cy.get(`[data-product-id="${productId}"] ${this.selectors.addToCartButton}`).click();
    return this;
  }

  filterBy(category) {
    cy.get(this.selectors.filterDropdown).select(category);
    return this;
  }

  confirmAddToCart() {
    this.modal
      .verifyVisible()
      .verifyTitle('添加到购物车')
      .confirm()
      .verifyClosed();
    return this;
  }
}

export default ProductsPage;

TypeScript 支持 #

页面对象类型定义 #

typescript
// cypress/support/pages/LoginPage.ts
import BasePage from './BasePage';

interface LoginCredentials {
  username: string;
  password: string;
  rememberMe?: boolean;
}

class LoginPage extends BasePage {
  selectors = {
    usernameInput: '[data-cy="username-input"]',
    passwordInput: '[data-cy="password-input"]',
    loginButton: '[data-cy="login-button"]',
    errorMessage: '[data-cy="error-message"]'
  };

  visit(): this {
    super.visit('/login');
    return this;
  }

  typeUsername(username: string): this {
    cy.get(this.selectors.usernameInput).clear().type(username);
    return this;
  }

  typePassword(password: string): this {
    cy.get(this.selectors.passwordInput).clear().type(password);
    return this;
  }

  clickLoginButton(): this {
    cy.get(this.selectors.loginButton).click();
    return this;
  }

  login(credentials: LoginCredentials): this {
    const { username, password, rememberMe = false } = credentials;
    
    this.visit()
      .typeUsername(username)
      .typePassword(password);
    
    if (rememberMe) {
      this.checkRememberMe();
    }
    
    this.clickLoginButton();
    return this;
  }
}

export default LoginPage;

最佳实践 #

1. 选择器集中管理 #

javascript
// ✅ 好的做法 - 选择器集中定义
class LoginPage {
  selectors = {
    usernameInput: '[data-cy="username"]',
    passwordInput: '[data-cy="password"]'
  };

  typeUsername(username) {
    cy.get(this.selectors.usernameInput).type(username);
  }
}

// ❌ 不好的做法 - 选择器分散
class LoginPage {
  typeUsername(username) {
    cy.get('[data-cy="username"]').type(username);
  }
  
  clearUsername() {
    cy.get('[data-cy="username"]').clear();  // 重复定义
  }
}

2. 返回 this 支持链式调用 #

javascript
// ✅ 好的做法 - 返回 this
class LoginPage {
  typeUsername(username) {
    cy.get(this.selectors.usernameInput).type(username);
    return this;
  }
}

// 使用
loginPage.typeUsername('user').typePassword('pass').clickLogin();

// ❌ 不好的做法 - 不返回 this
class LoginPage {
  typeUsername(username) {
    cy.get(this.selectors.usernameInput).type(username);
    // 没有返回值
  }
}

3. 组合方法封装常用操作 #

javascript
// ✅ 好的做法 - 封装常用操作
class LoginPage {
  login(username, password) {
    this.visit()
      .typeUsername(username)
      .typePassword(password)
      .clickLoginButton();
    return this;
  }
}

// 使用
loginPage.login('user', 'pass');

4. 验证方法独立 #

javascript
// ✅ 好的做法 - 验证方法独立
class LoginPage {
  verifyErrorMessage(message) {
    cy.get(this.selectors.errorMessage).should('contain', message);
    return this;
  }
}

// 使用
loginPage.verifyErrorMessage('密码错误');

完整示例 #

用户管理页面 #

javascript
// cypress/support/pages/UsersPage.js
import BasePage from './BasePage';

class UsersPage extends BasePage {
  selectors = {
    userList: '[data-cy="user-list"]',
    userItem: '[data-cy="user-item"]',
    searchInput: '[data-cy="search-input"]',
    searchButton: '[data-cy="search-button"]',
    addUserButton: '[data-cy="add-user-button"]',
    editButton: '[data-cy="edit-button"]',
    deleteButton: '[data-cy="delete-button"]',
    confirmDeleteButton: '[data-cy="confirm-delete"]',
    pagination: '[data-cy="pagination"]',
    nextPage: '[data-cy="next-page"]',
    prevPage: '[data-cy="prev-page"]'
  };

  visit() {
    super.visit('/users');
    return this;
  }

  getUserCount() {
    return cy.get(this.selectors.userItem).its('length');
  }

  searchUser(query) {
    cy.get(this.selectors.searchInput).clear().type(query);
    cy.get(this.selectors.searchButton).click();
    return this;
  }

  clickAddUser() {
    cy.get(this.selectors.addUserButton).click();
    return this;
  }

  editUser(userId) {
    cy.get(`[data-user-id="${userId}"] ${this.selectors.editButton}`).click();
    return this;
  }

  deleteUser(userId) {
    cy.get(`[data-user-id="${userId}"] ${this.selectors.deleteButton}`).click();
    cy.get(this.selectors.confirmDeleteButton).click();
    return this;
  }

  goToNextPage() {
    cy.get(this.selectors.nextPage).click();
    return this;
  }

  goToPrevPage() {
    cy.get(this.selectors.prevPage).click();
    return this;
  }

  verifyUserExists(username) {
    cy.get(this.selectors.userItem).contains(username).should('exist');
    return this;
  }

  verifyUserNotExists(username) {
    cy.get(this.selectors.userItem).contains(username).should('not.exist');
    return this;
  }
}

export default UsersPage;

测试文件 #

javascript
// cypress/e2e/users/users.cy.js
import UsersPage from '../../support/pages/UsersPage';
import LoginPage from '../../support/pages/LoginPage';

describe('用户管理', () => {
  const loginPage = new LoginPage();
  const usersPage = new UsersPage();

  beforeEach(() => {
    loginPage.login('admin', 'adminpass');
    usersPage.visit();
  });

  it('显示用户列表', () => {
    usersPage.getUserCount().should('be.greaterThan', 0);
  });

  it('搜索用户', () => {
    usersPage
      .searchUser('john')
      .verifyUserExists('John Doe');
  });

  it('添加新用户', () => {
    usersPage.clickAddUser();
    // 填写表单...
    usersPage.verifyUserExists('New User');
  });

  it('删除用户', () => {
    usersPage
      .deleteUser(123)
      .verifyUserNotExists('Deleted User');
  });

  it('分页导航', () => {
    usersPage
      .goToNextPage()
      .getUserCount()
      .should('be.greaterThan', 0);
  });
});

下一步 #

现在你已经掌握了 Cypress 页面对象模式的应用,接下来学习 高级特性 了解更多 Cypress 的高级功能!

最后更新:2026-03-28