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